spb/cancerindex
Public
TypeScript 97.2%
SQL 1.5%
CSS 0.6%
JavaScript 0.5%
1import type { Metadata } from 'next';2import Link from 'next/link';3import { PageHeader, Section, Note } from '@/components/ui/section';4import { EmptyState } from '@/components/ui/empty-state';5import { Badge, StatusBadge } from '@/components/ui/badge';6import { connectorStates, recentIngests } from '@/lib/queries/pulse';7import { fmtDate, fmtDateTime, fmtDuration, fmtInt, humanize, relativeTime } from '@/lib/format';89export const metadata: Metadata = { title: 'Data update log', description: 'Every connector run of the last 60 days with records created, updated and rejected, dataset versions, and the health and last successful import of each source.' };10export const revalidate = 600;1112/** Stale = an active scheduled connector without a success in the last 14 days (the worker uses 2× its interval; 14 days is the display rule here). */13const STALE_DAYS = 14;1415export default async function DataUpdatesPage() {16 const [runs, states] = await Promise.all([recentIngests(60, 400), connectorStates()]);17 const byDay = new Map<string, typeof runs>();18 for (const r of runs) {19 const d = String(r.started_at).slice(0, 10);20 byDay.set(d, [...(byDay.get(d) ?? []), r]);21 }22 const now = Date.now();23 return (24 <div className="pb-8">25 <PageHeader kicker="Data" title="Data update log" lede="Transparency on freshness (SPEC §61-62): each source is versioned and every ingestion is a run with counters; historical values are never silently replaced. Runs marked partial stopped on their time budget and resume from a cursor." />2627 <Section id="sources" kicker="Sources" title="Connector state" description="Health from the hourly probe, last successful import, schedule (UTC cron) and the dataset version the source reported last. Sources under license review or awaiting credentials never run.">28 <div className="ci-table-wrap">29 <table className="ci-table">30 <thead>31 <tr>32 <th>Source</th>33 <th>Category</th>34 <th>Status</th>35 <th>Health</th>36 <th>Last successful import</th>37 <th>Schedule</th>38 <th>Dataset version</th>39 <th className="num">Source records</th>40 </tr>41 </thead>42 <tbody>43 {states.map((s) => {44 const stale = s.status === 'active' && !!s.schedule && (!s.last_success_at || now - new Date(s.last_success_at).getTime() > STALE_DAYS * 86_400_000);45 return (46 <tr key={s.connector_id}>47 <td>48 <Link href={`/source/${s.connector_id}`} className="ci-link">49 {s.source_name ?? s.connector_id}50 </Link>51 </td>52 <td className="text-[12.5px]">{humanize(s.category)}</td>53 <td>54 <StatusBadge status={s.status} />55 {s.paused ? <Badge tone="warn" className="ml-1">paused</Badge> : null}56 </td>57 <td>58 <StatusBadge status={s.health} />59 {stale ? (60 <Badge tone="warn" className="ml-1" title={`No successful run in ${STALE_DAYS} days`}>61 stale62 </Badge>63 ) : null}64 </td>65 <td className="whitespace-nowrap text-[12.5px]" title={s.last_success_at ? fmtDateTime(s.last_success_at) : ''}>66 {s.last_success_at ? `${fmtDate(s.last_success_at)} (${relativeTime(s.last_success_at)})` : <span className="text-ink-4">never</span>}67 </td>68 <td className="ci-mono text-[11.5px]">{s.schedule ?? '—'}</td>69 <td className="ci-mono text-[11.5px]">{s.last_dataset_version ?? '—'}</td>70 <td className="num">{fmtInt(s.record_count)}</td>71 </tr>72 );73 })}74 </tbody>75 </table>76 </div>77 </Section>7879 <Section id="runs" kicker="Runs" title={`Ingestion runs, last 60 days (${fmtInt(runs.length)})`} description="Most recent first, grouped by day. Created / updated / unchanged / rejected are source records; the anomaly column shows refused destructive updates.">80 {runs.length ? (81 <div className="space-y-6">82 {[...byDay.entries()].map(([day, rows]) => (83 <div key={day}>84 <h3 className="mb-1 text-[15px]">{fmtDate(day)}</h3>85 <div className="ci-table-wrap">86 <table className="ci-table">87 <thead>88 <tr>89 <th>Started</th>90 <th>Source</th>91 <th>Mode</th>92 <th>Status</th>93 <th className="num">Fetched</th>94 <th className="num">Created</th>95 <th className="num">Updated</th>96 <th className="num">Unchanged</th>97 <th className="num">Rejected</th>98 <th>Duration</th>99 <th>Dataset version</th>100 <th>Anomaly</th>101 </tr>102 </thead>103 <tbody>104 {rows.map((r) => (105 <tr key={r.id}>106 <td className="whitespace-nowrap text-[12px]">{fmtDateTime(r.started_at)}</td>107 <td>108 <Link href={`/source/${r.connector_id}`} className="ci-link">109 {r.source_name ?? r.connector_id}110 </Link>111 </td>112 <td className="text-[12px]">{r.mode}</td>113 <td>114 <StatusBadge status={r.status} />115 </td>116 <td className="num">{fmtInt(r.records_fetched)}</td>117 <td className="num">{fmtInt(r.records_created)}</td>118 <td className="num">{fmtInt(r.records_updated)}</td>119 <td className="num text-ink-3">{fmtInt(r.records_unchanged)}</td>120 <td className={`num ${r.records_rejected > 0 ? 'text-warn' : ''}`}>{fmtInt(r.records_rejected)}</td>121 <td className="whitespace-nowrap text-[12px]">{fmtDuration(r.duration_ms)}</td>122 <td className="ci-mono text-[11.5px]">{r.dataset_version ?? '—'}</td>123 <td className="text-[12px] text-danger">{r.anomaly ?? ''}</td>124 </tr>125 ))}126 </tbody>127 </table>128 </div>129 </div>130 ))}131 </div>132 ) : (133 <EmptyState title="No ingestion run in the last 60 days" />134 )}135 </Section>136 <div className="mt-6">137 <Note>Run identifiers are `ING-<CONNECTOR>-YYYYMMDD-nnnnnn`; operators can open a run on the admin console. Derived layers (counters, intelligence, rankings) are recomputed after every run that changed records, then daily at 06:00, 06:15 and 06:30 UTC.</Note>138 </div>139 </div>140 );141}142