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%
4.9 KB · 98 lines tsx
Raw Blame History
1"use client";23import { useState } from "react";4import { useApi } from "@/lib/api";5import { fmtDate } from "@/lib/format";6import { Empty, Panel, Skeleton, Table, Tag } from "@/components/ui";78interface Schema {9  platform: string;10  shape_hash: string;11  fingerprint: { hostname: string; path_pattern: string; method: string; graphql_operation?: string; observed_entity_types?: string[] };12  schema: { candidate_entity_types: { type: string; confidence: number; path: string }[]; fields: { path: string; semantic?: { kind: string; confidence: number }; example?: string; types?: string[] }[]; object_count: number; repeated_object_paths: string[] };13  observed_count: number;14  sample_url: string;15  first_seen: string;16  last_seen: string;17}1819export default function Schemas() {20  const [platform, setPlatform] = useState("");21  const { data, loading } = useApi<Schema[]>(`/schemas${platform ? `?platform=${platform}` : ""}`, { refreshMs: 15_000 });22  const platforms = [...new Set((data ?? []).map((s) => s.platform))];23  return (24    <div className="space-y-4">25      <div className="flex flex-wrap items-end gap-3">26        <div>27          <h1 className="text-2xl font-semibold tracking-tight">Runtime APIs</h1>28          <p className="text-sm text-fg-2 max-w-2xl">Response shapes the browser received while a human-like session unfolded. Endpoints are fingerprinted by (host, path pattern, method, structure hash) and their fields are semantically inferred — nothing here was written by hand.</p>29        </div>30        <select value={platform} onChange={(e) => setPlatform(e.target.value)} className="ml-auto rounded-lg bg-bg-2 border border-line-2 px-2 py-1.5 text-sm">31          <option value="">all platforms</option>32          {["youtube", "reddit", ...platforms.filter((p) => !["youtube", "reddit"].includes(p))].map((p) => (33            <option key={p} value={p}>34              {p}35            </option>36          ))}37        </select>38      </div>39      {loading ? (40        <Skeleton rows={8} />41      ) : data?.length ? (42        <div className="space-y-3">43          {data.map((sc) => (44            <details key={sc.platform + sc.shape_hash} className="panel open:glow-cyan/30">45              <summary className="cursor-pointer px-4 py-3 flex flex-wrap items-center gap-2 text-sm">46                <Tag>{sc.platform}</Tag>47                <Tag color="#9d7bff">{sc.fingerprint.method}</Tag>48                <span className="mono">49                  {sc.fingerprint.hostname}50                  <span className="text-fg-2">{sc.fingerprint.path_pattern}</span>51                </span>52                {sc.fingerprint.graphql_operation && <Tag color="#ff7ad9">{sc.fingerprint.graphql_operation}</Tag>}53                <span className="ml-auto flex gap-1 flex-wrap">54                  {sc.schema.candidate_entity_types.slice(0, 5).map((c) => (55                    <Tag key={c.type + c.path} color="#38e1ff" title={c.path}>56                      {c.type} {Number(c.confidence).toFixed(2)}57                    </Tag>58                  ))}59                </span>60                <span className="mono text-xs text-dim whitespace-nowrap">61                  seen {sc.observed_count}× · {sc.schema.object_count} objects · #{sc.shape_hash.slice(0, 8)}62                </span>63              </summary>64              <div className="px-4 pb-4 space-y-3">65                <div className="text-[11px] text-dim mono break-all">66                  sample {sc.sample_url} · first {fmtDate(sc.first_seen)} · last {fmtDate(sc.last_seen)}67                </div>68                {sc.schema.repeated_object_paths?.length ? (69                  <div className="text-xs text-fg-2">70                    repeated object arrays (entity lists): <span className="mono">{sc.schema.repeated_object_paths.slice(0, 5).join(" · ")}</span>71                  </div>72                ) : null}73                <Table head={["field path", "inferred semantic", "conf", "types", "example"]} dense>74                  {sc.schema.fields.slice(0, 80).map((f) => (75                    <tr key={f.path}>76                      <td className="mono text-[11px] text-fg-2 max-w-[560px] truncate" title={f.path}>77                        {f.path}78                      </td>79                      <td>{f.semantic ? <Tag color={f.semantic.kind === "identifier" ? "#ffb347" : f.semantic.kind.includes("url") ? "#ff7ad9" : "#43e69a"}>{f.semantic.kind}</Tag> : "—"}</td>80                      <td className="mono text-[11px]">{f.semantic ? Number(f.semantic.confidence).toFixed(2) : ""}</td>81                      <td className="mono text-[11px] text-dim">{f.types?.join("|")}</td>82                      <td className="mono text-[11px] text-dim max-w-[320px] truncate">{f.example}</td>83                    </tr>84                  ))}85                </Table>86              </div>87            </details>88          ))}89        </div>90      ) : (91        <Panel>92          <Empty>No schemas discovered yet.</Empty>93        </Panel>94      )}95    </div>96  );97}98