repositories / will
will
owned by admin
web/src/main.ts
Raw/**
* 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 $ = <T extends HTMLElement>(sel: string): T => {
const el = document.querySelector<T>(sel)
if (!el) throw new Error(`missing element ${sel}`)
return el
}
const text = (s: string): Text => document.createTextNode(s)
function el<K extends keyof HTMLElementTagNameMap>(
tag: K,
attrs: Record<string, string> = {},
...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<void> {
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<void> {
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<void> {
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<string>()
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<TabId, () => Promise<void>> = {
timeline: loadTimeline,
chat: loadChat,
media: loadMedia,
deployments: loadDeployments,
health: () => Promise.all([loadHealth(), loadUsage()]).then(() => undefined),
docs: loadDocs,
}
async function loadTab(id: TabId): Promise<void> {
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')