Skip to content

Decision 0023 — Role×Screen Office/Mobile Portal Boundary

  • Status: ACCEPTED
  • Date: 2026-09-02
  • Deciders: CTC Product Owner, Architecture Review, NS3-046
  • Affected: PRODUCT.md, DESIGN.md, UI_FOUNDATION.md, ROADMAP.md, workspacePageCatalog.ts, AppShell.tsx, portal-surface-authority.test.mjs
  • Related: 0016-six-phase-delivery-and-tenant-productization.md, 0017-tenant-runtime-authority.md, NS3-046

Context

Phase 3 must reconstruct every current CTC screen. The current AppShell.tsx is Office-oriented with workspace tabs. The same route registry contains field screens (driver-operation, yard-instruction, inbound-loader) that require one-task-at-a-time execution. Viewport-based routing would divert Office users on phones to the wrong portal, and a user-level mobile flag would create a tenant-specific source branch.

Current facts (frozen 2026-09-02, qa@93ffba688):

  • AppShell.tsx:60 owns Office tabs; workspacePageCatalog.ts:12 owns 96 registered slugs including 3 field candidates
  • workspaceSession.ts:40 resolves principal via persisted T_ROLE/T_MENU + @ctc/policy module/capability; no User.mobile
  • authRoutes.ts:30 preserves sanitized next path; no Portal contract
  • driver-operation / yard-instruction / inbound-loader are PARTIAL/TARGET_ROLE but lack Portal classification
  • NS3-040 checkpoint preserves Office planning (yard-dispatch); NS3-045 owns visual evidence only

Problem: without an explicit Portal classification, the same narrow viewport or a future mobile user attribute could grant, deny or select Portal access, duplicating business authority and breaking Office-on-phone support.

Decision

Admit Option B — Separate Portal Shells Inside Current apps/web with a single exact-screen classification owner.

  • PortalKind = OFFICE | MOBILE | DEFERRED (deferred = not in current Phase-3 scope)
  • Classification owner: new-system/apps/web/src/composition/portalClassification.ts (new, tenant-neutral)
  • input: exact slug (e.g., driver-operation), workspacePageCatalog entry, legacyOperationsCatalog family
  • output: PortalKind + requiresCapability + provenance (Legacy PHP path or catalog)
  • OFFICE default; MOBILE only for driver-operation, yard-instruction, inbound-loader with explicit ADR per additional field screen
  • Access: portalAvailability = derived( authorizedScreens = resolve( Role/Menu × module × capability ) )
  • server resolves effective Role/Menu via workspaceSession + T_ROLE/T_MENU read; client never decides
  • OFFICE/MOBILE availability derived, not persisted; no User.mobile, device.isMobile, viewport → authorization
  • fail-closed on unknown Role/Menu/module/capability
  • Shells: AppShell.tsx remains Office (tabs); MobilePortalShell.tsx (new under src/composition/) is tabless (home/list → detail → command/readback)
  • Routing: app/(office)/[slug]/page.tsx and app/(mobile)/[slug]/page.tsx share the same classification owner; direct links preserve requested Portal if authorized, otherwise 403/404 or module-disabled
  • Generic login: if both Portals authorized, render explicit Portal chooser (approved by this ADR, not viewport); if one, redirect to that Portal's home; if none, access-denied
  • Narrow viewport: Office Portal remains reachable with horizontal scroll for dense tables; no field omission

Consequences

  • Positive: one classification owner serves both shells; no duplicate catalog; no device-based authority; Office-on-phone preserved; existing Delivery/Yard/Transload command/query/API/readback owners reused
  • Negative: workspacePageCatalog must be split to exact-screen entries; AppShell state must be factored to avoid Office/Mobile tab leakage
  • Production files: +2 (portalClassification.ts, MobilePortalShell.tsx), ~3 modified (AppShell, catalog, authRoutes next handling) — within NS3-047 limits 12/12/1600
  • Public exports: 0 new; dependency edges +2 (shell → classification)
  • Tenant-specific symbols: 0 (classification is tenant-neutral, selection via Role/Menu)
  • Architecture exceptions: 0
  • Rollback: revert classification + shells, restore single AppShell tab bar; no DB/provider effect
  • Revisit: if a fourth field screen requires Mobile or if Office table density cannot be preserved at 390px without omission, reopen ADR

Alternatives

Alternative 1 — One AppShell With Responsive/Role Modes

Keep single shell, hide tabs for field screens via conditional CSS/role checks.

  • Pros: fewest files, low removal cost
  • Cons: one shell owns contradictory Office/field navigation; role/device conditions leak through AppShell state; fails separate Portal boundary requirement
  • Verdict: REJECTED — does not satisfy Product-owner separate Portal direction

Alternative 2 — Separate Portal Shells Inside Current apps/web (Selected)

As decided above.

  • Pros: distinct navigation, single app/release/lineage, single classification owner, reuses business owners
  • Cons: requires catalog split and shell factoring; moderate removal cost
  • Verdict: ACCEPTED — measured ownership, bounded files, preserves Office-on-phone

Alternative 3 — Separate Mobile Web App / Deployment

New apps/mobile with separate hostname, auth, catalog, release.

  • Pros: strongest isolation
  • Cons: duplicate auth/catalog/dependencies/releases for same Product; cross-Portal consistency/rollback high cost; violates one Product/one lineage
  • Verdict: REJECTED — evidence that Option B meets immediate operational need; separate deployment deferred to explicit Horizon 3 decision

Revisit

Reopen if:

  • a new field screen is proposed for Mobile
  • Office-narrow horizontal scroll is rejected by Product Owner
  • portalClassification drifts from workspacePageCatalog or T_ROLE/T_MENU authority

References

  • NS3-046.md §1-10 (Product-owner direction, facts, authority model, contracts)
  • ARCHITECTURE_CHANGE_METHOD.md (facts→problem→options→ADR→separate implementation→verification)
  • EXECUTION.md (NS3-046 ACTIVE, NS3-045 ACTIVE parallel per NS3-049)

---

Source: new-system/docs/decisions/0023-role-screen-portal-boundary.md — ctc repository · This page is an English translation of the source document. Identifiers, file paths and code symbols are unchanged.