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`
`}
${error && html`
${error}
`}
${loading && !data
? html`<${Skeleton} rows=${6} />`
: !rows.length
? html`<${Empty}>Nothing here./>`
: html`
`}
${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`
| worker | last 1m | last 5m |
${i.workerRates.map((w) => html`
| ${w.worker} |
${num(w.n1m)} |
${num(w.n5m)} |
`)}
`}
/>
<${Card} title="Article queue">
${!(i.queue || []).length
? html`<${Empty}>Queue is empty./>`
: html`
| status | n |
${i.queue.map((q) => html`
| <${Pill} tone=${statusTone(q.status)}>${q.status}/> |
${num(q.n)} |
`)}
`}
/>
`;
}