Skip to main content

ADR-026: Frontend Framework + Build Pipeline

Status

Proposed (PSSaaS Architect, Phase 8 W2) — pending PO acceptance with the Phase 8 W2 completion review.

Context

The PowerFill module's Phase 8 splits into two workstreams:

  • W1 (Superset dashboards) — completed in 6d19d24 + the A54 fix arc; 8 dashboards × 25 charts at PSX Superset IDs 13-20 against PS_DemoData. Sentinel phase-8-superset-ready-a54-fixed.
  • W2 (React UI) — operator workflow on top of W1's analytical surface: Submit / Status / List / 8 Reports navigation + freshness-aware Note rendering per A60 + A66 + ADR-025 + "View in Superset" deep-links to the W1 dashboard IDs.

ADR-002 ("Frontend — React + TypeScript") is Accepted; this ADR records the specific framework + build-pipeline choice within React+TS that W2 ships.

PSSaaS has no precedent for a React build pipeline — the only existing front-end is Docusaurus (docs-site/) which is its own static-site generator. Whatever pattern W2 adopts will set the path for any future PSSaaS React surface (Pipeline Management UI, Risk Manager UI, etc.).

This ADR records the decision made by Phase 8 W2 plan §3 Decision A (Architect default, PO acknowledged at planning checkpoint via the AskQuestion skipped → "proceed with Architect defaults").

Option A — Vite + React + TypeScript (chosen)

Vite 8 + React 19 + TypeScript 6 + Tailwind CSS v4 + React Router 7. Production deploy: multi-stage node:22-alpine build → nginx:alpine serve from /usr/share/nginx/html/app/.

Pros:

  • Modern JS ecosystem default (2024-2026); fastest dev server (esbuild-powered HMR).
  • First-class TypeScript support out of the box.
  • Produces a static-asset bundle ideal for nginx serving.
  • Matches existing platform patterndocs-site/Dockerfile.prod uses the same multi-stage node:X-alpinenginx:alpine shape; W2 mirrors it 1:1.
  • No SSR overhead for an internal operator tool that doesn't need SEO.
  • Smallest possible deploy artifact (270KB gzipped JS / 17KB gzipped CSS empirically observed for the W2 v1 surface).
  • No Node runtime in production (nginx serves static assets only).

Cons:

  • No SSR (acceptable — operator tool, no public surface, no SEO concern).
  • Vite/React/TS major versions move fast; requires periodic dependency upkeep.

Option B — Next.js 14+ (App Router) + TypeScript

Pros:

  • SSR + ISR + RSC + filesystem routing + built-in API proxying.
  • Richer ecosystem; first-class Vercel deployment.

Cons:

  • Substantially heavier deploy footprint (Node runtime in production).
  • SSR overhead with no benefit for an internal tool.
  • Doesn't fit the established docs-site/Dockerfile.prod static-nginx pattern.
  • Introduces a Node-runtime container the platform doesn't have today.
  • More decisions to make about RSC vs CSR per route.

Rejected because: pure overhead. We don't need SSR; static nginx serving is the established platform pattern. Adopting Next.js would diverge from docs-site/ for no operator-tool benefit.

Option C — Create React App (CRA)

Rejected: deprecated by the React team in February 2025; not maintained.

Decision

