From 600985c913a16c09590b22c7b99137f4887114fe Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 4 Apr 2026 21:36:45 +0200 Subject: [PATCH] docs: add Phase 9 Frontend React Shell spec Co-Authored-By: Claude Opus 4.6 (1M context) --- ...2026-04-04-phase-9-frontend-react-shell.md | 316 ++++++++++++++++++ 1 file changed, 316 insertions(+) create mode 100644 docs/superpowers/specs/2026-04-04-phase-9-frontend-react-shell.md diff --git a/docs/superpowers/specs/2026-04-04-phase-9-frontend-react-shell.md b/docs/superpowers/specs/2026-04-04-phase-9-frontend-react-shell.md new file mode 100644 index 0000000..11d4032 --- /dev/null +++ b/docs/superpowers/specs/2026-04-04-phase-9-frontend-react-shell.md @@ -0,0 +1,316 @@ +# Phase 9: Frontend React Shell + +**Date:** 2026-04-04 +**Status:** Draft +**Depends on:** Phase 4 (Observability Pipeline + Inbound Routing) +**Gitea issue:** #31 + +## Context + +Phases 1-4 built the complete backend: tenants, licensing, environments, app deployment with JAR upload, async deployment pipeline, container logs, agent status, observability status, and inbound HTTP routing. The cameleer3-server observability dashboard is already served at `/dashboard`. But there is no management UI — all operations require curl/API calls. + +Phase 9 adds the SaaS management shell: a React SPA for managing tenants, environments, apps, and deployments. The observability UI is already handled by cameleer3-server — this shell covers everything else. + +## Key Decisions + +| Decision | Choice | Rationale | +|----------|--------|-----------| +| Location | `ui/` directory in cameleer-saas repo | Matches cameleer3-server pattern. Single build pipeline. Spring Boot serves the SPA. | +| Relationship to dashboard | Two separate SPAs, linked via navigation | SaaS shell at `/`, observability at `/dashboard`. Same design system = cohesive feel. No coupling. | +| Layout | Sidebar navigation | Consistent with cameleer3-server dashboard. Same AppShell pattern from design system. | +| Auth | Shared Logto OIDC session | Same client ID, same localStorage keys. True SSO between SaaS shell and observability dashboard. | +| Tech stack | React 19 + Vite + React Router + Zustand + TanStack Query | Identical to cameleer3-server SPA. Same patterns, same libraries, same conventions. | +| Design system | `@cameleer/design-system` v0.1.31 | Shared component library. CSS Modules + design tokens. Dark theme. | +| RBAC | Frontend role-based visibility | Roles from JWT claims. Hide/disable UI for unauthorized actions. Backend enforces — frontend is UX only. | + +## Tech Stack + +- **React 19** + TypeScript +- **Vite 8** (bundler + dev server) +- **React Router 7** (client-side routing) +- **Zustand** (auth state store) +- **TanStack React Query** (data fetching + caching) +- **@cameleer/design-system** (UI components) +- **Lucide React** (icons) + +## Auth Flow + +1. User navigates to `/` — `ProtectedRoute` checks `useAuthStore.isAuthenticated` +2. If not authenticated, redirect to Logto OIDC authorize endpoint +3. Logto callback at `/callback` — exchange code for tokens +4. Store `accessToken`, `refreshToken`, `username`, `roles` in Zustand + localStorage +5. Tokens stored with same keys as cameleer3-server SPA: `cameleer-access-token`, `cameleer-refresh-token` +6. API client injects `Authorization: Bearer {token}` on all requests +7. On 401, attempt token refresh; on failure, redirect to login + +## RBAC Model + +Roles from JWT or API response: + +| Role | Permissions | UI Access | +|------|------------|-----------| +| **OWNER** | All | Everything + tenant settings | +| **ADMIN** | All except tenant:manage, billing:manage | Environments CRUD, apps CRUD, routing, deploy | +| **DEVELOPER** | apps:deploy, secrets:manage, observe:read, observe:debug | Deploy, stop, restart, re-upload JAR, view logs | +| **VIEWER** | observe:read | View-only: dashboard, app status, logs, deployment history | + +Frontend RBAC implementation: +- `usePermissions()` hook reads roles from auth store, returns permission checks +- `` wrapper component hides children if unauthorized +- Buttons/actions disabled with tooltip "Insufficient permissions" for unauthorized roles +- Navigation items hidden entirely if user has no access to any action on that page + +## Pages + +### Login (`/login`) +- Logto OIDC redirect button +- Handles callback at `/callback` +- Stores tokens, redirects to `/` + +### Dashboard (`/`) +- Tenant overview: name, tier badge, license expiry +- Environment count, total app count +- Running/failed/stopped app summary (KPI strip) +- Recent deployments table (last 10) +- Quick actions: "New Environment", "View Observability Dashboard" +- **All roles** can view + +### Environments (`/environments`) +- Table: name (display_name), slug, app count, status badge +- "Create Environment" button (ADMIN+ only, enforces tier limit) +- Click row → navigate to environment detail +- **All roles** can view list + +### Environment Detail (`/environments/:id`) +- Environment name (editable inline for ADMIN+), slug, status +- App list table: name, slug, deployment status, agent status, last deployed +- "New App" button (DEVELOPER+ only) — opens JAR upload dialog +- "Delete Environment" button (ADMIN+ only, disabled if apps exist) +- **All roles** can view + +### App Detail (`/environments/:eid/apps/:aid`) +- Header: app name, slug, environment breadcrumb +- **Status card**: current deployment status (BUILDING/STARTING/RUNNING/FAILED/STOPPED) with auto-refresh polling (3s) +- **Agent status card**: registered/not, state, route IDs, link to observability dashboard +- **JAR info**: filename, size, checksum, upload date +- **Routing card**: exposed port, route URL (clickable), edit button (ADMIN+) +- **Actions bar**: + - Deploy (DEVELOPER+) — triggers new deployment + - Stop (DEVELOPER+) + - Restart (DEVELOPER+) + - Re-upload JAR (DEVELOPER+) — file picker dialog + - Delete app (ADMIN+) — confirmation dialog +- **Deployment history**: table with version, status, timestamps, error messages +- **Container logs**: LogViewer component from design system, auto-refresh, stream filter (stdout/stderr) +- **All roles** can view status/logs/history + +### License (`/license`) +- Current tier badge, features enabled/disabled, limits +- Expiry date, days remaining +- **All roles** can view + +## Sidebar Navigation + +``` +🐪 Cameleer SaaS +───────────────── +📊 Dashboard +🌍 Environments + └ {env-name} (expandable, shows apps) + └ {app-name} +📄 License +───────────────── +👁 View Dashboard → (links to /dashboard) +───────────────── +🔒 Logged in as {name} + Logout +``` + +- Sidebar uses `Sidebar` + `TreeView` components from design system +- Environment → App hierarchy is collapsible +- "View Dashboard" is an external link to `/dashboard` (cameleer3-server SPA) +- Sidebar collapses on small screens (responsive) + +## API Integration + +The SaaS shell talks to cameleer-saas REST API. All endpoints already exist from Phases 1-4. + +### API Client Setup + +- Vite proxy: `/api` → `http://localhost:8080` (dev mode) +- Production: Traefik routes `/api` to cameleer-saas +- Auth middleware injects Bearer token +- Handles 401/403 with refresh + redirect + +### React Query Hooks + +``` +useTenant() → GET /api/tenants/{id} +useLicense(tenantId) → GET /api/tenants/{tid}/license + +useEnvironments(tenantId) → GET /api/tenants/{tid}/environments +useCreateEnvironment(tenantId) → POST /api/tenants/{tid}/environments +useUpdateEnvironment(tenantId, eid) → PATCH /api/tenants/{tid}/environments/{eid} +useDeleteEnvironment(tenantId, eid) → DELETE /api/tenants/{tid}/environments/{eid} + +useApps(environmentId) → GET /api/environments/{eid}/apps +useCreateApp(environmentId) → POST /api/environments/{eid}/apps (multipart) +useDeleteApp(environmentId, appId) → DELETE /api/environments/{eid}/apps/{aid} +useUpdateRouting(environmentId, aid) → PATCH /api/environments/{eid}/apps/{aid}/routing + +useDeploy(appId) → POST /api/apps/{aid}/deploy +useDeployments(appId) → GET /api/apps/{aid}/deployments +useDeployment(appId, did) → GET /api/apps/{aid}/deployments/{did} (poll 3s) +useStop(appId) → POST /api/apps/{aid}/stop +useRestart(appId) → POST /api/apps/{aid}/restart + +useAgentStatus(appId) → GET /api/apps/{aid}/agent-status +useObservabilityStatus(appId) → GET /api/apps/{aid}/observability-status +useLogs(appId) → GET /api/apps/{aid}/logs +``` + +## File Structure + +``` +ui/ +├── index.html +├── package.json +├── vite.config.ts +├── tsconfig.json +├── src/ +│ ├── main.tsx — React root + providers +│ ├── router.tsx — React Router config +│ ├── auth/ +│ │ ├── auth-store.ts — Zustand store (same keys as cameleer3-server) +│ │ ├── LoginPage.tsx +│ │ ├── CallbackPage.tsx +│ │ └── ProtectedRoute.tsx +│ ├── api/ +│ │ ├── client.ts — fetch wrapper with auth middleware +│ │ └── hooks.ts — React Query hooks for all endpoints +│ ├── hooks/ +│ │ └── usePermissions.ts — RBAC permission checks +│ ├── components/ +│ │ ├── RequirePermission.tsx — RBAC wrapper +│ │ ├── Layout.tsx — AppShell + Sidebar + Breadcrumbs +│ │ ├── EnvironmentTree.tsx — Sidebar tree (envs → apps) +│ │ └── DeploymentStatusBadge.tsx +│ ├── pages/ +│ │ ├── DashboardPage.tsx +│ │ ├── EnvironmentsPage.tsx +│ │ ├── EnvironmentDetailPage.tsx +│ │ ├── AppDetailPage.tsx +│ │ └── LicensePage.tsx +│ └── types/ +│ └── api.ts — TypeScript types matching backend DTOs +``` + +## Traefik Routing + +```yaml +cameleer-saas: + labels: + # Existing API routes: + - traefik.http.routers.api.rule=PathPrefix(`/api`) + - traefik.http.routers.forwardauth.rule=Path(`/auth/verify`) + # New SPA route: + - traefik.http.routers.spa.rule=PathPrefix(`/`) + - traefik.http.routers.spa.priority=1 + - traefik.http.services.spa.loadbalancer.server.port=8080 +``` + +Spring Boot serves the SPA from `src/main/resources/static/` (built by Vite into this directory). A catch-all controller returns `index.html` for all non-API routes (SPA client-side routing). + +## Build Integration + +### Vite Build → Spring Boot Static Resources + +```bash +# In ui/ +npm run build +# Output: ui/dist/ + +# Copy to Spring Boot static resources +cp -r ui/dist/* src/main/resources/static/ +``` + +This can be automated in the Maven build via `frontend-maven-plugin` or a simple shell script in CI. + +### CI Pipeline + +Add a `ui-build` step before `mvn verify`: +1. `cd ui && npm ci && npm run build` +2. Copy `ui/dist/` to `src/main/resources/static/` +3. `mvn clean verify` packages the SPA into the JAR + +### Development + +```bash +# Terminal 1: backend +mvn spring-boot:run + +# Terminal 2: frontend (Vite dev server with API proxy) +cd ui && npm run dev +``` + +Vite dev server proxies `/api` to `localhost:8080`. + +## SPA Catch-All Controller + +Spring Boot needs a catch-all to serve `index.html` for SPA routes: + +```java +@Controller +public class SpaController { + @GetMapping(value = {"/", "/login", "/callback", "/environments/**", "/license"}) + public String spa() { + return "forward:/index.html"; + } +} +``` + +This ensures React Router handles client-side routing. API routes (`/api/**`) are not caught — they go to the existing REST controllers. + +## Design System Integration + +```json +{ + "dependencies": { + "@cameleer/design-system": "0.1.31" + } +} +``` + +Registry configuration in `.npmrc`: +``` +@cameleer:registry=https://gitea.siegeln.net/api/packages/cameleer/npm/ +``` + +Import in `main.tsx`: +```tsx +import '@cameleer/design-system/style.css'; +import { ThemeProvider, ToastProvider, BreadcrumbProvider } from '@cameleer/design-system'; +``` + +## Verification Plan + +1. `npm run dev` starts Vite dev server, SPA loads at localhost:5173 +2. Login redirects to Logto, callback stores tokens +3. Dashboard shows tenant overview with correct data from API +4. Environment list loads, create/rename/delete works (ADMIN+) +5. App upload (JAR + metadata) works, app appears in list +6. Deploy triggers async deployment, status polls and updates live +7. Agent status shows registered/connected +8. Container logs stream in LogViewer +9. "View Dashboard" link navigates to `/dashboard` (cameleer3-server SPA) +10. Shared auth: no re-login when switching between SPAs +11. RBAC: VIEWER cannot see deploy button, DEVELOPER cannot delete environments +12. Production build: `npm run build` + `mvn package` produces JAR with embedded SPA + +## What Phase 9 Does NOT Touch + +- No changes to cameleer3-server or its SPA +- No billing UI (Phase 6) +- No team management (Logto org admin — deferred) +- No tenant settings/profile page +- No super-admin multi-tenant view