Skip to content

feat(web): expose namespace list in prod for filter dropdowns (#582)#604

Merged
memtomem merged 2 commits intomainfrom
feat/582-4-10a-ns-readonly-prod
Apr 30, 2026
Merged

feat(web): expose namespace list in prod for filter dropdowns (#582)#604
memtomem merged 2 commits intomainfrom
feat/582-4-10a-ns-readonly-prod

Conversation

@memtomem
Copy link
Copy Markdown
Owner

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 static All Namespaces
option and a 0 count card.

Split the namespaces router so the list endpoint graduates to prod via a
new namespaces_read module while the admin surface (color/description
edit, 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 from
    list_namespaces; renamed module-level router to admin_router with a
    router = admin_router alias at module bottom so web/app.py's
    module.router include convention keeps mounting the admin surface in
    dev. PATCH / POST/rename / DELETE / GET-by-id all stay on admin_router.
  • web/routes/namespaces_read.py (new, ~12 lines): re-exports
    list_namespaces on a fresh APIRouter via add_api_route. Single
    source of truth — the handler body lives in namespaces.py, the
    registration is here.
  • web/app.py: imports namespaces_read, inserts it into _PROD_ROUTERS.
    No change to _DEV_ONLY_ROUTERS (alias preserves the contract).
  • static/settings-namespaces.js: drops the STATE.uiMode !== 'dev' early
    return in loadNamespaceDropdowns(). Keeps the equivalent gate on
    loadNamespacesTab() — that's the CRUD UI which still needs the admin
    router.
  • static/app.js: drops the devMode-gated nsPromise ternary in
    loadDashboard(). The devMode const stays for the /api/sessions +
    /api/scratch calls that remain dev-only.
  • static/index.html: bumped ?v= on app.js (75→76) and
    settings-namespaces.js (3→4) so disk cache picks up the dropped
    early returns.

Test changes

  • test_web_mode.py: removed ("/api/namespaces", "GET") from the
    parametrize list that pins dev-only paths as 404 in prod (it's no
    longer dev-only).
  • New 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.
  • New test_namespaces_list_remains_reachable_in_dev: pins exactly one
    GET handler for /api/namespaces in dev — guards against an accidental
    re-decoration of list_namespaces on admin_router (FastAPI would
    accept the duplicate via first-match-wins, but the OpenAPI docs would
    show it twice).

Full suite green: 3315 passed, 46 deselected on pytest -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 namespaces
  • PATCH /api/namespaces/foo → 404
  • POST /api/namespaces/foo/rename → 404
  • DELETE /api/namespaces/foo → 404
  • GET /api/namespaces/foo → 404 (per-namespace info stays dev-only)
  • ✅ Search / Timeline / Export NS dropdowns all populated (5 entries each)
  • ✅ Home dashboard NS count card shows 5
  • ✅ Namespaces settings nav button hidden (data-ui-tier="dev")

Dev (MEMTOMEM_WEB__MODE=dev mm web, port 8082):

  • GET /api/namespaces → 200
  • GET /api/namespaces/default → 200
  • PATCH /api/namespaces/default → 200
  • No regression vs current dev UX.

Prod attack surface — NamespaceOut field list

The list endpoint now exposes (verified against web/schemas/namespaces.py):

Field Type Constraint
namespace str (free)
chunk_count int (free)
description str = "" ≤500 chars (NamespaceMetaRequest validator)
color str = "" hex pattern ^#[0-9a-fA-F]{3,8}$|^$

No timestamps, no last_indexed_at, no user-supplied notes outside
description. Trust model is loopback-only (CORS allow-list at
web/app.py:163).

Note on the issue body

Issue #582's 4.10a section 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, and
test_web_mode.py:164 actively pinned ("/api/namespaces", "GET") as
404 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

  • NS CRUD prod surface (4.10b) — Edit/Rename/Delete buttons + color
    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 zero
    prod-visible JS callers (only PATCH/POST/DELETE call the per-namespace
    path) and zero schema gain (NamespaceOutNamespaceInfoResponse).
    Promote separately if a future card needs it.

Test plan

  • uv run ruff check + format --check
  • uv run pytest -m "not ollama" — 3315 passed
  • uv run mypy on changed files — clean
  • mm web prod manual smoke — dropdowns populate, admin paths 404
  • MEMTOMEM_WEB__MODE=dev mm web parity — admin surface intact

🤖 Generated with Claude Code

pandas-studio and others added 2 commits April 30, 2026 15:51
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]>
@memtomem memtomem merged commit 1ee92dd into main Apr 30, 2026
7 checks passed
@memtomem memtomem deleted the feat/582-4-10a-ns-readonly-prod branch April 30, 2026 07:00
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants