SPB Git forge
7commits 1branches 0releases
229.0 KBsize
maindefault branch
12 days agolast push
TypeScript 91.8% HTML 3.2% JavaScript 3% SQL 1.4% CSS 0.7%
7.6 KB · 127 lines tsx
Raw Blame History
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