import type { SourceKind } from "@dci/core"; import { SOURCE_KINDS } from "@dci/core"; import type { Metadata } from "next"; import { Breadcrumbs } from "@/components/entity/breadcrumbs"; import { ExtLink } from "@/components/entity/kv"; import { SectionNav } from "@/components/entity/section-nav"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Page, PageHeader, Section } from "@/components/ui/card"; import { FilterChips, type Chip } from "@/components/ui/filter-chips"; import { Metric, MetricCell, MetricRow } from "@/components/ui/metric"; import { EmptyState, OfflineNotice } from "@/components/ui/states"; import { RowLink, TBody, THead, Table, Td, Th, Tr } from "@/components/ui/table"; import { api } from "@/lib/api"; import type { SourceWithCounts } from "@/lib/dto"; import { fmtDate, fmtInt } from "@/lib/format"; import { SOURCE_KIND_LABEL } from "@/lib/labels"; import { routes } from "@/lib/routes"; import { pageMetadata } from "@/lib/seo"; import { compareBy, hrefWith, parseSort, sortTh, spGet, type SearchParams } from "@/lib/sort"; export const revalidate = 120; export const metadata: Metadata = pageMetadata({ title: "Sources", description: "Every source the index reads, with its licence, attribution and how much of the graph it backs. Facts only — each value links back to the page it was observed on.", path: routes.sources(), }); const SORT_KEYS = ["name", "kind", "documents", "provenance", "facilities", "updated"] as const; type SortKey = (typeof SORT_KEYS)[number]; const KIND_SET = new Set(SOURCE_KINDS); function getter(key: SortKey): (s: SourceWithCounts) => number | string | null { switch (key) { case "name": return (s) => s.name; case "kind": return (s) => SOURCE_KIND_LABEL[s.kind]; case "documents": return (s) => s.documents; case "provenance": return (s) => s.provenanceRows; case "facilities": return (s) => s.facilities; case "updated": return (s) => s.lastDocumentAt ?? s.updatedAt; } } const REDIST_TONE: Record = { allowed: "ok", attribution: "info", restricted: "warn", unknown: "neutral" }; const REDIST_LABEL: Record = { allowed: "Redistribution allowed", attribution: "Attribution required", restricted: "Restricted", unknown: "Re-use terms unknown" }; /** Redistribution / licence badge from the source registry (drives /download gating). */ function ReuseBadge({ s }: { s: SourceWithCounts }) { const r = s.redistribution ?? "unknown"; return ( {r === "attribution" ? "Attribution" : r === "allowed" ? "Allowed" : r === "restricted" ? "Restricted" : "Unknown"} {s.attributionRequired && r !== "attribution" && ( Attribution )} ); } function truncate(s: string | null | undefined, n: number): string { if (!s) return "—"; return s.length > n ? `${s.slice(0, n - 1)}…` : s; } function SourceTable({ items, sp, grouped, sort }: { items: SourceWithCounts[]; sp: SearchParams; grouped: boolean; sort: ReturnType> }) { const base = routes.sources(); const th = (k: SortKey) => sortTh(base, sp, sort, k); return ( {!grouped && } {items.map((s) => ( {!grouped && ( )} ))}
SourceKindRe-use License Attribution Documents Provenance rows Facilities Last document Link
{s.name}
{s.domain}
{SOURCE_KIND_LABEL[s.kind]} {truncate(s.license, 60)} {truncate(s.attribution, 48)} {fmtInt(s.documents)} {fmtInt(s.provenanceRows)} {fmtInt(s.facilities)} {fmtDate(s.lastDocumentAt)} {s.url ? {s.domain} : —}
); } export default async function SourcesPage({ searchParams }: { searchParams: Promise }) { const sp = await searchParams; const res = await api.sources(); const all = res.items; const q = spGet(sp, "q")?.trim().toLowerCase(); const kindParam = spGet(sp, "kind"); const kind = kindParam && KIND_SET.has(kindParam) ? (kindParam as SourceKind) : undefined; const sortGiven = Boolean(spGet(sp, "sort")); const sort = parseSort(sp, SORT_KEYS, { key: "provenance", order: "desc" }); let items = all; if (q) items = items.filter((s) => s.name.toLowerCase().includes(q) || s.domain.toLowerCase().includes(q)); if (kind) items = items.filter((s) => s.kind === kind); const cmp = compareBy(getter(sort.key), sort.order); items = [...items].sort((a, b) => cmp(a, b) || a.name.localeCompare(b.name)); const totals = { sources: all.length, documents: all.reduce((a, s) => a + s.documents, 0), provenance: all.reduce((a, s) => a + s.provenanceRows, 0), withData: all.filter((s) => s.provenanceRows > 0).length, }; const grouped = !sortGiven && !kind; const kindsPresent = SOURCE_KINDS.filter((k) => all.some((s) => s.kind === k)); const groups = grouped ? kindsPresent .map((k) => ({ kind: k, items: items.filter((s) => s.kind === k) })) .filter((g) => g.items.length) .sort((a, b) => b.items.reduce((x, s) => x + s.provenanceRows, 0) - a.items.reduce((x, s) => x + s.provenanceRows, 0)) : []; const chips: Chip[] = []; if (q) chips.push({ key: "q", label: "Search", value: q, removeHref: hrefWith(routes.sources(), sp, { q: undefined }) }); if (kind) chips.push({ key: "kind", label: "Kind", value: SOURCE_KIND_LABEL[kind], removeHref: hrefWith(routes.sources(), sp, { kind: undefined }) }); if (sortGiven) chips.push({ key: "sort", label: "Sorted by", value: `${sort.key} ${sort.order}`, removeHref: hrefWith(routes.sources(), sp, { sort: undefined, order: undefined }) }); return ( Coverage analytics → } /> {!res.items.length && !res.total && }
{sortGiven && } {sortGiven && }
{!items.length ? ( ) : grouped ? (
({ id: `kind-${g.kind}`, label: SOURCE_KIND_LABEL[g.kind], count: g.items.length }))} /> {groups.map((g) => (
a + s.provenanceRows, 0))} provenance rows`}>
))}
) : (
)}

Kinds rank source authority when values conflict (operator, government and filings first; news last) — see the{" "} reconciliation rules . Crawling policy: /bot.

); }