108 lines
4.3 KiB
TypeScript
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>`;
|
|
}
|