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",
|
||||
"version": "0.10.2",
|
||||
"version": "0.11.0",
|
||||
"description": "Local coordination layer for AI coding agents",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
||||
@ -2,7 +2,7 @@ import { listTasks } from './taskService.js';
|
||||
import { listMessages } from './messageService.js';
|
||||
import { listAsks } from './askService.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).
|
||||
@ -73,6 +73,8 @@ export interface AgentHealth {
|
||||
export interface HealthReport {
|
||||
status: 'ok';
|
||||
version: string;
|
||||
/** Short commit this hub runs from — '' for non-git installs. Compares machines. */
|
||||
buildRef: string;
|
||||
startedAt: string;
|
||||
uptimeSec: 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 {
|
||||
status: 'ok',
|
||||
version: VERSION,
|
||||
buildRef: buildRef(),
|
||||
startedAt: new Date(startedAtMs).toISOString(),
|
||||
uptimeSec: Math.max(0, Math.round((now - startedAtMs) / 1000)),
|
||||
counts: {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
*/
|
||||
|
||||
import { TaskStatus as TaskStatusSchema } from '../core/schema.js';
|
||||
import { versionLabel } from '../version.js';
|
||||
type TaskStatus = 'open' | 'in_progress' | 'review' | 'done' | 'cancelled';
|
||||
|
||||
/** CSS variables block matching the AgentHub dark design spec. */
|
||||
@ -272,6 +273,7 @@ export function pageHeader(projectName: string, current: 'board' | 'team' | 'act
|
||||
${mark}
|
||||
<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="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>
|
||||
<nav style="display:flex;gap:8px;align-items:center;">
|
||||
${navItem('Board', '/board', current === 'board')}
|
||||
@ -313,6 +315,13 @@ export function appHeaderOnlyCss(): string {
|
||||
border-bottom: 1px solid rgba(255,255,255,.06);
|
||||
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 b { font-weight: 700; letter-spacing: .01em; }
|
||||
.b2-logo-img { display: block; }
|
||||
@ -425,6 +434,7 @@ export function appHeader(projectName: string, current: HeaderPage): string {
|
||||
${link('Decisions', '/decisions', 'decisions')}
|
||||
</nav>
|
||||
<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>
|
||||
<span class="sse-status stale" id="sseStatus">
|
||||
<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). */
|
||||
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();
|
||||
expect(h.status).toBe('ok');
|
||||
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(h.uptimeSec).toBeGreaterThanOrEqual(0);
|
||||
expect(typeof h.startedAt).toBe('string');
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
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', () => {
|
||||
it('designTokensCss returns the dark palette variables', () => {
|
||||
@ -76,6 +77,25 @@ describe('appHeader (v2 shared header)', () => {
|
||||
expect(appHeaderOnlyCss()).toContain('.app-header');
|
||||
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', () => {
|
||||
const css = appHeaderOnlyCss();
|
||||
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