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%
6.0 KB · 95 lines tsx
Raw Blame History
1"use client";23import Link from "next/link";4import type { MouseEvent } from "react";5import type { EventItem } from "@/lib/api";6import { relTime, utcTime, utcDate, utcDateTime, localDateTime } from "@/lib/format";7import { useEventDrawer } from "./event-drawer";8import { FieldChangeInline } from "./field-changes";9import { usePrefs } from "./prefs";10import { Badge, Chip, Flag, Score, TypeChip } from "./ui";1112const CHANNEL_GROUPS: Record<string, string[]> = { finance: ["finance", "payments", "crypto", "commerce"], health: ["health", "pharma"], government: ["government", "statistics"], science: ["science", "space"], products: ["consumer-tech", "automotive", "semiconductors", "enterprise"], infrastructure: ["cloud", "developer", "internet", "standards"], news: ["news", "media"] };13function sameChannel(a: string, b: string): boolean {14  return CHANNEL_GROUPS[a]?.includes(b) ?? false;15}1617/**18 * Feed row (spec §31): TIME · ENTITY/SOURCE · SIGNAL · TYPE · TITLE · STATUS · TAGS, with badges19 * BREAKING / DEVELOPING / SILENT / FIRST PARTY / CONFIRMED / ANOMALOUS. Density comes from CSS20 * variables (spec §32). Clicking the title opens the intelligence drawer (spec §33); the link keeps21 * a real href so middle-click / crawlers / no-JS still reach the permanent page.22 */23export function EventRow({ ev, flash = false, showDate = false, now, fresh = false, replayed = false }: { ev: EventItem; flash?: boolean; showDate?: boolean; now?: number; fresh?: boolean; replayed?: boolean }) {24  const { open } = useEventDrawer();25  const { exactTime, density, mounted } = usePrefs();26  const iso = (() => {27    const d = new Date(ev.detected_at);28    return Number.isNaN(d.getTime()) ? ev.detected_at : d.toISOString();29  })();30  const cats = (ev.categories ?? []).filter((c) => !Object.keys(CHANNEL_GROUPS).includes(c) || !ev.categories.some((x) => x !== c && sameChannel(c, x))).slice(0, density === "compact" ? 1 : 2);31  const signal = ev.signal_score ?? ev.importance;32  const state = ev.cluster?.state;33  const isBreaking = state === "breaking" || (signal >= 80 && !state);34  const isDeveloping = state === "developing";35  const anomalous = (ev.anomaly_score ?? 0) >= 60;36  const clusterHref = ev.cluster?.slug ? `/cluster/${ev.cluster.slug}` : ev.cluster_id ? `/cluster/${ev.cluster_id}` : null;37  const onClick = (e: MouseEvent<HTMLAnchorElement>): void => {38    if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;39    e.preventDefault();40    open(ev.slug, ev);41  };42  return (43    <article className={`row-dense offscreen-ok grid grid-cols-[auto_1fr] gap-x-3 px-3 hairline sm:grid-cols-[6.25rem_1fr_auto] ${flash || fresh ? "animate-row-in" : ""} ${isBreaking ? "border-l-2 border-l-hot/70" : isDeveloping ? "border-l-2 border-l-high/60" : ev.silent_change ? "border-l-2 border-l-silent/60" : "border-l-2 border-l-transparent"}`}>44      <div className="flex flex-col font-mono meta leading-4 text-fg-subtle tabular">45        <time dateTime={iso} title={mounted ? localDateTime(ev.detected_at) : utcDateTime(ev.detected_at)} suppressHydrationWarning className="text-fg-muted">{utcTime(ev.detected_at)}</time>46        <span suppressHydrationWarning>{showDate ? utcDate(ev.detected_at) : exactTime ? "UTC" : relTime(ev.detected_at, now)}</span>47      </div>48      <div className="min-w-0 sm:col-start-2">49        <div className="flex flex-wrap items-center gap-x-2 gap-y-0.5">50          <Link href={`/source/${ev.source?.id ?? ev.source_id}`} className="font-mono meta font-semibold uppercase tracking-wide text-fg-muted hover:text-fg">51            {ev.source?.name ?? ev.source_id}52          </Link>53          {ev.country && <Flag code={ev.country} className="text-[11px]" />}54          <span className="sm:hidden">55            <Score value={signal} size="sm" kind="signal" />56          </span>57          {isBreaking && <Badge kind="breaking" compact={density === "compact"} />}58          {isDeveloping && <Badge kind="developing" compact={density === "compact"} />}59          {ev.silent_change && <Badge kind="silent" compact={density === "compact"} />}60          {ev.first_party === false && <Badge kind="external" compact />}61          {ev.evidence_label === "CONFIRMED" && <Badge kind="confirmed" compact={density === "compact"} />}62          {anomalous && <Badge kind="anomalous" compact />}63          {replayed && <Badge kind="replayed" compact />}64        </div>65        <Link href={`/event/${ev.slug}`} onClick={onClick} className={`mt-0.5 block font-medium leading-snug text-fg hover:underline [overflow-wrap:anywhere] ${density === "compact" ? "truncate" : ""}`}>66          {ev.title}67        </Link>68        {density !== "compact" && ev.field_changes && ev.field_changes.length > 0 && (69          <div className="mt-0.5">70            <FieldChangeInline items={ev.field_changes} max={density === "comfortable" ? 3 : 2} />71          </div>72        )}73        {density === "comfortable" && ev.summary && <p className="mt-0.5 line-clamp-2 text-[12.5px] text-fg-muted">{ev.summary}</p>}74        <div className="mt-1 flex flex-wrap items-center gap-1">75          <TypeChip type={ev.event_type} href={`/live?event_type=${ev.event_type}`} />76          {cats.map((c) => (77            <Chip key={c} href={`/category/${c}`}>{c}</Chip>78          ))}79          {ev.entities?.filter((x) => x.role === "subject").slice(0, 1).map((x) => (80            <Chip key={x.id} href={`/entity/${x.id}`} tone="default" className="!text-fg-muted">{x.name}</Chip>81          ))}82          {ev.cluster_size && ev.cluster_size > 1 && clusterHref && (83            <Chip tone="info" href={clusterHref} title="Signals in this event cluster">84              {ev.cluster_size} signals{ev.cluster?.source_count && ev.cluster.source_count > 1 ? ` · ${ev.cluster.source_count} sources` : ""}85            </Chip>86          )}87        </div>88      </div>89      <div className="hidden items-start pt-0.5 sm:flex">90        <Score value={signal} kind="signal" title={`Signal ${Math.round(signal)} · importance ${Math.round(ev.importance)} · confidence ${Math.round(ev.confidence)}`} />91      </div>92    </article>93  );94}95