Skip to content

Route the orphaned NamespacesAppPage as the real /app/namespaces console #16

Open

Description

@harrymove-ctrl

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/namespaces renders <NamespacesAppPage authToken={sessionToken} /> instead of <NamespacesSimplePage /> (swap at main.tsx:1575).
  • Each /api/hosted/* endpoint the page consumes is confirmed live in apps/api/src/worker.ts (audited and listed in the PR description).
  • Token create + revoke works against /api/hosted/namespaces/:ns/tokens and a freshly created token is shown once.
  • Visibility (private/public) + directoryEnabled toggle 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 is NamespacesAppPage({ authToken }) and it already uses authHeaders(authToken).
  • refresh() (main.tsx:5969) throws on non-ok responses — make sure the useEffect catch surfaces error state 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_id indicators 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

  1. added this to the Sui Overflow milestone on Jun 16, 2026
  2. added
    P0Demo-blocking: required for a working Sui Overflow demo
    featureUser- or agent-facing capability
    ui-uxWeb app UI/UX, accessibility, and responsive design
    on Jun 16, 2026
  3. harrymove-ctrl commented on Jun 19, 2026

    @harrymove-ctrl
    OwnerAuthor

    Implementation Plan

    Approach
    The literal ask — swap NamespacesSimplePage for NamespacesAppPage at apps/web/src/main.tsx:1575 — is a one-liner, and NamespacesAppPage (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/* at worker.ts:581-585), but all management handlers gate on requireImportAuthorization (a shared CONTEXTMEM_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 sends Bearer <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 (derive ownerId = hostedOwnerId(x-memwal-account-id) server-side), mirroring the existing run-auth pattern (resolveHostedRunAuth worker.ts:1462, hostedOwnerId worker.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

    • No blocking issues for slice 1 (route swap). Slice 2 (prod auth) is self-contained Worker work, no other issue gates it.
    • Enhanced by the skeleton/empty/error-states issue (this plan lands only minimal inline loaders) and the Sui wallet sign-in issue (cleaner sessionToken/identity). Neither blocks.
    • Deploy reality (from project memory): the Worker (apps/api/) is not shipped by CI — after merging slice 2 it needs a manual wrangler deploy; Pages auto-deploys the frontend. The frontend change alone will visibly 401 on prod until the Worker is redeployed.
    • No new bindings/secrets required. Slice 2 deliberately avoids needing the browser to hold CONTEXTMEM_NAMESPACE_IMPORT_TOKEN; it authorizes via the delegate headers the browser already sends.
    • Confirm git push uses the harrymove-ctrl gh account if it 404s.

    Step-by-step

    1. Route swap (AC1). In apps/web/src/main.tsx:1575, replace <NamespacesSimplePage /> with <NamespacesAppPage authToken={sessionToken} />. The component signature is NamespacesAppPage({ authToken }: { authToken: string }) (main.tsx:5943) and it already uses authHeaders(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 inside renderShell, which gates children on hasMemWalDelegate (main.tsx:1492-1510) — so the page only renders for delegate-imported users; keep that.

    2. 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 default
      • GET/POST /api/hosted/namespaces/:ns/tokens → listNamespaceTokens/createNamespaceToken (worker.ts:700-701,976,992); POST returns { token, readToken } (worker.ts:~1008) — page reads body.readToken
      • DELETE …/tokens/:id → revokeNamespaceToken (worker.ts:702)
      • PATCH /api/hosted/namespaces/:ns → updateNamespace (worker.ts:704,951) — handles visibility + directoryEnabled
      • POST /api/hosted/namespace-builds → createNamespaceBuild (worker.ts:718,1895) — uses input.ownerId from body
      • POST /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/directory is reachable browser-direct today.
    3. Minimal loading + error polish (AC5). refresh() (main.tsx:5969) already throws on non-ok and the useEffect catch (main.tsx:5988-5990) sets error, rendered at main.tsx:6182 ({error ? <div className="error">…}). This already satisfies "a failed refresh shows a visible error, not an unhandled throw." Add: (a) a loading boolean set around the initial refresh() 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-invokes refresh().catch(setError). Keep it inline — do not pull in the skeleton-system issue.

    4. Worker: delegate-based ownership for management routes (makes AC3/AC4 pass on prod). Add a helper resolveManagementOwner(request, env) in worker.ts that returns { ok, ownerId }:

      • if requireImportAuthorization(request, env).ok → honor explicit ownerId from query/body (preserves the Fastify-proxy path);
      • else if valid hosted delegate headers present (reuse resolveHostedRunAuth/the x-memwal-account-id + delegate parse at worker.ts:1462) → ownerId = hostedOwnerId(accountId) (worker.ts:1512), authorized;
      • else 401.
        Apply it at the route guards currently calling requireImportAuthorization for the namespace/token/build/schedule/alert routes (worker.ts:683,698,705,714,719,729,734,739,744). Then make handlers use the resolved ownerId instead 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 by updateNamespace/token handlers), and inject the resolved ownerId into the parsed body of createNamespaceBuild (input.ownerId, worker.ts:1916) and createSchedule when absent. This keeps owner_id values consistent (hosted:<memwalAccountId>) across create + list on the Worker-only prod topology. Ensure x-memwal-* headers are CORS-allowed (they already are — runs use them).
    5. SimplePage decision + no-regression check (AC6). Grep confirms NamespacesSimplePage has exactly one call site — the route being swapped (main.tsx:1575); its definition is main.tsx:5893. It is the only reader of the public seeded catalog GET /api/memwal/facts and 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, since NamespacesAppPage is 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/facts consumer).

    6. Verify owner_id continuity. Confirm that namespaces/schedules created via the page after slice 2 are listed back (same derived ownerId). Spot-check createNamespaceBuildJob stores owner_id = input.ownerId (worker.ts:1910-1916) and that listManagedNamespaces filters owner_id = ? (worker.ts:930).

    New / changed files

    • apps/web/src/main.tsx — route element at :1575 (swap); loading state + Retry button in NamespacesAppPage (~:5966-5990, :6180-6182); SimplePage left in place / repurposed.
    • apps/api/src/worker.ts — new resolveManagementOwner helper; updated route guards (:683,698,705,714,719,729,734,739,744) and handlers (listManagedNamespaces, listSchedules, listAlerts, assertManagedNamespace, createNamespaceBuild, createSchedule) to use delegate-derived ownerId.
    • apps/api/src/worker.test.ts — new auth/scoping cases (see below).
    • No changes needed to apps/api/src/index.ts (Fastify proxy keeps working; import-token path preserved).

    Test plan

    • Unit/integration (Worker): extend apps/api/src/worker.test.ts — for GET /api/namespaces, POST/DELETE tokens, PATCH /api/namespaces/:ns, POST /api/namespace-builds, GET /api/schedules|alerts: (a) with x-memwal-account-id + delegate headers → 200 and results scoped to hosted:<accountId>; (b) with the import token + explicit ownerId → 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.
    • Manual (local dev, bun Fastify + Vite): import a delegate, open /app/namespaces; create a token → assert freshToken shown 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.
    • Manual (prod/staging Worker after 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 returns readToken, GET lists it, DELETE flips revoked) against the deployed *.workers.dev.
    • CI: bun-check.yml (typecheck + test + Vite build) must stay green; the frontend swap must not break the build.

    Risks & gotchas

    • The auth gate is the whole risk. Without slice 2, the page renders but every management call 401s on the deployed Worker (browser lacks CONTEXTMEM_NAMESPACE_IMPORT_TOKEN and sends no ownerId). 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.
    • Worker is ~8.2k lines and the touched route guards are shared. requireImportAuthorization also gates /api/extractions, /api/namespaces/import, etc. Keep resolveManagementOwner additive (don't remove the import-token branch) so the Fastify proxy and import flow don't regress.
    • owner_id format mismatch would silently return empty lists. The derived ownerId must equal what create stored; on the Worker-only prod path both use hostedOwnerId(memwalAccountId), so they match — but historical namespaces created via Fastify carry the local account id, not hosted:*, and won't show under the delegate path. Note this migration caveat.
    • Deploy split: frontend ships via Pages automatically; the Worker change needs manual wrangler deploy (and a staging verify) or prod will 401.
    • sessionToken may be empty on hosted (the Worker has no import-delegate route, per project notes), so AC1's authToken={sessionToken} can be "". That's fine after slice 2 because authorization rides the x-memwal-* delegate headers (authHeaders merges them when API_BASE is non-local, main.tsx:7910-7913); the empty Bearer is harmless.
    • No skeleton system yet — keep loaders inline; do not block on the skeleton issue.
    • The visibility toggle (metadataDraft.visibility, main.tsx:5950) is the intended home for a future "Encrypted (SEAL)" badge / Harbor seal_policy_id indicators — don't restructure it; just expose it.

    Acceptance-criteria mapping

    • AC1 (route renders NamespacesAppPage) → Step 1.
    • AC2 (each endpoint confirmed live + listed in PR) → Step 2 (audit table + auth-gap note).
    • AC3 (token create + revoke; fresh token shown once) → Steps 1 + 4 (handlers worker.ts:992/702, freshToken UI main.tsx:6019/6040/6229); works local today, prod after Step 4.
    • AC4 (visibility + directoryEnabled persists across refresh) → Steps 1 + 4 (updateNamespace worker.ts:951; PATCH main.tsx:6064; refresh re-fetch).
    • AC5 (schedules/alerts/webhooks render & creatable; failed refresh shows visible error) → Steps 3 + 4 (existing catch main.tsx:5988-5990/render :6182, plus loader + Retry).
    • AC6 (keep/retire SimplePage documented; no call-site regresses) → Step 5 (single call site confirmed; recommend keep/repurpose).

    Effort

    • Slice 1 only (route swap + loader/error polish + SimplePage decision + audit): S, ~0.5 person-day.
    • Full issue incl. slice 2 (Worker delegate-auth/ownerId + tests + staging deploy verify so the ACs pass on prod): M, ~2-3 person-days.

    Implementation plan generated for execution (planning phase). Part of the roadmap (#4).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P0Demo-blocking: required for a working Sui Overflow demofeatureUser- or agent-facing capabilityui-uxWeb app UI/UX, accessibility, and responsive design

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions