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%
2.7 KB · 64 lines tsx
Raw Blame History
1import { traceValue } from '@cancerindex/ranking';2import { Section } from '@/components/ui/section';3import { JsonView } from '@/components/ui/json-view';4import { EmptyState } from '@/components/ui/empty-state';5import { db } from '@/lib/db';6import { str, type SP } from '@/lib/search-params';78const TABLES = ['rankings', 'epidemiology_observations', 'survival_observations', 'cancer_gene_frequencies', 'literature_counts'];910/** Lineage trace tool (§252-253): value → inputs → observation → provenance → raw record. */11export default async function TracePage({ searchParams }: { searchParams: Promise<SP> }) {12  const sp = await searchParams;13  const table = str(sp, 'table');14  const id = str(sp, 'id');15  let result: Record<string, unknown> | null = null;16  let error: string | null = null;17  if (table && id) {18    if (!TABLES.includes(table)) error = 'unsupported table';19    else if (!/^\d{1,12}$/.test(id)) error = 'id must be a numeric row id';20    else {21      try {22        result = await traceValue(db(), table, id);23      } catch (e) {24        error = (e as Error).message;25      }26    }27  }28  return (29    <div className="space-y-6">30      <header className="pt-4">31        <h1 className="text-3xl">Trace</h1>32        <p className="mt-1 text-[13.5px] text-ink-2">Follow any derived or observed value back to its provenance row and raw source record.</p>33      </header>34      <form method="get" action="/admin/trace" className="flex flex-wrap items-end gap-2 text-[13.5px]">35        <label className="flex flex-col gap-1">36          <span className="ci-kicker">Table</span>37          <select name="table" defaultValue={table || 'rankings'} className="border border-rule-strong bg-white px-2 py-1.5">38            {TABLES.map((t) => (39              <option key={t} value={t}>40                {t}41              </option>42            ))}43          </select>44        </label>45        <label className="flex flex-col gap-1">46          <span className="ci-kicker">Row id</span>47          <input name="id" defaultValue={id} className="ci-mono w-40 border border-rule-strong bg-white px-2 py-1.5" placeholder="123" />48        </label>49        <button type="submit" className="border border-ink bg-ink px-3 py-1.5 text-paper hover:bg-ink-2">50          Trace51        </button>52      </form>53      {error ? <p className="text-[13px] text-danger">{error}</p> : null}54      {result ? (55        <Section id="result" kicker={`${table} / ${id}`} title="Lineage">56          {'error' in result ? <EmptyState compact title={String(result.error)} /> : <JsonView data={result} />}57        </Section>58      ) : !table ? (59        <EmptyState compact title="Enter a table and row id">Ranking rows link here from every "Why this rank?" panel.</EmptyState>60      ) : null}61    </div>62  );63}64