Option A — Vite + React + TypeScript, with these specifics:

  • Vite 8 (^8.0.4) + React 19 (^19.2.4) + TypeScript 6 (~6.0.2).
  • Tailwind CSS v4 via @tailwindcss/vite plugin (per Phase 8 W2 plan §3 Decision C).
  • React Router 7 (^7.14.1) for client-side navigation.
  • State: React Context + useState/useReducer (per plan §3 Decision B); no React Query / SWR for v1.
  • Build: tsc -b && vite build produces dist/ (static assets); production Dockerfile.prod uses multi-stage node:22-alpine build → nginx:alpine serve.
  • Base path: /app/ (dev + prod) — chosen because /docs/ (Docusaurus) and /api/ (.NET) are already taken by the pssaas-staging ingress; /app/ is the natural operator-workspace path.
  • Image: GHCR at ghcr.io/kevinsawyer/powerseller-saas/frontend:{sha,latest} (per AGENTS.md GHCR all-lowercase requirement).
  • Deployment: frontend Deployment + ClusterIP Service in pssaas-staging namespace, ingress path /app (Prefix) added to pssaas-staging-ingress.
  • CI: extends .github/workflows/deploy-staging.yaml — adds path-filter for src/frontend/**, calls reusable _build-image.yaml, rolls deployment with kubectl set image + rollout restart (idempotent, mirrors docs + api pattern).

Specifically — directory layout

src/frontend/
├── package.json (vite + react + ts + tailwind + router + prettier)
├── tsconfig.json + tsconfig.app.json + tsconfig.node.json
├── vite.config.ts (base: '/app/', /api proxy in dev)
├── Dockerfile.prod (node:22-alpine build → nginx:alpine serve)
├── eslint.config.js + .prettierrc.json
├── index.html (<title>PowerFill — PSSaaS Operator</title>)
└── src/
├── main.tsx + App.tsx (router shell + tenant context)
├── index.css (@import 'tailwindcss')
├── api/
│ ├── types.ts (TypeScript mirrors of ReportContracts.cs / RunContracts.cs)
│ ├── client.ts (fetch helpers for 12 endpoints; X-Tenant-Id header; ApiError)
│ └── freshness.ts (4-verdict resolver + A66 2-source split)
├── components/
│ └── FreshnessBanner.tsx (4 banner states; A66 yellow vs blue distinction)
├── config/
│ └── supersetDashboards.ts (W1 dashboard IDs 13-20 deep-link map)
├── hooks/
│ ├── useApi.ts (derived loading + AbortController cleanup)
│ └── useReportFetch.ts (Phase 7 report-specific outcome hook)
├── state/
│ ├── TenantContext.tsx + tenantContextObject.ts + useTenant.ts + tenantConstants.ts
│ └ (component-only / hook / constants split per react-refresh rule)
└── pages/
├── Home.tsx + Submit.tsx + RunsList.tsx + RunStatus.tsx
└── reports/
├── ReportRouter.tsx
├── reportShell.tsx (shared <ReportPageShell> + <GenericTable>)
└── {Guide, Recap, Switching, PoolCandidates, ExistingDisposition,
PoolingGuide, CashTradeSlotting, Kickouts}ReportPage.tsx

Tenant identification

Per Phase 8 W2 kickoff §"Tenant context handling" — UI tenant-picker dropdown for v1; X-Tenant-Id header on every API call. Subdomain-based + auth-token-claim-based identification deferred to Phase 9+.

Auth

None for v1 — the existing PSSaaS API on dev/staging doesn't currently require auth. Phase 9+ scope (per kickoff §"Auth integration").

Rationale

W2's job is to ship the operator workflow surface, not to introduce a new platform pattern. Option A is the smallest viable change that satisfies the spec while preserving all existing ADRs (ADR-016 nginx proxy, ADR-020 single-replica AKS staging, ADR-002 React+TS). The static-bundle deploy reuses the existing docs-site/Dockerfile.prod template and the existing GHA build workflow infrastructure (_build-image.yaml); there is no operator burden beyond adding one path-filter line + one build job + one rollout step.

The /app/ ingress path is the natural choice because the alternative (root /) would conflict with the existing nginx.ingress.kubernetes.io/app-root: /docs/ annotation that redirects bare hostname requests to /docs/. Changing that annotation would be a UX-load-bearing change to the docs experience (PO routing via Collaborator) — out of scope for W2.

If/when PSSaaS adopts a richer client-side cache (React Query / SWR) or needs SSR (e.g. for SEO if a public marketing site lands in the same monorepo), the Vite + React surface can absorb additions incrementally without a framework rewrite — Option A's small initial footprint is the right v1 choice precisely because it doesn't lock in any of those decisions.

Consequences

Positive

  • Small initial footprint (~50 source files + 270KB gzipped bundle).
  • No new infrastructure beyond the existing GHCR + AKS + nginx pattern.
  • TypeScript-checked wire shapes against ReportContracts.cs / RunContracts.cs (Empirical-Citation Type Mismatch antipattern countermeasure: snake_case JSON properties mirrored exactly).
  • 4-verdict freshness banner (per A60 + A66) is the load-bearing UX primitive; it's a single 100-LOC component reused across all 8 report pages.
  • Reusable useReportFetch hook + ReportPageShell wrapper means each report page is ~30-40 LOC.
  • Production Docker image is independent of the host Node version (uses node:22-alpine base) — F-W2-TOOLING-1 mitigation (WSL Ubuntu's Node v12.22.9 was insufficient; Windows-host Node v22.22.0 used for dev-time scaffolding; Docker uses its own Node).

Negative

  • No SSR — irrelevant for v1 but locks out hydration-based optimizations if a future surface ever needs them.
  • React 19 / Vite 8 / TS 6 are cutting-edge; may produce upstream churn.
  • No automated testing for v1 (kickoff §"What success looks like" + completion-report verification arms cover the manual-walkthrough surface; unit tests for the React layer are deferred to Phase 9+ when the UI surface stabilizes beyond v1).
  • react-hooks/set-state-in-effect rule (React 19's new lint) required restructuring the loading-state pattern to derived state (loading: data === null && error === null) — non-trivial pattern; documented in code comments.

Operational

  • Pre-push docs-build check (per Phase 6e/7/8-W1 lessons): mandatory if any new docs-site/docs/** files land alongside the W2 changes. W2 ships ADR-026 + spec amendment + assumptions log A67 + W2 completion report + devlog + session-handoff bump = 6 new docs files; the docs-build check is required.
  • First-deploy boostrap: the K8s manifest must be applied via kubectl apply -f infra/azure/k8s/pssaas-staging/services.yaml BEFORE the first GHA kubectl set image rollout (the workflow's frontend-rollout step guards with kubectl get deployment/frontend so it's a no-op until then).
  • Greg-demo readiness (post-deploy): the operator can submit a run via the React UI, watch it transition through 4 active + 3 terminal states, click into any of 8 report pages, and drill into the corresponding Superset dashboard (IDs 13-20). On PS_DemoData, post-A54-fix Complete runs render the user-facing reports as empty with a BLUE info banner explaining A66 (UE rebuild on syn-trade-empty datasets) — load-bearing distinction from the YELLOW Failed/Cancelled banner per kickoff §"Inherited context" A66 row.

Alternatives Reconsidered

These options remain viable for later phases when their trade-offs change:

  • Option B (Next.js) — when PSSaaS gains a public marketing site or needs SSR for SEO. The React+TS source code is portable to Next.js with route-file restructuring; the Vite-specific config is the only piece that doesn't transfer.
  • React Query / SWR — when the server-cache surface grows beyond the W2 scope (e.g. cross-page run-list cache invalidation; optimistic updates on Submit). Swap useApi.ts + useReportFetch.ts internals without touching component code.
  • Storybook / component testing — when the freshness banner permutations + report table renderers grow past the manual-walkthrough verification surface.

Future Considerations

  • Multi-page real-time updates: the Run Status page polls GET /runs/{run_id} every 2s; when SignalR (or another WebSocket abstraction) lands in the .NET API, the polling can swap for push without changing the page contract.
  • Auth integration (Phase 9+): when ADR-013 Identity Strategy is accepted, the tenant-picker becomes auth-token-claim-driven; the existing X-Tenant-Id header pattern can stay (as a server-internal contract) or be replaced by a JWT claim with no UI rework beyond removing the picker.
  • Per-tenant Superset dashboard ID variance (Phase 9+ per A64): the supersetDashboards.ts config map currently hardcodes IDs 13-20 (staging single-DB-only). When multi-tenant Superset registration lands, the IDs become per-tenant-configurable.

Source References