Files
cameleer-saas/docs/superpowers/specs/2026-04-04-phase-9-frontend-react-shell.md
hsiegeln 600985c913
All checks were successful
CI / build (push) Successful in 28s
CI / docker (push) Successful in 4s
docs: add Phase 9 Frontend React Shell spec
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 21:36:45 +02:00

12 KiB

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
  • <RequirePermission permission="apps:deploy"> 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: /apihttp://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

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

# 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

# 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:

@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

{
  "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:

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