Skip to content

A customer portal that can configure, prove, fill and watch a deployment - #504

Open
bjmeetsfo wants to merge 5 commits into
mainfrom
feat/portal-setup-metrics
Open

A customer portal that can configure, prove, fill and watch a deployment#504
bjmeetsfo wants to merge 5 commits into
mainfrom
feat/portal-setup-metrics

Conversation

@bjmeetsfo

@bjmeetsfo bjmeetsfo commented Aug 31, 2026

Copy link
Copy Markdown
Collaborator

The portal could show which model endpoints a deployment talks to and could not change any of them, so configuring a managed deployment meant shell access to the host. It had no way to list the skills and resources it holds, no way to run the one check that covers the whole pipeline, and /v1/metrics carried only the bulk-import registry — the customer-facing API surface was invisible to the dashboards while the engine below it was fully instrumented.

Seven pages now, all served by the gateway itself.

Page What it does
/v1/admin Readiness checklist, each unfinished item carrying the steps to fix it; live import progress; a diagnostics bundle
/v1/admin/setup 79 settings (12 marked required), endpoint probe, live traffic, read-only deployment inventory, change log, export/import
/v1/admin/catalog List and filter skills and resources; read the stored text; retire a superseded skill
/v1/admin/explore Run a real retrieve, add a memory, upload documents with byte progress, browse what is stored
/v1/admin/ingestion Import a directory, watch it in detail, retry what failed
/v1/admin/portal Keys, with a scope picker that says what each scope allows
/v1/admin/api All 45 routes, each with the scope it needs and a curl that runs

Six tests that verify rather than assert

Documentation that drifts is worse than none, because it is trusted. Each of these compares a claim against the source and failed on its first run:

  • The live/needs restart label is checked by AST-walking every module under tools/ and classifying each environment read as import-time or per-call. It caught 20 mislabelled settings and one phantom knob that nothing outside the display function reads.
  • The route list is compared to the gateway's own path literals in both directions, and every documented example is executed and required to answer 2xx. It caught /v1/admin/routes undocumented and /v1/memory/by-key's example missing its required parameter.
  • The scope catalogue is pinned to MATRIXARK_TOOL_SCOPES both ways. It caught admin:sso.
  • The generated pages are regenerated in a temp copy and required to come out byte-identical.
  • The required-settings mark: a required key must exist, must not be a tuning knob, and a secret is required only alongside the variable naming its destination.
  • A timed concurrency guard on the readiness report: sequential 188 ms against a 60 ms stub, concurrent 68 ms.

Five defects found by building on top of the code

  • post_document gave up immediately on any 4xx — right for a 400, exactly backwards for a 429. The gateway rate-limits ingest and answers 429 with retry-after, so a large import against a busy deployment was abandoning precisely the documents it had been asked to resend. The job reported 1000 documents; the store held fewer, and nothing said so. Mutation-checked: 5 of the 6 new tests fail without the fix.
  • X-Scope on /v1/ingest_file is documented as a JSON scope object and was passed through unparsed. It reached the string branch of _apply_identity, which reads a bare string as a namespace label, so an upload carrying {"user_id":"alice"} was filed under acme/{"user_id":"alice"} with the user_id dropped. Silent; the only symptom is a retrieve that later cannot find it.
  • An unreachable blob tier on that route escaped as an unhandled 500 with a stack trace. Now a 502 naming the URL it could not reach.
  • --faint, the token carrying the help text under every field, measured 3.85:1 in dark and 3.04:1 in light — below AA, on exactly the prose explaining what each setting does.
  • .check meant both a checkbox label and a readiness row; the collision was already papered over with inline styles.

Safety posture

  • The writable settings are a closed registry; an unregistered key is a 400. Storage locations, cluster addresses and the auth mode are a read-only inventory — repointing a storage directory from a browser form does not reconfigure a running deployment, it strands its data.
  • Secrets are write-only: stored owner-only (0600), pushed into the variable named by the matching *_API_KEY_ENV, never returned by any read, and omitted rather than blanked from an export, because a blank is a write that would clear the target's key.
  • GET /v1/metrics stays credential-free: aggregate counters only, and the route label is a template, so a client cannot create unbounded series in an operator's Prometheus.

Also

GET /v1/skills, GET /v1/resources, GET /v1/users and POST /v1/skills/update — the backend has had the listing tools since the skill lane landed, reachable only through /v1/mcp. Grafana dashboard and alert rules for the edge. Accessibility: role="status" + aria-live on every region that changes, tab roles on Explore, both colour schemes checked at 375 px with no horizontal overflow.

