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%
5.4 KB · 79 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { EventRow } from "@/components/event-row";4import { LiveFeed } from "@/components/live-feed";5import { Badge, Chip, Empty, Flag, PageHeader, Panel, Score, StateLabelText } from "@/components/ui";6import { api, type Cluster, type EventItem } from "@/lib/api";7import { fmtOffset, relTime, typeLabel, utcTime } from "@/lib/format";89export const dynamic = "force-dynamic";10export const metadata: Metadata = { title: "Breaking", description: "Breaking now, developing, recently confirmed and watching — ranked by signal, velocity, novelty and confirmation, not by recency." };1112/** Breaking desk (spec §34). */13export default async function BreakingPage() {14  const [desk, live] = await Promise.all([api.breakingDesk(), api.breaking(40)]);15  const d = desk ?? { breaking_now: [], developing: [], recently_confirmed: [], watching: [], generated_at: "" };16  return (17    <>18      <PageHeader kicker={<span className="flex items-center gap-2"><Badge kind="breaking" /> <span className="text-fg-subtle">Breaking is not recent: it needs strong signal, velocity, novelty, source quality, confirmation and cross-source activity.</span></span>} title="Breaking" />19      <div className="grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr] [&>*]:min-w-0">20        <ClusterSection title="Breaking now" tone="hot" items={d.breaking_now} empty="Nothing is breaking right now." />21        <ClusterSection title="Developing" tone="high" items={d.developing} empty="No developing story: signals are not accumulating fast enough anywhere." />22      </div>23      <div className="mt-4 grid grid-cols-1 gap-4 xl:grid-cols-[1fr_1fr] [&>*]:min-w-0">24        <ClusterSection title="Recently confirmed" tone="ok" items={d.recently_confirmed} empty="No cluster reached independent confirmation in the last 48 h." />25        <Panel title="Watching · high signal, not yet breaking" dense>26          {d.watching.length ? d.watching.slice(0, 12).map((e) => <EventRow key={e.id} ev={e} />) : <Empty>Nothing to watch.</Empty>}27        </Panel>28      </div>29      <div className="mt-4">30        <LiveFeed initial={live.items} initialCursor={live.nextCursor} fixed="breaking" title="BREAKING · SIGNAL ≥ 80 · LIVE" showFilters={false} />31      </div>32    </>33  );34}3536function ClusterSection({ title, tone, items, empty }: { title: string; tone: "hot" | "high" | "ok"; items: Cluster[]; empty: string }) {37  const color = tone === "hot" ? "text-hot" : tone === "high" ? "text-high" : "text-ok";38  return (39    <Panel title={<span className={color}>{title} <span className="font-mono text-fg-subtle">{items.length}</span></span>} dense>40      {items.length === 0 ? (41        <Empty>{empty}</Empty>42      ) : (43        <ul className="divide-y divide-line">44          {items.map((c) => {45            const e = c.event as EventItem | null | undefined;46            return (47              <li key={c.id} className={`px-3 py-2.5 ${tone === "hot" ? "border-l-2 border-l-hot/70" : tone === "high" ? "border-l-2 border-l-high/60" : "border-l-2 border-l-ok/50"}`}>48                <div className="flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-fg-subtle">49                  <StateLabelText state={c.state} />50                  {e?.source && <Link href={`/source/${e.source.id}`} className="font-mono uppercase text-fg-muted hover:text-fg">{e.source.name}</Link>}51                  {e?.country && <Flag code={e.country} />}52                  {e?.event_type && <Chip>{typeLabel(e.event_type)}</Chip>}53                  {e?.silent_change && <Badge kind="silent" compact />}54                  <span className="ml-auto font-mono tabular">{utcTime(c.last_at)} · {relTime(c.last_at)}</span>55                </div>56                <div className="mt-1 flex items-start gap-3">57                  <Score value={e?.signal_score ?? c.max_importance} kind="signal" />58                  <div className="min-w-0 flex-1">59                    <Link href={e?.slug ? `/event/${e.slug}` : `/cluster/${c.slug ?? c.id}`} className="block font-medium leading-snug hover:underline">{c.title}</Link>60                    {e?.summary && <p className="mt-0.5 line-clamp-2 text-[12.5px] text-fg-muted">{e.summary}</p>}61                    <div className="mt-1 flex flex-wrap items-center gap-1.5 text-[11px] text-fg-subtle">62                      <Link href={`/cluster/${c.slug ?? c.id}`} className="text-info hover:underline">{c.event_count} signal{c.event_count === 1 ? "" : "s"} · {c.source_count ?? 1} source{(c.source_count ?? 1) === 1 ? "" : "s"}</Link>63                      {(c.first_party_count ?? 0) > 0 && <Chip tone="signal">{c.first_party_count} first-party</Chip>}64                      {(c.external_count ?? 0) > 0 && <Chip>{c.external_count} external</Chip>}65                      {c.velocity !== undefined && c.velocity >= 40 && <Chip tone="high">velocity {Math.round(c.velocity)}</Chip>}66                      {c.lead_time_ms && c.lead_time_ms > 0 ? <Chip tone="ok" title="WebSensor detected the first-party signal this long before the first external report">lead {fmtOffset(c.lead_time_ms).replace("+", "")}</Chip> : null}67                      {c.sources && c.sources.length > 1 && <span className="min-w-0 max-w-full truncate">{c.sources.slice(0, 4).map((s) => s.name).join(" · ")}{c.sources.length > 4 ? ` +${c.sources.length - 4}` : ""}</span>}68                    </div>69                  </div>70                </div>71              </li>72            );73          })}74        </ul>75      )}76    </Panel>77  );78}79