plans/ held 58 documents and exactly one said whether it was open. The rest
mixed finished work, reviews of shipped work, parked specs and genuinely
pending ones, with nothing distinguishing them, so "how many plans are in
the queue" had no answer short of reading all 58.
Now `grep -H '^Status:' plans/*.md` is the answer:
50 done 3 in progress 2 planned 2 reference 1 parked
Statuses were derived rather than guessed: CLAUDE.md's own built list and
"What's NOT built yet" section, plus checking the subject exists in the
code. A review of work that shipped counts as done -- it records what was
found, it is not a request for anything. `reference` separates the two docs
that are conventions rather than work items (admin-design-standards,
admin-work-framework), which otherwise read as permanently-open plans.
The vocabulary is deliberately five words. A larger one invites "mostly
done" and "blocked-ish", which is how the directory became unreadable.
test_plans_declare_status.py keeps it from rotting: a new plan without a
marker fails, as does an unknown status, one buried below the eighth line,
or an open status with no reason -- "planned" alone is the state that rots,
since nobody can tell later whether it waits on a decision, a dependency,
or just nobody's turn.
Also updates the sweep plan with what landed and what did not, including
that #9 was not a defect.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
17 KiB
Admin portal functional sweep -- 2026-09-08
Status: in progress -- 12 findings; #1-#8, #10-#12 fixed, #9 was not a defect
Every page, control, and API endpoint under /admin exercised end to end
against real data, on a throwaway instance bound to 8081.
How it was tested
sqlite3 /home/alee/Sources/6krrt/router.db \
"VACUUM INTO '<worktree>/router.db'" # real catalog + 23,844 decisions
PYTHONPATH=<worktree>/src HF_HOME=<repo>/.hf-cache \
.venv/bin/uvicorn dispatcher:app --host 127.0.0.1 --port 8081
cwd = the worktree, so config/config.yaml, router.db and the admin
overlay all resolve inside it. Production 8080 and
/home/alee/Sources/6krrt/config/config.local.yaml were untouched --
verified by md5 before and after (1ec1734..., unchanged) and by 8080
answering 200 throughout.
Not run, deliberately. Seed Energy spends real NeuralWatt credits
(?samples=5, 65+ billed completions) and Restart Service shells
systemctl --user restart llm-router.service, which is the production
unit regardless of which instance served the click. Both were read in
source instead.
Findings
Ordered by consequence. Every item was reproduced, not inferred.
1. Profile edit silently drops an allowed_model_ids entry that is not in the current catalog
admin/frontend/profiles.html
The editor's allowed_model_ids control is a <select multiple> populated
from the 446-row catalog. An id stored in the profile that has no matching
<option> is simply not selected -- and Save posts the selection, so the
stored value is gone.
Reproduced: a profile holding allowed_model_ids: ["totally-not-a-model"]
was opened with Edit and saved with no changes. It went from
admits 0 models (warning triangle) to no filters / admits 40 models.
This fails open. The realistic trigger is a model that left the catalog
-- deprecated by the poller, renamed upstream, or pruned by the OpenRouter
allowlist -- at which point an unrelated edit to min_tier quietly converts
a locked-down profile into one that admits everything. Nothing warns.
Ids that are in the catalog round-trip correctly (verified: gemma-4-31b,
kimi-k3 came back preselected), so this only bites on drift.
2. Admin request bodies are plain BaseModel, so unknown keys are ignored
src/admin.py -- _CloudFallbackBody, _ProfileCreateBody,
_ProfileUpdateBody, _ProviderUpdateBody, _AvailabilityBody,
_AllowlistAddBody, _ValueBody
CLAUDE.md already documents this exact hazard and closed it for config
models via StrictModel(extra="forbid"). The admin API surface never got
the same treatment.
Worst instance is destructive:
POST /admin/api/cloud-fallback-config
{"base_url":"https://api.neuralwatt.com/v1","model":"deepseek-v4-flash", ...}
-> 200 {"message":"A restart is required for this change to take effect"}
The body is nested ({"cloud_fallback": {...}}), so every flat key is
discarded, cloud_fallback defaults to None, and None is the documented
remove the block signal. A configured cloud fallback is deleted and the
caller is told the save succeeded. Confirmed: overlay went from a populated
classifier.cloud_fallback to no such key, with a 200 both times.
Note the asymmetry -- the inner dict is validated strictly
({"timeout_secondz":20} -> 422 "Extra inputs are not permitted"); only the
outer wrapper is loose.
Same class, non-destructive: POST /admin/api/profiles/
{"name":"typoprof","min_teir":3} returns 200 and creates a profile with
every filter null.
3. The stale availability override does nothing
admin/frontend/models.html offers active / deprecated / stale. Only
deprecated is ever read -- metrics._admin_deprecated_models and
dispatcher._admin_deprecated_models both query
WHERE availability = 'deprecated'.
Measured on /route (coding_refactor, tier 1, 5k ctx):
| state | candidates_considered |
|---|---|
glm-5.2-fast overridden to stale |
27 |
| stale override cleared | 27 |
gemma-4-31b overridden to deprecated |
27 |
| deprecated override cleared | 28 |
Deprecated works. Stale is a control that persists a row and changes nothing.
4. /v1/models advertises admin-deprecated models, and pins against them are not refused
src/dispatcher.py:3645 reads models.availability = 'active' straight from
the table and never consults admin_model_overrides.
_resolve_pinned_provider has no override check either.
With gemma-4-31b admin-deprecated, it stayed in /v1/models (51 entries)
while /route correctly excluded it. So a client enumerating the endpoint
sees a model the operator has switched off, pins it, and gets it dispatched.
The same function already hides ollama-local rows under gaming mode, with
a comment giving the reason: listing one would "advertise a model every pin
against it is about to be refused for". The override case has neither the
hiding nor the refusal.
5. Decisions search is silently scoped to the newest 1000 rows
admin/frontend/decisions.html. Subtitle: "Full routing decision history --
filter, search, and inspect".
The page loads a fixed 1000-row window; every filter and the search box operate on that client-side slice. The counter reads "N of 1000", which looks like a total.
route_decisionsholds 23,844 rows- 10,394 of them name a kimi model
- searching
kimion the page reports "0 of 1000"
A confident zero on a page that claims to hold the full history. The API
itself is fine (/admin/api/decisions?limit=5000 returned 2000 rows), so
this is a frontend windowing choice, not a backend limit.
Also on this page: rows are not clickable and there is no detail affordance, though the subtitle promises "inspect"; the FLAGS column has no legend.
6. Two reciprocal navigation gaps
admin/frontend/providers.htmlhas nohref="/admin/proficiency"admin/frontend/proficiency.htmlhas nohref="/admin/providers"
Every other page carries both. Net effect: you cannot get from Providers to Proficiency or back without editing the URL.
7. POST /admin/api/providers/{name} has no read-only guard, and the result can be unremovable
src/admin.py:1378. admin_provider_update validates the URL and the model,
then persists -- with no provenance check at all. Compare
admin_provider_delete (403 for a base-config provider) and
admin_profile_update (403, "add an overlay profile with the same name to
override it").
Editing neuralwatt -- shown in the UI as base config / read-only --
returned 200 and wrote a full dispatch_providers.neuralwatt block into
config.local.yaml.
The trap is what happens next: DELETE /admin/api/providers/neuralwatt
refuses with 422 because it is dispatch_settings.default_provider. The
overlay entry is now unremovable through the portal; the only way out is
hand-editing config.local.yaml, the one file in this deployment that is
not in git.
The UI hides the edit control for base-config providers, so this is API-reachable only -- but the portal is the only thing that writes that file.
8. cloud_fallback.base_url is not URL-validated
POST /admin/api/cloud-fallback-config
{"cloud_fallback":{"base_url":"notaurl","model":"x", ...}} -> 200
and base_url: notaurl lands in the overlay. POST /api/providers/{name}
rejects ftp://nope with 422 via _validate_provider_url; the same check is
absent here, and this block is what the router falls back to when the local
classifier dies.
9. Allowlist delete always reports success
DELETE /admin/api/providers/openrouter/allowlist/nope/nope -> 200
{"deleted":"nope/nope"}. Bare DELETE, no rowcount check. Profile delete
and model-override delete both 404 correctly on a missing target.
10. Allowlist add cannot update a note
INSERT OR IGNORE. Re-adding sandbox/test-model with note:"dup"
returned 200 carrying the original note. There is no way to correct a
note through the API or the UI.
Smaller things, all reproduced
- "Apply Feedback" only previews.
controls.html:541sends?dry_run=true. The button says Apply; there is no way to actually apply from the portal. - "Seed Energy" spends money with no confirmation and is styled
identically (
btn-success) to the free Refresh Catalog next to it.?samples=5across the catalog is 65+ billed completions. - "Restart Service" has no confirmation. It is
btn-danger, which helps, but it drops in-flight requests on one click -- while deleting a profile requires a two-step confirm. Manageon a provider that has no allowlist is a dead end. neuralwatt's panel opens, says "No models on the allowlist" (the truth is "this provider has no allowlist"), and Browse catalog preview fails with a generic "Could not load catalog preview." The real reason -- 409 "allowlist is not required for this provider" -- never reaches the user. The summary line "0 allowed" reads as zero permitted when it means unrestricted.- The profile editor's model picker offers all 446 rows, including allowlist-pruned and deprecated OpenRouter models, with no filter. The Models page has a "Show deprecated / stale" toggle defaulting to off; this picker has none, so picking one silently yields a zero-admit profile.
- Allowlist catalog search resets after every Add, so adding several models from one search means retyping it each time.
- Allowlist Remove has no confirm step, unlike profile Delete.
- Gaming-mode 409 after saving cloud fallback in the portal tells you to
"uncomment the classifier.cloud_fallback example in config/config.yaml"
rather than "restart to pick up the block you just saved".
cfgbinds at import, so the refusal itself is correct; the message points at the wrong remedy. - Switching classifier mode away from
local_encoderleaves theencoder:block in the overlay. Onlycloud_primaryis cleaned (deletions). Harmless -- validators accept it -- but it is stale state. - Overlay YAML writes nulls as bare
provider:keys (ruamel). Parses fine, reads oddly.
What works
Stated because it is most of the surface, and because the validation in particular is unusually good.
Pages. All seven render and populate from real data. Dashboard
Calls/Cost/Energy toggle, all five history ranges, the warnings popover, the
quota strip, verdict mix, category breakdown. Models list + "Show deprecated
/ stale" (45 -> 446). Decisions filters (kind, category, tier, search),
pagination, page-size. Proficiency matrix, category column-narrowing,
model search, only-measured, sort, and the source control's cell dimming
(413/495 cells at outcome_blended) with a legend that distinguishes
measured from inherited.
Profiles. Create, duplicate-with-prefill (onlycheaps ->
onlycheaps-copy), inline edit, two-step delete, live admission counts that
match select_candidates, the zero-admit warning chip, and the
restart-required banner. Built-ins correctly read-only.
Providers. Create, delete, allowlist add/remove, live catalog preview (422 models), search filter, "Added" state on entries already allowlisted.
Model overrides. Set and clear, including slash-bearing ids
(deepseek/deepseek-v4-flash), with effective_availability /
is_overridden reported correctly and deprecation genuinely removing the
model from routing.
Controls. All nine runtime knobs flip and read back. All twelve
persisted config keys write to the overlay with correct base / overlay
provenance.
Error paths -- every one of these returned the right status with a message that explains itself:
| probe | result |
|---|---|
min_tier: 3, max_tier: 1 |
422, names both values |
latency_tolerance: "turbo" |
422 |
logging.level: "screaming" |
422, lists the four valid values |
quality_tolerance: -5 |
422, "must be in [0, 1)" |
objective.credit_attenuation.enabled |
403, not editable (matches CLAUDE.md) |
default_profile: "ghostprofile" |
422, lists the five real profiles |
confidence_threshold: 80 |
422, "returns a 0-1 probability, not a percent" |
encoder.device: "tpu" |
422 |
cloud_llm with neither primary nor auto |
422 |
gaming mode without cloud_fallback |
409 (runtime) and 422 (persisted) |
| duplicate profile / builtin profile / missing profile | 409 / 403 / 404 |
delete the current default_profile |
422, says to repoint it first |
history?range=bogus |
400 |
| allowlist ops on a non-allowlist provider | 409 |
| unknown provider | 404 |
Triggers. Refresh Catalog ran clean (19 NeuralWatt + 29 OpenRouter
upserted, 393 pruned by the allowlist) and surfaced the catalog sanity-floor
warning CLAUDE.md asks for: "[openrouter] fetched only 29 models (current DB
has 425); proceeding but catalog may be truncated". Apply Feedback
(dry-run) produced a correct per-model failure/success tally.
Two more, found while fixing #1
11. The model picker offered every deprecated row
The allowed_model_ids control was populated from an unfiltered
/admin/api/models: 446 options, of which 393 were OpenRouter rows the
provider allowlist had already pruned and the poller had marked
deprecated. Picking one produced a profile that admits nothing, with
nothing on screen saying why. models.html filtered the same payload behind a
"Show deprecated / stale" toggle defaulting off; the picker did not.
12. config/config.local.yaml breaks 9 tests
Not a portal bug, but it bites anyone running the suite on a configured machine. Reproduced both directions on the same tree:
| overlay present | result |
|---|---|
| no | 1523 passed |
yes (classifier.mode: local_encoder) |
9 failed, 1514 passed |
The nine are in test_classifier_backoff.py (5), test_gaming_mode.py (3)
and test_classifier_modes_dispatch.py (1) -- e.g.
assert 'local_encoder' == 'local_llm'. They load the repo's real
config/config.yaml and pick up the machine-local overlay beside it, so a
deployment that has configured classifier.mode cannot run its own tests
clean. This deployment has exactly that overlay, so pytest fails 9 today
regardless of any change.
Fixed on fix/admin-profile-model-visibility
Findings #1, #11, and the picker half of the smaller items:
GET /admin/api/modelsnow defaults to routable rows only (50 here, of which the 30 OpenRouter ones are exactly the 30 allowlist entries).?include_unroutable=truereturns all 446, and models.html -- the availability table, the one page that legitimately wants them -- is the only caller that passes it. Every present and future picker gets the filtered default. Allowlist a model, poll, and it appears.- The profile detail pane renders
admitted_modelsas chips instead of hiding it in atitle=tooltip truncated to 8 of 40. - The
allowed_model_idscontrol is a two-pane Available / Selected picker. What is chosen is always on screen, filtering narrows Available only, "Add all" takes the whole filtered set, and a stored id that is no longer routable stays in Selected flaggednot routable-- which is what closes the silent-drop in #1: opening Edit and saving untouched now preserves it. - The list summary says
2 modelsrather than1 filter.
The rest, in the order they were done
- #2
6b9826c-- every admin body inherits_AdminBody(extra="forbid"). Immediately found a real client/server mismatch: the profiles UI sentnameon the update path, which the body model has no field for. It had been silently dropped. - #7
6b9826c--POST /api/providers/{name}refuses a base-only definition, as delete already did. Also corrected both profile 403s, which advised "add an overlay profile with the same name to override it" -- somethingadmin_profile_createrejects with a 409. - #3 / #4
43645d4-- both off-states exclude, and the override binds/v1/modelsand pinned requests as well as/route._admin_deprecated_modelsrenamed_admin_excluded_models, since "deprecated" no longer describes what it returns. - #8 / #10
281f4fb--_validate_provider_urlapplied tocloud_fallbackandcloud_primary; allowlist add upserts so a note can be corrected. - #5 / #6
48df93b--X-Total-Counton the decisions endpoint, and the page names the window and the history separately. Both nav gaps closed, with a test that derives the page list so a new admin page fails until it is linked. - #12
80d0e6a--ROUTER_IGNORE_LOCAL_CONFIG, set intests/conftest.py. 1613 tests now pass identically with and without the machine's overlay in place.
#9 was not a defect
The sweep flagged the allowlist DELETE returning 200 for an absent entry as
a false confirmation, and the fix was written -- then reverted, because it
broke test_allowlist_delete_idempotent, a test that pins that exact
behaviour with a docstring saying so. That is a decision, not an oversight.
The inconsistency it noticed is still real and is the one open question left here: profile delete and model-override delete both 404 on a missing target while this one does not. Which way that resolves is a call to make, not a bug to fix.
Smaller items still open
The nine UX items listed above are unaddressed except the picker ones: the
"Apply Feedback" button still only previews, "Seed Energy" still spends
money with no confirmation, Manage on a non-allowlist provider still
opens a dead-end panel, and the decisions FLAGS column still has no legend.