Five of the seven pages are generated by tools/portal/build_portal_pages.py; the other two are hand-maintained and only have their nav refreshed.

270 tests across the portal, ingestion and gateway suites.


Choosing models, driving the memory API, and batching a paste

Both model names were free text on the wire, and a name that is merely misspelt does not fail
loudly: extraction falls back to the local rules and embedding to hash vectors, and both answer
200. The deployment looks healthy while storing far less than it was asked to.

GET /v1/admin/models answers three questions per role — what is worth suggesting, what the
configured endpoint says it actually serves (an OpenAI-compatible server answers `GET

/models`, so asking it turns a guess into a choice), and for embeddings what the stored vectors were actually made with. The probe is opt-in; it is a request against somebody else's service.

Changing the encoder is the one setting on the page that can silently invalidate data already
held. The dangerous case is not a width change — that at least has a width to notice — but two
different encoders of the same width, where nothing in the stack sees a mismatch:
all-MiniLM-L6-v2 and paraphrase-multilingual-MiniLM-L12-v2 are both 384, BAAI/bge-m3 and
voyage-3 are both 1024. Each catalogue entry states its width and names the others it collides
with, derived from the widths rather than written into prose — the hand-written note called out the
384 pair and said nothing about the 1024 one, which a test caught. The warning appears while the
choice is being made, and only when it would actually strand something.

Three engine guards make that change survivable rather than silent. All three were broken and none
had a test:

  1. similarity scored the shared prefix of two vectors of different widths — a plausible cosine
    across two unrelated spaces, worse than no number because it ranks. It now refuses;
  2. ingest hashed provider.model, the chat model, while the drainer hashed
    provider.embedding_model, so the same encoder produced two hashes depending on which path
    embedded the node;
  3. retrieve scored a node embedded by one encoder against a query embedded by another. A vector
    whose recorded encoder is not the active one now falls through to the hybrid lexical pass. A
    hash of 0 means unknown, not foreign, and is still scored.

Six Rust tests, each verified by breaking its guard again on purpose — including the
over-correction that would treat unknown as foreign. Every mutation turns exactly one test red.

A console over the memory API on Explore, listing every method by the name it is called by
rather than by the URL that serves it, generated from one table so an operation that gains an
argument gains a field. Tests hold the table to the routes in both directions, and check each
declared scope against what the gateway gates on. Destructive operations are marked from the
route's own scope and will not run until their name is typed.

Batch ingest takes a paste or a file, one memory per line, and runs it as a server-side job
rather than a loop in the tab — a browser loop over ten thousand records is one closed laptop away
from a half-finished import nobody can resume or count. Job items are now either a path or a
record, so a batch inherits the progress, failure classification, retry-only-the-retryable, cancel
and metrics the directory import already had. Blank lines are dropped and counted rather than
queued to fail forever.

Also fixes the settings audit, which matched a startup function by the outermost segment of a
qualname; the request handler is nested inside the app factory, so every per-request read inside it
read as captured-at-boot. Nothing had noticed because no tracked setting was read there until now.

361 tests.

Seven pages served by the gateway itself, so a customer running MatrixArk as a
managed service can set it up, confirm it works, fill it, use it, watch it, and
retry what failed -- without shell access to the host.

Setup writes a closed registry of model settings (secrets write-only, stored
owner-only, never returned by a read); an endpoint probe calls the configured
providers, because extraction and embedding both fall back to a deterministic
path that answers 200 and is otherwise indistinguishable from a healthy
deployment. The readiness checklist carries the steps to fix each unfinished
item. /v1/metrics gains the edge's own request, latency and configuration-health
series, with a route-template label so a client cannot create unbounded series.
Ingestion shows what an import is doing and retries what is worth retrying.

Six tests compare a claim against the source rather than asserting it, and each
failed on its first run: the live/restart label against an AST walk of every
env read; the route list against the gateway's own path literals, with every
documented example executed; the scope catalogue against MATRIXARK_TOOL_SCOPES;
the generated pages against the generator; the required-settings mark; and a
timed guard on the readiness report's concurrency.

Five defects found on the way. The largest: post_document gave up immediately on
any 4xx, which is right for a 400 and exactly backwards for a 429 -- the gateway
rate-limits ingest and answers 429 with retry-after, so a large import against a
busy deployment abandoned precisely the documents it had been asked to resend.
The job reported 1000 documents; the store held fewer. Also X-Scope on
/v1/ingest_file was passed through unparsed and filed uploads under the wrong
scope silently, an unreachable blob tier escaped as a bare 500, the help-text
colour token was below AA in both schemes, and one CSS class meant two things.

