Sharang Parnerkar 582355a1f2
ci / shared (pull_request) Successful in 13s
ci / test (pull_request) Failing after 5m3s
ci / e2e (pull_request) Has been skipped
ci / image (pull_request) Has been skipped
fix(portal): pass Next.js 16's React-strict lint rules in M10.2
CI on PR #13 failed at `pnpm lint --max-warnings 0`. Four findings, all
new-in-N16 react-strict checks:

* ThemeToggle.tsx — "Calling setState synchronously within an effect"
  Rewrites the theme reader to use `useSyncExternalStore` with a
  `MutationObserver` on `<html data-theme>`. SSR snapshot stays "light"
  (matches the root layout); the head script and the toggle just write
  the attribute, the observer pushes the change into React. Drops the
  `mounted` flag because the icon now mirrors the DOM truthfully.
* WorkflowEditor.tsx — "Cannot access refs during render"
  `stateRef.current = { pan, zoom }` was a direct ref-mutation in the
  component body so the global mousemove handler could read the latest
  viewport without re-subscribing. Moves the mirror into a `useEffect`
  keyed on `[pan, zoom]` — same semantics, satisfies the rule.
* MockWorker.tsx — drops an unused `eslint-disable-next-line no-console`
  (the `no-console` rule isn't enabled).
* public/mockServiceWorker.js — auto-generated by `msw init`; adds it to
  the eslint flat-config `ignores` so the lint pass never crosses it.

Local: `pnpm lint` + `pnpm typecheck` both clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-04 17:30:24 +02:00

portal

Next.js 16 customer area + backstage.

Part of the Breakpilot Platform. For the big picture see platform/docs: Architecture · Infrastructure · Product Integration Spec · Implementation Plan

What this is

Next.js 16 customer area + backstage. Scaffolded under milestone M5.1. See platform/docs for the full architecture context.

Plane: Control Owner: @sharang Status: pre-alpha Linked milestone: M5.1

Run locally

# Prerequisites: Node 20+, pnpm 9+, the dev stack running.

# 1. Bring up Keycloak + Postgres + Redis (separate clone):
cd /path/to/platform/orca-platform && make dev-up

# 2. Run tenant-registry (separate clone):
cd /path/to/platform/tenant-registry && make dev

# 3. Run this app:
make install      # pnpm install --frozen-lockfile
make dev          # next dev on http://localhost:3000

# Or hit a real tenant immediately:
# open http://acme.localhost:3000  →  redirects to Keycloak  →  back to /acme/dashboard

Seed login (from the dev-stack realm): test@breakpilot.dev / test.

AUTH_URL gotcha: Auth.js v5 builds the OAuth redirect_uri from AUTH_URL — not from the request Host header, even with AUTH_TRUST_HOST=true. For multi-tenant dev work, pin AUTH_URL to the subdomain you log in on (e.g., http://acme.localhost:3000); otherwise Keycloak rejects the token exchange with invalid_grant: Incorrect redirect_uri. In prod, orca-proxy passes the right host via X-Forwarded-Host and AUTH_URL is set to the apex (https://breakpilot.com).

make test / make lint / make typecheck / make build run vitest / eslint / tsc / next build respectively.

Env vars live in .env.example. Copy to .env.local for local overrides (gitignored).

Surface

Route Renders
http://localhost:3000/ Apex landing — pointer to tenant subdomains
http://<slug>.localhost:3000/ Middleware rewrites to /[slug]/ → redirects to /[slug]/dashboard
http://<slug>.localhost:3000/dashboard OIDC-gated dashboard; signed-out users see "Sign in with Keycloak"
http://backstage.localhost:3000/ (Skeleton) backstage route — rewritten to /__backstage__/*
/api/auth/[...nextauth] Auth.js v5 endpoints (callback, signin, signout, jwt)

Architecture notes

  • Host → slug routing: src/middleware.ts parses Host header via parseHost() (in src/lib/host.ts) and rewrites the request path to /<slug>/.... URL bar stays unchanged. Apex hosts and unknown subdomains fall through unmodified.
  • Tenant context: src/app/[slug]/layout.tsx fetches the tenant from tenant-registry (src/lib/tenant-registry.ts). 404 → notFound(); HTTP errors bubble up.
  • Auth: src/auth.ts is the Auth.js v5 config — Keycloak provider, tenant-context claims (tenant_id, tenant_slug, org_roles, products, plan, tenant_status) propagated via JWT/session callbacks. Real RBAC enforcement lands in M5.2 / M10.1.

Deployment

Env URL How
dev http://localhost:3000 make dev
stage https://portal.stage.breakpilot.com auto on merge to main
prod https://portal.breakpilot.com manual: tag vX.Y.Z + sign-off

Rollback: orca rollout undo portal --env={{env}}.

Observability

  • Traces, logs, metrics: SigNoz — service name portal
  • Audit events: Tenant Registry /audit (Retraced-shape schema)
  • On-call: oncall@breakpilot.com · runbook at platform/docs/runbooks/portal.md

Contributing

See CONTRIBUTING.md. TL;DR: branch from main, open a PR, 1 review + green CI, squash-merge.

End-to-end tests (M5.3)

Playwright config at playwright.config.ts. Tests under tests/e2e/.

make e2e-install     # one-time: pnpm exec playwright install chromium
# bring up the dev stack + tenant-registry + portal in three separate terminals,
# then:
make e2e             # pnpm playwright test

Test groups (filter with --grep):

File What it asserts
tests/e2e/apex.spec.ts Apex landing page renders
tests/e2e/tenant.spec.ts Tenant subdomain serves signed-out dashboard + 404 on unknown slug
tests/e2e/health.spec.ts The whole dev stack is reachable: portal API, tenant-registry, Keycloak

@needs-stack in a test title means the dev stack must be running. We don't yet have a full OIDC click-through test — Keycloak in headless mode is flaky, so we assert the gate (Sign-in button visible) rather than completing the login.

In CI, the e2e job is gated behind the repo variable RUN_E2E == 'true' so it stays off until stage exists. Lint / typecheck / build / vitest still run on every PR.

License

Proprietary — all rights reserved. Copyright (c) 2026 Sharang Parnerkar and Benjamin Boenisch. See LICENSE.

S
Description
Next.js 15 customer area + backstage.
Readme 510 KiB
Languages
TypeScript 79.9%
CSS 15.3%
JavaScript 4.1%
Makefile 0.4%
Dockerfile 0.3%