spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1"use client";23import Link from "next/link";4import { ArrowRight } from "lucide-react";5import { useApi } from "@/lib/api";6import { ago, fmtNum, truncate } from "@/lib/format";7import { Bar, Empty, Kpi, Panel, Skeleton, StatusPill, Tag } from "@/components/ui";8import { ActivityChart, SurfaceBars, TypeDonut, YieldBars } from "@/components/charts";9import { LiveFeed } from "@/components/LiveFeed";1011interface Overview {12 totals: { sessions: number; entities: number; media: number; schemas: number; actions: number; observations: number; relationships: number; patterns_learned: number; feed_items: number };13 by_type: { platform: string; entity_type: string; n: number }[];14 running: { session_id: string; platform: string; started_at: string }[];15 recent_sessions: { session_id: string; platform: string; goal: string | null; mode: string | null; started_at: string; ended_at: string | null; health: string; actions: number; entities: number }[];16 series: { bucket: string; entities: number; responses: number; actions: number; schemas: number }[];17 surfaces: { dom: number; network: number; both: number; agreements: number; conflicts: number };18 actions: { action_type: string; n: number; avg_gain: number | null; success_rate: number | null; avg_new_entities: number | null }[];19 platforms: { platform: string; learned: boolean; adapter: boolean; confidence: number; page_types: number; entity_types: number; network_schemas: number; sessions: number; has_manifest: boolean }[];20}2122export default function Observatory() {23 const { data, loading } = useApi<Overview>("/overview", { refreshMs: 8000 });24 const t = data?.totals;25 const s = data?.surfaces;26 const agreement = s && s.agreements + s.conflicts > 0 ? s.agreements / (s.agreements + s.conflicts) : null;27 const spark = (k: keyof Overview["series"][number]) => (data?.series ?? []).slice(-30).map((r) => Number(r[k]));28 return (29 <div className="space-y-6">30 <div className="flex flex-wrap items-end gap-4">31 <div>32 <h1 className="text-2xl font-semibold tracking-tight">Observatory</h1>33 <p className="text-sm text-fg-2 mt-1 max-w-2xl">The crawler does not download pages. It watches social applications run inside an authenticated browser, fuses what the network and the DOM reveal, and learns the platform as it goes.</p>34 </div>35 <div className="ml-auto flex gap-2">36 {data?.running.map((r) => (37 <Link key={r.session_id} href={`/sessions/${r.session_id}`} className="panel px-3 py-1.5 text-xs flex items-center gap-2 hover:border-amber/50">38 <StatusPill status="running" /> {r.platform} <span className="text-dim">{ago(r.started_at)}</span>39 </Link>40 ))}41 <Link href="/jobs" className="rounded-lg bg-cyan/90 hover:bg-cyan text-bg text-sm font-semibold px-3 py-1.5 flex items-center gap-1">42 New mission <ArrowRight className="h-4 w-4" />43 </Link>44 </div>45 </div>4647 <div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">48 <Kpi label="entities" value={fmtNum(t?.entities)} hint={`${fmtNum(t?.relationships)} relationships`} spark={spark("entities")} />49 <Kpi label="media" value={fmtNum(t?.media)} hint="videos & images with metadata" accent="pink" />50 <Kpi label="runtime schemas" value={fmtNum(t?.schemas)} hint={`${fmtNum(t?.patterns_learned)} patterns learned`} accent="violet" spark={spark("schemas")} />51 <Kpi label="actions" value={fmtNum(t?.actions)} hint={`${fmtNum(t?.sessions)} sessions`} accent="amber" spark={spark("actions")} />52 <Kpi label="observations" value={fmtNum(t?.observations)} hint="raw, append-only" accent="fg" spark={spark("responses")} />53 <Kpi label="surface agreement" value={agreement === null ? "—" : `${Math.round(agreement * 100)}%`} hint={s ? `${fmtNum(s.both)} entities seen on both surfaces` : ""} accent="green" />54 </div>5556 <div className="grid grid-cols-1 xl:grid-cols-3 gap-4">57 <Panel title="Activity" sub="10-minute buckets, last 48 h" className="xl:col-span-2">58 {loading ? <Skeleton rows={6} /> : data?.series.length ? <ActivityChart data={data.series} /> : <Empty>No activity yet — launch a mission.</Empty>}59 </Panel>60 <Panel title="Entity mix" sub="canonical entities by type">61 {data?.by_type.length ? <TypeDonut data={data.by_type} /> : <Empty>—</Empty>}62 </Panel>63 </div>6465 <div className="grid grid-cols-1 xl:grid-cols-3 gap-4">66 <Panel title="Live observation stream" sub="every event, as the browser sees it" className="xl:col-span-2" pad={false}>67 <div className="p-2">68 <LiveFeed max={80} height={380} />69 </div>70 </Panel>71 <div className="space-y-4">72 <Panel title="Network vs DOM" sub="where entities were confirmed">73 {s ? <SurfaceBars dom={s.dom} network={s.network} both={s.both} /> : <Skeleton />}74 <div className="text-xs text-dim mt-2">75 field agreements <span className="mono text-fg">{fmtNum(s?.agreements)}</span> · conflicts <span className="mono text-fg">{fmtNum(s?.conflicts)}</span>76 </div>77 </Panel>78 <Panel title="Action yield" sub="average new entities per action type">79 {data?.actions.length ? <YieldBars data={data.actions} height={190} /> : <Empty>—</Empty>}80 </Panel>81 </div>82 </div>8384 <div className="grid grid-cols-1 xl:grid-cols-2 gap-4">85 <Panel title="Recent sessions" right={<Link href="/sessions" className="hover:text-cyan">all →</Link>} pad={false}>86 {data?.recent_sessions.length ? (87 <ul className="divide-y divide-line/60">88 {data.recent_sessions.map((r) => (89 <li key={r.session_id}>90 <Link href={`/sessions/${r.session_id}`} className="flex items-center gap-3 px-4 py-2.5 hover:bg-white/[0.03]">91 <StatusPill status={r.ended_at ? "done" : r.health} />92 <Tag>{r.platform}</Tag>93 <span className="text-sm truncate">{truncate(r.goal ?? "(no goal)", 70)}</span>94 <span className="ml-auto mono text-xs text-dim whitespace-nowrap">95 {r.actions} steps · {r.entities} ent · {ago(r.started_at)}96 </span>97 </Link>98 </li>99 ))}100 </ul>101 ) : (102 <Empty>No sessions yet.</Empty>103 )}104 </Panel>105 <Panel title="Connectors" sub="learned platform knowledge" right={<Link href="/platforms" className="hover:text-cyan">details →</Link>}>106 <div className="space-y-3">107 {data?.platforms108 .filter((p) => p.adapter || p.learned)109 .map((p) => (110 <Link key={p.platform} href={`/platforms/${p.platform}`} className="block group">111 <div className="flex items-center gap-3 text-sm">112 <span className="w-20 capitalize group-hover:text-cyan">{p.platform}</span>113 <Bar value={p.confidence / 100} color={p.confidence >= 70 ? "#43e69a" : p.confidence >= 40 ? "#38e1ff" : "#ffb347"} className="flex-1" />114 <span className="mono text-xs w-10 text-right">{p.confidence}%</span>115 </div>116 <div className="text-[10.5px] text-dim mono ml-[92px]">117 {p.page_types} page types · {p.entity_types} entity types · {p.network_schemas} schemas · {p.sessions} sessions{p.has_manifest ? " · compiled" : ""}118 </div>119 </Link>120 ))}121 </div>122 </Panel>123 </div>124 </div>125 );126}127