TypeScript 55.4%
Python 43.2%
SQL 1.2%
1import Link from "next/link";2import type { Cluster, EventItem, TrendingItem } from "@/lib/api";3import { fmtOffset, fmtScore, relTime } from "@/lib/format";4import { Badge, Chip, Empty, Mark, Panel, Score, StateLabelText } from "./ui";56export function TrendingPanel({ items, title = "Trending entities", hours = 24 }: { items: TrendingItem[]; title?: string; hours?: number }) {7 return (8 <Panel title={`${title} · ${hours} h`} dense action={<Link href="/explore?tab=trending" className="text-[11px] text-fg-subtle hover:text-fg">all →</Link>}>9 {items.length === 0 ? (10 <Empty>Trending is computed from the last {hours} h of events.</Empty>11 ) : (12 <ol className="divide-y divide-line">13 {items.map((t, i) => (14 <li key={t.id} className="grid grid-cols-[1.25rem_1.25rem_1fr_auto] items-center gap-2 px-3 py-1.5 text-[13px]">15 <span className="font-mono text-[11px] text-fg-subtle tabular">{i + 1}</span>16 <Mark name={t.name} />17 <div className="min-w-0">18 <Link href={`/entity/${t.id}`} className="block truncate font-medium hover:underline">{t.name}</Link>19 <div className="truncate text-[11px] text-fg-subtle">20 {t.events} signals · {t.sources} source{t.sources === 1 ? "" : "s"}{t.first_party ? ` · ${t.first_party} first-party` : ""}{t.silent ? ` · ${t.silent} silent` : ""}21 </div>22 </div>23 <span className={`font-mono text-[12px] font-semibold tabular ${t.direction === "up" ? "text-signal" : t.direction === "down" ? "text-fg-subtle" : "text-fg-muted"}`} title={`trend score ${fmtScore(t.score)} · ${t.events} vs ${t.prev_events} in the previous window`}>24 {t.direction === "up" ? "↑" : t.direction === "down" ? "↓" : "→"} {fmtScore(t.score)}25 </span>26 </li>27 ))}28 </ol>29 )}30 </Panel>31 );32}3334export function ClustersPanel({ items, title = "Event clusters" }: { items: Cluster[]; title?: string }) {35 const multi = items.filter((c) => c.event_count > 1).slice(0, 8);36 return (37 <Panel title={title} dense action={<Link href="/explore?tab=clusters" className="text-[11px] text-fg-subtle hover:text-fg">all →</Link>}>38 {multi.length === 0 ? (39 <Empty>Related observations are grouped into clusters as they arrive.</Empty>40 ) : (41 <ul className="divide-y divide-line">42 {multi.map((c) => (43 <li key={c.id} className="flex items-start gap-2 px-3 py-1.5 text-[13px]">44 <Score value={c.max_importance} size="sm" />45 <div className="min-w-0 flex-1">46 <Link href={`/cluster/${c.slug ?? c.id}`} className="line-clamp-2 font-medium hover:underline">{c.title}</Link>47 <div className="flex flex-wrap items-center gap-x-2 text-[11px] text-fg-subtle">48 <StateLabelText state={c.state} />49 <span>{c.event_count} signals</span>50 {c.source_count && c.source_count > 1 && <span>{c.source_count} sources</span>}51 {(c.first_party_count ?? 0) > 0 && <span className="text-signal">{c.first_party_count} 1st-party</span>}52 {c.lead_time_ms && c.lead_time_ms > 0 ? <span title="WebSensor lead time before the first external report">lead {fmtOffset(c.lead_time_ms).replace("+", "")}</span> : null}53 <span>{relTime(c.last_at)}</span>54 </div>55 </div>56 </li>57 ))}58 </ul>59 )}60 </Panel>61 );62}6364/** Breaking now rail (spec §94): primary event of each breaking / developing cluster. */65export function BreakingRail({ items, developing }: { items: Cluster[]; developing?: Cluster[] }) {66 const all = [...items.map((c) => ({ c, state: "breaking" })), ...(developing ?? []).map((c) => ({ c, state: "developing" }))].slice(0, 8);67 return (68 <Panel title={<span className="text-hot">Breaking now</span>} dense action={<Link href="/breaking" className="text-[11px] text-fg-subtle hover:text-fg">desk →</Link>}>69 {all.length === 0 ? (70 <Empty>Nothing is breaking right now. Breaking requires strong signal, freshness and confirmation — not just recency.</Empty>71 ) : (72 <ul className="divide-y divide-line">73 {all.map(({ c, state }) => {74 const e = c.event as EventItem | null | undefined;75 return (76 <li key={c.id} className={`px-3 py-2 text-[13px] ${state === "breaking" ? "border-l-2 border-l-hot/70" : "border-l-2 border-l-high/60"}`}>77 <div className="flex items-center gap-1.5">78 <Badge kind={state as "breaking" | "developing"} compact />79 <span className="truncate font-mono text-[10.5px] uppercase text-fg-subtle">{e?.source?.name ?? c.source?.name ?? ""}</span>80 <span className="ml-auto font-mono text-[10.5px] text-fg-subtle">{relTime(c.last_at)}</span>81 </div>82 <Link href={e?.slug ? `/event/${e.slug}` : `/cluster/${c.slug ?? c.id}`} className="mt-0.5 line-clamp-2 font-medium hover:underline">{c.title}</Link>83 <div className="mt-0.5 flex flex-wrap items-center gap-1 text-[11px] text-fg-subtle">84 <Score value={e?.signal_score ?? c.max_importance} size="sm" kind="signal" />85 <span>{c.event_count} signal{c.event_count === 1 ? "" : "s"} · {c.source_count ?? 1} source{(c.source_count ?? 1) === 1 ? "" : "s"}</span>86 {(c.first_party_count ?? 0) > 0 && <Chip tone="signal">{c.first_party_count} first-party</Chip>}87 </div>88 </li>89 );90 })}91 </ul>92 )}93 </Panel>94 );95}9697export function AnomalyRail({ items }: { items: { id: string; name: string; domain: string; changes_2h: number; baseline_per_day: number; activity_score: number; pct_vs_baseline?: number | null }[] }) {98 const top = items.filter((s) => s.activity_score >= 45).slice(0, 6);99 return (100 <Panel title="Anomalous activity" dense action={<Link href="/radar" className="text-[11px] text-fg-subtle hover:text-fg">radar →</Link>}>101 {top.length === 0 ? (102 <Empty>No source is far above its baseline right now.</Empty>103 ) : (104 <ul className="divide-y divide-line">105 {top.map((s) => (106 <li key={s.id} className="px-3 py-1.5 text-[13px]">107 <div className="flex items-center gap-2">108 <Link href={`/source/${s.id}`} className="min-w-0 flex-1 truncate font-medium hover:underline">{s.name}</Link>109 <span className={`font-mono text-[12px] font-semibold tabular ${s.activity_score >= 70 ? "text-hot" : "text-high"}`}>{s.pct_vs_baseline !== null && s.pct_vs_baseline !== undefined ? `+${Math.min(9999, Math.round(s.pct_vs_baseline)).toLocaleString()}%` : fmtScore(s.activity_score)}</span>110 </div>111 <div className="text-[11px] text-fg-subtle">{s.changes_2h} changes in 2 h · baseline {s.baseline_per_day}/day</div>112 </li>113 ))}114 </ul>115 )}116 </Panel>117 );118}119120export function SilentRail({ items }: { items: EventItem[] }) {121 return (122 <Panel title={<span className="text-silent">Silent changes</span>} dense action={<Link href="/silent" className="text-[11px] text-fg-subtle hover:text-fg">all →</Link>}>123 {items.length === 0 ? (124 <Empty>No silent change in this window.</Empty>125 ) : (126 <ul className="divide-y divide-line">127 {items.slice(0, 6).map((e) => (128 <li key={e.id} className="px-3 py-1.5 text-[13px]">129 <div className="flex items-center gap-2 text-[11px] text-fg-subtle">130 <span className="truncate font-mono uppercase">{e.source?.name}</span>131 <span className="ml-auto">{relTime(e.detected_at)}</span>132 </div>133 <Link href={`/event/${e.slug}`} className="line-clamp-2 hover:underline">{e.title}</Link>134 {e.field_changes?.[0] && (135 <div className="mt-0.5 truncate font-mono text-[11px] text-fg-muted">136 {e.field_changes[0].label}: <span className="diff-line-del rounded px-0.5">{e.field_changes[0].before ?? "∅"}</span> → <span className="diff-line-add rounded px-0.5">{e.field_changes[0].after ?? "∅"}</span>137 </div>138 )}139 </li>140 ))}141 </ul>142 )}143 </Panel>144 );145}146