HTML 77.2%
TypeScript 10.5%
Python 9.6%
JavaScript 2.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { AdminTitle, JsonPre, Mono, StatusChip, Trunc } from '@/components/admin/ui';4import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';5import { LiveAgo } from '@/components/ui/live';6import { Stat, StatGrid } from '@/components/ui/section';7import { Unavailable } from '@/components/ui/unavailable';8import { adminApi, load, requireAdmin } from '@/lib/admin/admin-api';9import { fmtBytes, fmtDuration, fmtInt, num } from '@/lib/format';1011export const metadata: Metadata = { title: 'Overview', robots: { index: false, follow: false } };12export const dynamic = 'force-dynamic';1314export default async function AdminOverviewPage() {15 await requireAdmin();16 const res = await load(adminApi.overview());17 if (!res.ok) {18 return (19 <>20 <AdminTitle title="Overview" />21 <Unavailable what="Overview" reason={res.error} />22 </>23 );24 }25 const o = res.data;26 const s = o.stats ?? {};27 const hb = Object.entries(o.heartbeats ?? {});28 const queue = Object.entries(o.queue ?? {});29 const health = Object.entries(o.connectors ?? {});30 return (31 <>32 <AdminTitle title="Overview" lede={s.computed_at ? <>stats computed <LiveAgo at={String(s.computed_at)} /></> : undefined} />33 <StatGrid cols={4}>34 <Stat label="Entities" value={fmtInt(s.entities_total)} />35 <Stat label="Documents" value={fmtInt(s.documents)} hint={`${fmtInt(s.snapshots)} snapshots`} />36 <Stat label="Current claims" value={fmtInt(s.claims_current)} hint={`${fmtInt(s.claims)} total`} />37 <Stat label="Events · 24 h" value={fmtInt(s.change_events_24h)} hint={`${fmtInt(s.change_events)} total`} />38 <Stat label="Current prices" value={fmtInt(s.prices_current)} />39 <Stat label="Benchmark results" value={fmtInt(s.benchmark_results)} />40 <Stat label="Review pending" value={fmtInt(o.review_pending)} href="/admin/review" />41 <Stat label="Recent errors" value={fmtInt(o.recent_errors)} href="/admin/errors" />42 </StatGrid>4344 <div className="mt-8 grid gap-8 lg:grid-cols-3">45 <section>46 <p className="eyebrow mb-2">Queue depth</p>47 {queue.length === 0 ? (48 <p className="text-sm text-ink-3">Queue empty.</p>49 ) : (50 <DataTable compact stack={false} caption="Queue depth">51 <thead>52 <tr>53 <Th>Kind</Th>54 <Th>Status</Th>55 <Th num>n</Th>56 </tr>57 </thead>58 <tbody>59 {queue.flatMap(([kind, byStatus]) =>60 Object.entries(byStatus).map(([st, n]) => (61 <tr key={`${kind}-${st}`}>62 <Td><Mono>{kind}</Mono></Td>63 <Td><StatusChip value={st} /></Td>64 <Td num className="tnum">{fmtInt(n)}</Td>65 </tr>66 )),67 )}68 </tbody>69 </DataTable>70 )}71 <p className="mt-2 text-xs"><Link href="/admin/jobs" className="link">Jobs →</Link></p>72 </section>73 <section>74 <p className="eyebrow mb-2">Connector health</p>75 <ul className="divide-y divide-rule border-y border-rule text-sm">76 {health.map(([k, n]) => (77 <li key={k} className="flex items-center justify-between py-1.5">78 <StatusChip value={k} />79 <span className="tnum">{fmtInt(n)}</span>80 </li>81 ))}82 {health.length === 0 && <li className="py-2 text-ink-3">No connectors.</li>}83 </ul>84 <p className="mt-2 text-xs"><Link href="/admin/connectors" className="link">Connectors →</Link></p>85 {o.review_by_kind && (86 <>87 <p className="eyebrow mt-6 mb-2">Review by kind</p>88 <ul className="divide-y divide-rule border-y border-rule text-sm">89 {Object.entries(o.review_by_kind).map(([k, n]) => (90 <li key={k} className="flex items-center justify-between py-1.5">91 <Link href={`/admin/review?kind=${encodeURIComponent(k)}`} className="mono text-xs text-ink-2 hover:text-accent">{k}</Link>92 <span className="tnum">{fmtInt(n)}</span>93 </li>94 ))}95 </ul>96 </>97 )}98 </section>99 <section>100 <p className="eyebrow mb-2">Heartbeats</p>101 {hb.length === 0 ? <p className="text-sm text-ink-3">None reported (scheduler / workers have not written a heartbeat).</p> : <JsonPre value={o.heartbeats} maxHeight="14rem" />}102 {o.archive && (103 <>104 <p className="eyebrow mt-6 mb-2">Archive</p>105 <p className="tnum text-sm text-ink-2">106 raw {fmtBytes(o.archive.raw_bytes)} · {fmtInt(o.archive.raw_files)} files · text {fmtBytes(o.archive.text_bytes)} · {fmtInt(o.archive.text_files)} files107 </p>108 </>109 )}110 </section>111 </div>112113 <section className="mt-8">114 <p className="eyebrow mb-2">115 LLM factory · 24 h {o.llm?.available === false && <StatusChip value="unavailable" className="ml-2" />}116 </p>117 <p className="tnum text-sm text-ink-2">118 {fmtInt(o.llm?.jobs_24h)} jobs · {fmtInt(o.llm?.tokens_24h)} tokens{o.llm?.failed_24h !== undefined && <> · <span className={num(o.llm.failed_24h) ? 'text-danger' : ''}>{fmtInt(o.llm.failed_24h)} failed</span></>}119 </p>120 {o.llm?.by_stage?.length ? (121 <DataTable compact stack={false} caption="LLM jobs by stage" className="mt-2 max-w-md">122 <thead>123 <tr>124 <Th>Stage</Th>125 <Th>Status</Th>126 <Th num>n</Th>127 </tr>128 </thead>129 <tbody>130 {o.llm.by_stage.map((r, i) => (131 <tr key={i}>132 <Td><Mono>{r.stage}</Mono></Td>133 <Td><StatusChip value={r.status} /></Td>134 <Td num className="tnum">{fmtInt(r.n)}</Td>135 </tr>136 ))}137 </tbody>138 </DataTable>139 ) : (140 <p className="mt-2 text-xs text-ink-3">No LLM jobs in the last 24 h.</p>141 )}142 <p className="mt-2 text-xs"><Link href="/admin/llm-jobs" className="link">LLM accounting →</Link></p>143 </section>144145 <section className="mt-8">146 <p className="eyebrow mb-2">Recent runs</p>147 <DataTable compact scroll caption="Recent runs">148 <thead>149 <tr>150 <Th>Connector</Th>151 <Th>Started</Th>152 <Th>Status</Th>153 <Th num>Duration</Th>154 <Th num>Fetched</Th>155 <Th num>Changed</Th>156 <Th num>Failed</Th>157 <Th num>Claims</Th>158 <Th num>Events</Th>159 <Th>Error</Th>160 </tr>161 </thead>162 <tbody>163 {!o.recent_runs?.length && <EmptyRow cols={10}>No runs yet.</EmptyRow>}164 {o.recent_runs?.map((r) => (165 <tr key={r.id}>166 <Td><Link href={`/admin/runs?connector=${encodeURIComponent(r.connector_name)}`} className="mono text-xs text-ink hover:text-accent">{r.connector_name}</Link></Td>167 <Td className="text-xs"><LiveAgo at={r.started_at} /></Td>168 <Td><StatusChip value={r.status} /></Td>169 <Td num className="tnum text-xs">{num(r.duration_ms) === null ? '—' : fmtDuration(Math.round((num(r.duration_ms) ?? 0) / 1000))}</Td>170 <Td num className="tnum text-xs">{fmtInt(r.docs_fetched)}</Td>171 <Td num className="tnum text-xs">{fmtInt(r.docs_changed)}</Td>172 <Td num className={`tnum text-xs ${num(r.docs_failed) ? 'text-danger' : ''}`}>{fmtInt(r.docs_failed)}</Td>173 <Td num className="tnum text-xs">{fmtInt(r.claims_written)}</Td>174 <Td num className="tnum text-xs">{fmtInt(r.events_emitted)}</Td>175 <Td className="text-xs text-ink-2"><Trunc text={r.error} max={60} /></Td>176 </tr>177 ))}178 </tbody>179 </DataTable>180 <p className="mt-2 text-xs"><Link href="/admin/runs" className="link">All runs →</Link></p>181 </section>182 </>183 );184}185