← all branches

feat/soft-launch

needs attentionviewing older commit
a4d7b63 · fullPR #263reviewed 2026-07-07 01:39 UTC1H · 4M · 4L · 1I
The branch
Purpose
Gate not-yet-ready PR-207 surfaces (Sites, Assets, Tarifas, metrics) so only Bills + Credentials are reachable at soft launch, without removing any code.
Goal
Bills + credentials-only soft launch: one SSOT drives both sidebar treatment and route reachability so they can never drift; reveal a module later = one code edit.
Sub-goals
  • SG-1: SSOT lib/soft-launch.ts + sidebar grey-out (Sites/Tarifas greyed, Assets hidden), admin bypass
  • SG-2: Single <SoftLaunchGuard> in (dashboard)/layout.tsx redirects all gated routes; /credentials/metrics tab hidden
  • SG-3: Login default + bare /dashboard → /bills/contratos (post-login always lands on a live page)
The changes (whole branch)
What
New SSOT module (lib/soft-launch.ts: isGated + navState), new SoftLaunchGuard component mounted once in the dashboard layout, sidebar nav updated to use navState for grey/hide treatment, credentials layout updated to hide the metrics tab under soft-launch.
Why
Soft launch: PR-207 landed Sites/Assets/Tarifas/metrics surfaces that are not yet ready for customers. Reachability-gating (not code removal) keeps the surfaces dark while internal ops (admins) retain full access. Reversible by a code edit.
Areas
apps/platform/src/lib+1490apps/platform/src/components+482apps/platform/src/app/[locale]/(dashboard)+196
Blast
6 code files, +216/-8 lines; apps/platform frontend only — no domains, packages, API contracts, DB, CDK, or apps/web touched.
frontend-only no-backend-gate hardcoded-ssot-not-env-var
ci· CI check status unavailable (token scope)coderabbit· No .coderabbit.yaml in repotypecheck· PR description confirms pnpm typecheck passestests· PR description: full suite green (409 tests), incl. 12 new soft-launch tests

Findings · 10

correctness2

medium

DashboardSidebar href construction breaks for default locale — gated nav items inherit the bug

apps/platform/src/components/DashboardSidebar.tsx:73

Pre-existing: `DashboardSidebar` uses `usePathname()` from `next/navigation` and derives locale via `pathname.split('/')[1] || 'en'` to build hrefs like `/${locale}/sites`. Under `localePrefix: 'as-needed'`, the default locale (`en`) has no path prefix — so on an English user's `/bills/contratos`, `split('/')[1]` is `'bills'`, not `'en'`, producing `/bills/sites`, `/bills/tarifas`, etc. (404s). This PR's new gated items (`sitesNav`, `tarifasNav`) feed into exactly these broken hrefs, making the bug live for the newly-greyed-out nav items. Fix: use `Link` and `usePathname` from `@/i18n/navigation` in `DashboardSidebar`.

low

/metrics is in GATED_PATHS but has no GREYED/HIDDEN nav treatment — intentional but undocumented

apps/platform/src/lib/soft-launch.ts:28

`navState('metrics')` returns `'live'` because `metrics` is absent from both `GREYED` and `HIDDEN`. This is intentional — `/metrics` has no top-level sidebar entry (it is only accessible via the `/credentials/metrics` tab). However, this creates a latent trap: if a metrics sidebar item is added later, it would appear live while the route is blocked. A comment on the `GATED_PATHS` entry explaining 'no sidebar nav item exists for this path — sub-route gate only' would prevent the confusion.

conventions1

medium

Login redirect in (dashboard)/layout.tsx still uses raw next/navigation locale parse — same bug this PR fixed in SoftLaunchGuard

apps/platform/src/app/[locale]/(dashboard)/layout.tsx:59

The commit `a4d7b63a` is titled 'fix as-needed locale bug' and correctly fixes `SoftLaunchGuard` by switching to `@/i18n/navigation`. However, `(dashboard)/layout.tsx` still uses `usePathname()` from `next/navigation` and derives the locale via `pathname.split('/')[1] || 'en'`. Under `localePrefix: 'as-needed'`, English users get a pathname like `/bills/contratos`, so `split('/')[1]` yields `'bills'` — the unauthenticated redirect goes to `/bills/login` (a 404). Fix: import `useRouter` from `@/i18n/navigation` and call `router.replace('/login')` without a hand-rolled locale prefix.

tests4

high

SOFT_LAUNCH=false master off-switch is completely untested

apps/platform/src/lib/__tests__/soft-launch.test.ts

