diff --git a/Sprint-1-Plan.md b/Sprint-1-Plan.md new file mode 100644 index 0000000..a0a5f54 --- /dev/null +++ b/Sprint-1-Plan.md @@ -0,0 +1,374 @@ +# Sprint 1 — Plan ("Spark") + +**Date:** Pre-Sprint 1 planning +**Module:** Sparkboard (greenfield) +**Author:** Patrick Plate / Lumen (Planner) +**Status:** Entwurf v1 +**Basis:** [Sprint 1 Assessment](Sprint-1-Assessment.md) — Option A approved +**Predecessor:** [plate-auth v0.1.0](https://git.plate-software.de/pplate/plate-auth/wiki/Roadmap#v010--ship-sprint-0-deliverable) must be published before W2. + +--- + +## 0. How to read this plan + +The plan is broken into **seven workstreams** (W0–W6). They are partially parallel — W0/W1 must complete before W2, but W2/W3/W4 can be developed in parallel by the same human, and W5/W6 are tail-end. Each workstream lists: + +- **Goal** — what the workstream produces +- **Deliverables** — files / artifacts that must exist +- **Acceptance gate** — what proves the workstream is done +- **Code sketches** — illustrative, not final + +The acceptance criteria A1–A6 from the [Assessment](Sprint-1-Assessment.md#7-acceptance-criteria-a1a6) are mapped to workstreams in the [implementation order](Sprint-1-Plan-Part-4.md#implementation-order) section at the end. + +Code paths shown as `[`backend/...`](../backend/...)` will resolve once the `sparkboard` code repo is cloned alongside this wiki. + +--- + +## 1. Background + +Sparkboard does not exist yet. There is no codebase, no DB, no DNS. Sprint 1 builds the walking skeleton: a Spring Boot 4.1 + Java 25 backend consuming `plate-auth-starter:0.1.0`, a Next.js 15 frontend consuming `@platesoft/auth:0.1.0`, a Postgres database, and a deployment to `sparkboard.plate-software.de`. + +The whole sprint is an integration exercise. The product surface (one entity, two endpoints, two pages) is intentionally tiny so that all the difficulty surfaces in the integration. See [Assessment §1](Sprint-1-Assessment.md#1-problem-analysis) for the strategic reasoning. + +--- + +## 2. Architecture summary + +See [Architecture](Architecture.md) for the full picture. Sprint 1 instantiates exactly one section of it: + +- One Spring Boot app: `de.plate.sparkboard.SparkboardApplication` — embeds plate-auth's auto-config. +- One Postgres database: `sparkboard` — with two Flyway histories side-by-side (`flyway_schema_history` for Sparkboard's tables, `flyway_schema_history_auth` for plate-auth's). +- One Next.js app: `frontend/` — embeds `@platesoft/auth/next-auth` factory + `@platesoft/auth/proxy` handlers. +- One Sparkboard SPI implementation: `SparkboardOnboardingHook`. +- One Sparkboard domain table: `ideas` (and one Sparkboard-owned reference table: `spark_org`). +- Deployed to TrueNAS via Gitea Actions, exposed via frps port 30011 + IONOS Apache. + +--- + +## 3. Repository layout + +``` +sparkboard/ +├── README.md +├── docker-compose.yml # local dev: backend + frontend + postgres +├── docker-compose.prod.yml # TrueNAS: backend + frontend + postgres + caddy/apache +├── .gitea/workflows/ +│ ├── ci.yml # build + test on every push +│ └── deploy.yml # SSH deploy to TrueNAS on main +├── deploy/ +│ ├── caddy/Caddyfile # internal reverse proxy on TrueNAS +│ ├── deploy.sh # SSH-driven deploy +│ └── smoke-test.sh # post-deploy health checks +├── backend/ +│ ├── pom.xml +│ ├── Dockerfile +│ └── src/main/ +│ ├── java/de/plate/sparkboard/ +│ │ ├── SparkboardApplication.java +│ │ ├── onboarding/{SparkboardOnboardingHook, SparkboardAdminProperties}.java +│ │ ├── idea/{Idea, IdeaStatus, IdeaRepository, IdeaService, IdeaController, IdeaDto, CreateIdeaRequest}.java +│ │ └── config/SparkboardSecurityCustomizations.java # optional +│ └── resources/ +│ ├── application.yml +│ ├── application-prod.yml +│ └── db/migration/ +│ ├── V1__init.sql # ideas + spark_org tables +│ └── V2__seed_family_spark_org.sql +└── frontend/ + ├── package.json # depends on @platesoft/auth: 0.1.0 + ├── next.config.ts + ├── auth.ts # createAuthConfig from @platesoft/auth/next-auth + ├── middleware.ts # optional re-export + ├── Dockerfile + └── app/ + ├── layout.tsx + ├── globals.css + ├── api/ + │ ├── auth/[...nextauth]/route.ts # exports handlers from auth.ts + │ └── backend/[...path]/route.ts # createProxyHandlers + ├── (auth)/login/page.tsx + └── (app)/ + ├── layout.tsx + ├── page.tsx # redirect to /ideas + └── ideas/ + ├── page.tsx # list (server component) + ├── new/page.tsx # create form + └── components/{idea-form, idea-list, idea-card}.tsx +``` + +--- + +## 4. Workstreams + +### W0 — Skeleton + +**Goal:** an empty but buildable Sparkboard. `mvn package` produces a runnable JAR. `pnpm build` produces a Next.js standalone build. CI runs both on every push. + +**Deliverables:** + +1. Gitea repo `pplate/sparkboard` created. +2. `backend/pom.xml` declaring Spring Boot 4.1.0 parent, Java 25 source/target, Maven build. +3. `backend/src/main/java/de/plate/sparkboard/SparkboardApplication.java` — empty `@SpringBootApplication`. +4. `frontend/package.json` declaring Next.js 15, React 19, TypeScript 5. +5. `frontend/app/page.tsx` — placeholder "Sparkboard" string. +6. `docker-compose.yml` for local dev. +7. `.gitea/workflows/ci.yml` building both subprojects. +8. `README.md` with quick-start. + +**Acceptance gate:** +- `git push` triggers CI; CI builds backend + frontend in < 5 min wall-clock; CI is **green**. + +**Code sketch — backend `pom.xml` excerpt:** + +```xml + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.1.0 + + + + de.plate + sparkboard-backend + 0.1.0-SNAPSHOT + + + 25 + 25 + + + + + plate-software-gitea + https://git.plate-software.de/api/packages/platesoft/maven + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + org.postgresql + postgresql + + + org.flywaydb + flyway-database-postgresql + + + + + de.platesoft + plate-auth-starter + 0.1.0 + + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.testcontainers + postgresql + 1.20.6 + test + + + +``` + +**Code sketch — frontend `package.json` excerpt:** + +```json +{ + "name": "sparkboard-frontend", + "version": "0.1.0", + "scripts": { + "dev": "next dev --port 3000", + "build": "next build", + "start": "node .next/standalone/server.js", + "lint": "next lint" + }, + "dependencies": { + "@platesoft/auth": "0.1.0", + "next": "15.1.0", + "next-auth": "5.0.0-beta.25", + "react": "19.0.0", + "react-dom": "19.0.0" + }, + "devDependencies": { + "@playwright/test": "1.50.0", + "tailwindcss": "3.4.17", + "typescript": "5.6.3" + } +} +``` + +**Code sketch — `.gitea/workflows/ci.yml`:** + +```yaml +name: CI +on: + push: + branches: [main, '**'] + pull_request: + +jobs: + backend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: '25' + distribution: 'temurin' + - run: mvn -B verify + working-directory: backend + frontend: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + - uses: pnpm/action-setup@v4 + - run: pnpm install --frozen-lockfile + working-directory: frontend + - run: pnpm build + working-directory: frontend +``` + +--- + +### W1 — plate-auth wire-up + +**Goal:** the four allowlisted Google users can sign in. Sparkboard has not yet implemented `OnboardingHook`, so a successful login leaves them with `auth_identities` row but no `memberships` row — they are signed in but "homeless". The list page will look empty until W2 ships the hook. + +**Pre-requisite:** [plate-auth v0.1.0](https://git.plate-software.de/pplate/plate-auth/wiki/Roadmap) is **published and pullable** from `https://git.plate-software.de/api/packages/platesoft/maven` and `https://git.plate-software.de/api/packages/platesoft/npm`. If this is not the case at the start of W1, **stop and ping plate-auth Sprint 0**. + +**Deliverables:** + +1. `backend/src/main/resources/application.yml` with `plate.auth.*` block — see [Architecture §10.1](Architecture.md#101-backend-applicationyml) for the full block. +2. `backend/src/main/resources/application-prod.yml` overriding env-derived values for production. +3. `frontend/auth.ts` calling `createAuthConfig({ ... })` from `@platesoft/auth/next-auth`. +4. `frontend/app/api/auth/[...nextauth]/route.ts` exporting the NextAuth handlers from `auth.ts`. +5. `frontend/app/api/backend/[...path]/route.ts` calling `createProxyHandlers({ ... })` from `@platesoft/auth/proxy`. +6. `frontend/app/(auth)/login/page.tsx` with a "Sign in with Google" button (calls `signIn('google')`). +7. Google Cloud Console: OAuth client with redirect URIs: + - `http://localhost:3000/api/auth/callback/google` (dev) + - `https://sparkboard.plate-software.de/api/auth/callback/google` (prod) +8. Local `.env.local` and TrueNAS `/mnt/tank/sparkboard/.env` with all required secrets — see [Integration Guide §2](Integration-Guide.md#2-secrets-and-env-vars). + +**Acceptance gate:** +- A Gmail account in `plate.auth.allowlist.emails` can sign in **locally** (`http://localhost:3000`) and reach `/ideas` (which is still placeholder content). +- A Gmail account NOT in the allowlist gets a "not authorised" message and **no row is created** in `auth_identities`. +- The `auth_identities` table contains one row per successful login (idempotent on subsequent logins). +- The `memberships` table is **empty** (W2 will fill it). +- This satisfies **A1** (allowlisted user can sign in) and **A2** (non-allowlisted user is rejected) **except** for the post-login `/ideas` page being meaningful — that part of A1 lands in W4. + +**Code sketch — `frontend/auth.ts`:** + +```typescript +import NextAuth from "next-auth"; +import { createAuthConfig } from "@platesoft/auth/next-auth"; + +export const { handlers, auth, signIn, signOut } = NextAuth( + createAuthConfig({ + backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, // http://backend:8080 + exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!, + nextAuthSecret: process.env.NEXTAUTH_SECRET!, + providers: { + google: { + clientId: process.env.GOOGLE_CLIENT_ID!, + clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + }, + }, + pages: { signIn: "/login" }, + }) +); +``` + +**Code sketch — `frontend/app/api/auth/[...nextauth]/route.ts`:** + +```typescript +export { handlers as GET, handlers as POST } from "@/auth"; +``` + +**Code sketch — `frontend/app/api/backend/[...path]/route.ts`:** + +```typescript +import { createProxyHandlers } from "@platesoft/auth/proxy"; + +export const { GET, POST, PUT, PATCH, DELETE } = createProxyHandlers({ + backendUrl: process.env.PLATE_AUTH_BACKEND_URL!, + exchangeSecret: process.env.PLATE_AUTH_EXCHANGE_SECRET!, +}); + +export const runtime = "nodejs"; // proxy requires Node runtime (duplex: 'half') +``` + +**Code sketch — `frontend/app/(auth)/login/page.tsx`:** + +```tsx +"use client"; +import { signIn } from "next-auth/react"; + +export default function LoginPage() { + return ( +
+ +
+ ); +} +``` + +**Code sketch — `backend/src/main/resources/application.yml` (plate.auth excerpt only):** + +```yaml +plate: + auth: + jwt: + secret: ${PLATE_AUTH_JWT_SECRET} + access-expiration: PT15M + refresh-expiration: P30D + exchange: + secret: ${PLATE_AUTH_EXCHANGE_SECRET} + registration: + enabled: false + allowlist: + enabled: true + emails: ${PLATE_AUTH_ALLOWLIST_EMAILS} # comma-separated env var + providers: + google: + enabled: true + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} +``` + +Note: zero Java code is written in W1. Everything is configuration and frontend wiring. **That is the win.** + +--- + +_Continued in [Sprint-1-Plan-Part-2](Sprint-1-Plan-Part-2.md): W2 (domain) and W3 (API)._ +_Continued in [Sprint-1-Plan-Part-3](Sprint-1-Plan-Part-3.md): W4 (frontend), W5 (seed data), W6 (deploy + CI/CD)._ +_Continued in [Sprint-1-Plan-Part-4](Sprint-1-Plan-Part-4.md): implementation order, acceptance mapping, open questions._