/** * will phenotype frontend: handmade, no framework (spec AC21). * Read-only views over /api/*; SSE timeline with durable-id resume. */ interface TimelineRow { seq?: number id: string ts: string kind: string payload: unknown archived: boolean } interface ChatRow { id: string ts: string senderName?: string direction: 'in' | 'out' text?: string media?: { digest: string; type: string }[] hidden: boolean } interface DeployRow { id: string ts: string kind: string revision?: string } interface StatusRow { storageState: 'ok' | 'warn' | 'stop' storage: { freePct: number } counts: { audit: number; chat: number; deployments: number; media: number } dbSizeBytes: number schemaVersion: number } const $ = (sel: string): T => { const el = document.querySelector(sel) if (!el) throw new Error(`missing element ${sel}`) return el } const text = (s: string): Text => document.createTextNode(s) function el( tag: K, attrs: Record = {}, ...children: Node[] ): HTMLElementTagNameMap[K] { const node = document.createElement(tag) for (const [k, v] of Object.entries(attrs)) { if (k === 'class') node.className = v else node.setAttribute(k, v) } for (const c of children) node.append(c) return node } /* ---------- tabs ---------- */ const tabs = ['timeline', 'chat', 'media', 'deployments', 'health', 'docs'] as const type TabId = (typeof tabs)[number] function activateTab(id: TabId): void { for (const t of tabs) { $(`#tab-${t}`).setAttribute('aria-selected', String(t === id)) const panel = $(`#${t}`) panel.hidden = t !== id } history.replaceState(null, '', `#${id}`) void loadTab(id) } for (const t of tabs) { $(`#tab-${t}`).addEventListener('click', () => activateTab(t)) $(`#tab-${t}`).addEventListener('keydown', (e) => { const idx = tabs.indexOf(t) const dir = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0 if (dir === 0) return e.preventDefault() const next = tabs[(idx + dir + tabs.length) % tabs.length] as TabId $(`#tab-${next}`).focus() activateTab(next) }) } /* ---------- timeline (SSE with durable resume) ---------- */ let lastEventId = 0 let stream: EventSource | undefined function renderTimelineRow(row: TimelineRow, fresh: boolean): HTMLElement { const time = el('time', { class: 'archived' }, text(row.ts)) time.dateTime = row.ts if (!row.archived) time.className = '' const body = el('div', {}) body.append(el('span', { class: 'kind' }, text(row.kind))) body.append( el( 'pre', {}, text(typeof row.payload === 'string' ? row.payload : JSON.stringify(row.payload, null, 1)), ), ) const li = el('li', { class: fresh ? 'event new' : 'event' }, time, body) return li } function connectStream(): void { const state = $('#stream-state') stream?.close() const url = lastEventId > 0 ? `/api/stream?lastEventId=${lastEventId}` : '/api/stream' stream = new EventSource(url) stream.addEventListener('open', () => { state.textContent = 'live' state.className = 'live' }) stream.addEventListener('error', () => { state.textContent = 'reconnecting…' state.className = 'down' }) stream.addEventListener('audit', (ev: MessageEvent) => { const row = JSON.parse(ev.data) as TimelineRow if (row.seq !== undefined && row.seq <= lastEventId) return if (row.seq !== undefined) lastEventId = row.seq const list = $('#timeline-list') const node = renderTimelineRow(row, true) list.prepend(node) while (list.children.length > 200) list.lastElementChild?.remove() }) } /* ---------- panels ---------- */ async function loadTimeline(): Promise { const res = await fetch('/api/timeline?limit=50') const page = (await res.json()) as { rows: TimelineRow[] } const list = $('#timeline-list') list.replaceChildren() for (const row of page.rows) list.append(renderTimelineRow(row, false)) const last = page.rows[0]?.seq if (last !== undefined && last > lastEventId) lastEventId = last connectStream() } async function loadChat(): Promise { const res = await fetch('/api/chat?limit=50') const page = (await res.json()) as { rows: ChatRow[] } const list = $('#chat-list') list.replaceChildren() for (const m of page.rows) { const li = el('li', { class: `chat-msg ${m.direction}` }) li.append(el('div', { class: 'meta' }, text(`${m.ts} · ${m.senderName ?? 'unknown'}`))) if (m.hidden) li.append(el('div', { class: 'hidden-note' }, text('hidden by request'))) else { if (m.text) li.append(text(m.text)) for (const media of m.media ?? []) { if (media.type.startsWith('image/')) { const img = el('img', { src: `/api/media/${media.digest}`, alt: `media ${media.digest.slice(0, 8)}`, }) li.append(img) } } } list.append(li) } } async function loadMedia(): Promise { const res = await fetch('/api/chat?limit=100') const page = (await res.json()) as { rows: ChatRow[] } const list = $('#media-list') list.replaceChildren() const seen = new Set() for (const m of page.rows) { for (const media of m.media ?? []) { if (seen.has(media.digest) || m.hidden) continue seen.add(media.digest) const li = el('li', {}) if (media.type.startsWith('image/')) { li.append( el('img', { src: `/api/media/${media.digest}`, alt: `media ${media.digest.slice(0, 8)}`, }), ) } else { li.append(el('a', {}, text(`${media.digest.slice(0, 12)}…`))) const a = li.querySelector('a') if (a) a.href = `/api/media/${media.digest}` } list.append(li) } } } async function loadDeployments(): Promise { const res = await fetch('/api/deployments?limit=50') const page = (await res.json()) as { rows: DeployRow[] } const list = $('#deployment-list') list.replaceChildren() for (const d of page.rows) { const li = el('li', { class: 'event' }) li.append(el('time', {}, text(d.ts))) li.append( el('div', {}, el('span', { class: 'kind' }, text(d.kind)), text(` ${d.revision ?? ''}`)), ) list.append(li) } } const fmtUsd = (usd: number | null): string => usd === null ? 'n/a' : usd < 0.01 && usd > 0 ? `<$0.01` : `$${usd.toFixed(2)}` const fmtTok = (n: number): string => n >= 1e6 ? `${(n / 1e6).toFixed(1)}M` : n >= 1e3 ? `${(n / 1e3).toFixed(1)}k` : String(n) interface UsageReportRow { days: { date: string input: number output: number cacheRead: number cacheWrite: number responses: number costUsd: number | null }[] totals: { input: number output: number cacheRead: number cacheWrite: number responses: number costUsd: number | null } projection: { todayUsd: number | null; mtdUsd: number | null; projectedEomUsd: number | null } } async function loadUsage(): Promise { const res = await fetch('/api/usage') const report = (await res.json()) as UsageReportRow const body = $('#usage-days') const foot = $('#usage-totals') body.replaceChildren() const row = (cells: string[]): HTMLTableRowElement => { const tr = document.createElement('tr') for (const c of cells) tr.append(el('td', {}, text(c))) return tr } for (const d of [...report.days].reverse()) { body.append( row([ d.date, fmtTok(d.input), fmtTok(d.output), fmtTok(d.cacheRead), fmtTok(d.cacheWrite), String(d.responses), fmtUsd(d.costUsd), ]), ) } foot.replaceChildren( row([ 'total', fmtTok(report.totals.input), fmtTok(report.totals.output), fmtTok(report.totals.cacheRead), fmtTok(report.totals.cacheWrite), String(report.totals.responses), fmtUsd(report.totals.costUsd), ]), ) const p = report.projection $('#usage-projection').replaceChildren( text( `today ${fmtUsd(p.todayUsd)} · month-to-date ${fmtUsd(p.mtdUsd)} · projected end of month ${fmtUsd(p.projectedEomUsd)}`, ), ) } async function loadHealth(): Promise { const res = await fetch('/api/status') const s = (await res.json()) as StatusRow const dl = $('#health-facts') dl.replaceChildren( el('dt', {}, text('storage')), el( 'dd', { class: s.storageState === 'stop' ? 'stop' : '' }, text(`${s.storage.freePct}% free (${s.storageState})`), ), el('dt', {}, text('db')), el('dd', {}, text(`${(s.dbSizeBytes / 1_048_576).toFixed(1)} MiB`)), el('dt', {}, text('schema')), el('dd', {}, text(String(s.schemaVersion))), el('dt', {}, text('counts')), el('dd', {}, text(JSON.stringify(s.counts))), ) } async function loadDocs(): Promise { const list = $('#docs-list') if (list.children.length === 0) { const res = await fetch('/api/docs') const manifest = (await res.json()) as { pages: string[] } for (const p of manifest.pages) { const a = el('a', {}, text(p)) a.href = `/docs/${p}` a.addEventListener('click', (e) => { e.preventDefault() void showDoc(p) }) list.append(el('li', {}, a)) } } const first = list.querySelector('a') if (first) void showDoc(first.textContent ?? '') } async function showDoc(page: string): Promise { const res = await fetch(`/docs/${encodeURIComponent(page)}`) const html = await res.text() $('#docs-content').replaceChildren() const parsed = new DOMParser().parseFromString(html, 'text/html') const article = parsed.querySelector('article') if (article) $('#docs-content').append(...Array.from(article.childNodes)) } const loaders: Record Promise> = { timeline: loadTimeline, chat: loadChat, media: loadMedia, deployments: loadDeployments, health: () => Promise.all([loadHealth(), loadUsage()]).then(() => undefined), docs: loadDocs, } async function loadTab(id: TabId): Promise { try { await loaders[id]() } catch (err) { console.error('panel load failed', err) } } const initial = (location.hash.slice(1) as TabId) || 'timeline' activateTab(tabs.includes(initial) ? initial : 'timeline')