270 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bjmeetsfo
bjmeetsfo requested a review from superhaiou as a code owner August 31, 2026 04:13
bjmeetsfo and others added 2 commits August 30, 2026 21:38
Ingest can defer encoding, so between the write and the drainer catching up a
chunk exists and cannot be matched on meaning -- a retrieve over that window
returns less than it should and says nothing about why. matrixark_embedding_status
counts encoded against waiting, with the models and vector widths in use; GET
/v1/admin/embeddings serves it and the portal shows it moving.

The configured encoder travels with the counts (with none, nothing is waiting
because nothing will ever be encoded), mixed vector widths are called out (they
cannot be compared, so some memories can never match), and a backend that cannot
answer is an error rather than an empty backlog.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GET /v1/admin/events is a server-sent stream carrying traffic, imports in
progress, the encoding backlog and the configuration-warning count; the overview
and setup pages read it and show "live". Three pages were each running timers
against three endpoints, so an import that finished between two polls left a
stale bar until the next one.

Read over fetch rather than EventSource, which cannot set an Authorization
header -- the alternative puts the key in a query string, where it lands in every
access log between the browser and the gateway. X-Accel-Buffering is set because
nginx buffers a proxied response by default, which turns a live stream into one
delivery at the end. A stream is excluded from the latency histogram: a
ten-minute subscription left in there gives a p99 of ten minutes, describing
nothing anyone waited for.

Monitoring: a test compares the dashboards against the metrics this build emits,
both ways. A panel querying a series nobody emits is a blank panel, and a blank
panel reads as "no traffic"; a metric emitted and charted nowhere is work nobody
sees. It caught six, including documents waiting for a retry and when the
configuration last changed. The dashboards and alert rules are now served at
GET /v1/admin/monitoring/{gateway|ingestion|alerts} rather than named as a repo
path a managed customer cannot reach.

305 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bjmeetsfo
bjmeetsfo force-pushed the feat/portal-setup-metrics branch from 447b8b7 to b96f0f4 Compare August 31, 2026 07:24
bjmeetsfo and others added 2 commits August 31, 2026 02:14
… a paste as a real job

Ported from the private tree. Commits in this branch, newest first:

  - Choose the models and drive the memory API from the portal, and batch a paste as a real job
  - Stop scoring a vector against a query from a different encoder
  - Hand the customer the dashboards instead of naming a file they cannot reach
  - Push the live state instead of polling for it, and chart what was emitted and shown nowhere
  - Say how much of the store is encoded, and show it moving
  - Say which twelve of the seventy-nine settings actually have to be set
  - Show a running import, and the failures waiting, on the page people land on
  - Let a running upload be cancelled, and stop polling a page nobody is watching
  - Show an upload happening, let it be retried, and say how to fix each setup item
  - Retry an import's failures, and stop dropping the ones the gateway asked us to resend
  - Make the key list answer the two questions anyone asks of it
  - Put the page generator in the repo, and hold the pages to it
  - Give the readiness rows a class of their own
  - Put back the executable bit on files this work never touched
  - Stop the readiness page paying three times over
  - Record what each configuration write changed, and from what
  - Match the repo's file mode for the new tools modules
  - Stop the settings audit reading the API documentation as a reader
  - Serve the API contract from the process that implements it, and execute the examples
  - Move a configuration between deployments, and say when one changed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ported from the private tree. Commits in this branch, newest first:

  - A vector nobody can compare must not count as one that was scored
  - Choose the models and drive the memory API from the portal, and batch a paste as a real job
  - Stop scoring a vector against a query from a different encoder
  - Hand the customer the dashboards instead of naming a file they cannot reach
  - Push the live state instead of polling for it, and chart what was emitted and shown nowhere
  - Say how much of the store is encoded, and show it moving
  - Say which twelve of the seventy-nine settings actually have to be set
  - Show a running import, and the failures waiting, on the page people land on
  - Let a running upload be cancelled, and stop polling a page nobody is watching
  - Show an upload happening, let it be retried, and say how to fix each setup item
  - Retry an import's failures, and stop dropping the ones the gateway asked us to resend
  - Make the key list answer the two questions anyone asks of it
  - Put the page generator in the repo, and hold the pages to it
  - Give the readiness rows a class of their own
  - Put back the executable bit on files this work never touched
  - Stop the readiness page paying three times over
  - Record what each configuration write changed, and from what
  - Match the repo's file mode for the new tools modules
  - Stop the settings audit reading the API documentation as a reader
  - Serve the API contract from the process that implements it, and execute the examples

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant