agenthub/src/server/board/sidebar.ts
chahinebrini 6d088648da feat(hub): Korrektheits- und Realtime-Härtung — Alias, Lifecycle, Presence, Architekten-Pulse (TSK-0242/0245/0249)
Drei vom Architekten abgenommene Tasks, gebündelt als Checkpoint:

- TSK-0242: Agent-Alias-Mapping (kimi-ah → kimi kanonisiert, Rollen → preferredAgent),
  reopenTask räumt claimedBy ab, fsWatch reindiziert direkte Datei-Edits,
  work-Default 300s → 50s, task_list mit Limit.
- TSK-0245: zwei Agent-Klassen (dispatch loop|architect). Watchdog mahnt
  architekt-getriebene Agenten nur noch EINMAL statt im Minutentakt;
  `task dispatch` startet sie explizit, `task record` trägt extern
  erledigte Arbeit mit origin=external nach.
- TSK-0249: Lifecycle wird serverseitig erzwungen (open→review scheitert mit
  klarer Meldung), claimedBy/doneBy überleben bis done, Presence pro Agent,
  Review-Watchdog, GET /architect/pulse (1.4 kB statt 34 kB, since-Cursor,
  omitted statt stillem Abschneiden), unbekannter Agent → 400 statt 500.

Alle Punkte live am laufenden Hub nachgemessen, nicht aus Agenten-Logs übernommen.
Tests: 242 → 279 grün, tsc sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-29 17:48:24 +02:00

100 lines
4.3 KiB
TypeScript

import { throughputSeries, areaPath, dayStart } from './viewmodel.js';
/**
* Sidebar skeleton: budget card + live feed card. The budget card has no tab
* bar of its own — the Token/Kosten/Verlauf tabs live inside the donut card
* (rendered client-side by the ported v1 budget JS in v1Budget.ts). The
* subhead keeps the v1 mount points: [data-budget-mode] (Session/Gesamt),
* #budgetTotal, #budgetReset; #budgetRows is where the donut card mounts.
*/
export function sidebarHtml(): string {
return `
<aside class="b2-side">
<div class="b2-panel b2-glass" id="b2Budget">
<div class="b2-subhead">
<h6>Budget</h6>
<span class="seg" aria-label="Token insight range">
<button type="button" data-budget-mode="session" class="active">Session</button>
<button type="button" data-budget-mode="total">Gesamt</button>
</span>
<span class="total" id="budgetTotal"></span>
<button type="button" class="reset-btn" id="budgetReset">Reset</button>
</div>
<div id="budgetRows"><div class="budget-empty">no agent activity yet</div></div>
</div>
<div class="b2-panel b2-glass" id="b2AgentsPanel">
<h6>Agents</h6>
<div class="b2-agents" id="b2Agents"><div class="budget-empty">no agents yet</div></div>
</div>
<div class="b2-panel b2-glass">
<h6>Live</h6>
<div class="b2-feed" id="b2Feed"></div>
</div>
</aside>`;
}
/**
* Inline script: throughput area chart renderer (used by the Verlauf tab in
* the donut card) + live feed, followed by the ported v1 budget JS.
*/
export function sidebarJs(v1BudgetJs: string): string {
return `
var DAY_MS = 24 * 3600 * 1000;
${dayStart.toString()}
${throughputSeries.toString()}
${areaPath.toString()}
window.__b2RenderThroughput = function (tasks) {
var box = document.getElementById('b2Throughput');
var tip = document.getElementById('b2ThroughputTip');
if (!box) return; // Verlauf tab not mounted right now — nothing to update
var s = throughputSeries(tasks, 14);
var p = areaPath(s, 220, 80);
box.innerHTML = '<svg width="100%" height="80" viewBox="0 0 220 80" preserveAspectRatio="none">'
+ '<path fill="rgba(56,189,248,.25)" d="' + p.area + '"/>'
+ '<path class="b2-spark" fill="none" stroke="#38bdf8" stroke-width="1.6" d="' + p.line + '"/></svg>';
if (tip) {
var avg = s.reduce(function (a, b) { return a + b; }, 0) / s.length;
tip.textContent = 'Erledigte Tasks · 14 Tage · Ø ' + avg.toFixed(1) + '/Tag';
}
};
window.__b2FeedPush = function (html) {
var feed = document.getElementById('b2Feed');
if (!feed) return;
var div = document.createElement('div');
div.innerHTML = html;
feed.insertBefore(div, feed.firstChild);
while (feed.children.length > 5) feed.removeChild(feed.lastChild);
};
// ── Agent health traffic lights (GET /health) ─────────────────────────────
// active (green) <2min since last action · busy (blue, on TSK-X) ·
// idle (gray) · stale/offline (red, >10min with an open assignment).
window.__b2RenderAgents = function (report) {
var box = document.getElementById('b2Agents');
if (!box) return;
var agents = report && report.agents ? report.agents : [];
if (!agents.length) { box.innerHTML = '<div class="budget-empty">no agents yet</div>'; return; }
box.innerHTML = agents.map(function (a) {
var loop = a.dispatch === 'architect'
? (a.taskId ? 'architect started' : a.waitingTaskId ? 'wartet auf Architekten-Start' : 'architect dispatch')
: a.inLoop
? 'loop ' + compact(a.loopSinceAgoSec || 0)
: a.loopExitReason
? 'out ' + compact(a.loopExitAgoSec || 0) + ' · ' + a.loopExitReason
: 'loop ?';
var work = a.taskId ? a.taskId : a.waitingTaskId ? a.waitingTaskId + ' waiting' : '';
var detail = loop + (work ? ' · ' + work : '');
return '<div class="b2-agent-row" title="' + esc(a.name) + ': ' + esc(detail) + '">' +
'<span class="b2-light st-' + esc(a.state) + '" aria-hidden="true"></span>' +
'<span class="b2-agent-name">' + esc(a.name) + '</span>' +
'<span class="b2-agent-state">' + esc(detail) + '</span></div>';
}).join('');
};
async function refreshAgentHealth() {
try { window.__b2RenderAgents(await getJSON('/health')); } catch (_) {}
}
window.refreshAgentHealth = refreshAgentHealth;
refreshAgentHealth();
setInterval(refreshAgentHealth, 5000);
${v1BudgetJs}`;
}