SPB Git forge

spb/cancerindex

Public
37commits 1branches 0releases
2.9 MBsize
maindefault branch
10 days agolast push
TypeScript 97.2% SQL 1.5% CSS 0.6% JavaScript 0.5%
7.3 KB · 142 lines tsx
Raw Blame History
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-&lt;CONNECTOR&gt;-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