The `!SOFT_LAUNCH` early-return branch in both `isGated` and `navState` is dead code from the test suite's perspective — the constant is imported live and never stubbed. If the condition were accidentally inverted (e.g. `if (SOFT_LAUNCH || ...)`) all routes would gate forever and no test would catch it. The master off-switch is the intended "lift the gate everywhere" lever (documented in the source comment) and deserves at least one test asserting that setting it to `false` makes `isGated('/sites')` return `false` and `navState('sites')` return `'live'`. Requires `vi.mock` or exporting a testable factory.

medium

Trailing-slash variant /sites/ is gated but never asserted

apps/platform/src/lib/__tests__/soft-launch.test.ts

The implementation correctly returns `true` for `isGated('/sites/')` because `'/sites/'.startsWith('/sites/')`. next-intl's `usePathname()` can return a trailing slash when the user lands on the root of a segment. No test asserts this, so a refactor removing the trailing slash from the `startsWith` check would silently break the gate for root-segment navigations. Add: `expect(isGated('/sites/')).toBe(true)`.

medium

No test documents the intentional nav ↔ route asymmetry for /metrics and /credentials/metrics

apps/platform/src/lib/__tests__/soft-launch.test.ts

`GATED_PATHS` includes `/metrics` and `/credentials/metrics` (routes are dark) but neither has a key in `GREYED` or `HIDDEN` (no top-level sidebar item for them). The no-drift invariant test's `moduleRoute` map deliberately omits them, but there is no assertion or comment making this explicit. A future developer seeing the mismatch will add them to the invariant test, discover `navState('metrics')` returns `'live'`, and conclude there's a bug. A comment or a positive assertion (`expect(navState('metrics')).toBe('live')` annotated with 'no sidebar entry by design') would prevent this.

low

isPlatformAdmin=false and isPlatformAdmin=undefined not explicitly tested

apps/platform/src/lib/__tests__/soft-launch.test.ts

The test for non-admin behaviour omits the explicit `isPlatformAdmin` argument (testing `undefined`) and covers `null` explicitly, but never tests `false`. The sidebar passes `isPlatformAdmin` from `useOrgContext()` which is typed `boolean` — so `false` is the most common non-admin value at call sites. A future refactor that changes the guard to `isPlatformAdmin === true` (strict equality) rather than the current truthy check would still pass for `null` but fail for `undefined`. Low risk with current logic, but asserting all three falsy variants makes the intent unambiguous.

improvement3

low

GATED_PATHS, GREYED, and HIDDEN lack a compile-time sync check — drift is possible

apps/platform/src/lib/soft-launch.ts

A module can be added to `GATED_PATHS` (route blocked) without being added to `GREYED` or `HIDDEN` (nav untreated), or vice versa, with no type-level warning. Today `/metrics` intentionally omits the nav treatment, but there is no mechanism preventing accidental drift. A TypeScript approach: derive GREYED/HIDDEN keys from a typed record keyed by GATED_PATHS elements, or add a runtime assertion (checked in tests) that every nav key in GREYED/HIDDEN has a corresponding GATED_PATHS entry.

low

|| undefined pattern on disabled prop is correct but non-obvious

apps/platform/src/components/DashboardSidebar.tsx:88

`disabled: sitesNav === 'greyed' || undefined` works because `false || undefined` collapses to `undefined`, which satisfies the `NavItem` type (`boolean | undefined`). Without this, `disabled: false` would be a no-op in React but might fail the `NavItem` type or render as a `disabled` attribute on DOM elements depending on how it's spread. The intent is implicit — a one-line comment ('undefined omits the prop; false would propagate as an attribute') would stop the next reader from 'simplifying' it to `disabled: sitesNav === 'greyed'`.

info

Client-side gate only — middleware-level gate possible if isPlatformAdmin reaches the JWT

apps/platform/src/components/SoftLaunchGuard.tsx

The current guard is client-side (useEffect + null render), which means there is a single null-render cycle before the redirect fires. This is the correct approach given that `isPlatformAdmin` is only available client-side via `useOrgContext`. If `isPlatformAdmin` is ever surfaced in the Supabase JWT and read in Next.js middleware, moving the redirect there would make gated routes truly unreachable (no render cycle) and eliminate the null frame. Not a current issue — worth noting for when the auth token shape evolves.

History · 4 commits

  1. 3709e2eneeds attentionincremental0H · 3M · 3L2026-07-07 02:18
  2. a4d7b63needs attentionfull1H · 4M · 4L2026-07-07 01:39current
  3. 682c40cneeds attentionincremental5H · 4M · 6L2026-07-07 01:21
  4. 0385797needs attentionfull7H · 15M · 10L2026-07-06 19:45