SPB Git

spb/websensor

Public

web: /coverage and /coverage/[sector] pages (were hidden by the coverage/ gitignore rule, now anchored to the repo root)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Sep 13, 2026) parent b31ffaf Browse files

3 changed files +171 −1

modified .gitignore +3 −1
@@ -7,7 +7,9 @@ dist/
7 !.env.example 7 !.env.example
8 logs/ 8 logs/
9 tmp/ 9 tmp/
10−coverage/ 10+/coverage/
11+apps/*/coverage/
12+packages/*/coverage/
11 .DS_Store 13 .DS_Store
12 .claude/ 14 .claude/
13 deploy/*.mld.json 15 deploy/*.mld.json
added apps/web/src/app/coverage/[sector]/page.tsx +101 −0
@@ -0,0 +1,101 @@
1+import Link from "next/link";
2+import type { Metadata } from "next";
3+import { notFound } from "next/navigation";
4+import { Bar, Chip, Flag, PageHeader, Panel, Stat, Table, Td } from "@/components/ui";
5+import { api } from "@/lib/api";
6+import { fmtInt, fmtPct, relTime } from "@/lib/format";
7+
8+export const dynamic = "force-dynamic";
9+
10+export async function generateMetadata({ params }: { params: Promise<{ sector: string }> }): Promise<Metadata> {
11+ const { sector } = await params;
12+ const c = await api.coverageSector(sector);
13+ if (!c) return { title: "Coverage" };
14+ return { title: `${c.label} · coverage ${c.score.toFixed(1)}`, description: `${fmtInt(c.covered)} of ${fmtInt(c.members)} ${c.label.toLowerCase()} organizations observed by WebSensor (${c.universes.length} universes).`, alternates: { canonical: `/coverage/${c.sector}` } };
15+}
16+
17+function band(score: number): string {
18+ return score >= 80 ? "text-signal" : score >= 60 ? "text-high" : score >= 40 ? "text-warn" : "text-fg-muted";
19+}
20+
21+type Params = { u?: string; status?: string };
22+
23+export default async function CoverageSectorPage({ params, searchParams }: { params: Promise<{ sector: string }>; searchParams: Promise<Params> }) {
24+ const { sector } = await params;
25+ const sp = await searchParams;
26+ const c = await api.coverageSector(sector);
27+ if (!c) notFound();
28+ const universes = sp.u ? c.universes.filter((u) => u.key === sp.u) : c.universes;
29+ const filter = (m: { covered: boolean; shadow: number; seed_status: string | null }): boolean => {
30+ if (sp.status === "observed") return m.covered;
31+ if (sp.status === "shadow") return !m.covered && m.shadow > 0;
32+ if (sp.status === "queued") return !m.covered && m.shadow === 0 && (m.seed_status === "queued" || m.seed_status === "discovering");
33+ if (sp.status === "missing") return !m.covered && m.shadow === 0 && !(m.seed_status === "queued" || m.seed_status === "discovering");
34+ return true;
35+ };
36+ const href = (patch: Params): string => {
37+ const n = { ...sp, ...patch };
38+ const q = new URLSearchParams();
39+ if (n.u) q.set("u", n.u);
40+ if (n.status) q.set("status", n.status);
41+ const s = q.toString();
42+ return `/coverage/${c.sector}${s ? `?${s}` : ""}`;
43+ };
44+ return (
45+ <>
46+ <PageHeader
47+ compact
48+ kicker={<Link href="/coverage" className="hover:underline">Coverage</Link>}
49+ title={<span>{c.label} <span className={`font-mono ${band(c.score)}`}>{c.score.toFixed(1)}</span></span>}
50+ description={`${c.description ?? ""} ${fmtInt(c.covered)} of ${fmtInt(c.members)} organizations observed · breadth ${fmtPct(c.breadth)} · depth ${fmtPct(c.depth)} · ${fmtInt(c.sensors)} sensors matched · ${fmtInt(c.shadow_members)} in shadow · ${fmtInt(c.queued)} queued for discovery. Updated ${relTime(c.generated_at)}.`}
51+ />
52+ <div className="panel mb-4 grid grid-cols-2 divide-x divide-y divide-line sm:grid-cols-3 lg:grid-cols-6 lg:divide-y-0">
53+ <Stat label="Score" value={<span className={band(c.score)}>{c.score.toFixed(1)}</span>} hint={`weight ×${c.weight}`} />
54+ <Stat label="Breadth" value={fmtPct(c.breadth)} hint="importance-weighted share observed" />
55+ <Stat label="Depth" value={fmtPct(c.depth)} hint="sensors per organization / 5" />
56+ <Stat label="Observed" value={`${fmtInt(c.covered)} / ${fmtInt(c.members)}`} tone="signal" />
57+ <Stat label="Shadow" value={fmtInt(c.shadow_members)} tone="info" hint="validated, proving signal" />
58+ <Stat label="Countries" value={fmtInt(c.by_country.filter((x) => x.country !== "—").length)} hint={`${c.universes.length} universes`} />
59+ </div>
60+ <div className="mb-2 flex flex-wrap items-center gap-1">
61+ <span className="label mr-1">status</span>
62+ {[["", "all"], ["observed", "observed"], ["shadow", "shadow"], ["queued", "queued"], ["missing", "not yet seeded"]].map(([k, l]) => (
63+ <Chip key={k} href={href({ status: k || undefined })} tone={(sp.status ?? "") === k ? "signal" : "default"}>{l}</Chip>
64+ ))}
65+ <span className="label mx-1">·</span>
66+ <span className="label mr-1">universe</span>
67+ <Chip href={href({ u: undefined })} tone={!sp.u ? "signal" : "default"}>all</Chip>
68+ {c.universes.map((u) => (
69+ <Chip key={u.key} href={href({ u: u.key })} tone={sp.u === u.key ? "signal" : "default"}>{u.label} <span className="font-mono text-[10.5px] text-fg-subtle">{u.covered}/{u.members}</span></Chip>
70+ ))}
71+ </div>
72+ {universes.map((u) => {
73+ const items = u.items.filter(filter);
74+ return (
75+ <Panel key={u.key} dense className="mb-4" title={<span>{u.label} <span className={`font-mono text-[12px] ${band(u.score)}`}>{u.score.toFixed(1)}</span> <span className="font-mono text-[11px] text-fg-subtle">· {u.covered}/{u.members} observed · {fmtInt(u.sensors)} sensors</span></span>} action={u.provenance ? <span className="max-w-[40ch] truncate text-[10.5px] text-fg-subtle" title={u.provenance}>{u.provenance}</span> : undefined}>
76+ {items.length === 0 ? (
77+ <div className="p-3 text-[12px] text-fg-subtle">No member matches this filter.</div>
78+ ) : (
79+ <div className="max-h-[32rem] overflow-auto">
80+ <Table head={["Organization", "Domain", "Country", "Imp.", "Status", "Sensors", "Depth", ""]}>
81+ {items.map((m) => (
82+ <tr key={`${u.key}/${m.domain}`} className="hover:bg-panel-2/60">
83+ <Td>{m.source_id ? <Link href={`/source/${m.source_id}`} className="font-medium hover:underline">{m.name}</Link> : <span className="font-medium">{m.name}</span>}</Td>
84+ <Td mono><Link href={`/domain/${m.domain}`} className="text-fg-muted hover:underline">{m.domain}</Link></Td>
85+ <Td>{m.country ? <span className="inline-flex items-center gap-1 font-mono text-[11px] text-fg-muted"><Flag code={m.country} /> {m.country}</span> : <span className="text-fg-subtle">—</span>}</Td>
86+ <Td mono className="text-fg-subtle">{m.importance}</Td>
87+ <Td>{m.covered ? <Chip tone="ok">observed</Chip> : m.shadow > 0 ? <Chip tone="info">shadow ×{m.shadow}</Chip> : m.seed_status === "queued" || m.seed_status === "discovering" ? <Chip tone="warn">{m.seed_status}</Chip> : m.seed_status === "blocked" ? <Chip tone="danger">blocked</Chip> : m.seed_status ? <Chip>{m.seed_status}</Chip> : <Chip>not seeded</Chip>}</Td>
88+ <Td mono>{m.sensors}{m.shadow ? <span className="text-fg-subtle"> +{m.shadow}</span> : null}</Td>
89+ <Td><div className="w-16"><Bar value={m.depth * 100} tone={m.depth >= 0.8 ? "signal" : m.depth >= 0.4 ? "mid" : "info"} /></div></Td>
90+ <Td>{m.source_id && <Link href={`/source/${m.source_id}`} className="whitespace-nowrap text-[11px] text-fg-subtle hover:text-fg">source →</Link>}</Td>
91+ </tr>
92+ ))}
93+ </Table>
94+ </div>
95+ )}
96+ </Panel>
97+ );
98+ })}
99+ </>
100+ );
101+}
added apps/web/src/app/coverage/page.tsx +67 −0
@@ -0,0 +1,67 @@
1+import Link from "next/link";
2+import type { Metadata } from "next";
3+import { Bar, Empty, PageHeader, Panel, Stat, Table, Td } from "@/components/ui";
4+import { api } from "@/lib/api";
5+import { fmtInt, fmtPct, relTime } from "@/lib/format";
6+
7+export const dynamic = "force-dynamic";
8+export const metadata: Metadata = { title: "Coverage", description: "Global Observation Coverage Score: how much of the world's high-value public web WebSensor observes, sector by sector — listed companies, governments, AI, cloud, cybersecurity, markets, science, healthcare, energy, transport, telecom, logistics, open source and commerce.", alternates: { canonical: "/coverage" } };
9+
10+function band(score: number): string {
11+ return score >= 80 ? "text-signal" : score >= 60 ? "text-high" : score >= 40 ? "text-warn" : "text-fg-muted";
12+}
13+
14+export default async function CoveragePage() {
15+ const c = await api.coverage();
16+ if (!c || !c.sectors.length) {
17+ return (
18+ <>
19+ <PageHeader compact kicker="Global Observation Coverage Score" title="Coverage" description="How much of the world's high-value public web do we observe?" />
20+ <Panel><Empty>Coverage universes are not loaded yet.</Empty></Panel>
21+ </>
22+ );
23+ }
24+ const factoryQueued = (c.factory.queued ?? 0) + (c.factory.discovering ?? 0);
25+ return (
26+ <>
27+ <PageHeader
28+ compact
29+ kicker="Global Observation Coverage Score"
30+ title={<span>Coverage <span className={`font-mono ${band(c.global_score)}`}>{c.global_score.toFixed(1)}</span><span className="text-[13px] text-fg-subtle"> / 100</span></span>}
31+ description={`How much of the world's high-value public web do we observe? ${fmtInt(c.covered)} of ${fmtInt(c.members)} organizations in ${c.sectors.length} sector universes have at least one active sensor. The denominator is explicit (config/coverage), the score is explainable (breadth 70 % · depth 30 %), and the Source Factory expands it continuously: ${fmtInt(c.monitored.shadow)} sensors are in shadow monitoring, ${fmtInt(factoryQueued)} organizations are queued for discovery.`}
32+ />
33+ <div className="panel mb-4 grid grid-cols-2 divide-x divide-y divide-line sm:grid-cols-3 lg:grid-cols-6 lg:divide-y-0">
34+ <Stat label="Global score" value={<span className={band(c.global_score)}>{c.global_score.toFixed(1)}</span>} hint="sector-weighted" />
35+ <Stat label="Universe members" value={fmtInt(c.members)} hint={`${c.sectors.length} sectors`} />
36+ <Stat label="Observed" value={fmtInt(c.covered)} hint={fmtPct(c.members ? c.covered / c.members : null)} tone="signal" />
37+ <Stat label="Sensors matched" value={fmtInt(c.sensors_matched)} hint={`${fmtInt(c.monitored.sensors)} active in total`} />
38+ <Stat label="In shadow" value={fmtInt(c.monitored.shadow)} hint="observed, not yet published" tone="info" />
39+ <Stat label="Factory queue" value={fmtInt(factoryQueued)} hint={`${fmtInt(c.factory.discovered ?? 0)} discovered · ${fmtInt(c.factory.blocked ?? 0)} blocked`} />
40+ </div>
41+ <Panel dense title="Sectors" action={<span className="text-[11px] text-fg-subtle">updated {relTime(c.generated_at)}</span>}>
42+ <Table head={["Sector", "Score", "", "Breadth", "Depth", "Members", "Observed", "Shadow", "Queued", "Sensors", "Weight", ""]}>
43+ {c.sectors.map((s) => (
44+ <tr key={s.sector} className="hover:bg-panel-2/60">
45+ <Td><Link href={`/coverage/${s.sector}`} className="font-medium hover:underline">{s.label}</Link><div className="text-[11px] text-fg-subtle">{s.description ?? s.sector}</div></Td>
46+ <Td mono className={`font-semibold ${band(s.score)}`}>{s.score.toFixed(1)}</Td>
47+ <Td><div className="w-24 sm:w-40"><Bar value={s.score} tone={s.score >= 80 ? "signal" : s.score >= 60 ? "high" : s.score >= 40 ? "mid" : "info"} /></div></Td>
48+ <Td mono>{fmtPct(s.breadth)}</Td>
49+ <Td mono>{fmtPct(s.depth)}</Td>
50+ <Td mono>{fmtInt(s.members)}</Td>
51+ <Td mono className="text-signal">{fmtInt(s.covered)}</Td>
52+ <Td mono className={s.shadow_members ? "text-info" : "text-fg-subtle"}>{fmtInt(s.shadow_members)}</Td>
53+ <Td mono className="text-fg-subtle">{fmtInt(s.queued)}</Td>
54+ <Td mono>{fmtInt(s.sensors)}</Td>
55+ <Td mono className="text-fg-subtle">×{s.weight}</Td>
56+ <Td><Link href={`/coverage/${s.sector}`} className="whitespace-nowrap text-[11px] text-fg-subtle hover:text-fg">members →</Link></Td>
57+ </tr>
58+ ))}
59+ </Table>
60+ </Panel>
61+ <p className="mt-3 text-[11.5px] leading-relaxed text-fg-subtle">
62+ Method — {c.method}. A member counts as observed when at least one active (non-shadow) sensor lives on its registrable domain or on a host it declares. Shadow sensors are new sensors the Source Factory found and validated; they are polled and their evidence is kept, but nothing is published until they prove signal over noise. Universes live in <code>config/coverage/*.yaml</code> with provenance and retrieval dates; they are the denominator, not the registry.
63+ {c.issues.length > 0 && <span className="text-warn"> {c.issues.length} universe file{c.issues.length > 1 ? "s" : ""} could not be loaded.</span>}
64+ </p>
65+ </>
66+ );
67+}
68
Menu

Theme