From 4c8c8efbe58a05a37938508d5b3e71aa92435dcd Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Sat, 4 Apr 2026 22:06:36 +0200 Subject: [PATCH] feat: add SPA controller, Traefik route, CI frontend build, and HOWTO update - SpaController catch-all forwards non-API routes to index.html - Traefik SPA route at priority=1 catches all unmatched paths - CI pipeline builds frontend before Maven - Dockerfile adds multi-stage frontend build - HOWTO.md documents frontend development workflow Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/ci.yml | 6 +++ Dockerfile | 8 ++++ HOWTO.md | 46 +++++++++++++++++++ docker-compose.yml | 3 ++ .../cameleer/saas/config/SpaController.java | 13 ++++++ 5 files changed, 76 insertions(+) create mode 100644 src/main/java/net/siegeln/cameleer/saas/config/SpaController.java diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c31d03d..9bb6927 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -27,6 +27,12 @@ jobs: key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-maven- + - name: Build Frontend + run: | + cd ui + npm ci + npm run build + - name: Build and Test (unit tests only) run: >- mvn clean verify -B diff --git a/Dockerfile b/Dockerfile index 9570571..1ba3ab4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,18 @@ # syntax=docker/dockerfile:1 +FROM node:22-alpine AS frontend +WORKDIR /ui +COPY ui/package.json ui/package-lock.json ui/.npmrc ./ +RUN npm ci +COPY ui/ . +RUN npm run build + FROM eclipse-temurin:21-jdk-alpine AS build WORKDIR /build COPY .mvn/ .mvn/ COPY mvnw pom.xml ./ RUN --mount=type=cache,target=/root/.m2/repository ./mvnw dependency:go-offline -B COPY src/ src/ +COPY --from=frontend /ui/dist/ src/main/resources/static/ RUN --mount=type=cache,target=/root/.m2/repository ./mvnw package -DskipTests -B FROM eclipse-temurin:21-jre-alpine diff --git a/HOWTO.md b/HOWTO.md index 7398635..679d4e4 100644 --- a/HOWTO.md +++ b/HOWTO.md @@ -302,6 +302,52 @@ Query params: `since`, `until` (ISO timestamps), `limit` (default 500), `stream` | HIGH | Unlimited | 50 | 90 days | + Debugger, Replay | | BUSINESS | Unlimited | Unlimited | 365 days | All features | +## Frontend Development + +The SaaS management UI is a React SPA in the `ui/` directory. + +### Setup + +```bash +cd ui +npm install +``` + +### Dev Server + +```bash +cd ui +npm run dev +``` + +The Vite dev server starts on http://localhost:5173 and proxies `/api` to `http://localhost:8080` (the Spring Boot backend). Run the backend in another terminal with `mvn spring-boot:run` or via Docker Compose. + +### Environment Variables + +| Variable | Purpose | Default | +|----------|---------|---------| +| `VITE_LOGTO_ENDPOINT` | Logto OIDC endpoint | `http://localhost:3001` | +| `VITE_LOGTO_CLIENT_ID` | Logto application client ID | (empty) | + +Create a `ui/.env.local` file for local overrides: +```bash +VITE_LOGTO_ENDPOINT=http://localhost:3001 +VITE_LOGTO_CLIENT_ID=your-client-id +``` + +### Production Build + +```bash +cd ui +npm run build +``` + +Output goes to `src/main/resources/static/` (configured in `vite.config.ts`). The subsequent `mvn package` bundles the SPA into the JAR. In Docker builds, the Dockerfile handles this automatically via a multi-stage build. + +### SPA Routing + +Spring Boot serves `index.html` for all non-API routes via `SpaController.java`. React Router handles client-side routing. The SPA lives at `/`, while the observability dashboard (cameleer3-server) is at `/dashboard`. + ## Development ### Running Tests diff --git a/docker-compose.yml b/docker-compose.yml index 3220989..de2b6d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -79,6 +79,9 @@ services: - traefik.http.services.api.loadbalancer.server.port=8080 - traefik.http.routers.forwardauth.rule=Path(`/auth/verify`) - traefik.http.services.forwardauth.loadbalancer.server.port=8080 + - traefik.http.routers.spa.rule=PathPrefix(`/`) + - traefik.http.routers.spa.priority=1 + - traefik.http.services.spa.loadbalancer.server.port=8080 networks: - cameleer diff --git a/src/main/java/net/siegeln/cameleer/saas/config/SpaController.java b/src/main/java/net/siegeln/cameleer/saas/config/SpaController.java new file mode 100644 index 0000000..64f5bdd --- /dev/null +++ b/src/main/java/net/siegeln/cameleer/saas/config/SpaController.java @@ -0,0 +1,13 @@ +package net.siegeln.cameleer.saas.config; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class SpaController { + + @GetMapping(value = {"/", "/login", "/callback", "/environments/**", "/license"}) + public String spa() { + return "forward:/index.html"; + } +}