feat(hub): Version + Build-Commit im Header und in /health (v0.11.0)
Zwei Maschinen liessen sich nicht auseinanderhalten. Der Watchdog-Fix von 6fa9411 ging ohne Versions-Bump raus, also meldeten beide Hubs 0.10.2, waehrend nur einer den Fix hatte. Zusaetzlich faehrt der Hub aus dist/ — ein Neustart ohne Build zieht still den alten Code wieder hoch. Beides zusammen hat heute eine Diagnose in die falsche Richtung geschickt. - Version auf 0.11.0 (Feature + Fix seit 0.10.2). - version.ts liefert zusaetzlich buildRef(): den kurzen Commit, aus dem der laufende Code stammt. Bewusst LAZY und gecacht — VERSION wird von jedem CLI-Aufruf importiert, ein git-Aufruf beim Import wuerde Befehle verteuern, die ihn nie anzeigen. Bei Nicht-git-Installs faellt versionLabel() sauber auf die blosse Version zurueck. - Board-Header (appHeader) und der aeltere pageHeader zeigen "v0.11.0 · <commit>", mit Tooltip warum die Nummer allein nicht reicht. - /health liefert buildRef, damit sich zwei Maschinen ueber die Leitung vergleichen lassen, ohne das Board zu oeffnen. 318 Tests gruen, tsc --noEmit sauber. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6fa94116a4
commit
b8ad801c1d
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "agenthub",
|
"name": "agenthub",
|
||||||
"version": "0.10.2",
|
"version": "0.11.0",
|
||||||
"description": "Local coordination layer for AI coding agents",
|
"description": "Local coordination layer for AI coding agents",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./dist/index.js",
|
"main": "./dist/index.js",
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { listTasks } from './taskService.js';
|
|||||||
import { listMessages } from './messageService.js';
|
import { listMessages } from './messageService.js';
|
||||||
import { listAsks } from './askService.js';
|
import { listAsks } from './askService.js';
|
||||||
import { getRoster } from './rosterService.js';
|
import { getRoster } from './rosterService.js';
|
||||||
import { VERSION } from '../../version.js';
|
import { VERSION, buildRef } from '../../version.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-agent presence (lastSeen) + the /health report (TSK-0226).
|
* Per-agent presence (lastSeen) + the /health report (TSK-0226).
|
||||||
@ -73,6 +73,8 @@ export interface AgentHealth {
|
|||||||
export interface HealthReport {
|
export interface HealthReport {
|
||||||
status: 'ok';
|
status: 'ok';
|
||||||
version: string;
|
version: string;
|
||||||
|
/** Short commit this hub runs from — '' for non-git installs. Compares machines. */
|
||||||
|
buildRef: string;
|
||||||
startedAt: string;
|
startedAt: string;
|
||||||
uptimeSec: number;
|
uptimeSec: number;
|
||||||
counts: { tasks: number; open: number; inProgress: number; review: number; unreadMessages: number; pendingAsks: number; oldestAskAgeSec?: number };
|
counts: { tasks: number; open: number; inProgress: number; review: number; unreadMessages: number; pendingAsks: number; oldestAskAgeSec?: number };
|
||||||
@ -199,6 +201,7 @@ export function computeHealth(cwd: string, startedAtMs: number, now: number = Da
|
|||||||
return {
|
return {
|
||||||
status: 'ok',
|
status: 'ok',
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
|
buildRef: buildRef(),
|
||||||
startedAt: new Date(startedAtMs).toISOString(),
|
startedAt: new Date(startedAtMs).toISOString(),
|
||||||
uptimeSec: Math.max(0, Math.round((now - startedAtMs) / 1000)),
|
uptimeSec: Math.max(0, Math.round((now - startedAtMs) / 1000)),
|
||||||
counts: {
|
counts: {
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { TaskStatus as TaskStatusSchema } from '../core/schema.js';
|
import { TaskStatus as TaskStatusSchema } from '../core/schema.js';
|
||||||
|
import { versionLabel } from '../version.js';
|
||||||
type TaskStatus = 'open' | 'in_progress' | 'review' | 'done' | 'cancelled';
|
type TaskStatus = 'open' | 'in_progress' | 'review' | 'done' | 'cancelled';
|
||||||
|
|
||||||
/** CSS variables block matching the AgentHub dark design spec. */
|
/** CSS variables block matching the AgentHub dark design spec. */
|
||||||
@ -272,6 +273,7 @@ export function pageHeader(projectName: string, current: 'board' | 'team' | 'act
|
|||||||
${mark}
|
${mark}
|
||||||
<h1 style="font-size:18px;margin:0;font-weight:600;">AgentHub</h1>
|
<h1 style="font-size:18px;margin:0;font-weight:600;">AgentHub</h1>
|
||||||
<span style="color:var(--muted);font-size:12px;">${escapeHtml(projectName)}</span>
|
<span style="color:var(--muted);font-size:12px;">${escapeHtml(projectName)}</span>
|
||||||
|
<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--muted);border:1px solid var(--border);border-radius:5px;padding:2px 6px;cursor:help;" title="Version und Commit dieses Hubs — zum Abgleich zwischen Maschinen.">${escapeHtml(versionLabel())}</span>
|
||||||
<span style="flex:1;"></span>
|
<span style="flex:1;"></span>
|
||||||
<nav style="display:flex;gap:8px;align-items:center;">
|
<nav style="display:flex;gap:8px;align-items:center;">
|
||||||
${navItem('Board', '/board', current === 'board')}
|
${navItem('Board', '/board', current === 'board')}
|
||||||
@ -313,6 +315,13 @@ export function appHeaderOnlyCss(): string {
|
|||||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||||
background: rgba(9, 12, 24, .82); backdrop-filter: blur(12px);
|
background: rgba(9, 12, 24, .82); backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
.app-header .b2-build {
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||||
|
font-size: 11px; color: var(--b2-muted, #7d8aa5);
|
||||||
|
padding: 3px 7px; border-radius: 5px;
|
||||||
|
border: 1px solid var(--b2-border, rgba(255,255,255,.08));
|
||||||
|
white-space: nowrap; cursor: help;
|
||||||
|
}
|
||||||
.app-header .b2-brand { display: flex; align-items: center; gap: 10px; font-size: 16px; }
|
.app-header .b2-brand { display: flex; align-items: center; gap: 10px; font-size: 16px; }
|
||||||
.app-header .b2-brand b { font-weight: 700; letter-spacing: .01em; }
|
.app-header .b2-brand b { font-weight: 700; letter-spacing: .01em; }
|
||||||
.b2-logo-img { display: block; }
|
.b2-logo-img { display: block; }
|
||||||
@ -425,6 +434,7 @@ export function appHeader(projectName: string, current: HeaderPage): string {
|
|||||||
${link('Decisions', '/decisions', 'decisions')}
|
${link('Decisions', '/decisions', 'decisions')}
|
||||||
</nav>
|
</nav>
|
||||||
<span class="b2-hdr-right">
|
<span class="b2-hdr-right">
|
||||||
|
<span class="b2-build" title="Version und Commit, aus dem dieser Hub laeuft — zum Abgleich zwischen Maschinen. Die Versionsnummer allein reicht nicht: ein Fix kann ohne Bump ausgeliefert werden.">${escapeHtml(versionLabel())}</span>
|
||||||
<button class="b2-btn" id="newTaskBtn" type="button" aria-haspopup="dialog" aria-expanded="false">+ New task</button>
|
<button class="b2-btn" id="newTaskBtn" type="button" aria-haspopup="dialog" aria-expanded="false">+ New task</button>
|
||||||
<span class="sse-status stale" id="sseStatus">
|
<span class="sse-status stale" id="sseStatus">
|
||||||
<span class="b2-live-dot" aria-hidden="true"></span>
|
<span class="b2-live-dot" aria-hidden="true"></span>
|
||||||
|
|||||||
@ -1,2 +1,64 @@
|
|||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { existsSync, readFileSync } from 'node:fs';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join, parse } from 'node:path';
|
||||||
|
|
||||||
/** Single source of truth for the agenthub version (package.json, CLI, MCP, /health). */
|
/** Single source of truth for the agenthub version (package.json, CLI, MCP, /health). */
|
||||||
export const VERSION = '0.10.2';
|
export const VERSION = '0.11.0';
|
||||||
|
|
||||||
|
/** Locate the agenthub install dir by walking up from this file. */
|
||||||
|
function findPackageRoot(): string | undefined {
|
||||||
|
let dir = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const { root } = parse(dir);
|
||||||
|
for (;;) {
|
||||||
|
const pkgPath = join(dir, 'package.json');
|
||||||
|
if (existsSync(pkgPath)) {
|
||||||
|
try {
|
||||||
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8')) as { name?: string };
|
||||||
|
if (pkg.name === 'agenthub') return dir;
|
||||||
|
} catch {
|
||||||
|
/* malformed package.json — keep walking */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dir === root) return undefined;
|
||||||
|
dir = dirname(dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Memoised so repeated header renders never re-shell out. `''` once resolved empty. */
|
||||||
|
let cachedBuildRef: string | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Short commit the running code was built from — `''` for non-git installs.
|
||||||
|
*
|
||||||
|
* The version alone cannot tell two machines apart: a fix can ship without a
|
||||||
|
* version bump, and then both hubs report the same number while running
|
||||||
|
* different code. That happened on 2026-08-09 and sent a diagnosis down the
|
||||||
|
* wrong path, so every surface that shows the version shows this next to it.
|
||||||
|
*
|
||||||
|
* Lazy on purpose: `VERSION` is imported by every CLI invocation, and shelling
|
||||||
|
* out to git at import time would tax commands that never display it.
|
||||||
|
*/
|
||||||
|
export function buildRef(): string {
|
||||||
|
if (cachedBuildRef !== undefined) return cachedBuildRef;
|
||||||
|
cachedBuildRef = '';
|
||||||
|
try {
|
||||||
|
const root = findPackageRoot();
|
||||||
|
if (root) {
|
||||||
|
cachedBuildRef = execFileSync('git', ['rev-parse', '--short', 'HEAD'], {
|
||||||
|
cwd: root,
|
||||||
|
encoding: 'utf-8',
|
||||||
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
|
}).trim();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* not a git checkout, or git unavailable — the version has to stand alone */
|
||||||
|
}
|
||||||
|
return cachedBuildRef;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** `v0.11.0 · 6fa9411`, or just `v0.11.0` when the commit is unknown. */
|
||||||
|
export function versionLabel(): string {
|
||||||
|
const ref = buildRef();
|
||||||
|
return ref ? `v${VERSION} · ${ref}` : `v${VERSION}`;
|
||||||
|
}
|
||||||
|
|||||||
@ -44,6 +44,10 @@ describe('GET /health + lastSeen stamping (TSK-0226)', () => {
|
|||||||
const h = await getHealth();
|
const h = await getHealth();
|
||||||
expect(h.status).toBe('ok');
|
expect(h.status).toBe('ok');
|
||||||
expect(h.version).toBe(VERSION);
|
expect(h.version).toBe(VERSION);
|
||||||
|
// The version alone cannot tell two hubs apart — a fix can ship without a
|
||||||
|
// bump. /health therefore carries the commit, so machines are comparable
|
||||||
|
// over the wire without opening the board.
|
||||||
|
expect(typeof h.buildRef).toBe('string');
|
||||||
expect(typeof h.uptimeSec).toBe('number');
|
expect(typeof h.uptimeSec).toBe('number');
|
||||||
expect(h.uptimeSec).toBeGreaterThanOrEqual(0);
|
expect(h.uptimeSec).toBeGreaterThanOrEqual(0);
|
||||||
expect(typeof h.startedAt).toBe('string');
|
expect(typeof h.startedAt).toBe('string');
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { agentAvatar, designTokensCss, escapeHtml, liveTimerJs, statusPill, appHeader, appHeaderCss, appHeaderOnlyCss } from '../src/server/ui-shared.js';
|
import { agentAvatar, designTokensCss, escapeHtml, liveTimerJs, statusPill, appHeader, appHeaderCss, appHeaderOnlyCss } from '../src/server/ui-shared.js';
|
||||||
|
import { VERSION, buildRef, versionLabel } from '../src/version.js';
|
||||||
|
|
||||||
describe('ui-shared helpers', () => {
|
describe('ui-shared helpers', () => {
|
||||||
it('designTokensCss returns the dark palette variables', () => {
|
it('designTokensCss returns the dark palette variables', () => {
|
||||||
@ -76,6 +77,25 @@ describe('appHeader (v2 shared header)', () => {
|
|||||||
expect(appHeaderOnlyCss()).toContain('.app-header');
|
expect(appHeaderOnlyCss()).toContain('.app-header');
|
||||||
expect(appHeaderCss()).toContain('.modal-backdrop');
|
expect(appHeaderCss()).toContain('.modal-backdrop');
|
||||||
});
|
});
|
||||||
|
it('shows version + build commit in the header so two machines can be compared', () => {
|
||||||
|
// On 2026-08-09 a fix shipped without a version bump: both hubs reported
|
||||||
|
// 0.10.2 while running different code, and the mismatch was invisible.
|
||||||
|
// The header therefore carries the commit next to the version.
|
||||||
|
const html = appHeader('proj', 'board');
|
||||||
|
expect(html).toContain(versionLabel());
|
||||||
|
expect(html).toContain(`v${VERSION}`);
|
||||||
|
expect(appHeaderOnlyCss()).toContain('.app-header .b2-build');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('versionLabel falls back to the bare version when the commit is unknown', () => {
|
||||||
|
// Non-git installs must still render a header — never an empty badge.
|
||||||
|
const label = versionLabel();
|
||||||
|
expect(label.startsWith(`v${VERSION}`)).toBe(true);
|
||||||
|
const ref = buildRef();
|
||||||
|
if (ref) expect(label).toBe(`v${VERSION} · ${ref}`);
|
||||||
|
else expect(label).toBe(`v${VERSION}`);
|
||||||
|
});
|
||||||
|
|
||||||
it('makes the mobile nav horizontally scrollable with 44px tap targets', () => {
|
it('makes the mobile nav horizontally scrollable with 44px tap targets', () => {
|
||||||
const css = appHeaderOnlyCss();
|
const css = appHeaderOnlyCss();
|
||||||
expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav \{[^}]*overflow-x: auto/);
|
expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav \{[^}]*overflow-x: auto/);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user