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%
4.0 KB · 57 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { FieldChanges } from "@/components/field-changes";4import { LiveFeed } from "@/components/live-feed";5import { Badge, Chip, Empty, Flag, PageHeader, Panel, Score } from "@/components/ui";6import { api } from "@/lib/api";7import { agoIso, relTime, typeLabel, utcDateTime } from "@/lib/format";89export const dynamic = "force-dynamic";10export const metadata: Metadata = { title: "Silent changes", description: "Pricing, terms, API limits, documentation and product pages that changed quietly — no announcement matched. Previous state, current state, exact diff." };1112/** Dedicated silent-change experience (spec §23). */13export default async function SilentPage() {14  const [top, recent, count24] = await Promise.all([api.events({ silent_change: true, limit: 12, order: "signal", after: agoIso(48 * 3600e3) }), api.events({ silent_change: true, limit: 60 }), api.eventCount({ silent_change: true, after: agoIso(24 * 3600e3) })]);15  const byType = new Map<string, number>();16  for (const e of recent.items) byType.set(e.event_type, (byType.get(e.event_type) ?? 0) + 1);17  return (18    <>19      <PageHeader20        kicker={<span className="flex items-center gap-2"><Badge kind="silent" /> <span className="text-fg-subtle">{count24.count} in the last 24 h · first-party pages only · importance ≥ 45 · no matching announcement within 12 h</span></span>}21        title="Silent changes"22        description="Something was modified without an obvious public announcement: a price quietly changed, documentation rewritten, a feature removed, terms edited, an API limit adjusted, a product page gone. Each item shows the previous state, the current state and the exact diff."23        actions={<div className="flex flex-wrap gap-1">{[...byType.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8).map(([t, n]) => <Chip key={t} href={`/live?silent_change=true&event_type=${t}`}>{typeLabel(t)} <span className="font-mono text-fg-subtle">{n}</span></Chip>)}</div>}24      />25      <Panel title="Most significant · 48 h" dense className="mb-4">26        {top.items.length === 0 ? (27          <Empty>No silent change detected in this period.</Empty>28        ) : (29          <ul className="grid gap-px bg-line md:grid-cols-2 xl:grid-cols-3">30            {top.items.map((e) => (31              <li key={e.id} className="flex flex-col gap-2 bg-panel p-3">32                <div className="flex items-center gap-2 text-[11px] text-fg-subtle">33                  <Link href={`/source/${e.source?.id ?? e.source_id}`} className="truncate font-mono uppercase text-fg-muted hover:text-fg">{e.source?.name}</Link>34                  {e.country && <Flag code={e.country} />}35                  <Chip>{typeLabel(e.event_type)}</Chip>36                  <span className="ml-auto whitespace-nowrap font-mono tabular" title={utcDateTime(e.detected_at)}>{relTime(e.detected_at)}</span>37                </div>38                <div className="flex items-start gap-2">39                  <Score value={e.signal_score ?? e.importance} kind="signal" size="sm" />40                  <Link href={`/event/${e.slug}`} className="font-medium leading-snug hover:underline">{e.title}</Link>41                </div>42                {e.field_changes?.length ? <FieldChanges items={e.field_changes} compact max={3} /> : <p className="line-clamp-3 text-[12px] text-fg-muted">{e.summary}</p>}43                <div className="mt-auto flex items-center gap-2 text-[11px]">44                  <Link href={`/event/${e.slug}#diff`} className="text-info hover:underline">exact diff</Link>45                  {e.old_snapshot_id && e.new_snapshot_id && <Link href={`/compare?a=${e.old_snapshot_id}&b=${e.new_snapshot_id}`} className="text-info hover:underline">before / after</Link>}46                  <span className="ml-auto text-fg-subtle">{e.evidence_label}</span>47                </div>48              </li>49            ))}50          </ul>51        )}52      </Panel>53      <LiveFeed initial={recent.items} initialCursor={recent.nextCursor} fixed="silent" title="SILENT CHANGES · LIVE" showFilters={false} />54    </>55  );56}57