agenthub/src/server/decisions.ts
2026-07-01 01:52:32 +02:00

108 lines
4.3 KiB
TypeScript

import { loadConfig } from '../core/config.js';
import { getDecision, listDecisions } from '../core/services/decisionService.js';
import { designTokensCss, escapeHtml } from './ui-shared.js';
import type { Decision } from '../core/schema.js';
function ago(iso: string): string {
const t = Date.parse(iso);
if (Number.isNaN(t)) return '';
const s = Math.max(0, Math.floor((Date.now() - t) / 1000));
if (s < 60) return `${s}s ago`;
const m = Math.floor(s / 60);
if (m < 60) return `${m}m ago`;
const h = Math.floor(m / 60);
if (h < 24) return `${h}h ago`;
return `${Math.floor(h / 24)}d ago`;
}
export function renderDecisionsHtml(cwd: string): string {
const config = loadConfig(cwd);
const decisions: Decision[] = listDecisions(cwd).map((entry) => {
try {
return getDecision(cwd, entry.id).decision;
} catch {
return {
id: entry.id,
title: entry.title,
status: 'accepted',
context: '',
decision: entry.content,
consequences: [],
alternatives: [],
relatedDecisions: [],
createdAt: entry.createdAt,
updatedAt: entry.updatedAt,
};
}
});
const rows = decisions.length
? decisions
.map(
(d) => `<article class="decision" id="${escapeHtml(d.id)}">
<div class="decision-top">
<span class="id">${escapeHtml(d.id)}</span>
<span class="status">${escapeHtml(d.status)}</span>
<span class="when">${escapeHtml(ago(d.updatedAt || d.createdAt))}</span>
</div>
<h2>${escapeHtml(d.title)}</h2>
<p>${escapeHtml(d.decision)}</p>
${
d.consequences?.length
? `<div class="meta"><strong>Consequences</strong>${d.consequences.map((c) => `<span>${escapeHtml(c)}</span>`).join('')}</div>`
: ''
}
</article>`,
)
.join('')
: '<div class="empty">No decisions yet.</div>';
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<title>AgentHub Decisions</title>
<style>
${designTokensCss()}
body { padding: 0 20px 32px; }
.app-header { position:sticky;top:0;z-index:10;display:flex;align-items:center;gap:12px;margin:0 -20px 18px;padding:14px 20px;border-bottom:1px solid var(--border);background:rgba(15,23,42,.96); }
.brand { font-weight:700;font-size:17px; }
.project { color:var(--muted);font:12px/1.4 var(--font-mono);overflow:hidden;text-overflow:ellipsis;white-space:nowrap; }
.spacer { flex:1; }
.nav { display:flex;gap:4px;border:1px solid var(--border);background:var(--surface);padding:3px;border-radius:8px; }
.nav a { color:var(--muted);text-decoration:none;padding:7px 10px;border-radius:6px;font-size:13px; }
.nav a.active,.nav a:hover { color:var(--text);background:var(--raised); }
main { max-width:980px;margin:0 auto;display:grid;gap:10px; }
.decision { background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:14px; }
.decision-top { display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px; }
.id,.when { color:var(--muted);font:12px/1.4 var(--font-mono); }
.status { color:var(--status-done);border:1px solid var(--status-done);border-radius:999px;padding:1px 8px;font:11px/1.4 var(--font-mono); }
h1 { font-size:18px;margin:0 0 10px; }
h2 { font-size:15px;margin:0 0 8px; }
p { margin:0;color:var(--text);overflow-wrap:anywhere; }
.meta { margin-top:10px;display:grid;gap:5px;color:var(--muted);font-size:12px; }
.empty { color:var(--muted);background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:14px; }
@media (max-width:640px){ .app-header{align-items:flex-start;flex-wrap:wrap}.spacer{display:none}.nav{width:100%}.nav a{flex:1;text-align:center}.project{width:100%} }
</style>
</head>
<body>
<header class="app-header">
<span class="brand">AgentHub</span>
<span class="project">${escapeHtml(config.projectName)}</span>
<span class="spacer"></span>
<nav class="nav" aria-label="Primary">
<a href="/board">Board</a>
<a href="/team">Team</a>
<a href="/activity">Activity</a>
<a class="active" href="/decisions">Decisions</a>
</nav>
</header>
<main>
<h1>Decision Log</h1>
${rows}
</main>
</body>
</html>`;
}