Luigit
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')