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. Sentinelphase-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 pattern —
docs-site/Dockerfile.produses the same multi-stagenode:X-alpine→nginx:alpineshape; 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.prodstatic-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/viteplugin (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 buildproducesdist/(static assets); productionDockerfile.produses multi-stagenode:22-alpinebuild →nginx:alpineserve. - Base path:
/app/(dev + prod) — chosen because/docs/(Docusaurus) and/api/(.NET) are already taken by thepssaas-stagingingress;/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:
frontendDeployment + ClusterIP Service inpssaas-stagingnamespace, ingress path/app(Prefix) added topssaas-staging-ingress. - CI: extends
.github/workflows/deploy-staging.yaml— adds path-filter forsrc/frontend/**, calls reusable_build-image.yaml, rolls deployment withkubectl 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
useReportFetchhook +ReportPageShellwrapper means each report page is ~30-40 LOC. - Production Docker image is independent of the host Node version (uses
node:22-alpinebase) — 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-effectrule (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.yamlBEFORE the first GHAkubectl set imagerollout (the workflow's frontend-rollout step guards withkubectl get deployment/frontendso 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.tsinternals 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-Idheader 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.tsconfig map currently hardcodes IDs 13-20 (staging single-DB-only). When multi-tenant Superset registration lands, the IDs become per-tenant-configurable.
Related ADRs
- ADR-002: Frontend — React + TypeScript — the broader frontend choice this ADR specializes within
- ADR-016: Nginx Proxy + Docker Compose Profiles — local dev nginx pattern (W2 dev uses
vite devproxy →pssaas.powerseller.local) - ADR-020: Shared Kubernetes Cluster with PSX — single-replica AKS staging makes the static-bundle pattern viable
- ADR-024: PowerFill Async Run Pattern — the run lifecycle (4 active + 3 terminal states) the Run Status page polls
- ADR-025: PowerFill Report API Pattern — the 4 freshness verdicts (Current / Stale / TerminalEmpty / RunNotFound) the W2 banner UX renders
Source References
- Phase 8 W2 kickoff — task specification + scope
- PowerFill Engine Spec §Output APIs — the 8 endpoints W2 fetches
- PowerFill Engine Spec §Run APIs — the 4 run-management endpoints W2 calls
- PowerFill Assumptions Log A60 — 4-verdict latest-Complete-wins semantics
- PowerFill Assumptions Log A66 — 2-source TerminalEmpty distinction (Failed/Cancelled vs Complete + syn-trade-empty)
- PowerFill Phase 8 W2 completion report — full PoC verification + decision provenance
src/frontend/— the W2 React UI source treesrc/frontend/Dockerfile.prod— production imageinfra/azure/k8s/pssaas-staging/services.yaml— K8s Deployment + Service for the frontendinfra/azure/k8s/ingress/pssaas-ingress.yaml— ingress path/app→frontendservice.github/workflows/deploy-staging.yaml— CI build + deploy pipeline