SPB Git forge

spb/websensor

Public
33commits 1branches 0releases
3.4 MBsize
maindefault branch
10 days agolast push
TypeScript 55.4% Python 43.2% SQL 1.2%
8.3 KB · 146 lines tsx
Raw Blame History
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