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.9 KB · 110 lines tsx
Raw Blame History
1import Link from "next/link";2import type { Metadata } from "next";3import { ENTITY_TYPES } from "@websensor/core/client";4import { Chip, Empty, PageHeader, Panel, Score, Table, Tabs, Td } from "@/components/ui";5import { api } from "@/lib/api";6import { fmtInt, fmtScore, relTime, typeLabel } from "@/lib/format";78export const dynamic = "force-dynamic";9export const metadata: Metadata = { title: "Entities", description: "Organizations, products, models, APIs and other entities that events resolve to — ranked by importance, activity, velocity, quality and confirmation.", alternates: { canonical: "/entities" } };1011type Search = { tab?: string; type?: string; q?: string };1213export default async function EntitiesPage({ searchParams }: { searchParams: Promise<Search> }) {14  const sp = await searchParams;15  const tab = sp.tab === "all" || sp.type || sp.q ? "all" : "ranked";16  return (17    <>18      <PageHeader compact title="Entities" description="Everything important resolves to an entity. Each entity has a timeline — the equivalent of git history for its public Web presence." />19      <Tabs20        className="mb-3"21        current={tab}22        items={[23          { key: "ranked", label: "Ranked", href: "/entities" },24          { key: "all", label: "All", href: "/entities?tab=all" },25        ]}26      />27      {tab === "ranked" ? <Ranked /> : <All sp={sp} />}28    </>29  );30}3132async function Ranked() {33  const { items } = await api.rank(200);34  return (35    <Panel36      title={<>WebSensor rank <span className="font-mono text-fg-subtle">{fmtInt(items.length)}</span></>}37      dense38      action={<span className="hidden text-[11px] text-fg-subtle md:inline">importance × activity × velocity × quality × confirmation — not raw counts</span>}39    >40      <p className="border-b border-line px-3 py-2 text-[12px] text-fg-muted md:hidden">Ranking uses importance × activity × velocity × quality × confirmation, not raw counts.</p>41      {items.length === 0 ? (42        <Empty>The ranking is computed from the last 7 days of activity — nothing to rank yet.</Empty>43      ) : (44        <Table head={["#", "Entity", "Type", "Rank", "24 h", "7 d", "Sources", "Avg signal", "Confirmed", "Silent 24 h", "Breaking 24 h", "Last"]}>45          {items.map((e) => (46            <tr key={e.id} className="hover:bg-panel-2/60">47              <Td mono className="text-fg-subtle">{e.rank}</Td>48              <Td>49                <Link href={`/entity/${e.id}`} className="font-medium hover:underline">{e.name}</Link>50                {e.domain && <div className="truncate font-mono text-[10.5px] text-fg-subtle">{e.domain}</div>}51              </Td>52              <Td><Chip href={`/entities?tab=all&type=${e.type}`}>{typeLabel(e.type)}</Chip></Td>53              <Td><Score value={e.rank_score} size="sm" title={`Rank score ${fmtScore(e.rank_score)} · entity importance ${fmtScore(e.importance)}`} /></Td>54              <Td mono className={e.events_24h ? "" : "text-fg-subtle"}>{fmtInt(e.events_24h)}</Td>55              <Td mono className="text-fg-muted">{fmtInt(e.events_7d)}</Td>56              <Td mono>{fmtInt(e.sources)}</Td>57              <Td mono>{fmtScore(e.avg_signal)}</Td>58              <Td mono className={e.confirmed_ratio > 0 ? "text-ok" : "text-fg-subtle"}>{Math.round(e.confirmed_ratio * 100)}%</Td>59              <Td mono className={e.silent_24h ? "text-silent" : "text-fg-subtle"}>{fmtInt(e.silent_24h)}</Td>60              <Td mono className={e.breaking_24h ? "text-hot" : "text-fg-subtle"}>{fmtInt(e.breaking_24h)}</Td>61              <Td mono className="whitespace-nowrap text-fg-subtle">{relTime(e.last_at)}</Td>62            </tr>63          ))}64        </Table>65      )}66    </Panel>67  );68}6970async function All({ sp }: { sp: Search }) {71  const { items } = await api.entities({ type: sp.type, q: sp.q, limit: 400 });72  const types = new Set(items.map((e) => e.type));73  return (74    <>75      <form className="mb-3 flex flex-wrap items-center gap-2" action="/entities">76        <input type="hidden" name="tab" value="all" />77        <input name="q" defaultValue={sp.q ?? ""} placeholder="Search entities…" className="h-8 w-64 max-w-full rounded-md border border-line bg-panel px-2.5 text-[13px] placeholder:text-fg-subtle" />78        {sp.type && <input type="hidden" name="type" value={sp.type} />}79        <button type="submit" className="h-8 rounded-md border border-line bg-panel-2 px-3 text-[12.5px]">Search</button>80        <span className="font-mono text-[11px] text-fg-subtle tabular">{fmtInt(items.length)} entit{items.length === 1 ? "y" : "ies"}</span>81      </form>82      <div className="mb-3 flex flex-wrap gap-1">83        <Chip href={sp.q ? `/entities?tab=all&q=${encodeURIComponent(sp.q)}` : "/entities?tab=all"} tone={!sp.type ? "signal" : "default"}>all</Chip>84        {ENTITY_TYPES.filter((t) => types.has(t) || t === sp.type).map((t) => (85          <Chip key={t} href={`/entities?tab=all&type=${t}${sp.q ? `&q=${encodeURIComponent(sp.q)}` : ""}`} tone={sp.type === t ? "signal" : "default"}>{typeLabel(t)}</Chip>86        ))}87      </div>88      <Panel dense>89        {items.length === 0 ? (90          <Empty>No entities match.</Empty>91        ) : (92          <Table head={["Entity", "Type", "Domain", "Importance", "Events", "24 h", "Last event"]}>93            {items.map((e) => (94              <tr key={e.id} className="hover:bg-panel-2/60">95                <Td><Link href={`/entity/${e.id}`} className="font-medium hover:underline">{e.name}</Link></Td>96                <Td><Chip>{typeLabel(e.type)}</Chip></Td>97                <Td mono className="text-fg-subtle">{e.domain ?? "—"}</Td>98                <Td mono>{fmtScore(e.importance)}</Td>99                <Td mono>{fmtInt(e.event_count)}</Td>100                <Td mono className={e.events_24h ? "text-signal" : "text-fg-subtle"}>{e.events_24h ?? 0}</Td>101                <Td mono className="whitespace-nowrap text-fg-subtle">{e.last_event_at ? relTime(e.last_event_at) : "—"}</Td>102              </tr>103            ))}104          </Table>105        )}106      </Panel>107    </>108  );109}110