Repository navigation
Route the orphaned NamespacesAppPage as the real /app/namespaces console #16
Description
Context
A fully-built namespace management console, NamespacesAppPage, exists at apps/web/src/main.tsx:5943 (token create/revoke, public/private visibility + directory flag, multi-source builder, schedules, alerts, webhooks) but is never routed. The /app/namespaces route (apps/web/src/main.tsx:1575) instead renders the lightweight read-only NamespacesSimplePage (main.tsx:5893). The orphaned page already calls live hosted endpoints (/api/hosted/namespaces, /api/hosted/directory, /api/hosted/schedules, /api/hosted/alerts, /api/hosted/namespaces/:ns/tokens), so all namespace/memory CRUD is built but unreachable from the UI.
Goal / user story
As an authenticated user, I can open /app/namespaces and manage my namespaces end-to-end — create/revoke read tokens, flip public/private + directory visibility, run the multi-source builder, and view/edit schedules, alerts, and webhooks — without dropping to the CLI or API.
Acceptance criteria
-
/app/namespacesrenders<NamespacesAppPage authToken={sessionToken} />instead of<NamespacesSimplePage />(swap atmain.tsx:1575). - Each
/api/hosted/*endpoint the page consumes is confirmed live inapps/api/src/worker.ts(audited and listed in the PR description). - Token create + revoke works against
/api/hosted/namespaces/:ns/tokensand a freshly created token is shown once. - Visibility (
private/public) +directoryEnabledtoggle persists via the metadata save and survives a refresh. - Schedules, alerts, and webhooks lists render and are creatable; a failed
refresh()shows a visible error (not an unhandled throw). - The decision to keep or retire
NamespacesSimplePage(e.g. reuse it for the public showcase browse) is documented; no other call site regresses.
Implementation notes
- Single-line route swap at
apps/web/src/main.tsx:1575; the component signature isNamespacesAppPage({ authToken })and it already usesauthHeaders(authToken). refresh()(main.tsx:5969) throws on non-ok responses — make sure theuseEffectcatch surfaceserrorstate in the UI (this pairs with the skeleton/error-card issue).- The visibility toggle (
metadataDraft.visibility, ~main.tsx:5950) is the exact surface where a future "Encrypted (SEAL)" badge and Harbor bucket/seal_policy_idindicators will live — exposing it now is the prerequisite for that on-chain UI. - Gotcha: no skeleton/loading exists in this page yet; either land minimal inline loaders here or sequence after the skeleton-system issue.
Sui Overflow angle
This console is where private-vs-public namespace control becomes visible. Once routed, it is the natural home to surface Harbor Bucket + seal_policy_id objects and Sui explorer deep-links — i.e. the place the on-chain provenance/encryption story gets demoed. It is the single biggest UX unlock and a prerequisite for every later Sui-native namespace UI.
Dependencies
None to ship the route swap. Enhanced by the skeleton/empty/error-states issue and the Sui wallet sign-in issue (for sessionToken/identity).
Part of the ContextMEM roadmap (#4) • Sui Overflow build.
Activity
harrymove-ctrl commentedon Jun 19, 2026
Implementation Plan
Approach
The literal ask — swap
NamespacesSimplePageforNamespacesAppPageatapps/web/src/main.tsx:1575— is a one-liner, andNamespacesAppPage(main.tsx:5943) already exists, typechecks, and hits the right/api/hosted/*paths. The real work is making those endpoints actually authorize browser-direct against the deployed Worker. Audit finding: every consumed endpoint is live (the Worker rewrites/api/hosted/*→/api/*atworker.ts:581-585), but all management handlers gate onrequireImportAuthorization(a sharedCONTEXTMEM_NAMESPACE_IMPORT_TOKEN,worker.ts:4436-4441) and scope data by a?ownerId=query param. Today only the local Fastify proxy (apps/api/src/index.ts:887-934) supplies both (it validates the user session, then calls the Worker with the import token +ownerId=auth.account.id). In production (Pages →*.workers.dev, no Fastify) the page sendsBearer <sessionToken>+x-memwal-*delegate headers and no ownerId, so the calls 401. The key design decision: add a delegate-based ownership path to the Worker's namespace/token/schedule/alert routes (deriveownerId = hostedOwnerId(x-memwal-account-id)server-side), mirroring the existing run-auth pattern (resolveHostedRunAuthworker.ts:1462,hostedOwnerIdworker.ts:1512). Ship as two slices: (1) the route swap + error/loading polish + SimplePage decision (works on local-dev immediately, renders cleanly on prod), and (2) the Worker auth/scoping change that makes token/visibility/schedule CRUD actually pass on the deployed Worker.Prerequisites & dependencies
sessionToken/identity). Neither blocks.apps/api/) is not shipped by CI — after merging slice 2 it needs a manualwrangler deploy; Pages auto-deploys the frontend. The frontend change alone will visibly 401 on prod until the Worker is redeployed.CONTEXTMEM_NAMESPACE_IMPORT_TOKEN; it authorizes via the delegate headers the browser already sends.git pushuses theharrymove-ctrlgh account if it 404s.Step-by-step
Route swap (AC1). In
apps/web/src/main.tsx:1575, replace<NamespacesSimplePage />with<NamespacesAppPage authToken={sessionToken} />. The component signature isNamespacesAppPage({ authToken }: { authToken: string })(main.tsx:5943) and it already usesauthHeaders(authToken). No new imports — every icon/type it uses (HostedNamespaceSummary,Clipboard,KeyRound, etc.) is already in the file. Optionally update the route's shell description string (currently "open one to browse its knowledge graph") to reflect management. Note the route is insiderenderShell, which gates children onhasMemWalDelegate(main.tsx:1492-1510) — so the page only renders for delegate-imported users; keep that.Endpoint audit for the PR description (AC2). Document this verified mapping (page call → Worker rewrite → handler), all confirmed live:
GET /api/hosted/namespaces→GET /api/namespaces→listManagedNamespaces(worker.ts:682,921) — gate:requireImportAuthorization, scope:?ownerId=GET /api/hosted/directory→GET /api/directory→listDirectoryNamespaces(worker.ts:687) — public, no auth (always works)GET /api/hosted/schedules→listSchedules(worker.ts:733,1703) — scope?ownerId=defaulting to"anonymous"GET /api/hosted/alerts→listAlerts(worker.ts:743,1741) — same defaultGET/POST /api/hosted/namespaces/:ns/tokens→listNamespaceTokens/createNamespaceToken(worker.ts:700-701,976,992); POST returns{ token, readToken }(worker.ts:~1008) — page readsbody.readTokenDELETE …/tokens/:id→revokeNamespaceToken(worker.ts:702)PATCH /api/hosted/namespaces/:ns→updateNamespace(worker.ts:704,951) — handlesvisibility+directoryEnabledPOST /api/hosted/namespace-builds→createNamespaceBuild(worker.ts:718,1895) — usesinput.ownerIdfrom bodyPOST /api/hosted/schedules/PATCH …/:id→createSchedule/updateSchedule(worker.ts:728,738)Call out the gap explicitly in the PR: management routes need the import token or (after slice 2) delegate headers, and need
ownerId; only/api/directoryis reachable browser-direct today.Minimal loading + error polish (AC5).
refresh()(main.tsx:5969) already throws on non-ok and theuseEffectcatch (main.tsx:5988-5990) setserror, rendered atmain.tsx:6182({error ? <div className="error">…}). This already satisfies "a failed refresh shows a visible error, not an unhandled throw." Add: (a) aloadingboolean set around the initialrefresh()so the page shows an inline "Loading namespaces…" instead of an empty flash (no skeleton system needed); (b) a "Retry" button on the error card that re-invokesrefresh().catch(setError). Keep it inline — do not pull in the skeleton-system issue.Worker: delegate-based ownership for management routes (makes AC3/AC4 pass on prod). Add a helper
resolveManagementOwner(request, env)inworker.tsthat returns{ ok, ownerId }:requireImportAuthorization(request, env).ok→ honor explicitownerIdfrom query/body (preserves the Fastify-proxy path);resolveHostedRunAuth/thex-memwal-account-id+ delegate parse atworker.ts:1462) →ownerId = hostedOwnerId(accountId)(worker.ts:1512), authorized;Apply it at the route guards currently calling
requireImportAuthorizationfor the namespace/token/build/schedule/alert routes (worker.ts:683,698,705,714,719,729,734,739,744). Then make handlers use the resolvedownerIdinstead of trusting the raw query param:listManagedNamespaces(worker.ts:921-948),listSchedules/listAlerts(default to the resolved owner, not"anonymous",worker.ts:1704,1742),assertManagedNamespace(used byupdateNamespace/token handlers), and inject the resolvedownerIdinto the parsed body ofcreateNamespaceBuild(input.ownerId,worker.ts:1916) andcreateSchedulewhen absent. This keeps owner_id values consistent (hosted:<memwalAccountId>) across create + list on the Worker-only prod topology. Ensurex-memwal-*headers are CORS-allowed (they already are — runs use them).SimplePage decision + no-regression check (AC6). Grep confirms
NamespacesSimplePagehas exactly one call site — the route being swapped (main.tsx:1575); its definition ismain.tsx:5893. It is the only reader of the public seeded catalogGET /api/memwal/factsand deep-links into/app/memory?ns=. Recommendation: keep it, repurpose for the public/unauthenticated browse (e.g. a/showcase-adjacent public namespace list) rather than delete, sinceNamespacesAppPageis delegate-gated and shows only owned namespaces. Document this in the PR. If not repurposed this milestone, leave the function in place (unreferenced) with a one-line comment so the public-browse issue can pick it up; do not delete (avoids churn + preserves the/api/memwal/factsconsumer).Verify owner_id continuity. Confirm that namespaces/schedules created via the page after slice 2 are listed back (same derived
ownerId). Spot-checkcreateNamespaceBuildJobstoresowner_id = input.ownerId(worker.ts:1910-1916) and thatlistManagedNamespacesfiltersowner_id = ?(worker.ts:930).New / changed files
apps/web/src/main.tsx— route element at:1575(swap);loadingstate + Retry button inNamespacesAppPage(~:5966-5990,:6180-6182); SimplePage left in place / repurposed.apps/api/src/worker.ts— newresolveManagementOwnerhelper; updated route guards (:683,698,705,714,719,729,734,739,744) and handlers (listManagedNamespaces,listSchedules,listAlerts,assertManagedNamespace,createNamespaceBuild,createSchedule) to use delegate-derivedownerId.apps/api/src/worker.test.ts— new auth/scoping cases (see below).apps/api/src/index.ts(Fastify proxy keeps working; import-token path preserved).Test plan
apps/api/src/worker.test.ts— forGET /api/namespaces,POST/DELETEtokens,PATCH /api/namespaces/:ns,POST /api/namespace-builds,GET /api/schedules|alerts: (a) withx-memwal-account-id+ delegate headers → 200 and results scoped tohosted:<accountId>; (b) with the import token + explicitownerId→ still 200 (Fastify path regression guard); (c) with neither → 401. Add a round-trip: create a namespace-build as account A, assert it appears in A's list and NOT in B's.bunFastify + Vite): import a delegate, open/app/namespaces; create a token → assertfreshTokenshown once (main.tsx:6229) then appears in the list as active, revoke → shows revoked; flip visibility public + Directory, Save, hard-refresh the page → assert persisted (proves PATCH + refresh re-fetch); create a schedule → appears in list; trigger a failing refresh (e.g. invalid token) → assert error card + Retry, no white-screen.wrangler deploy): from the deployed Pages app with a delegate imported, repeat the token create/revoke + visibility flow and confirm 200s (proves the delegate-auth path). What proves it works end-to-end: a created read token round-trips (POST returnsreadToken, GET lists it, DELETE flipsrevoked) against the deployed*.workers.dev.bun-check.yml(typecheck + test + Vite build) must stay green; the frontend swap must not break the build.Risks & gotchas
CONTEXTMEM_NAMESPACE_IMPORT_TOKENand sends noownerId). Local-dev works because Fastify injects both. If the team scopes this issue to "route swap only," explicitly state that AC3/AC4 are satisfied on local-dev and slice 2 is required for prod.requireImportAuthorizationalso gates/api/extractions,/api/namespaces/import, etc. KeepresolveManagementOwneradditive (don't remove the import-token branch) so the Fastify proxy and import flow don't regress.ownerIdmust equal what create stored; on the Worker-only prod path both usehostedOwnerId(memwalAccountId), so they match — but historical namespaces created via Fastify carry the local account id, nothosted:*, and won't show under the delegate path. Note this migration caveat.wrangler deploy(and a staging verify) or prod will 401.sessionTokenmay be empty on hosted (the Worker has no import-delegate route, per project notes), so AC1'sauthToken={sessionToken}can be"". That's fine after slice 2 because authorization rides thex-memwal-*delegate headers (authHeadersmerges them when API_BASE is non-local,main.tsx:7910-7913); the empty Bearer is harmless.metadataDraft.visibility,main.tsx:5950) is the intended home for a future "Encrypted (SEAL)" badge / Harborseal_policy_idindicators — don't restructure it; just expose it.Acceptance-criteria mapping
NamespacesAppPage) → Step 1.worker.ts:992/702,freshTokenUImain.tsx:6019/6040/6229); works local today, prod after Step 4.updateNamespaceworker.ts:951; PATCHmain.tsx:6064; refresh re-fetch).main.tsx:5988-5990/render:6182, plus loader + Retry).Effort
Implementation plan generated for execution (planning phase). Part of the roadmap (#4).