Skip to content

Phase 5: Merchant Ops UI v1 — Findings

Status: Complete ✅ (staging end-to-end validated via browser test 2026-05-12; all PR D/E.1/E.2 endpoints return real data; sidebar Soon-affordance for un-built routes; only Phase 4 DCA + catalog index pages remain deferred) Predecessor: Phase 5 Kickoff Plan · Implementation Plan Exit criterion target: "Operator dashboard live at dashboard-staging.exzentcg.com, gated by Cloudflare Access, talking to staging Medusa, with orders / exceptions / inventory / staff all rendering real data."

1. What shipped

Twelve PRs merged on ExzenTCG/tcg-platform between 2026-05-09 and 2026-05-12:

PR Title Effect
#51 Phase 5 PR A — monorepo cutover apps/server/ + apps/dashboard/ + packages/shared-types/; yarn workspaces
#52 Phase 5 PR B — staff RBAC + auth bridge user_role table, capabilities, COOKIE_DOMAIN=.exzentcg.com
#54 CLAUDE.md bootstrap Agent context for monorepo + dashboard
#55 Phase 5 PR C — dashboard skeleton Next 16 App Router scaffold, login, AppShell, 18 Playwright tests
#56 Phase 5 PR D — orders + exceptions /admin/dashboard/orders[/:id][/:id/cancel], /admin/dashboard/exceptions[/:id][/:id/retry][/:id/resolve], 10 more E2E tests
#57 Phase 5 PR E.1 — inventory + transfer /admin/dashboard/inventory/by-location, /admin/dashboard/inventory/transfer
#58 Phase 5 PR E.2 — variant editor /admin/dashboard/products/sku-generator, /admin/dashboard/products/:id/variants/bulk, per-product variant editor
#59 Phase 5 PR F — staff + polish Staff CRUD UI, breadcrumbs, segment error boundary, 404 page
#60 Phase 5 PR G — CF Workers deploy apps/dashboard/wrangler.jsonc, open-next.config.ts, GH Actions deploy workflow
#61 fix(dashboard): proxy.ts → middleware.ts Edge-runtime compat for @opennextjs/cloudflare (see §3)
#62 fix(dashboard): CF Access service-token headers Worker→Medusa server-to-server fetch through Access (see §3)
#67 fix: yarn hoist @medusajs/medusa + sidebar Soon pills Unblocks staging Medusa rebuild (see §3)

Cross-link: dashboard lives at https://dashboard-staging.exzentcg.com. Worker name tcg-dashboard-staging. The full staging runbook lives in homelab/05_service_deployments/tcg-staging.md.

2. End-to-end browser verification (2026-05-12)

Playwright drove the dashboard against staging through Cloudflare Access + Medusa. All authenticated routes returned real data:

Route Result Data shape
/ (home) ✅ Renders, 0 console errors Operator role, 9 capabilities
/orders 2 Shopee orders (the Phase 3 staging fixtures), filters present, pagination scaffold
/exceptions 4 open exceptions (2× signature mismatch from partner-portal probes, 1× cross-shop, 1× Phase 3 negative-SKU test)
/inventory 23 SKUs (Pokemon, One Piece, Yu-Gi-Oh + accessories); reserved count correct on the Pikachu RC01 row that the Shopee order consumed
/staff 1-row table (admin), Invite link
/products, /variants, /finance/costing n/a (Soon pill, no Link) PR #67 added a comingSoon flag to NavItem — those three sidebar items now render as disabled <span> + "Soon" badge instead of broken Link prefetches

3. Key gotchas discovered

3a. Next 16 proxy.ts is incompatible with Cloudflare Workers' Edge runtime

@opennextjs/cloudflare only supports Edge-runtime middleware. Next 16 introduced proxy.ts as a Node-runtime replacement for the legacy middleware.ts, and adding export const config = { runtime: 'edge' } to a proxy.ts errors at build time ("Proxy does not support Edge runtime"). The legacy middleware.ts filename is still supported in Next 16 for backward compatibility, and ships on Edge by default. PR #61 renamed the file back. Comments in the file explain why; future Next upgrades should not auto-codemod it back to proxy.ts.

3b. Worker → Medusa fetch needs a Cloudflare Access service token

