import { html, usePoll, useState, num, compact, ago } from './core.js'; import { Card, Stat, Pill, Skeleton, Empty } from './ui.js'; const PAGE = 50; function shortUrl(url) { try { return new URL(url).hostname.replace(/^www\./, ''); } catch (error) { return String(url || '').slice(0, 40); } } function statusTone(status) { if (status === 'ready') return 'ok'; if (status === 'failed') return 'bad'; if (status === 'pending') return 'warn'; return ''; } // One list component behind both Articles and Events. They differ only in columns // and endpoint, and having two near-identical files is how they drift apart. function ListView({ title, path, columns, searchable }) { const [offset, setOffset] = useState(0); const [query, setQuery] = useState(''); const [applied, setApplied] = useState(''); const url = `${path}?limit=${PAGE}&offset=${offset}${applied ? `&q=${encodeURIComponent(applied)}` : ''}`; const { data, error, loading } = usePoll(url, 20000); const rows = (data && data.rows) || []; const total = data && data.total; return html`
<${Card} title=${title} right=${html`${total === undefined ? '' : `${num(total)} total`}`}> ${searchable && html`
{ e.preventDefault(); setOffset(0); setApplied(query.trim()); }}> setQuery(e.target.value)} /> ${applied && html``}
`} ${error && html`
${error}
`} ${loading && !data ? html`<${Skeleton} rows=${6} />` : !rows.length ? html`<${Empty}>Nothing here.` : html`
${columns.map((c) => html` `)} ${rows.map((row) => html` ${columns.map((c) => html` `)} `)}
${c.label}
${c.render(row)}
`}
${rows.length ? `${num(offset + 1)}–${num(offset + rows.length)}` : '—'}
`; } export const Articles = () => html`<${ListView} title="Articles" path="/admin/api/articles" searchable columns=${[ { key: 'title', label: 'title', render: (r) => html` ${r.title || '(untitled)'}
${shortUrl(r.url)}
` }, { key: 'source', label: 'source', render: (r) => html`${r.source}` }, { key: 'status', label: 'content', render: (r) => html` <${Pill} tone=${statusTone(r.content_status)}>${r.content_status || 'unfetched'}` }, { key: 'pub', label: 'published', render: (r) => html` ${String(r.pub_date_effective || r.pub_date || '').slice(0, 16)}` }, ]} />`; export const Events = () => html`<${ListView} title="Events" path="/admin/api/events" columns=${[ { key: 'title', label: 'title', render: (r) => r.title || '(untitled)' }, { key: 'articles', label: 'articles', num: true, render: (r) => num(r.article_count) }, { key: 'created', label: 'created', render: (r) => html` ${String(r.created_at || '').slice(0, 16)}` }, ]} />`; export function Intelligence() { const summary = usePoll('/admin/api/stats/summary', 15000); const intel = usePoll('/admin/api/intelligence/stats', 15000); const s = summary.data || {}; const i = intel.data || {}; return html`
<${Stat} label="Articles" value=${compact(s.total)} sub=${`${compact(s.withContent)} with content · ${compact(s.withEmbedding)} embedded`} /> <${Stat} label="Events" value=${compact(s.eventCount)} sub="clustered" /> <${Stat} label="Knowledge rows" value=${compact(i.knowledge)} sub=${`${compact(i.companies)} companies tracked`} />
<${Card} title="Worker rates" right=${html`rows written`}> ${!(i.workerRates || []).length ? html`<${Empty}>No worker activity recorded.` : html` ${i.workerRates.map((w) => html` `)}
workerlast 1mlast 5m
${w.worker} ${num(w.n1m)} ${num(w.n5m)}
`} <${Card} title="Article queue"> ${!(i.queue || []).length ? html`<${Empty}>Queue is empty.` : html` ${i.queue.map((q) => html` `)}
statusn
<${Pill} tone=${statusTone(q.status)}>${q.status} ${num(q.n)}
`}
`; }