Admin console for spada
  • TypeScript 84%
  • CSS 9.9%
  • Go 5.5%
  • Just 0.3%
  • Dockerfile 0.2%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Yannick Koechlin cb96ab5307
All checks were successful
build-spada-admin / build (push) Successful in 48s
overview: average the ingest rate, and explain what is being counted
The ingest figures read 0.0 almost always and spiked once in a while. That was
not a display glitch — it is what differencing two consecutive snapshots does to
the counter being differenced.

Sealed documents are a STEP function. A document is searchable the moment it is
written, but it lands in the in-memory tail; it becomes "sealed" only when the
tail reaches write.seal_threshold and a segment is built — thousands of
documents at once (2048 on the staging cluster). The cluster rate differenced
that counter over one 2-second round, so every round without a seal read 0.0 and
the round containing one read the whole segment divided by two seconds. Neither
number was ever the ingest rate.

Two changes fix it, and they are independent:

  - COUNT ARRIVALS, NOT SEALS. Where the cluster tail is exact, tail and sealing
    documents now count alongside sealed ones, so the total moves as writes land.
    A seal then merely reclassifies documents between stages without changing the
    sum. A split namespace has no exact cluster tail, so it still contributes
    sealed only — and the window below absorbs its steps. Lower-bound namespaces
    stay excluded: their totals jump when an unreachable node returns, and that
    is a disclosure artefact, not ingest.

  - AVERAGE OVER MINUTES. lib/rate holds a sliding window (3 min) and reports
    (last - first) / elapsed, which is literally "documents added, over the time
    they were added in". The window restarts rather than lying whenever the
    counter stops being comparable: on reconnect, when the counted set changes,
    and when the counter goes backwards (a deleted namespace must never render as
    negative ingest).

The baseline is now shown next to the figure, because a rate over 12 seconds and
one over 3 minutes deserve different trust. Under 15s the card shows "—" and says
the window is still filling: a number over a few seconds of a bursty counter is
either 0.0 or a spike, which is the failure being fixed, and a page load is
exactly when someone is looking.

Measured against a live server ingesting continuously: successive readings of
14.9, 15.1, 14.8, 14.9, 14.4 /s where the old code alternated 0.0 and spikes.

The same treatment applies to this-node ingest, which had the same shape at
1-second resolution.

Also, since these figures are the ones people ask about: a "?" beside "sealed
documents" and "sealed segments" opens an overlay explaining the tail → sealing →
sealed lifecycle, why the cluster total is a union over segment digests rather
than a sum across replicas, when a segment gets written, and why segment count
shows up in both memory and query latency. No threshold VALUES are quoted — the
console cannot read the server's config, and a guess stated as fact is worse than
the explanation of the mechanism.

The dialog renders through a portal. Written inline it was a DOM descendant of
the stat card's label and inherited `text-transform: uppercase`, which rendered
the entire explanation in capitals.

Claude-Session: https://claude.ai/code/session_01LcwsLmrU7PmHZAb7Y4pcgq
2026-08-10 01:05:30 +02:00
.forgejo/workflows ci: publish the timestamped tag Flux orders on 2026-08-09 07:07:21 +02:00
cmd/spada-admin spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
gen/spada proto+bridge: vendor ClusterStats from spada 26c6781, pump it to the browser (S-157) 2026-08-09 00:24:56 +02:00
internal overview: average the ingest rate, and explain what is being counted 2026-08-10 01:05:30 +02:00
proto/spada proto+bridge: vendor ClusterStats from spada 26c6781, pump it to the browser (S-157) 2026-08-09 00:24:56 +02:00
ui overview: average the ingest rate, and explain what is being counted 2026-08-10 01:05:30 +02:00
.air.toml spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
.gitignore spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
buf.gen.yaml spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
buf.yaml spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
Dockerfile spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
go.mod spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
go.sum spada-admin: the admin console — connect bridge, 5 screens, SQL→schema, live stats 2026-08-08 01:54:17 +02:00
justfile query console: real snippets, every field, every knob 2026-08-09 06:57:26 +02:00
PROVENANCE.md proto+bridge: vendor ClusterStats from spada 26c6781, pump it to the browser (S-157) 2026-08-09 00:24:56 +02:00
README.md docs: README describes the cluster overview and its disclosure rules 2026-08-09 00:32:21 +02:00

spada-admin

The admin console for spada: explore content, query with receipts rendered as the proof they are, create namespaces (interactive schema builder, .proto upload, or SQL translation), delete them behind the server's verbatim-confirm arming switch, watch live cluster and server stats, and see the auth posture stated honestly.

The overview reads cluster → nodes → this connection's node. Cluster figures come from the S-157 ClusterStats fan-out and carry their own derivation: a total spada computed by unioning distinct segment digests across every node renders plainly; one it could not complete because a node was unreachable renders as ≥ N with an amber badge, above a banner naming the node and the error. A split namespace has no exact cluster tail, so its tail cell says per-node. Per-node storage figures are machine facts of one node and are never summed. Both live streams reconnect with capped backoff (1 s → 15 s), keeping the last snapshot on screen marked stale rather than passing a frozen number off as live.

  • Backend: Go. Speaks connectRPC (connect / gRPC-web) to the browser and gRPC to spada (spada.v1 + the S-152 spada.admin.v1 operator surface). The React bundle is embedded via go:embed — one static binary.
  • Frontend: React + Vite, design system imported from sognario's admin-ui (the Linea kit: tokens, Surface/StatCard/DataTable/TimeSeries…). Live updates ride the ServerStats and ClusterStats server-streams over connect-web (lib/stream.ts drives the reconnect for both).
  • SQL translation (/api/translate-sql): a stated Postgres-ish subset — every lossy mapping is a named warning, bytea is skipped out loud, and the full-text column is elected (content/body/text/… else first text).

Development

just ui-deps       # once
just gen           # after proto changes
SPADA=127.0.0.1:50051 just dev   # air (backend :8787) + vite (UI :5173)
just test

Build & run

just build         # UI bundle → embed → bin/spada-admin
./bin/spada-admin --listen :8787 --spada 127.0.0.1:50051
# SPADA_TOKEN=... for a server with the dev bearer token set.

CI (Forgejo actions) builds and pushes tcr.teixos.net/yannick/spada-admin (sha-<sha12>, dev, main tags; linux/amd64 — the ovh-stag nodes).

Protos under proto/ are vendored from spada @ the commit in PROVENANCE.md; spada/admin/v1/admin.proto's canonical copy lives in the spada repo.