The dashboard hostname and the Medusa hostname both sit behind separate Cloudflare Access apps. Browser users carry an Access SSO cookie that the Worker cannot replicate when it does its own server-side fetch() to Medusa — the Worker hits Access, gets a 302 to the SSO HTML login page, and res.json() throws Unexpected token '<'. Fix (PR #62): Cloudflare Access service token + Service Auth policy on the Medusa Access app, and the Worker sends CF-Access-Client-Id + CF-Access-Client-Secret headers on every outbound. The headers are emitted only when both env vars are present, so local dev (Medusa on localhost behind no gate) is unaffected. Implementation: getCfAccessHeaders() in apps/dashboard/lib/medusa.ts; same helper threaded into the lone direct fetch in apps/dashboard/lib/auth.ts for the /auth/session step.

3c. Yarn 4 demoted @medusajs/medusa to a nested install after PR G

PR G added @opennextjs/cloudflare + wrangler to apps/dashboard. Those new transitives shifted yarn 4's hoist decisions: @medusajs/utils stayed hoisted at root (multiple consumers), but @medusajs/medusa got demoted to apps/server/node_modules/@medusajs/medusa/ because it has a single consumer. The require chain inside @medusajs/utils/dist/common/dynamic-import.js then walked up from /server/node_modules/@medusajs/utils/..., didn't find @medusajs/medusa at root, and threw MODULE_NOT_FOUND. db:sync-links exited non-zero, the entrypoint's set -e killed the container, docker restart loop. Fix (PR #67): pin @medusajs/medusa@2.14.1 as a root devDependency in the monorepo's package.json to force the hoist. Verification:

node -e "console.log(require.resolve('@medusajs/medusa/link-modules'))"
# → node_modules/@medusajs/medusa/dist/modules/link-modules.js (the wildcard "./*": "./dist/modules/*.js" export does the remap)

If another single-consumer Medusa package gets similarly demoted in the future, the fix is the same line.

3d. CT 105's 20 GB rootfs hits 100% after a few rebuilds

The full rebuild cycle for PR #67 needed about 8 GB of layer extraction; the previous rebuild's BuildKit cache filled the remaining headroom and the build ENOSPC'd partway through. docker system prune -af (without --volumes — the Postgres data lives in tcg-platform_postgres-data) reclaimed 11.27 GB cleanly. Adding to the staging runbook.

4. Sidebar comingSoon affordance

apps/dashboard/components/shell/nav-config.ts learned a comingSoon?: boolean field on NavItem. The sidebar renders flagged items as a non-link <span> with aria-disabled="true", title="Coming soon", a "Soon" pill, and reduced opacity. Currently flagged:

  • /products — a catalog index doesn't exist yet (variant-editor is reached via /products/[id]/variant-editor)
  • /variants — there is no top-level variant browser
  • /finance/costing — Phase 4 DCA, on D2's track

Browser test confirmed the three 404 prefetches that the sidebar previously triggered are gone. When a real page lands, just remove the flag from nav-config.ts.

5. Open follow-ups

  • Catalog index pages. Either build a /products list view (cheap; the inventory page already lists SKUs and could be templated) or remove /products + /variants from the sidebar entirely. The variant editor is currently only reachable from /products/[id]/variant-editor — no top-level entry point.
  • Phase 4 DCA / costing surface. When DCA ships, the existing PR D order-detail page's Totals section should grow a per-channel breakdown.
  • Exception triage hardening. During verification, four open exceptions surfaced — two were Shopee partner-portal probes (event_type: 99, signature: null), one was a cross-shop webhook (different shop_id than the merchant's), one was the Phase 3 deliberate negative-SKU test. A one-line filter in the Shopee webhook handler (event_type == 99 → return 200 without persisting an exception) would keep test pushes from cluttering the operator UI. Not urgent.
  • Snapshot housekeeping. CT 105 snapshot chain is now: initial-deploy → phase-2-seeded → pre-phase5-pra-test → pre-phase5-prb-test → pre-phase5-prd-prg-test → pre-yarn-hoist-fix → current. After one week of stable Phase 5 operation (so on/after 2026-05-19), the four pre-phase5-* and pre-yarn-hoist-fix snapshots are safe to drop.
  • CI actions/setup-node@v4 deprecation. GitHub will force the Node-20-on-runner actions to Node 24 from 2026-06-02. deploy-dashboard-staging.yml uses actions/checkout@v4 + actions/setup-node@v4 — bump to @v5 ahead of June.

6. What "complete" means here

  • All four primary operator surfaces (orders / exceptions / inventory / staff) render real data on staging.
  • The Worker → Access → Medusa security path is intact and documented.
  • The monorepo's yarn hoist is pinned against the regression that knocked staging out for a day.
  • The dead-link sidebar entries are visibly marked deferred rather than failing silently.

Phase 6 begins when the merchant has used the dashboard in daily operations for at least a week and we have concrete UX-iteration tickets from real use, rather than synthetic ones from the implementation plan.