spb/countryatlas
Public
TypeScript 57%
Python 38.6%
JavaScript 3.6%
CSS 0.6%
1import Link from 'next/link';2import { t } from '@/i18n';3import { adminApi, adminErrorMessage } from '@/lib/admin-api';4import { ApiError } from '@/lib/api';5import { grouped } from '@/lib/format';6import { routes } from '@/lib/site';7import type { AdminOverview } from '@/lib/types-explore';8import { AdminSection, AdminTable, Kv, Pill, bytes, statusTone, ts } from '@/components/admin/admin-ui';9import { clearCacheAction, refreshAction } from '../actions';1011export const dynamic = 'force-dynamic';1213function hb(o: AdminOverview, key: string): string | null {14 const h = o.scheduler.heartbeat;15 if (!h) return null;16 const v = h[key];17 return v == null ? null : String(v);18}1920export default async function AdminOverviewPage({ searchParams }: { searchParams: Promise<{ ok?: string; err?: string }> }) {21 const sp = await searchParams;22 let o: AdminOverview | null = null;23 let error: string | null = null;24 try {25 o = await adminApi.overview();26 } catch (e) {27 error = e instanceof ApiError ? t('admin.unavailable', { status: e.status || 'network' }) + ` ${adminErrorMessage(e)}` : adminErrorMessage(e);28 }2930 const actions = (31 <>32 <form action={refreshAction}>33 <button type="submit" className="inline-flex min-h-[36px] items-center rounded-sm bg-ink px-3 text-xs font-medium text-paper hover:bg-accent hover:text-accent-ink" title={t('admin.actions.refreshHint')}>34 {t('admin.actions.refresh')}35 </button>36 </form>37 <form action={clearCacheAction}>38 <button type="submit" className="inline-flex min-h-[36px] items-center rounded-sm border border-rule px-3 text-xs text-ink hover:bg-surface-2">39 {t('admin.actions.clearCache')}40 </button>41 </form>42 </>43 );4445 return (46 <>47 {sp.ok ? <p className="mt-3 rounded-sm border border-up/40 px-3 py-2 text-xs text-up">{t('admin.actions.done', { msg: sp.ok })}</p> : null}48 {sp.err ? <p className="mt-3 rounded-sm border border-down/40 px-3 py-2 text-xs text-down">{t('admin.actions.failed', { msg: sp.err })}</p> : null}49 {error ? <p className="mt-3 rounded-sm border border-down/40 px-3 py-2 text-xs text-down">{error}</p> : null}5051 {o ? (52 <>53 <AdminSection title={t('admin.overview.title')} actions={actions}>54 <div className="grid gap-6 md:grid-cols-3">55 <div>56 <h3 className="mb-1 text-xs font-semibold text-ink-2">{t('admin.snapshot')}</h3>57 <Kv58 rows={[59 ['status', <Pill key="s" tone={o.status === 'ok' ? 'ok' : 'warn'}>{o.status}</Pill>],60 ['run_id', o.meta?.run_id ?? null],61 ['built_at', ts(o.meta?.built_at ?? null)],62 ['schema', o.meta?.schema_version != null ? String(o.meta.schema_version) : null],63 ['build_duration_s', o.meta?.build_duration_s != null ? String(o.meta.build_duration_s) : null],64 ]}65 />66 </div>67 <div>68 <h3 className="mb-1 text-xs font-semibold text-ink-2">{t('admin.db')}</h3>69 <Kv rows={[['path', o.db.path], [t('admin.db.size'), o.db.exists ? bytes(o.db.size_bytes) : t('admin.db.missing')], ['data_dir', o.settings.data_dir], ['api', `${o.settings.api_host}:${o.settings.api_port}`]]} />70 </div>71 <div>72 <h3 className="mb-1 text-xs font-semibold text-ink-2">{t('admin.scheduler')}</h3>73 <Kv74 rows={[75 ['state', o.scheduler.pid == null ? t('admin.scheduler.unknown') : o.scheduler.alive ? <Pill key="a" tone="ok">{t('admin.scheduler.alive', { pid: o.scheduler.pid })}</Pill> : <Pill key="d" tone="bad">{t('admin.scheduler.dead', { pid: o.scheduler.pid })}</Pill>],76 [t('admin.scheduler.last'), ts(hb(o, 'last_run_at') ?? hb(o, 'last_run') ?? hb(o, 'finished_at'))],77 [t('admin.scheduler.next'), ts(hb(o, 'next_run_at') ?? hb(o, 'next_run'))],78 ['heartbeat', ts(hb(o, 'heartbeat_at') ?? hb(o, 'updated_at') ?? hb(o, 'at'))],79 ['refresh', t('admin.scheduler.refreshAt', { time: o.settings.refresh_at })],80 ]}81 />82 </div>83 </div>84 </AdminSection>8586 <div className="grid gap-x-8 md:grid-cols-3">87 <AdminSection title={t('admin.counts')}>88 <Kv rows={Object.entries(o.counts ?? {}).map(([k, v]) => [k, grouped(v)] as [string, string])} />89 </AdminSection>90 <AdminSection title={t('admin.statuses')}>91 <Kv rows={Object.entries(o.observation_statuses ?? {}).map(([k, v]) => [k, grouped(v)] as [string, string])} />92 </AdminSection>93 <AdminSection title={t('admin.cache')}>94 <Kv rows={Object.entries(o.cache ?? {}).map(([k, v]) => [k, String(v)] as [string, string])} />95 </AdminSection>96 </div>9798 <AdminSection title={t('admin.connectors.title')} sub={t('admin.connectors.sub')}>99 <AdminTable100 rows={o.connectors ?? []}101 rowKey={(r) => r.connector}102 columns={[103 { key: 'c', header: t('admin.connectors.connector'), cell: (r) => <Link href={`${routes.admin('runs')}?connector=${r.connector}`} className="font-medium text-ink hover:text-accent">{r.connector}</Link> },104 { key: 'n', header: t('admin.connectors.runs'), numeric: true, cell: (r) => grouped(r.n_runs) },105 { key: 'last', header: t('admin.connectors.last'), cell: (r) => ts(r.last_finished_at) },106 { key: 's', header: t('admin.connectors.status'), cell: (r) => <Pill tone={statusTone(r.last_status)}>{r.last_status ?? t('admin.na')}</Pill> },107 { key: 'ok', header: t('admin.connectors.ok'), numeric: true, cell: (r) => grouped(r.n_ok) },108 { key: 'f', header: t('admin.connectors.failed'), numeric: true, cell: (r) => (r.n_failed ? <span className="text-down">{grouped(r.n_failed)}</span> : '0') },109 { key: 'rows', header: t('admin.connectors.rows'), numeric: true, cell: (r) => (r.rows_valid != null ? grouped(r.rows_valid) : t('admin.na')) },110 { key: 'w', header: t('admin.connectors.warnings'), numeric: true, cell: (r) => (r.warnings != null ? grouped(r.warnings) : t('admin.na')) },111 { key: 'e', header: t('admin.connectors.errors'), numeric: true, cell: (r) => (r.errors ? <span className="text-down">{grouped(r.errors)}</span> : '0') },112 ]}113 />114 </AdminSection>115116 <AdminSection title={t('admin.stale.title')} sub={t('admin.stale.sub')}>117 <AdminTable118 rows={o.sources ?? []}119 rowKey={(r) => r.source_id}120 columns={[121 { key: 's', header: t('admin.stale.source'), cell: (r) => <Link href={routes.source(r.source_id)} className="font-medium text-ink hover:text-accent">{r.source_id}</Link> },122 { key: 'n', header: t('admin.stale.observations'), numeric: true, cell: (r) => grouped(r.n_observations) },123 { key: 'u', header: t('admin.stale.updated'), cell: (r) => ts(r.source_updated_at) },124 { key: 'r', header: t('admin.stale.retrieved'), cell: (r) => ts(r.retrieved_at) },125 { key: 'st', header: t('admin.stale.n'), numeric: true, cell: (r) => (r.n_stale ? <span className="text-warn">{grouped(r.n_stale)}</span> : '0') },126 ]}127 />128 </AdminSection>129 </>130 ) : !error ? (131 <p className="py-6 text-xs text-ink-3">{t('admin.empty')}</p>132 ) : null}133 </>134 );135}136