feat(web): expose namespace list in prod for filter dropdowns (#582)#604
Merged
feat(web): expose namespace list in prod for filter dropdowns (#582)#604
Conversation
The Search / Timeline / Export NS filter dropdowns and the Home dashboard namespace count + chart all called GET /api/namespaces, which lived in _DEV_ONLY_ROUTERS. Prod users saw only the static "All Namespaces" option and a 0 count card. Issue #582 item 4.10a asked for the dropdowns to populate in prod while keeping NS CRUD (color/description/rename/delete) dev-only — that's the 4.10b track and gates on an ADR (#586). Split the namespaces router into a read surface and an admin surface: - list_namespaces is registered exactly once, on namespaces_read.router (in _PROD_ROUTERS) via add_api_route. The function definition in namespaces.py loses its @router.get decorator and gains a comment pointing readers at the sibling module. - get_namespace, update_metadata, rename_namespace, delete_namespace stay decorated on admin_router in namespaces.py. The module exports router = admin_router so web/app.py's include loop keeps the dev-only mount. get_namespace stays dev-only by design: zero prod-visible JS callers (verified — only PATCH/POST/DELETE in settings-namespaces.js call the per-namespace path) and zero schema gain (NamespaceOut and NamespaceInfoResponse have identical fields, so the list response is a strict superset). Frontend drops the STATE.uiMode !== 'dev' early return in loadNamespaceDropdowns and the devMode-gated nsPromise ternary in loadDashboard. The Namespaces management *tab* (Edit/Rename/Delete cards) keeps its dev-only gate — the buttons need the admin surface. Test coverage: - test_web_mode.py: ("/api/namespaces", "GET") removed from the parametrize list that pins dev-only paths as 404 in prod. - New test_namespaces_list_is_prod_mounted_but_admin_routes_blocked: GET /api/namespaces is 200 in prod; GET-by-id, PATCH, POST/rename, DELETE all 404 in prod. - New test_namespaces_list_remains_reachable_in_dev: dev mode mounts the read router and the admin router, but only one GET handler is registered for /api/namespaces — guards against an accidental re-decoration of list_namespaces on admin_router. Bumped ?v= on app.js (75→76) and settings-namespaces.js (3→4) so the disk cache picks up the dropped early returns. Verified manually: - prod (port 8081): GET /api/namespaces returns 200 with 5 namespaces; PATCH/POST/DELETE/GET-by-id all 404; filter dropdowns populated; Home count card shows 5; Namespaces settings nav button hidden. - dev (port 8082): GET /api/namespaces 200, GET /default 200, PATCH /default 200 — admin surface intact. Prod attack surface widens by exactly one read endpoint exposing the NamespaceOut shape: namespace (str), chunk_count (int), description (str ≤500 chars per NamespaceMetaRequest validator), color (hex pattern). No timestamps, no last-indexed-at fields exist in the schema. Trust model is loopback-only (CORS allow-list at app.py:163). Note: issue #582 body claimed "Backend /api/namespaces already has no dev gating — frontend-only change, ~1 line." That was incorrect — the namespaces module was in _DEV_ONLY_ROUTERS (verified by the now-removed test_web_mode.py:164 parametrize entry). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
The const was declared at the top of loadDashboard but only used 28 lines later for the sessions+scratch dev gate. Inline the predicate at the use site and drop the const — review feedback on the namespace-list-in-prod PR. The JS-source pin in test_web_mode.py is updated to match the new shape (still asserts the gate exists, just on a different string). Co-Authored-By: Claude <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Issue #582 item 4.10a — the Search / Timeline / Export NS filter dropdowns
and the Home dashboard NS count + chart all called
GET /api/namespaces,which was in
_DEV_ONLY_ROUTERS. Prod users got the staticAll Namespacesoption and a 0 count card.
Split the namespaces router so the list endpoint graduates to prod via a
new
namespaces_readmodule while the admin surface (color/descriptionedit, rename, delete, per-namespace info) stays dev-only — that's the 4.10b
track gated on #586.
Implementation
web/routes/namespaces.py: stripped the@router.get("")decorator fromlist_namespaces; renamed module-levelroutertoadmin_routerwith arouter = admin_routeralias at module bottom soweb/app.py'smodule.routerinclude convention keeps mounting the admin surface indev. PATCH / POST/rename / DELETE / GET-by-id all stay on
admin_router.web/routes/namespaces_read.py(new, ~12 lines): re-exportslist_namespaceson a freshAPIRouterviaadd_api_route. Singlesource of truth — the handler body lives in
namespaces.py, theregistration is here.
web/app.py: importsnamespaces_read, inserts it into_PROD_ROUTERS.No change to
_DEV_ONLY_ROUTERS(alias preserves the contract).static/settings-namespaces.js: drops theSTATE.uiMode !== 'dev'earlyreturn in
loadNamespaceDropdowns(). Keeps the equivalent gate onloadNamespacesTab()— that's the CRUD UI which still needs the adminrouter.
static/app.js: drops thedevMode-gatednsPromiseternary inloadDashboard(). ThedevModeconst stays for the/api/sessions+/api/scratchcalls that remain dev-only.static/index.html: bumped?v=onapp.js(75→76) andsettings-namespaces.js(3→4) so disk cache picks up the droppedearly returns.
Test changes
test_web_mode.py: removed("/api/namespaces", "GET")from theparametrize list that pins dev-only paths as 404 in prod (it's no
longer dev-only).
test_namespaces_list_is_prod_mounted_but_admin_routes_blocked:GET non-404 in prod; PATCH, POST/rename, DELETE, and GET-by-id
all 404 in prod.
test_namespaces_list_remains_reachable_in_dev: pins exactly oneGET handler for
/api/namespacesin dev — guards against an accidentalre-decoration of
list_namespacesonadmin_router(FastAPI wouldaccept the duplicate via first-match-wins, but the OpenAPI docs would
show it twice).
Full suite green:
3315 passed, 46 deselectedonpytest -m "not ollama".Ruff check + format clean. Mypy clean on the changed files.
Manual verification
Prod (
uv run mm web, port 8081):GET /api/namespaces→ 200 with 5 namespacesPATCH /api/namespaces/foo→ 404POST /api/namespaces/foo/rename→ 404DELETE /api/namespaces/foo→ 404GET /api/namespaces/foo→ 404 (per-namespace info stays dev-only)5data-ui-tier="dev")Dev (
MEMTOMEM_WEB__MODE=dev mm web, port 8082):GET /api/namespaces→ 200GET /api/namespaces/default→ 200PATCH /api/namespaces/default→ 200Prod attack surface — NamespaceOut field list
The list endpoint now exposes (verified against
web/schemas/namespaces.py):namespacestrchunk_countintdescriptionstr = ""NamespaceMetaRequestvalidator)colorstr = ""^#[0-9a-fA-F]{3,8}$|^$No timestamps, no
last_indexed_at, no user-supplied notes outsidedescription. Trust model is loopback-only (CORS allow-list atweb/app.py:163).Note on the issue body
Issue #582's 4.10a section claimed "Backend
/api/namespacesalreadyhas no dev gating — frontend-only change, ~1 line." That was incorrect.
The
namespacesmodule was in_DEV_ONLY_ROUTERS, andtest_web_mode.py:164actively pinned("/api/namespaces", "GET")as404 in prod. A frontend-only change would have left the dev-only test
guard in place and produced a guaranteed 404 for every prod user opening
the Search tab.
Out of scope
pickers stay dev-only. Tracked in ADR placeholder: namespace CRUD model in prod (auto-create vs predefine) #586 (auto-create vs predefine ADR).
get_namespace(GET /api/namespaces/{name}) — verified zeroprod-visible JS callers (only PATCH/POST/DELETE call the per-namespace
path) and zero schema gain (
NamespaceOut≡NamespaceInfoResponse).Promote separately if a future card needs it.
Test plan
uv run ruff check + format --checkuv run pytest -m "not ollama"— 3315 passeduv run mypyon changed files — cleanmm webprod manual smoke — dropdowns populate, admin paths 404MEMTOMEM_WEB__MODE=dev mm webparity — admin surface intact🤖 Generated with Claude Code