Migrate project artifacts to spec-kit format

- Move cross-cutting docs (personas, design system, implementation phases,
  Ideen.md) to .specify/memory/
- Move cross-cutting research and plans to .specify/memory/research/ and
  .specify/memory/plans/
- Extract 5 setup tasks from spec/setup-tasks.md into individual
  specs/001-005/spec.md files with spec-kit template format
- Extract 20 user stories from spec/userstories.md into individual
  specs/006-026/spec.md files with spec-kit template format
- Relocate feature-specific research and plan docs into specs/[feature]/
- Add spec-kit constitution, templates, scripts, and slash commands
- Slim down CLAUDE.md to Claude-Code-specific config, delegate principles
  to .specify/memory/constitution.md
- Update ralph.sh with stream-json output and per-iteration logging
- Delete old spec/ and docs/agents/ directories
- Gitignore Ralph iteration JSONL logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 20:19:41 +01:00
parent 0b2b84dafc
commit 6aeb4b8bca
83 changed files with 6486 additions and 660 deletions

View File

@@ -1,487 +0,0 @@
---
date: 2026-03-04T01:40:21+01:00
git_commit: a55174b32333d0f46a55d94a50604344d1ba33f6
branch: master
topic: "Backpressure for Agentic Coding"
tags: [plan, backpressure, hooks, checkstyle, spotbugs, archunit, quality]
status: complete
---
# Backpressure for Agentic Coding — Implementation Plan
## Overview
Implement automated feedback mechanisms (backpressure) that force the AI agent to self-correct before a human reviews the output. The approach follows the 90/10 rule: 90% deterministic constraints (types, linting, architecture tests), 10% agentic review.
## Current State vs. Desired State
| Layer | Backend (now) | Backend (after) | Frontend (now) | Frontend (after) |
|-------|---------------|-----------------|----------------|------------------|
| Type System | Java 25 (strong) | *unchanged* | TS strict + `noUncheckedIndexedAccess` | *unchanged* |
| Static Analysis | **None** | Checkstyle (Google Style) + SpotBugs | ESLint + oxlint + Prettier | *unchanged* |
| Architecture Tests | **None** | ArchUnit (hexagonal enforcement) | N/A | N/A |
| Unit Tests | JUnit 5 | JUnit 5 + fail-fast (`skipAfterFailureCount: 1`) | Vitest | Vitest + fail-fast (`bail: 1`) |
| PostToolUse Hook | **None** | `./mvnw compile -q` (incl. Checkstyle) | **None** | `vue-tsc --noEmit` |
| Stop Hook | **None** | `./mvnw test` | **None** | `npm run test:unit -- --run` |
## What We're NOT Doing
- **Error Prone** — overlaps with SpotBugs, Java 25 compatibility uncertain, more invasive setup
- **Custom ESLint rules** — add later when recurring agent mistakes are observed
- **MCP LSP Server** — experimental, high setup cost, unclear benefit vs. hooks
- **Pre-commit git hooks** — orthogonal concern, not part of this plan
- **CI/CD pipeline** — out of scope, this is about local agent feedback
## Design Decisions
- **Hook matchers** are regex on **tool names** (not file paths). File-path filtering must happen inside the hook script via `tool_input.file_path` from stdin JSON.
- **Context-efficient output**: ✓ on success, full error on failure. Don't waste the agent's context window with passing output.
- **Fail-fast**: one failure at a time. Prevents context-switching between multiple bugs.
- **Stop hook** checks `git status --porcelain` to determine if code files changed. Skips test runs on conversational responses.
- **Checkstyle** bound to Maven `validate` phase — automatically triggered by `./mvnw compile`, which means the PostToolUse hook gets Checkstyle for free.
- **SpotBugs** bound to Maven `verify` phase — NOT hooked, run manually via `./mvnw verify`.
- **ArchUnit**: use `archunit-junit5` 1.4.1 only. Do NOT use `archunit-hexagonal` addon (dormant since Jul 2023, pulls Kotlin, pinned to ArchUnit 1.0.1, expects different package naming).
---
## Task List
Tasks are ordered by priority. Execute one task per iteration, in order. Each task includes the exact changes and a verification command.
### T-BP-01: Create backend compile-check hook script `[x]`
**File**: `.claude/hooks/backend-compile-check.sh` (new, create `.claude/hooks/` directory if needed)
```bash
#!/usr/bin/env bash
set -euo pipefail
# Read hook input from stdin (JSON with tool_input.file_path)
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
# Only run for Java files under backend/
case "$FILE_PATH" in
*/backend/src/*.java) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/backend"
# Run compile (includes validate phase → Checkstyle if configured)
# Context-efficient: suppress output on success, show full output on failure
if OUTPUT=$(./mvnw compile -q 2>&1); then
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Backend compile passed."}}'
else
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
fi
```
Make executable: `chmod +x .claude/hooks/backend-compile-check.sh`
**Verify**: `echo '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` → should output JSON with "✓ Backend compile passed."
**Verify skip**: `echo '{"tool_input":{"file_path":"README.md"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` → should exit 0 silently.
---
### T-BP-02: Create frontend type-check hook script `[x]`
**File**: `.claude/hooks/frontend-type-check.sh` (new)
```bash
#!/usr/bin/env bash
set -euo pipefail
# Read hook input from stdin (JSON with tool_input.file_path)
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
# Only run for TS/Vue files under frontend/
case "$FILE_PATH" in
*/frontend/src/*.ts|*/frontend/src/*.vue) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/frontend"
# Run type-check
# Context-efficient: suppress output on success, show full output on failure
if OUTPUT=$(npx vue-tsc --noEmit 2>&1); then
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ Frontend type-check passed."}}'
else
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
fi
```
Make executable: `chmod +x .claude/hooks/frontend-type-check.sh`
**Verify**: `echo '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.sh` → should output JSON with "✓ Frontend type-check passed."
---
### T-BP-03: Create stop hook script (test gate) `[x]`
**File**: `.claude/hooks/run-tests.sh` (new)
Runs after the agent finishes its response. Checks `git status` for changed source files and runs the relevant test suites. Skips on conversational responses (no code changes).
```bash
#!/usr/bin/env bash
set -euo pipefail
cd "$CLAUDE_PROJECT_DIR"
# Check for uncommitted changes in backend/frontend source
HAS_BACKEND=$(git status --porcelain backend/src/ 2>/dev/null | head -1)
HAS_FRONTEND=$(git status --porcelain frontend/src/ 2>/dev/null | head -1)
# Nothing changed — skip
if [[ -z "$HAS_BACKEND" && -z "$HAS_FRONTEND" ]]; then
exit 0
fi
ERRORS=""
PASSED=""
# Run backend tests if Java sources changed
if [[ -n "$HAS_BACKEND" ]]; then
if OUTPUT=$(cd backend && ./mvnw test -q 2>&1); then
PASSED+="✓ Backend tests passed. "
else
ERRORS+="Backend tests failed:\n$OUTPUT\n\n"
fi
fi
# Run frontend tests if TS/Vue sources changed
if [[ -n "$HAS_FRONTEND" ]]; then
if OUTPUT=$(cd frontend && npm run test:unit -- --run 2>&1); then
PASSED+="✓ Frontend tests passed. "
else
ERRORS+="Frontend tests failed:\n$OUTPUT\n\n"
fi
fi
if [[ -n "$ERRORS" ]]; then
ESCAPED=$(printf '%s' "$ERRORS" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"additionalContext\":$ESCAPED}}"
else
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"Stop\",\"additionalContext\":\"$PASSED\"}}"
fi
```
Make executable: `chmod +x .claude/hooks/run-tests.sh`
**Verify**: `CLAUDE_PROJECT_DIR=. .claude/hooks/run-tests.sh` → if no uncommitted changes in source dirs, should exit 0 silently.
---
### T-BP-04: Create `.claude/settings.json` with hook configuration `[x]`
**File**: `.claude/settings.json` (new — do NOT modify `.claude/settings.local.json`, that has permissions)
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/backend-compile-check.sh\"",
"timeout": 120
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/frontend-type-check.sh\"",
"timeout": 60
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/run-tests.sh\"",
"timeout": 300
}
]
}
]
}
}
```
**Verify**: File exists and is valid JSON: `python3 -c "import json; json.load(open('.claude/settings.json'))"`
---
### T-BP-05: Configure Vitest fail-fast `[x]`
**File**: `frontend/vitest.config.ts` (modify)
Add `bail: 1` to the test configuration object. The result should look like:
```typescript
export default mergeConfig(
viteConfig,
defineConfig({
test: {
environment: 'jsdom',
exclude: [...configDefaults.exclude, 'e2e/**'],
root: fileURLToPath(new URL('./', import.meta.url)),
bail: 1,
},
}),
)
```
**Verify**: `cd frontend && npm run test:unit -- --run` passes.
---
### T-BP-06: Configure Maven Surefire fail-fast `[x]`
**File**: `backend/pom.xml` (modify)
Add `maven-surefire-plugin` configuration within `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Fail-fast: stop on first test failure -->
<skipAfterFailureCount>1</skipAfterFailureCount>
</configuration>
</plugin>
```
**Verify**: `cd backend && ./mvnw test` passes.
---
### T-BP-07: Add Checkstyle plugin + fix violations `[x]`
**File**: `backend/pom.xml` (modify)
Add `maven-checkstyle-plugin` within `<build><plugins>`:
```xml
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.6.0</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>13.3.0</version>
</dependency>
</dependencies>
<configuration>
<configLocation>google_checks.xml</configLocation>
<consoleOutput>true</consoleOutput>
<failOnViolation>true</failOnViolation>
<violationSeverity>warning</violationSeverity>
</configuration>
<executions>
<execution>
<id>checkstyle-validate</id>
<phase>validate</phase>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
```
Then run `cd backend && ./mvnw checkstyle:check` to find violations. Fix all violations in existing source files (`FeteApplication.java`, `HealthController.java`, `FeteApplicationTest.java`, all `package-info.java` files). Google Style requires: 2-space indentation, specific import order, Javadoc on public types, max line length 100.
**Verify**: `cd backend && ./mvnw checkstyle:check` passes with zero violations AND `cd backend && ./mvnw compile` passes (Checkstyle now runs during validate phase).
---
### T-BP-08: Add SpotBugs plugin + verify `[x]`
**File**: `backend/pom.xml` (modify)
Add `spotbugs-maven-plugin` within `<build><plugins>`:
```xml
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>4.9.8.2</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<xmlOutput>true</xmlOutput>
<failOnError>true</failOnError>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
```
Run `cd backend && ./mvnw verify` — if SpotBugs finds issues, fix them.
**Verify**: `cd backend && ./mvnw verify` passes (includes compile + test + SpotBugs).
---
### T-BP-09: Add ArchUnit dependency + write architecture tests `[x]`
**File 1**: `backend/pom.xml` (modify) — add within `<dependencies>`:
```xml
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit-junit5</artifactId>
<version>1.4.1</version>
<scope>test</scope>
</dependency>
```
Do NOT use the `archunit-hexagonal` addon.
**File 2**: `backend/src/test/java/de/fete/HexagonalArchitectureTest.java` (new)
```java
package de.fete;
import com.tngtech.archunit.core.importer.ImportOption;
import com.tngtech.archunit.junit.AnalyzeClasses;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.Architectures.onionArchitecture;
@AnalyzeClasses(packages = "de.fete", importOptions = ImportOption.DoNotIncludeTests.class)
class HexagonalArchitectureTest {
@ArchTest
static final ArchRule onion_architecture_is_respected = onionArchitecture()
.domainModels("de.fete.domain.model..")
.domainServices("de.fete.domain.port.in..", "de.fete.domain.port.out..")
.applicationServices("de.fete.application.service..")
.adapter("web", "de.fete.adapter.in.web..")
.adapter("persistence", "de.fete.adapter.out.persistence..")
.adapter("config", "de.fete.config..");
@ArchTest
static final ArchRule domain_must_not_depend_on_adapters = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter..");
@ArchTest
static final ArchRule domain_must_not_depend_on_application = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.application..");
@ArchTest
static final ArchRule domain_must_not_depend_on_config = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("de.fete.config..");
@ArchTest
static final ArchRule inbound_ports_must_be_interfaces = classes()
.that().resideInAPackage("de.fete.domain.port.in..")
.should().beInterfaces();
@ArchTest
static final ArchRule outbound_ports_must_be_interfaces = classes()
.that().resideInAPackage("de.fete.domain.port.out..")
.should().beInterfaces();
@ArchTest
static final ArchRule domain_must_not_use_spring = noClasses()
.that().resideInAPackage("de.fete.domain..")
.should().dependOnClassesThat().resideInAPackage("org.springframework..");
@ArchTest
static final ArchRule web_must_not_depend_on_persistence = noClasses()
.that().resideInAPackage("de.fete.adapter.in.web..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.out.persistence..");
@ArchTest
static final ArchRule persistence_must_not_depend_on_web = noClasses()
.that().resideInAPackage("de.fete.adapter.out.persistence..")
.should().dependOnClassesThat().resideInAPackage("de.fete.adapter.in.web..");
}
```
**Verify**: `cd backend && ./mvnw test` passes and output shows `HexagonalArchitectureTest` with 9 tests.
---
### T-BP-10: Update CLAUDE.md `[x]`
**File**: `CLAUDE.md` (modify)
Add two rows to the Build Commands table:
| What | Command |
|------|---------|
| Backend checkstyle | `cd backend && ./mvnw checkstyle:check` |
| Backend full verify | `cd backend && ./mvnw verify` |
---
### T-BP-11: Final verification `[x]`
Run all verification commands to confirm the complete backpressure stack works:
1. `test -x .claude/hooks/backend-compile-check.sh`
2. `test -x .claude/hooks/frontend-type-check.sh`
3. `test -x .claude/hooks/run-tests.sh`
4. `python3 -c "import json; json.load(open('.claude/settings.json'))"`
5. `cd backend && ./mvnw verify` (triggers: Checkstyle → compile → test w/ ArchUnit → SpotBugs)
6. `cd frontend && npm run test:unit -- --run`
7. `echo '{"tool_input":{"file_path":"backend/src/main/java/de/fete/FeteApplication.java"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh`
8. `echo '{"tool_input":{"file_path":"frontend/src/App.vue"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/frontend-type-check.sh`
9. `echo '{"tool_input":{"file_path":"README.md"}}' | CLAUDE_PROJECT_DIR=. .claude/hooks/backend-compile-check.sh` (should be silent)
All commands must exit 0. If any fail, go back and fix the issue before marking complete.
---
## Addendum: Implementation Deviations (2026-03-04)
Changes made during implementation that deviate from or extend the original plan:
1. **Stop hook JSON schema**: The plan used `hookSpecificOutput` with `hookEventName: "Stop"` for the stop hook output. This is invalid — `hookSpecificOutput` is only supported for `PreToolUse`, `PostToolUse`, and `UserPromptSubmit` events. Fixed to use top-level `"decision": "approve"/"block"` with `"reason"` field.
2. **Stop hook loop prevention**: Added `stop_hook_active` check from stdin JSON to prevent infinite re-engagement loops. Not in original plan.
3. **Context-efficient test output**: Added `logback-test.xml` (root level WARN), `redirectTestOutputToFile=true`, and `trimStackTrace=true` to Surefire config. The stop hook script filters output to `[ERROR]` lines only, stripping Maven boilerplate. Not in original plan — added after observing that raw test failure output consumed excessive context.
4. **Hook path matching**: Case patterns in hook scripts extended to match both absolute and relative file paths (`*/backend/src/*.java|backend/src/*.java`). Original plan only had `*/backend/src/*.java` which doesn't match relative paths.
5. **Checkstyle `includeTestSourceDirectory`**: Set to `true` so test sources also follow Google Style. Not in original plan. `FeteApplicationTest.java` was reformatted to 2-space indentation with correct import order (static imports first).
6. **ArchUnit field naming**: Changed from `snake_case` (`onion_architecture_is_respected`) to `camelCase` (`onionArchitectureIsRespected`) to comply with Google Checkstyle rules.
---
## References
- Research document: `docs/agents/research/2026-03-04-backpressure-agentic-coding.md`
- Geoffrey Huntley: [Don't waste your back pressure](https://ghuntley.com/pressure/)
- JW: [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure)
- HumanLayer: [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure)
- Claude Code: [Hooks Reference](https://code.claude.com/docs/en/hooks)
- ArchUnit: [User Guide](https://www.archunit.org/userguide/html/000_Index.html)

View File

@@ -1,179 +0,0 @@
# Implementation Plan: T-1 — Initialize Monorepo Structure
## Context
This is the first setup task for the fete project — a privacy-focused, self-hostable PWA for event announcements and RSVPs. The repository currently contains only documentation, specs, and Ralph loop infrastructure. No application code exists yet.
The tech stack was decided during the research phase (see `docs/agents/research/2026-03-04-t1-monorepo-setup.md`):
- **Backend:** Java 25 (latest LTS), ~~Spring Boot 4.0~~ Spring Boot 3.5.11 (see addendum), Maven, hexagonal architecture, base package `de.fete`
- **Java installation:** Via SDKMAN! (`sdk install java 25-open`), no system-level JDK
- **Frontend:** Vue 3, Vite, TypeScript, Vue Router, Vitest
- **Node.js:** Latest LTS (24)
## Phase 1: Backend Scaffold
### 1.1 Create directory structure
Create `backend/` with the hexagonal architecture package layout:
```
backend/
├── pom.xml
├── mvnw, mvnw.cmd
├── .mvn/wrapper/maven-wrapper.properties
└── src/
├── main/
│ ├── java/de/fete/
│ │ ├── FeteApplication.java
│ │ ├── domain/
│ │ │ ├── model/package-info.java
│ │ │ └── port/
│ │ │ ├── in/package-info.java
│ │ │ └── out/package-info.java
│ │ ├── application/
│ │ │ └── service/package-info.java
│ │ ├── adapter/
│ │ │ ├── in/web/package-info.java
│ │ │ └── out/persistence/package-info.java
│ │ └── config/package-info.java
│ └── resources/
│ └── application.properties
└── test/
└── java/de/fete/
└── FeteApplicationTest.java
```
### 1.2 Create `pom.xml`
- Parent: `spring-boot-starter-parent` (latest 4.0.x, or fall back to 3.5.x if 4.0 is unavailable)
- GroupId: `de.fete`, ArtifactId: `fete-backend`
- Java version: 25
- Dependencies:
- `spring-boot-starter-web` (embedded Tomcat, Spring MVC)
- `spring-boot-starter-test` (scope: test)
- NO JPA yet (deferred to T-4)
### 1.3 Add Maven Wrapper
Generate via `mvn wrapper:wrapper` (or manually create the wrapper files). This ensures contributors and Docker builds don't need a Maven installation.
### 1.4 Create application class
`FeteApplication.java` in `de.fete` — minimal `@SpringBootApplication` with `main()`.
### 1.5 Create a minimal health endpoint
A simple `@RestController` in `adapter/in/web/` that responds to `GET /health` with HTTP 200 and a JSON body like `{"status": "ok"}`. This satisfies:
- The user's requirement that a REST request returns 200
- Prepares for T-2's health-check requirement
### 1.6 Create package-info.java markers
One per leaf package, documenting the package's purpose and architectural constraints. These serve as Git directory markers AND developer documentation.
### 1.7 Write integration test
`FeteApplicationTest.java`:
- `@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)`
- Test 1: Spring context loads successfully
- Test 2: HTTP GET to `/health` returns 200
### 1.8 Verify backend
- [x] `cd backend && ./mvnw test` — tests pass (context loads + health endpoint returns 200)
- [x] `cd backend && ./mvnw spring-boot:run` — app starts on port 8080
- [x] `curl http://localhost:8080/health` — returns 200 with `{"status": "ok"}`
## Phase 2: Frontend Scaffold
### 2.1 Scaffold Vue project
Run `npm create vue@latest` in `frontend/` with these options:
- TypeScript: Yes
- Vue Router: Yes
- Pinia: No
- Vitest: Yes
- ESLint: Yes
- Prettier: Yes
- E2E: No
If the interactive prompts can't be automated, create the project manually based on the `create-vue` template output.
### 2.2 Add `composables/` directory
Create `src/composables/` (empty, with a `.gitkeep` or initial placeholder) — convention for Composition API composables needed later.
### 2.3 Verify frontend builds and tests
- [x] `cd frontend && npm install`
- [x] `cd frontend && npm run build` — builds successfully
- [x] `cd frontend && npm run test:unit` — Vitest runs (sample test passes)
### 2.4 Verify via browser
- [x] `cd frontend && npm run dev` — starts dev server
- [x] Use `browser-interactive-testing` skill to visit the dev server URL and verify the page loads
## Phase 3: Shared Files
### 3.1 Update `.gitignore`
Extend the existing `.gitignore` with sections for:
- Java/Maven (target/, *.class, *.jar, Maven release files, crash logs)
- Node.js/Vue/Vite (node_modules/, dist/, build/, vite temp files)
- Environment files (.env, .env.* but NOT .env.example)
- Editor swap files (*.swp, *.swo, *~)
- Spring Boot (.springBeans, .sts4-cache)
Keep existing entries (IDE, OS, Claude settings) intact.
### 3.2 Verify .gitignore
Run `git status` after building both projects to confirm build artifacts are properly ignored.
- [x] `git status` shows no `target/`, `node_modules/`, `dist/`, or other build artifacts
## Verification Checklist (Done Criteria)
### Backend
- [x] `cd backend && ./mvnw test` passes (integration test: context loads + GET /health → 200)
- [x] `cd backend && ./mvnw spring-boot:run` starts successfully
- [x] `curl http://localhost:8080/health` returns HTTP 200
- [x] Hexagonal package structure exists with package-info.java markers
### Frontend
- [x] `cd frontend && npm run build` succeeds
- [x] `cd frontend && npm run test:unit` runs and passes
- [x] Browser verification via `browser-interactive-testing` skill confirms page loads
### Shared
- [x] `.gitignore` covers Java/Maven + Node/Vue artifacts
- [x] `git status` shows no unintended tracked build artifacts
- [x] T-1 acceptance criteria from `spec/setup-tasks.md` are met
## Files to Create/Modify
**Create:**
- `backend/pom.xml`
- `backend/mvnw`, `backend/mvnw.cmd`, `backend/.mvn/wrapper/maven-wrapper.properties`
- `backend/src/main/java/de/fete/FeteApplication.java`
- `backend/src/main/java/de/fete/adapter/in/web/HealthController.java`
- `backend/src/main/java/de/fete/domain/model/package-info.java`
- `backend/src/main/java/de/fete/domain/port/in/package-info.java`
- `backend/src/main/java/de/fete/domain/port/out/package-info.java`
- `backend/src/main/java/de/fete/application/service/package-info.java`
- `backend/src/main/java/de/fete/adapter/in/web/package-info.java`
- `backend/src/main/java/de/fete/adapter/out/persistence/package-info.java`
- `backend/src/main/java/de/fete/config/package-info.java`
- `backend/src/main/resources/application.properties`
- `backend/src/test/java/de/fete/FeteApplicationTest.java`
- `frontend/` (entire scaffolded project via create-vue)
**Modify:**
- `.gitignore` (extend with Java/Maven + Node/Vue sections)
## Addendum: Spring Boot 4.0 → 3.5 Pivot
During implementation, Spring Boot 4.0.3 was abandoned in favor of **3.5.11**. The 4.0 release reorganized test infrastructure into new modules and packages (`TestRestTemplate``spring-boot-resttestclient`, `AutoConfigureMockMvc` → unknown location) without adequate migration documentation. Multiple attempts to resolve compilation and auto-configuration errors failed, making it impractical for the scaffold phase.
The pivot has no impact on project architecture or future tasks. Migration to 4.x can be revisited once the ecosystem matures. See the research report addendum for full details.

View File

@@ -1,366 +0,0 @@
---
date: 2026-03-04T17:40:16+00:00
git_commit: 96ef8656bd87032696ae82198620d99f20d80d3b
branch: master
topic: "T-2: Docker Deployment Setup"
tags: [plan, docker, deployment, spring-boot, spa]
status: draft
---
# T-2: Docker Deployment Setup
## Overview
Create a multi-stage Dockerfile that builds both backend (Spring Boot) and frontend (Vue 3) and produces a single runnable container. Spring Boot serves the SPA's static files directly — one process, one port, one JAR. This requires migrating from `server.servlet.context-path=/api` to `addPathPrefix` and adding SPA forwarding so Vue Router's history mode works.
## Current State Analysis
**What exists:**
- Backend: Spring Boot 3.5.11, Java 25, Maven with wrapper, Actuator health endpoint
- Frontend: Vue 3, Vite, TypeScript, Vue Router (history mode), openapi-fetch client
- `server.servlet.context-path=/api` scopes everything under `/api` (including static resources)
- Health test at `backend/src/test/java/de/fete/FeteApplicationTest.java:27` uses `get("/actuator/health")`
- OpenAPI spec at `backend/src/main/resources/openapi/api.yaml` has `servers: [{url: /api}]`
- Frontend client at `frontend/src/api/client.ts` uses `baseUrl: "/api"`
- ArchUnit registers `de.fete.config..` as an adapter package — config classes belong there
- `package-lock.json` exists (needed for `npm ci`)
- Maven wrapper `.properties` present (JAR downloads automatically)
- No Dockerfile, no `.dockerignore` exist yet
**What's missing:**
- Dockerfile (multi-stage build)
- `.dockerignore`
- `WebConfig` to replace context-path with `addPathPrefix`
- SPA forwarding config (so Vue Router history mode works when served by Spring Boot)
### Key Discoveries:
- `FeteApplicationTest.java:27`: MockMvc test uses `get("/actuator/health")` — path is relative to context path in MockMvc, so removing context-path does NOT break this test
- `api.yaml:11`: OpenAPI `servers: [{url: /api}]` — stays unchanged, `addPathPrefix` produces the same routing
- `client.ts:4`: `baseUrl: "/api"` — stays unchanged
- `HexagonalArchitectureTest.java:22`: `config` is an adapter in ArchUnit → new config classes go in `de.fete.config`
- `router/index.ts:5`: Uses `createWebHistory` → history mode, needs server-side SPA forwarding
## Desired End State
After implementation:
1. `docker build -t fete .` succeeds at the repo root
2. `docker run -p 8080:8080 fete` starts the container
3. `curl http://localhost:8080/actuator/health` returns `{"status":"UP"}`
4. `curl http://localhost:8080/` returns the Vue SPA's `index.html`
5. `curl http://localhost:8080/some/spa/route` also returns `index.html` (SPA forwarding)
6. `curl http://localhost:8080/assets/...` returns actual static files
7. All existing tests still pass (`./mvnw test` and `npm run test:unit`)
## What We're NOT Doing
- Database wiring (`DATABASE_URL`, JPA, Flyway) — deferred to T-4
- docker-compose example — deferred to T-4
- Environment variable configuration (Unsplash key, max events) — deferred to T-4
- README deployment documentation — deferred to T-4
- Production hardening (JVM flags, memory limits, graceful shutdown) — not in scope
- TLS/HTTPS — hoster's responsibility (reverse proxy)
- gzip compression config — premature optimization at this scale
## Implementation Approach
Three phases, strictly sequential: first the Spring Boot config changes (testable locally without Docker), then the Dockerfile and `.dockerignore`, then end-to-end verification.
---
## Phase 1: Spring Boot Configuration Changes
### Overview
Replace `context-path=/api` with `addPathPrefix` so that API endpoints live under `/api/*` but static resources and SPA routes are served at `/`. Add SPA forwarding so non-API, non-static requests return `index.html`.
### Changes Required:
#### [x] 1. Remove context-path from application.properties
**File**: `backend/src/main/resources/application.properties`
**Changes**: Remove `server.servlet.context-path=/api`. Explicitly set Actuator base path to keep it outside `/api`.
```properties
spring.application.name=fete
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=never
```
Note: Actuator defaults to `/actuator` which is exactly where we want it (outside `/api`). No extra config needed.
#### [x] 2. Create WebConfig with addPathPrefix
**File**: `backend/src/main/java/de/fete/config/WebConfig.java`
**Changes**: New `@Configuration` class that prefixes only `@RestController` handlers with `/api`.
```java
package de.fete.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import java.io.IOException;
/** Configures API path prefix and SPA static resource serving. */
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api",
c -> c.isAnnotationPresent(RestController.class));
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requested = location.createRelative(resourcePath);
return (requested.exists() && requested.isReadable())
? requested
: new ClassPathResource("/static/index.html");
}
});
}
}
```
This single class handles both concerns:
- `configurePathMatch`: prefixes `@RestController` endpoints with `/api`
- `addResourceHandlers`: serves static files from `classpath:/static/`, falls back to `index.html` for SPA routes
The SPA forwarding only activates when `classpath:/static/index.html` exists (i.e. in the Docker image where the frontend is bundled). During local backend development without frontend assets, requests to `/` will simply 404 as before — no behavior change for the dev workflow.
#### [x] 3. Write test for WebConfig behavior
**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java`
**Changes**: New test class verifying the `/api` prefix routing and actuator independence.
```java
package de.fete.config;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
class WebConfigTest {
@Autowired
private MockMvc mockMvc;
@Test
void actuatorHealthIsOutsideApiPrefix() throws Exception {
mockMvc.perform(get("/actuator/health"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("UP"));
}
@Test
void apiPrefixNotAccessibleWithoutIt() throws Exception {
// /health without /api prefix should not resolve to the API endpoint
mockMvc.perform(get("/health"))
.andExpect(status().isNotFound());
}
}
```
Note: The existing `FeteApplicationTest.healthEndpointReturns200()` already tests `/actuator/health`. Since we're removing context-path but MockMvc paths are relative to the servlet context anyway, the existing test continues to pass without changes. The new test adds explicit verification that the `/api` prefix isolation works correctly.
### Success Criteria:
#### Automated Verification:
- [x] `cd backend && ./mvnw test` — all tests pass (existing + new)
- [x] `cd backend && ./mvnw checkstyle:check` — no style violations
#### Manual Verification:
- [x] `cd backend && ./mvnw spring-boot:run`, then `curl http://localhost:8080/actuator/health` returns `{"status":"UP"}`
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 2: Dockerfile and .dockerignore
### Overview
Create the multi-stage Dockerfile (frontend build → backend build with static assets → JRE runtime) and a `.dockerignore` to keep the build context clean.
### Changes Required:
#### [x] 1. Create .dockerignore
**File**: `.dockerignore`
**Changes**: Exclude build artifacts, dependencies, IDE files, and dev-only files from the Docker build context.
```
# Build artifacts
**/target/
**/dist/
**/build/
# Dependencies (rebuilt inside Docker)
**/node_modules/
# IDE
.idea/
.vscode/
**/*.iml
# Git
.git/
.gitignore
# CI/CD
.gitea/
# Agent/dev files
.claude/
.ralph/
.rodney/
.agent-tests/
docs/
spec/
# OS files
.DS_Store
Thumbs.db
# Environment
.env
.env.*
# Generated files (rebuilt in Docker)
frontend/src/api/schema.d.ts
```
#### [x] 2. Create multi-stage Dockerfile
**File**: `Dockerfile`
**Changes**: Three-stage build — frontend, backend, runtime.
```dockerfile
# Stage 1: Build frontend
FROM node:24-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
# OpenAPI spec needed for type generation (npm run build runs generate:api)
COPY backend/src/main/resources/openapi/api.yaml \
../backend/src/main/resources/openapi/api.yaml
RUN npm run build
# Stage 2: Build backend with frontend assets baked in
FROM eclipse-temurin:25-jdk-alpine AS backend-build
WORKDIR /app/backend
COPY backend/ ./
COPY --from=frontend-build /app/frontend/dist src/main/resources/static/
RUN ./mvnw -B -DskipTests -Dcheckstyle.skip -Dspotbugs.skip package
# Stage 3: Runtime
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
COPY --from=backend-build /app/backend/target/*.jar app.jar
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
```
Design decisions:
- **Frontend first**: `npm run build` triggers `generate:api` which needs `api.yaml` — copied from the build context
- **Layer caching**: `npm ci` before `COPY frontend/` so dependency install is cached unless `package*.json` changes
- **`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip`**: Tests run in CI (T-3), not during image build
- **JRE-only runtime**: No JDK, no Node, no Maven in the final image
- **Alpine images**: Minimal size
- **HEALTHCHECK directive**: Docker-native health checking via Actuator
Note on Java 25: `eclipse-temurin:25-jdk-alpine` / `eclipse-temurin:25-jre-alpine` may not yet be published. If unavailable, fall back to `eclipse-temurin:21-jdk-alpine` / `eclipse-temurin:21-jre-alpine` (current LTS). The Java version in the Dockerfile does not need to match the development Java version exactly — Spring Boot 3.5.x runs on both 21 and 25.
### Success Criteria:
#### Automated Verification:
- [x] `docker build -t fete .` succeeds without errors
#### Manual Verification:
- [x] `docker run --rm -p 8080:8080 fete` starts the container
- [x] `curl http://localhost:8080/actuator/health` returns `{"status":"UP"}`
- [x] `curl http://localhost:8080/` returns HTML (the Vue SPA's index.html)
- [x] `docker images fete --format '{{.Size}}'` shows a reasonable size (< 400MB)
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 3: Finalize and Verify
### Overview
Run all existing tests to confirm no regressions, check off T-2 acceptance criteria in the spec, and commit.
### Changes Required:
#### [x] 1. Check off T-2 acceptance criteria
**File**: `spec/setup-tasks.md`
**Changes**: Mark completed acceptance criteria for T-2.
#### [x] 2. Commit the research report
**File**: `docs/agents/research/2026-03-04-spa-springboot-docker-patterns.md`
**Changes**: Stage and commit the already-written research report alongside the implementation.
### Success Criteria:
#### Automated Verification:
- [x] `cd backend && ./mvnw test` — all tests pass
- [x] `cd frontend && npm run test:unit -- --run` — all tests pass
- [x] `cd backend && ./mvnw verify` — full verification including SpotBugs
- [x] `docker build -t fete .` — still builds cleanly
#### Manual Verification:
- [x] Container starts and health check responds
- [x] SPA is accessible at `http://localhost:8080/`
- [x] All T-2 acceptance criteria in `spec/setup-tasks.md` are checked off
---
## Testing Strategy
### Unit Tests:
- `WebConfigTest.actuatorHealthIsOutsideApiPrefix()` — actuator accessible at `/actuator/health`
- `WebConfigTest.apiPrefixNotAccessibleWithoutIt()``/health` without prefix returns 404
- Existing `FeteApplicationTest.healthEndpointReturns200()` — regression test (unchanged)
### Integration Tests:
- Docker build succeeds end-to-end
- Container starts and serves both API and static content
### Manual Testing Steps:
1. `docker build -t fete .`
2. `docker run --rm -p 8080:8080 fete`
3. Open `http://localhost:8080/` in browser — should see Vue SPA
4. Open `http://localhost:8080/actuator/health` — should see `{"status":"UP"}`
5. Navigate to a non-existent SPA route like `http://localhost:8080/events/test` — should still see Vue SPA (SPA forwarding)
## Migration Notes
- `server.servlet.context-path=/api` is removed → any external clients that previously called `http://host:8080/api/actuator/health` must now call `http://host:8080/actuator/health`. Since the app is not yet deployed, this has zero impact.
- The OpenAPI spec's `servers: [{url: /api}]` is unchanged. The `addPathPrefix` produces identical routing to the old context-path for all `@RestController` endpoints.
## References
- Research report: `docs/agents/research/2026-03-04-spa-springboot-docker-patterns.md`
- T-2 spec: `spec/setup-tasks.md` (lines 24-37)
- Existing health test: `backend/src/test/java/de/fete/FeteApplicationTest.java:26-30`
- ArchUnit config adapter: `backend/src/test/java/de/fete/HexagonalArchitectureTest.java:22`
- Spring PathMatchConfigurer docs: https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/path-matching.html

View File

@@ -1,266 +0,0 @@
---
date: "2026-03-04T18:46:12.266203+00:00"
git_commit: 316137bf1c391577e884ce525af780f45e34da86
branch: master
topic: "T-3: CI/CD Pipeline — Gitea Actions"
tags: [plan, ci-cd, gitea, docker, buildah]
status: implemented
---
# T-3: CI/CD Pipeline Implementation Plan
## Overview
Set up a Gitea Actions CI/CD pipeline that runs on every push (tests only) and on SemVer-tagged releases (tests + build + publish). Single workflow file, three jobs.
## Current State Analysis
- All dependencies completed: T-1 (monorepo), T-2 (Dockerfile), T-5 (API-first tooling)
- Multi-stage Dockerfile exists and works (`Dockerfile:1-26`)
- Dockerfile skips tests (`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip`) by design — quality gates belong in CI
- `.dockerignore` already excludes `.gitea/`
- No CI/CD configuration exists yet
- Backend has full verify lifecycle: Checkstyle, JUnit 5, ArchUnit, SpotBugs
- Frontend has lint (oxlint + ESLint), type-check (`vue-tsc`), tests (Vitest), and Vite production build
## Desired End State
A single workflow file at `.gitea/workflows/ci.yaml` that:
1. Triggers on every push (branches and tags)
2. Runs backend and frontend quality gates in parallel
3. On SemVer tags only: builds the Docker image via Buildah and publishes it with rolling tags
The Docker image build is **not** run on regular pushes. All quality gates (tests, lint, type-check, production build) already run in the test jobs. The Dockerfile itself changes rarely, and any breakage surfaces at the next tagged release. This avoids a redundant image build on every push.
### Verification:
- Push a commit → pipeline runs tests only, no image build
- Push a non-SemVer tag → same behavior
- Push a SemVer tag (e.g. `1.0.0`) → tests + build + publish with 4 tags
### Pipeline Flow:
```
git push (any branch/tag)
┌───────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ backend-test │ │ frontend-test │
│ │ │ │
│ JDK 25 │ │ Node 24 │
│ ./mvnw -B │ │ npm ci │
│ verify │ │ npm run lint │
│ │ │ npm run type-check │
│ │ │ npm run test:unit │
│ │ │ -- --run │
│ │ │ npm run build │
└────────┬─────────┘ └──────────┬───────────┘
│ │
└───────────┬─────────────┘
┌───────────────────┐
│ SemVer-Tag? │── nein ──► DONE
└────────┬──────────┘
│ ja
┌─────────────────────┐
│ build-and-publish │
│ │
│ buildah bud │
│ buildah tag ×4 │
│ buildah push ×4 │
└─────────────────────┘
```
## What We're NOT Doing
- No deployment automation (how/where to deploy is the hoster's responsibility)
- No branch protection rules (Gitea admin concern, not pipeline scope)
- No automated versioning (no release-please, no semantic-release — manual `git tag`)
- No caching optimization (can be added later if runner time becomes a concern)
- No separate staging/production pipelines
- No notifications (Slack, email, etc.)
## Implementation Approach
Single phase — this is one YAML file with well-defined structure. The workflow uses Buildah for image builds to avoid Docker-in-Docker issues on the self-hosted runner.
## Phase 1: Gitea Actions Workflow
### Overview
Create the complete CI/CD workflow file with three jobs: backend-test, frontend-test, build-and-publish (SemVer tags only).
### Changes Required:
#### [x] 1. Create workflow directory
**Action**: `mkdir -p .gitea/workflows/`
#### [x] 2. Explore OpenAPI spec access in CI
The `npm run type-check` and `npm run build` steps need access to the OpenAPI spec because `generate:api` runs as a pre-step. In CI, the checkout includes both `backend/` and `frontend/` as sibling directories, so the relative path `../backend/src/main/resources/openapi/api.yaml` from `frontend/` should resolve correctly.
**Task:** During implementation, verify whether the relative path works from the CI checkout structure. If it does, no `cp` step is needed. If it doesn't, add an explicit copy step. The workflow YAML below includes a `cp` as a safety measure — **remove it if the relative path works without it**.
**Resolution:** The relative path works. The `cp` was removed. An explicit `generate:api` step was added before `type-check` so that `schema.d.ts` exists when `vue-tsc` runs (since `type-check` alone doesn't trigger code generation).
#### [x] 3. Create workflow file
**File**: `.gitea/workflows/ci.yaml`
```yaml
name: CI
on:
push:
jobs:
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
- name: Run backend verify
run: cd backend && ./mvnw -B verify
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: cd frontend && npm ci
- name: Lint
run: cd frontend && npm run lint
- name: Type check
run: |
cp backend/src/main/resources/openapi/api.yaml frontend/
cd frontend && npm run type-check
- name: Unit tests
run: cd frontend && npm run test:unit -- --run
- name: Production build
run: cd frontend && npm run build
build-and-publish:
needs: [backend-test, frontend-test]
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Parse SemVer tag
id: semver
run: |
TAG="${{ github.ref_name }}"
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Not a valid SemVer tag: $TAG"
exit 1
fi
MAJOR="${TAG%%.*}"
MINOR="${TAG%.*}"
echo "full=$TAG" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
- name: Build image
run: |
REGISTRY="${{ github.server_url }}"
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
REPO="${{ github.repository }}"
IMAGE="${REGISTRY}/${REPO}"
buildah bud -t "${IMAGE}:${{ steps.semver.outputs.full }}" .
buildah tag "${IMAGE}:${{ steps.semver.outputs.full }}" \
"${IMAGE}:${{ steps.semver.outputs.minor }}" \
"${IMAGE}:${{ steps.semver.outputs.major }}" \
"${IMAGE}:latest"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
- name: Push to registry
run: |
buildah login -u "${{ github.repository_owner }}" \
-p "${{ secrets.REGISTRY_TOKEN }}" \
"${IMAGE%%/*}"
buildah push "${IMAGE}:${{ steps.semver.outputs.full }}"
buildah push "${IMAGE}:${{ steps.semver.outputs.minor }}"
buildah push "${IMAGE}:${{ steps.semver.outputs.major }}"
buildah push "${IMAGE}:latest"
```
### Success Criteria:
#### Automated Verification:
- [x] YAML is valid: `python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/ci.yaml'))"`
- [x] File is in the correct directory: `.gitea/workflows/ci.yaml`
- [x] Workflow triggers on `push` (all branches and tags)
- [x] `backend-test` uses JDK 25 and runs `./mvnw -B verify`
- [x] `frontend-test` uses Node 24 and runs lint, type-check, tests, and build
- [x] `build-and-publish` depends on both test jobs (`needs: [backend-test, frontend-test]`)
- [x] `build-and-publish` only runs on SemVer tags (`if` condition)
- [x] SemVer parsing correctly extracts major, minor, and full version
- [x] Registry URL is derived from `github.server_url` with protocol stripped
- [x] Authentication uses `secrets.REGISTRY_TOKEN` (not the built-in token)
#### Manual Verification:
- [x] Push a commit to a branch → pipeline runs `backend-test` and `frontend-test` only — no image build
- [x] Push a SemVer tag → pipeline runs all three jobs, image appears in Gitea container registry with 4 tags
- [ ] Break a test intentionally → pipeline fails, `build-and-publish` does not run (skipped — guaranteed by `needs` dependency, verified implicitly)
- [x] Push a non-SemVer tag → pipeline runs tests only, no image build
**Implementation Note**: After creating the workflow file and passing automated verification, the manual verification requires pushing to the actual Gitea instance. Pause here for the human to test on the real runner.
---
## Testing Strategy
### Automated (pre-push):
- YAML syntax validation
- Verify file structure and job dependencies match the spec
### Manual (post-push, on Gitea):
1. Push a normal commit → verify only test jobs run, no image build
2. Intentionally break a backend test → verify pipeline fails at `backend-test`
3. Intentionally break a frontend lint rule → verify pipeline fails at `frontend-test`
4. Fix and push a SemVer tag (e.g. `0.1.0`) → verify all 3 jobs run, image published with rolling tags
5. Verify image is pullable: `docker pull {registry}/{owner}/fete:0.1.0`
## Performance Considerations
- Backend and frontend tests run in parallel (separate jobs) — this is the main time saver
- Docker image build only runs on SemVer tags — no wasted runner time on regular pushes
- No Maven/npm caching configured — can be added later if runner time becomes a problem
## Configuration Prerequisites
The following must be configured in Gitea **before** the pipeline can publish images:
1. **Repository secret** `REGISTRY_TOKEN`: A Gitea Personal Access Token with `package:write` permission
2. **Docker** must be available on the runner (act_runner provides this via socket forwarding)
### Addendum: Buildah → Docker pivot
Buildah was the original choice to avoid Docker-in-Docker issues. However, the act_runner does not have Buildah installed, and running it inside a container would require elevated privileges. Since the runner already has Docker available via socket forwarding, the workflow was switched to `docker build/tag/push`. This is not classic DinD — it uses the host Docker daemon directly.
## References
- Research: `docs/agents/research/2026-03-04-t3-cicd-pipeline.md`
- Spec: `spec/setup-tasks.md:41-55`
- Dockerfile: `Dockerfile:1-26`
- Frontend scripts: `frontend/package.json:6-17`
- Backend plugins: `backend/pom.xml:56-168`

View File

@@ -1,567 +0,0 @@
---
date: 2026-03-04T20:09:31.044992+00:00
git_commit: cb0bcad145b03fec63be0ee3c1fca46ee545329e
branch: master
topic: "T-4: Development Infrastructure Setup"
tags: [plan, database, liquibase, testcontainers, configuration, docker-compose]
status: draft
---
# T-4: Development Infrastructure Setup — Implementation Plan
## Overview
Set up the remaining development infrastructure needed before the first user story (US-1) can be implemented with TDD. This adds JPA + Liquibase for database migrations, PostgreSQL connectivity via environment variables, Testcontainers for integration tests, app-specific configuration properties, and README deployment documentation with a docker-compose example.
## Current State Analysis
**Already complete (no work needed):**
- SPA router: Vue Router with `createWebHistory`, backend SPA fallback in `WebConfig.java`
- Frontend test infrastructure: Vitest + `@vue/test-utils`, sample test passing
- Both test suites executable: `./mvnw test` (3 tests) and `npm run test:unit` (1 test)
**Missing (all work in this plan):**
- JPA, Liquibase, PostgreSQL driver — no database dependencies in `pom.xml`
- Testcontainers — not configured
- Database connectivity — no datasource properties
- App-specific config — no `@ConfigurationProperties`
- Profile separation — no `application-prod.properties`
- Deployment docs — no docker-compose in README
### Key Discoveries:
- `backend/pom.xml:1-170` — Spring Boot 3.5.11, no DB dependencies
- `backend/src/main/resources/application.properties:1-4` — Only app name + actuator
- `HexagonalArchitectureTest.java:22``config` is already an adapter in ArchUnit rules
- `FeteApplicationTest.java` — Uses `@SpringBootTest` + MockMvc; will need datasource after JPA is added
- `Dockerfile:26` — No `SPRING_PROFILES_ACTIVE` set
- `.gitignore:47-51``.env*` patterns exist but no `application-local.properties`
## Desired End State
After this plan is complete:
- `./mvnw test` runs all backend tests (including new Testcontainers-backed integration tests) against a real PostgreSQL without external setup
- `./mvnw spring-boot:run -Dspring-boot.run.profiles=local` starts the app against a local PostgreSQL
- Docker container starts with `DATABASE_URL`/`DATABASE_USERNAME`/`DATABASE_PASSWORD` env vars, runs Liquibase migrations, and responds to health checks
- README contains a copy-paste-ready docker-compose example for deployment
- `FeteProperties` scaffolds `fete.unsplash.api-key` and `fete.max-active-events` (no business logic yet)
### Verification:
- `cd backend && ./mvnw verify` — all tests green, checkstyle + spotbugs pass
- `cd frontend && npm run test:unit -- --run` — unchanged, still green
- `docker build .` — succeeds
- docker-compose (app + postgres) — container starts, `/actuator/health` returns `{"status":"UP"}`
## What We're NOT Doing
- No JPA entities or repositories — those come with US-1
- No domain model classes — those come with US-1
- No business logic for `FeteProperties` (Unsplash, max events) — US-13/US-16
- No standalone `docker-compose.yml` file in repo — inline in README per CLAUDE.md
- No `application-local.properties` committed — only the `.example` template
- No changes to frontend code — AC 4/6/7 are already met
---
## Phase 1: JPA + Liquibase + PostgreSQL Dependencies
### Overview
Add all database-related dependencies to `pom.xml`, create the Liquibase changelog structure with an empty baseline changeset, and update `application.properties` with JPA and Liquibase settings.
### Changes Required:
#### [x] 1. Add database dependencies to `pom.xml`
**File**: `backend/pom.xml`
**Changes**: Add four dependencies after the existing `spring-boot-starter-validation` block.
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.liquibase</groupId>
<artifactId>liquibase-core</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
```
Add Testcontainers dependencies in test scope (after `archunit-junit5`):
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
```
Spring Boot's dependency management handles versions for all of these — no explicit version tags needed (except `archunit-junit5` which is already versioned).
#### [x] 2. Create Liquibase master changelog
**File**: `backend/src/main/resources/db/changelog/db.changelog-master.xml` (new)
**Changes**: Create the master changelog that includes individual changesets.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<include file="db/changelog/000-baseline.xml"/>
</databaseChangeLog>
```
#### [x] 3. Create empty baseline changeset
**File**: `backend/src/main/resources/db/changelog/000-baseline.xml` (new)
**Changes**: Empty changeset that proves the tooling works. Liquibase creates its tracking tables (`databasechangelog`, `databasechangeloglock`) automatically.
```xml
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<!-- T-4: Baseline changeset. Proves Liquibase tooling works.
First real schema comes with US-1. -->
<changeSet id="000-baseline" author="fete">
<comment>Baseline changeset — Liquibase tooling verification</comment>
</changeSet>
</databaseChangeLog>
```
#### [x] 4. Update application.properties with JPA and Liquibase settings
**File**: `backend/src/main/resources/application.properties`
**Changes**: Add JPA and Liquibase configuration (environment-independent, always active).
```properties
spring.application.name=fete
# JPA
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false
# Liquibase
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
# Actuator
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=never
```
### Success Criteria:
#### Automated Verification:
- [ ] `cd backend && ./mvnw compile` succeeds (dependencies resolve, checkstyle passes)
- [ ] Changelog XML files are well-formed (Maven compile does not fail on resource processing)
#### Manual Verification:
- [ ] Verify `pom.xml` has all six new dependencies with correct scopes
- [ ] Verify changelog directory structure: `db/changelog/db.changelog-master.xml` includes `000-baseline.xml`
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 2: Profile-Based Configuration and App Properties
### Overview
Create the profile-based property files for production and local development, add the `FeteProperties` configuration class, update `.gitignore`, and set the production profile in the Dockerfile.
### Changes Required:
#### [x] 1. Create production properties file
**File**: `backend/src/main/resources/application-prod.properties` (new)
**Changes**: Production profile with environment variable placeholders. Activated in Docker via `SPRING_PROFILES_ACTIVE=prod`.
```properties
# Database (required)
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}
# App-specific (optional)
fete.unsplash.api-key=${UNSPLASH_API_KEY:}
fete.max-active-events=${MAX_ACTIVE_EVENTS:0}
```
#### [x] 2. Create local development properties template
**File**: `backend/src/main/resources/application-local.properties.example` (new)
**Changes**: Template that developers copy to `application-local.properties` (which is gitignored).
```properties
# Local development database
# Copy this file to application-local.properties and adjust as needed.
# Start with: ./mvnw spring-boot:run -Dspring-boot.run.profiles=local
spring.datasource.url=jdbc:postgresql://localhost:5432/fete
spring.datasource.username=fete
spring.datasource.password=fete
```
#### [x] 3. Add `application-local.properties` to `.gitignore`
**File**: `.gitignore`
**Changes**: Add the gitignore entry for the local properties file (under the Environment section).
```
# Spring Boot local profile (developer-specific, not committed)
backend/src/main/resources/application-local.properties
```
#### ~~4. Create `FeteProperties` configuration properties class~~ (deferred)
**File**: `backend/src/main/java/de/fete/config/FeteProperties.java` (new)
**Changes**: Type-safe configuration for app-specific settings. Both properties are only scaffolded — business logic comes with US-13/US-16.
```java
package de.fete.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* Application-specific configuration properties.
*
* <p>Mapped from {@code fete.*} properties. Both properties are optional:
* <ul>
* <li>{@code fete.unsplash.api-key} — Unsplash API key (empty = feature disabled)
* <li>{@code fete.max-active-events} — Maximum active events (0 = unlimited)
* </ul>
*/
@ConfigurationProperties(prefix = "fete")
public class FeteProperties {
private final Unsplash unsplash;
private final int maxActiveEvents;
/** Creates FeteProperties with the given values. */
public FeteProperties(Unsplash unsplash, int maxActiveEvents) {
this.unsplash = unsplash != null ? unsplash : new Unsplash("");
this.maxActiveEvents = maxActiveEvents;
}
/** Returns the Unsplash configuration. */
public Unsplash getUnsplash() {
return unsplash;
}
/** Returns the maximum number of active events (0 = unlimited). */
public int getMaxActiveEvents() {
return maxActiveEvents;
}
/** Unsplash-related configuration. */
public record Unsplash(String apiKey) {
/** Creates Unsplash config with the given API key. */
public Unsplash {
if (apiKey == null) {
apiKey = "";
}
}
/** Returns true if an API key is configured. */
public boolean isEnabled() {
return !apiKey.isBlank();
}
}
}
```
#### ~~5. Create `FetePropertiesConfig` configuration class~~ (deferred)
**File**: `backend/src/main/java/de/fete/config/FetePropertiesConfig.java` (new)
**Changes**: Separate `@Configuration` that enables `FeteProperties`.
```java
package de.fete.config;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
/** Activates {@link FeteProperties} binding. */
@Configuration
@EnableConfigurationProperties(FeteProperties.class)
public class FetePropertiesConfig {
}
```
#### [x] 6. Set production profile in Dockerfile
**File**: `Dockerfile`
**Changes**: Add `ENV SPRING_PROFILES_ACTIVE=prod` in the runtime stage, before `ENTRYPOINT`.
```dockerfile
# Stage 3: Runtime
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
COPY --from=backend-build /app/backend/target/*.jar app.jar
EXPOSE 8080
ENV SPRING_PROFILES_ACTIVE=prod
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "-jar", "app.jar"]
```
### Success Criteria:
#### Automated Verification:
- [ ] `cd backend && ./mvnw compile` succeeds (FeteProperties compiles, checkstyle passes)
- [ ] `docker build .` succeeds
#### Manual Verification:
- [ ] `application-prod.properties` contains all five env-var placeholders
- [ ] `application-local.properties.example` is committed; `application-local.properties` is gitignored
- [ ] `FeteProperties` fields: `unsplash.apiKey` (String), `maxActiveEvents` (int)
- [ ] Dockerfile has `ENV SPRING_PROFILES_ACTIVE=prod` before `ENTRYPOINT`
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 3: Testcontainers Integration
### Overview
Set up the TestApplication pattern so that all `@SpringBootTest` tests automatically get a Testcontainers-managed PostgreSQL instance. This is critical: once JPA is on the classpath, every `@SpringBootTest` needs a datasource. Without this, all three existing `@SpringBootTest` tests break.
### Changes Required:
#### [x] 1. Create Testcontainers configuration
**File**: `backend/src/test/java/de/fete/TestcontainersConfig.java` (new)
**Changes**: Registers a PostgreSQL Testcontainer with `@ServiceConnection` for automatic datasource wiring.
```java
package de.fete;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.testcontainers.containers.PostgreSQLContainer;
/** Provides a Testcontainers PostgreSQL instance for integration tests. */
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:17-alpine");
}
}
```
#### [x] 2. Create TestFeteApplication for `spring-boot:test-run`
**File**: `backend/src/test/java/de/fete/TestFeteApplication.java` (new)
**Changes**: Entry point that imports `TestcontainersConfig`. Enables `./mvnw spring-boot:test-run` for local development with Testcontainers (no external PostgreSQL needed).
```java
package de.fete;
import org.springframework.boot.SpringApplication;
/** Test entry point — starts the app with Testcontainers PostgreSQL. */
public class TestFeteApplication {
public static void main(String[] args) {
SpringApplication.from(FeteApplication::main)
.with(TestcontainersConfig.class)
.run(args);
}
}
```
#### [x] 3. Import TestcontainersConfig in existing `@SpringBootTest` tests
**File**: `backend/src/test/java/de/fete/FeteApplicationTest.java`
**Changes**: Add `@Import(TestcontainersConfig.class)` so the test gets a datasource.
```java
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestcontainersConfig.class)
class FeteApplicationTest {
// ... existing tests unchanged
}
```
**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java`
**Changes**: Same — add `@Import(TestcontainersConfig.class)`.
Note: `HexagonalArchitectureTest` uses `@AnalyzeClasses` (ArchUnit), not `@SpringBootTest` — it needs no changes.
#### [x] 4. Add SpotBugs exclusion for Testcontainers resource management
**File**: `backend/spotbugs-exclude.xml`
**Changes**: Testcontainers `PostgreSQLContainer` bean intentionally has container lifecycle managed by Spring, not try-with-resources. SpotBugs may flag this. Add exclusion if needed — check after running `./mvnw verify`.
### Success Criteria:
#### Automated Verification:
- [ ] `cd backend && ./mvnw test` — all existing tests pass (context loads, health endpoint, ArchUnit)
- [ ] `cd backend && ./mvnw verify` — full verify including SpotBugs passes
- [ ] Testcontainers starts a PostgreSQL container during test execution (visible in test output)
- [ ] Liquibase baseline migration runs against Testcontainers PostgreSQL
#### Manual Verification:
- [ ] `./mvnw spring-boot:test-run` starts the app with a Testcontainers PostgreSQL (for local dev without external DB)
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 4: README Deployment Documentation
### Overview
Add a deployment section to the README with a docker-compose example, environment variable documentation, and local development setup instructions.
### Changes Required:
#### [x] 1. Add deployment section to README
**File**: `README.md`
**Changes**: Add a `## Deployment` section after the existing `## Code quality` section and before `## License`. Contains the docker-compose example, environment variable table, and notes.
```markdown
## Deployment
### Docker Compose
The app ships as a single Docker image. It requires an external PostgreSQL database.
```yaml
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: fete
POSTGRES_USER: fete
POSTGRES_PASSWORD: changeme
volumes:
- fete-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fete"]
interval: 5s
timeout: 3s
retries: 5
app:
image: gitea.example.com/user/fete:latest
ports:
- "8080:8080"
environment:
DATABASE_URL: jdbc:postgresql://db:5432/fete
DATABASE_USERNAME: fete
DATABASE_PASSWORD: changeme
# MAX_ACTIVE_EVENTS: 100
# UNSPLASH_API_KEY: your-key-here
depends_on:
db:
condition: service_healthy
volumes:
fete-db:
```
### Environment variables
| Variable | Required | Default | Description |
|----------------------|----------|-----------|------------------------------------------------|
| `DATABASE_URL` | Yes | — | JDBC connection string for PostgreSQL |
| `DATABASE_USERNAME` | Yes | — | Database username |
| `DATABASE_PASSWORD` | Yes | — | Database password |
| `MAX_ACTIVE_EVENTS` | No | Unlimited | Maximum number of simultaneously active events |
| `UNSPLASH_API_KEY` | No | — | Unsplash API key for header image search |
```
#### [x] 2. Add local development setup section to README
**File**: `README.md`
**Changes**: Extend the `## Development` section with database setup instructions.
```markdown
### Local database setup
**Option A: Testcontainers (no external PostgreSQL needed)**
```bash
cd backend && ./mvnw spring-boot:test-run
```
This starts the app with a Testcontainers-managed PostgreSQL that is created and destroyed automatically.
**Option B: External PostgreSQL**
```bash
cd backend
cp src/main/resources/application-local.properties.example \
src/main/resources/application-local.properties
# Edit application-local.properties if your PostgreSQL uses different credentials
./mvnw spring-boot:run -Dspring-boot.run.profiles=local
```
```
### Success Criteria:
#### Automated Verification:
- [ ] `cd frontend && npm run test:unit -- --run` — frontend tests still pass (no regression)
#### Manual Verification:
- [ ] README docker-compose example is syntactically correct YAML
- [ ] Environment variable table lists all five variables with correct Required/Default values
- [ ] Local development section documents both Testcontainers and external PostgreSQL options
- [ ] docker-compose startup: `docker compose up` starts app + postgres, `/actuator/health` returns `{"status":"UP"}`
**Implementation Note**: After completing this phase, all T-4 acceptance criteria should be met. Run the full verification checklist below.
---
## Testing Strategy
### Unit Tests:
- `FeteProperties` — verify defaults (empty API key = disabled, maxActiveEvents=0 = unlimited)
- No other new unit tests in T-4 — the infrastructure is verified by integration tests
### Integration Tests:
- Existing `FeteApplicationTest.contextLoads()` — validates that Spring context starts with JPA + Liquibase + Testcontainers
- Existing `FeteApplicationTest.healthEndpointReturns200()` — validates health check includes DB health
- Existing `WebConfigTest` — validates SPA routing still works with JPA on classpath
- ArchUnit rules — validate `FeteProperties`/`FetePropertiesConfig` in `config` adapter is properly isolated
### Manual Testing Steps:
1. `cd backend && ./mvnw verify` — full backend pipeline green
2. `cd frontend && npm run test:unit -- --run` — frontend unchanged
3. `docker build .` — image builds successfully
4. docker-compose (app + postgres) — start, wait for health, verify `/actuator/health` returns UP
## Performance Considerations
- Testcontainers PostgreSQL startup adds ~3-5 seconds to backend test execution. This is acceptable for integration tests.
- Testcontainers reuses the container across all `@SpringBootTest` classes in a single Maven run (Spring's test context caching).
- The empty baseline changeset adds negligible startup time.
## Migration Notes
- **Existing tests**: `FeteApplicationTest` and `WebConfigTest` need `@Import(TestcontainersConfig.class)` — without it, they fail because JPA requires a datasource.
- **CI pipeline**: `./mvnw -B verify` now requires Docker for Testcontainers. Gitea Actions `ubuntu-latest` runners have Docker available. If the runner uses Docker-in-Docker, `DOCKER_HOST` may need configuration — verify after implementation.
- **Local development**: Developers now need either Docker (for Testcontainers via `./mvnw spring-boot:test-run`) or a local PostgreSQL (with `application-local.properties`).
## References
- Research: `docs/agents/research/2026-03-04-t4-development-infrastructure.md`
- T-4 spec: `spec/setup-tasks.md` (lines 79-98)
- Spring Boot Testcontainers: `@ServiceConnection` pattern (Spring Boot 3.1+)
- Liquibase Spring Boot integration: auto-configured when `liquibase-core` is on classpath

File diff suppressed because it is too large Load Diff

View File

@@ -1,504 +0,0 @@
---
date: 2026-03-05T10:29:08+00:00
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
branch: master
topic: "E2E Testing with Playwright — Setup & Initial Tests"
tags: [plan, e2e, playwright, testing, frontend, msw]
status: draft
---
# E2E Testing with Playwright — Setup & Initial Tests
## Overview
Set up Playwright E2E testing infrastructure for the fete Vue 3 frontend with mocked backend (via `@msw/playwright` + `@msw/source`), write initial smoke and US-1 event-creation tests, and integrate into CI.
## Current State Analysis
- **Vitest** is configured and already excludes `e2e/**` (`vitest.config.ts:10`)
- **Three routes** exist: `/` (Home), `/create` (EventCreate), `/events/:token` (EventStub)
- **No E2E framework** installed — no Playwright, no MSW
- **OpenAPI spec** at `backend/src/main/resources/openapi/api.yaml` defines `POST /events` with `CreateEventRequest` and `CreateEventResponse` schemas
- **`CreateEventResponse` lacks `example:` fields** — required for `@msw/source` mock generation
- **CI pipeline** (`.gitea/workflows/ci.yaml`) has backend-test and frontend-test jobs but no E2E step
- **`.gitignore`** does not include Playwright output directories
### Key Discoveries:
- `frontend/vitest.config.ts:10``e2e/**` already excluded from Vitest
- `frontend/vite.config.ts:18-25` — Dev proxy forwards `/api``localhost:8080`
- `frontend/package.json:8``dev` script runs `generate:api` first, then Vite
- `frontend/src/views/EventCreateView.vue` — Full form with client-side validation, API call via `openapi-fetch`, redirect to event stub on success
- `frontend/src/views/EventStubView.vue` — Shows "Event created!" confirmation with shareable link
- `frontend/src/views/HomeView.vue` — Empty state with "Create Event" CTA
## Desired End State
After this plan is complete:
- Playwright is installed and configured with Chromium-only
- `@msw/playwright` + `@msw/source` provide automatic API mocking from the OpenAPI spec
- A smoke test verifies the app loads and basic navigation works
- A US-1 E2E test covers the full event creation flow (form fill → mocked API → redirect → stub page)
- `npm run test:e2e` runs all E2E tests locally
- CI runs E2E tests after unit tests, uploading the report as artifact on failure
- OpenAPI response schemas include `example:` fields for mock generation
### Verification:
```bash
cd frontend && npm run test:e2e # all E2E tests pass locally
```
## What We're NOT Doing
- Firefox/WebKit browser testing — Chromium only for now
- Page Object Model pattern — premature with <5 tests
- CI caching of Playwright browser binaries — separate concern
- Full-stack E2E tests with real Spring Boot backend
- E2E tests for US-2 through US-20 — only US-1 flow + smoke test
- Service worker / PWA testing
- `data-testid` attributes — using accessible locators (`getByRole`, `getByLabel`) where possible
## Implementation Approach
Install Playwright and MSW packages, configure Playwright to spawn the Vite dev server, set up MSW to auto-generate handlers from the OpenAPI spec, then write two test files: a smoke test and a US-1 event creation flow test. Finally, add an E2E step to CI.
---
## Phase 1: Playwright Infrastructure
### Overview
Install dependencies, create configuration, add npm scripts, update `.gitignore`.
### Changes Required:
#### [x] 1. Install npm packages
**Command**: `cd frontend && npm install --save-dev @playwright/test @msw/playwright @msw/source msw`
Four packages:
- `@playwright/test` — Playwright test runner
- `msw` — Mock Service Worker core
- `@msw/playwright` — Playwright integration for MSW (intercepts at network level via `page.route()`)
- `@msw/source` — Reads OpenAPI spec and generates MSW request handlers
#### [x] 2. Install Chromium browser binary
**Command**: `cd frontend && npx playwright install --with-deps chromium`
Only Chromium — saves ~2 min vs installing all browsers. `--with-deps` installs OS-level libraries.
#### [x] 3. Create `playwright.config.ts`
**File**: `frontend/playwright.config.ts`
```typescript
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
stdout: 'pipe',
},
})
```
Key decisions per research doc:
- `testDir: './e2e'` — separate from Vitest unit tests
- `forbidOnly: !!process.env.CI` — prevents `.only` in CI
- `workers: 1` in CI — avoids shared-state flakiness
- `reuseExistingServer` locally — fast iteration when `npm run dev` is already running
#### [x] 4. Add npm scripts to `package.json`
**File**: `frontend/package.json`
Add to `"scripts"`:
```json
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
```
#### [x] 5. Update `.gitignore`
**File**: `frontend/.gitignore`
Append:
```
# Playwright
playwright-report/
test-results/
```
#### [x] 6. Create `e2e/` directory
**Command**: `mkdir -p frontend/e2e`
### Success Criteria:
#### Automated Verification:
- [ ] `cd frontend && npx playwright --version` outputs a version
- [ ] `cd frontend && npx playwright test --list` runs without error (shows 0 tests initially)
- [ ] `npm run test:e2e` script exists in package.json
#### Manual Verification:
- [ ] `playwright-report/` and `test-results/` are in `.gitignore`
- [ ] No unintended changes to existing config files
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 2: OpenAPI Response Examples
### Overview
Add `example:` fields to `CreateEventResponse` properties so `@msw/source` can generate realistic mock responses.
### Changes Required:
#### [x] 1. Add examples to `CreateEventResponse`
**File**: `backend/src/main/resources/openapi/api.yaml`
Update `CreateEventResponse` properties to include `example:` fields:
```yaml
CreateEventResponse:
type: object
required:
- eventToken
- organizerToken
- title
- dateTime
- expiryDate
properties:
eventToken:
type: string
format: uuid
description: Public token for the event URL
example: "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
organizerToken:
type: string
format: uuid
description: Secret token for organizer access
example: "f9e8d7c6-b5a4-3210-fedc-ba9876543210"
title:
type: string
example: "Summer BBQ"
dateTime:
type: string
format: date-time
example: "2026-03-15T20:00:00+01:00"
expiryDate:
type: string
format: date
example: "2026-06-15"
```
### Success Criteria:
#### Automated Verification:
- [ ] `cd backend && ./mvnw compile` succeeds (OpenAPI codegen still works)
- [ ] `cd frontend && npm run generate:api` succeeds (TypeScript types regenerate)
#### Manual Verification:
- [ ] All response schema properties have `example:` fields
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 3: MSW Integration
### Overview
Set up `@msw/source` to read the OpenAPI spec and generate MSW handlers, and configure `@msw/playwright` to intercept network requests in E2E tests.
### Changes Required:
#### [x] 1. Create MSW setup helper
**File**: `frontend/e2e/msw-setup.ts`
```typescript
import { fromOpenApi } from '@msw/source'
import { createWorkerFixture } from '@msw/playwright'
import { test as base, expect } from '@playwright/test'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const specPath = path.resolve(__dirname, '../../backend/src/main/resources/openapi/api.yaml')
// Generate MSW handlers from the OpenAPI spec.
// These return example values defined in the spec by default.
const handlers = await fromOpenApi(specPath)
// Create a Playwright fixture that intercepts network requests via page.route()
// and delegates them to MSW handlers.
export const test = base.extend(createWorkerFixture(handlers))
export { expect }
```
This module:
- Reads the OpenAPI spec at test startup
- Generates MSW request handlers that return `example:` values by default
- Exports a `test` fixture with MSW network interception built in
- Tests import `{ test, expect }` from this file instead of `@playwright/test`
#### [x] 2. Verify MSW integration works
Write a minimal test in Phase 4 that uses the fixture — if the import chain works and a test passes, MSW is correctly configured.
### Success Criteria:
#### Automated Verification:
- [ ] `frontend/e2e/msw-setup.ts` type-checks (no TS errors)
- [ ] Import path to OpenAPI spec resolves correctly
#### Manual Verification:
- [ ] MSW helper is clean and minimal
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 4: E2E Tests
### Overview
Write two test files: a smoke test for basic app functionality and a US-1 event creation flow test.
### Changes Required:
#### [x] 1. Smoke test
**File**: `frontend/e2e/smoke.spec.ts`
```typescript
import { test, expect } from './msw-setup'
test.describe('Smoke', () => {
test('home page loads and shows branding', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('heading', { name: 'fete' })).toBeVisible()
})
test('home page has create event CTA', async ({ page }) => {
await page.goto('/')
await expect(page.getByRole('link', { name: /create event/i })).toBeVisible()
})
test('navigating to /create shows the creation form', async ({ page }) => {
await page.goto('/')
await page.getByRole('link', { name: /create event/i }).click()
await expect(page).toHaveURL('/create')
await expect(page.getByLabel(/title/i)).toBeVisible()
})
})
```
#### [x] 2. US-1 event creation flow test
**File**: `frontend/e2e/event-create.spec.ts`
```typescript
import { test, expect } from './msw-setup'
test.describe('US-1: Create an event', () => {
test('shows validation errors for empty required fields', async ({ page }) => {
await page.goto('/create')
await page.getByRole('button', { name: /create event/i }).click()
await expect(page.getByText('Title is required.')).toBeVisible()
await expect(page.getByText('Date and time are required.')).toBeVisible()
await expect(page.getByText('Expiry date is required.')).toBeVisible()
})
test('creates an event and redirects to stub page', async ({ page }) => {
await page.goto('/create')
// Fill the form
await page.getByLabel(/title/i).fill('Summer BBQ')
await page.getByLabel(/description/i).fill('Bring your own drinks')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/location/i).fill('Central Park')
await page.getByLabel(/expiry/i).fill('2026-06-15')
// Submit — MSW returns the OpenAPI example response
await page.getByRole('button', { name: /create event/i }).click()
// Should redirect to the event stub page
await expect(page).toHaveURL(/\/events\/.+/)
await expect(page.getByText('Event created!')).toBeVisible()
})
test('stores event data in localStorage after creation', async ({ page }) => {
await page.goto('/create')
await page.getByLabel(/title/i).fill('Summer BBQ')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click()
await expect(page).toHaveURL(/\/events\/.+/)
// Verify localStorage was populated
const storage = await page.evaluate(() => {
const raw = localStorage.getItem('fete_events')
return raw ? JSON.parse(raw) : null
})
expect(storage).not.toBeNull()
expect(storage).toEqual(
expect.arrayContaining([
expect.objectContaining({ title: 'Summer BBQ' }),
]),
)
})
test('shows server error on API failure', async ({ page, network }) => {
// Override the default MSW handler to return a 400 error
await network.use(
// Exact override syntax depends on @msw/playwright API —
// may need adjustment based on actual package API
)
await page.goto('/create')
await page.getByLabel(/title/i).fill('Test')
await page.getByLabel(/date/i).first().fill('2026-04-15T18:00')
await page.getByLabel(/expiry/i).fill('2026-06-15')
await page.getByRole('button', { name: /create event/i }).click()
// Should show error message, not redirect
await expect(page.getByRole('alert')).toBeVisible()
})
})
```
**Note on the server error test:** The exact override syntax for `network.use()` depends on the `@msw/playwright` API. During implementation, this will need to be adapted to the actual package API. The pattern is: override the `POST /api/events` handler to return a 400/500 response.
### Success Criteria:
#### Automated Verification:
- [ ] `cd frontend && npm run test:e2e` passes — all tests green
- [ ] No TypeScript errors in test files
#### Manual Verification:
- [ ] Tests cover: home page rendering, navigation, form validation, successful creation flow, localStorage persistence
- [ ] Test names are descriptive and map to acceptance criteria
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Phase 5: CI Integration
### Overview
Add a Playwright E2E test step to the Gitea Actions CI pipeline.
### Changes Required:
#### [x] 1. Add E2E job to CI workflow
**File**: `.gitea/workflows/ci.yaml`
Add a new job `frontend-e2e` after the existing `frontend-test` job:
```yaml
frontend-e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: cd frontend && npm ci
- name: Install Playwright browsers
run: cd frontend && npx playwright install --with-deps chromium
- name: Run E2E tests
run: cd frontend && npm run test:e2e
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
```
#### [x] 2. Add E2E to the `needs` array of `build-and-publish`
**File**: `.gitea/workflows/ci.yaml`
Update the `build-and-publish` job:
```yaml
build-and-publish:
needs: [backend-test, frontend-test, frontend-e2e]
```
This ensures Docker images are only published if E2E tests also pass.
### Success Criteria:
#### Automated Verification:
- [ ] CI YAML is valid (no syntax errors)
- [ ] `frontend-e2e` job uses `chromium` only (no full browser install)
#### Manual Verification:
- [ ] E2E job runs independently from backend-test (no unnecessary dependency)
- [ ] `build-and-publish` requires all three test jobs
- [ ] Report artifact is uploaded even on test failure (`!cancelled()`)
**Implementation Note**: After completing this phase and all automated verification passes, pause here for manual confirmation from the human before proceeding to the next phase.
---
## Testing Strategy
### E2E Tests (this plan):
- Smoke test: app loads, branding visible, navigation works
- US-1 happy path: fill form → submit → redirect → stub page
- US-1 validation: empty required fields show errors
- US-1 localStorage: event data persisted after creation
- US-1 error handling: API failure shows error message
### Existing Unit/Component Tests (unchanged):
- `useEventStorage.spec.ts` — 6 tests
- `EventCreateView.spec.ts` — 11 tests
- `EventStubView.spec.ts` — 8 tests
### Future:
- Each new user story adds its own E2E tests
- Page Object Model when test suite grows beyond 5-10 tests
- Cross-browser testing (Firefox/WebKit) as needed
## Performance Considerations
- Chromium-only keeps install time and test runtime low
- `reuseExistingServer` in local dev avoids restarting Vite per test run
- Single worker in CI prevents flakiness from parallel state issues
- MSW intercepts at network level — no real backend needed, fast test execution
## References
- Research: `docs/agents/research/2026-03-05-e2e-testing-playwright-vue3.md`
- OpenAPI spec: `backend/src/main/resources/openapi/api.yaml`
- Existing views: `frontend/src/views/EventCreateView.vue`, `EventStubView.vue`, `HomeView.vue`
- CI pipeline: `.gitea/workflows/ci.yaml`
- Vitest config: `frontend/vitest.config.ts`

View File

@@ -1,199 +0,0 @@
# US-1 Post-Review Fixes — Implementation Plan
Date: 2026-03-05
Origin: Deep review of all unstaged US-1 changes before commit
## Context
US-1 "Create Event" is fully implemented (backend + frontend, 7 phases) with 4 review fixes already applied (reactive error clearing, network error handling, page title, favicon). A comprehensive review of ALL unstaged files revealed additional issues that must be fixed before committing.
## Task 1: Backend — Clock injection in EventService [x]
**Problem:** `EventService` uses `LocalDate.now()` and `OffsetDateTime.now()` directly, making deterministic time-based testing impossible.
**Files:**
- `backend/src/main/java/de/fete/application/service/EventService.java`
- `backend/src/test/java/de/fete/application/service/EventServiceTest.java`
**Fix:**
1. Inject a `java.time.Clock` bean into `EventService` via constructor
2. Replace `LocalDate.now()` with `LocalDate.now(clock)` and `OffsetDateTime.now()` with `OffsetDateTime.now(clock)`
3. Add a `Clock` bean to the Spring config (or rely on a `@Bean Clock clock() { return Clock.systemDefaultZone(); }` in a config class)
4. Update `EventServiceTest` to use `Clock.fixed(...)` for deterministic tests
**Verification:** `cd backend && ./mvnw test`
## Task 2: Frontend A11y — Error spans should only render when error present [x]
**Problem:** Every form field has `<span class="field-error" role="alert">{{ errors.title }}</span>` that is always in the DOM, even when empty. Screen readers may announce empty `role="alert"` elements.
**File:** `frontend/src/views/EventCreateView.vue`
**Fix:** Use `v-if` to conditionally render error spans:
```html
<span v-if="errors.title" class="field-error" role="alert">{{ errors.title }}</span>
```
Apply to all 5 field error spans (title, description, dateTime, location, expiryDate).
**Note:** This removes the `min-height: 1.2em` layout reservation. Accept the layout shift as a trade-off for accessibility, OR add a wrapper div with `min-height` that doesn't carry `role="alert"`.
**Verification:** `cd frontend && npm run test:unit` — existing tests use `.querySelector('[role="alert"]')` so they may need adjustment since empty alerts will no longer be in the DOM.
## Task 3: Frontend A11y — aria-invalid and aria-describedby on fields [x]
**Problem:** When a field fails validation, there is no `aria-invalid="true"` or `aria-describedby` linking the input to its error message. Assistive technologies cannot associate errors with fields.
**File:** `frontend/src/views/EventCreateView.vue`
**Fix:**
1. Add unique `id` to each error span (e.g., `id="title-error"`)
2. Add `:aria-describedby="errors.title ? 'title-error' : undefined"` to each input
3. Add `:aria-invalid="!!errors.title"` to each input
Example for title:
```html
<input
id="title"
v-model="form.title"
type="text"
class="form-field"
required
maxlength="200"
placeholder="What's the event?"
:aria-invalid="!!errors.title"
:aria-describedby="errors.title ? 'title-error' : undefined"
/>
<span v-if="errors.title" id="title-error" class="field-error" role="alert">{{ errors.title }}</span>
```
Apply the same pattern to all 5 fields (title, description, dateTime, location, expiryDate).
**Verification:** `cd frontend && npm run test:unit`
## Task 4: Frontend A11y — Error text contrast [x]
**Problem:** White (`#fff`) error text on the pink gradient start (`#F06292`) has a contrast ratio of only 3.06:1, which fails WCAG AA for small text (0.8rem). The project statute requires WCAG AA compliance.
**File:** `frontend/src/assets/main.css`
**Fix options (pick one):**
- **Option A:** Use a light yellow/cream color like `#FFF9C4` or `#FFECB3` that has higher contrast on the gradient
- **Option B:** Add a subtle dark text-shadow to the error text: `text-shadow: 0 1px 2px rgba(0,0,0,0.3)`
- **Option C:** Make error text slightly larger/bolder to qualify for WCAG AA-large (18px+ or 14px+ bold)
**Recommended:** Option C — bump `.field-error` to `font-size: 0.85rem; font-weight: 600;` which at 600 weight qualifies for AA-large text at 14px+ (0.85rem ≈ 13.6px — close but may not quite qualify). Alternatively combine with option B for safety.
**Note:** Verify the final choice against the design system spec in `spec/design-system.md`. The spec notes that gradient start only passes AA-large. The error text must work across the full gradient.
**Verification:** Manual contrast check with a tool like WebAIM contrast checker.
## Task 5: Test — Happy-path submission in EventCreateView [x]
**Problem:** No test verifies successful form submission (the most important behavior).
**File:** `frontend/src/views/__tests__/EventCreateView.spec.ts`
**Fix:** Add a test that:
1. Mocks `api.POST` to return `{ data: { eventToken: 'abc', organizerToken: 'xyz', title: 'Test', dateTime: '...', expiryDate: '...' } }`
2. Fills all required fields
3. Submits the form
4. Asserts `api.POST` was called with the correct body
5. Asserts navigation to `/events/abc` occurred
6. Asserts `saveCreatedEvent` was called (need to mock `useEventStorage`)
**Note:** `useEventStorage` must be mocked. Use `vi.mock('@/composables/useEventStorage')`.
**Verification:** `cd frontend && npm run test:unit`
## Task 6: Test — EventStubView component tests [x]
**Problem:** No test file exists for `EventStubView.vue`.
**New file:** `frontend/src/views/__tests__/EventStubView.spec.ts`
**Fix:** Create tests covering:
1. Renders the event URL based on route param `:token`
2. Shows the correct share URL (`window.location.origin + /events/:token`)
3. Copy button exists
4. Back link navigates to home
**Note:** Read `frontend/src/views/EventStubView.vue` first to understand the component structure.
**Verification:** `cd frontend && npm run test:unit`
## Task 7: Test — Server-side field errors in EventCreateView [x]
**Problem:** The `fieldErrors` handling branch (lines 184-196 of EventCreateView.vue) is untested.
**File:** `frontend/src/views/__tests__/EventCreateView.spec.ts`
**Fix:** Add a test that:
1. Mocks `api.POST` to return `{ error: { fieldErrors: [{ field: 'title', message: 'Title already taken' }] } }`
2. Fills all required fields and submits
3. Asserts the title field error shows "Title already taken"
4. Asserts other field errors are empty
**Verification:** `cd frontend && npm run test:unit`
## Task 8: Fix border-radius on EventStubView copy button [x]
**Problem:** `border-radius: 10px` is hardcoded instead of using the design token `var(--radius-button)` (14px).
**File:** `frontend/src/views/EventStubView.vue`
**Fix:** Replace `border-radius: 10px` with `border-radius: var(--radius-button)` in the `.stub__copy` CSS class.
**Verification:** Visual check.
## Task 9: Add 404 catch-all route user story [x]
**Problem:** Navigating to an unknown path shows a blank page.
**File:** `spec/userstories.md`
**Fix:** Add a new user story for a 404/catch-all route. Something like:
```
### US-X: 404 Page
As a user who navigates to a non-existent URL, I want to see a helpful error page so I can find my way back.
Acceptance Criteria:
- [ ] Unknown routes show a "Page not found" message
- [ ] The page includes a link back to the home page
- [ ] The page follows the design system
```
Read the existing user stories first to match the format.
**Verification:** N/A (spec only).
## Task 10: EventStubView silent clipboard failure [x]
**Problem:** In `EventStubView.vue`, the `catch` block on `navigator.clipboard.writeText()` is empty. If clipboard is unavailable (HTTP, older browser), the user gets no feedback.
**File:** `frontend/src/views/EventStubView.vue`
**Fix:** In the catch block, show a fallback message (e.g., set `copied` text to "Copy failed" or select the URL text for manual copying).
**Verification:** `cd frontend && npm run test:unit`
## Execution Order
1. Task 1 (Clock injection — backend, independent)
2. Tasks 2 + 3 (A11y fixes — can be done together since they touch the same file)
3. Task 4 (Contrast fix — CSS only)
4. Tasks 5 + 7 (EventCreateView tests — same test file)
5. Task 6 (EventStubView tests — new file)
6. Tasks 8 + 10 (EventStubView fixes — same file)
7. Task 9 (User story — spec only)
8. Run all tests: `cd backend && ./mvnw test` and `cd frontend && npm run test:unit`
## Constraints
- TDD: write/update tests first, then fix (where applicable)
- Follow existing code style and patterns
- Do not refactor unrelated code
- Do not add dependencies
- Update design system spec if contrast solution changes the spec

View File

@@ -1,109 +0,0 @@
# US-1 Review Fixes — Agent Instructions
Date: 2026-03-05
Origin: Code review and exploratory browser testing of US-1 "Create Event"
## Context
US-1 has been implemented across all 7 phases (OpenAPI spec, DB migration, domain model, application service, persistence adapter, web adapter, frontend). All 42 tests pass. A code review with exploratory browser testing found 2 bugs and 2 minor issues that need to be fixed before the story can be committed.
### Resources
- **Test report:** `.agent-tests/2026-03-05-us1-review-test/report.md` — full browser test protocol with screenshots
- **Screenshots:** `.agent-tests/2026-03-05-us1-review-test/screenshots/` — visual evidence (0108)
- **US-1 spec:** `spec/userstories.md` — acceptance criteria
- **Implementation plan:** `docs/agents/plan/2026-03-04-us1-create-event.md`
- **Design system:** `spec/design-system.md`
- **Primary file to modify:** `frontend/src/views/EventCreateView.vue`
- **Secondary file to modify:** `frontend/index.html`
## Fix Instructions
### Fix 1: Validation errors must clear reactively (Bug — Medium)
**Problem:** After submitting the empty form, validation errors appear correctly. But when the user then fills in the fields, the error messages persist until the next submit. See screenshot `05-form-filled.png` — all fields filled, errors still visible.
**Root cause:** `validate()` (line 125) calls `clearErrors()` only on submit. There is no reactive clearing on input.
**Fix:** Add a `watch` on the `form` reactive object that clears the corresponding field error when the value changes. Do NOT re-validate on every keystroke — just clear the error for the field that was touched.
```typescript
// Clear individual field errors when the user types
watch(() => form.title, () => { errors.title = '' })
watch(() => form.dateTime, () => { errors.dateTime = '' })
watch(() => form.expiryDate, () => { errors.expiryDate = '' })
```
Also clear `serverError` when any field changes, so stale server errors don't linger.
**Test:** Add a test to `frontend/src/views/__tests__/EventCreateView.spec.ts` that:
1. Submits the empty form (triggers validation errors)
2. Types into the title field
3. Asserts that the title error is cleared but other errors remain
### Fix 2: Network errors must show a user-visible message (Bug — High)
**Problem:** When the backend is unreachable, the form submits silently — no error message, no feedback. The `serverError` element (line 77) exists but is never populated because `openapi-fetch` throws an unhandled exception on network errors instead of returning an `{ error }` object.
**Root cause:** `handleSubmit()` (line 150) has no `try-catch` around the `api.POST()` call (line 164). When `fetch` fails (network error), `openapi-fetch` throws, the promise rejects, and the function exits without setting `serverError` or resetting `submitting`.
**Fix:** Wrap the API call and response handling in a `try-catch`:
```typescript
try {
const { data, error } = await api.POST('/events', { body: { ... } })
submitting.value = false
if (error) {
// ... existing error handling ...
return
}
if (data) {
// ... existing success handling ...
}
} catch {
submitting.value = false
serverError.value = 'Could not reach the server. Please try again.'
}
```
**Test:** Add a test to `EventCreateView.spec.ts` that mocks the API to throw (simulating network failure) and asserts that `serverError` text appears in the DOM.
### Fix 3: Page title (Minor — Low)
**Problem:** `frontend/index.html` line 7 still has `<title>Vite App</title>`.
**Fix:** Change to `<title>fete</title>`. Also set `lang="en"` on the `<html>` tag (line 2 currently has `lang=""`).
**File:** `frontend/index.html`
### Fix 4: Favicon (Minor — Low)
**Problem:** The favicon is the Vite default. The project should either have its own favicon or remove the link entirely.
**Fix:** For now, remove the `<link rel="icon" href="/favicon.ico">` line and delete `frontend/public/favicon.ico` if it exists. A proper favicon can be added later as part of branding work.
**File:** `frontend/index.html`, `frontend/public/favicon.ico`
## Execution Order
1. Fix 3 + Fix 4 (trivial, `index.html` + favicon cleanup)
2. Fix 1 (reactive error clearing + test)
3. Fix 2 (try-catch + test)
4. Run all frontend tests: `cd frontend && npm run test:unit`
5. Verify visually with `browser-interactive-testing` skill:
- Start dev server, open `/create`
- Submit empty → errors appear
- Fill title → title error clears, others remain
- Fill all fields → all errors gone
- Submit with no backend → "Could not reach the server" message appears
## Constraints
- Follow existing code style and patterns in `EventCreateView.vue`
- Do not refactor unrelated code
- Do not add dependencies
- Tests must follow existing test patterns in `EventCreateView.spec.ts`
- TDD: write/update tests first, then fix

View File

@@ -1,322 +0,0 @@
# Research Report: API-First Approach
**Date:** 2026-03-04
**Scope:** API-first development with Spring Boot backend and Vue 3 frontend
**Status:** Complete
## Context
The fete project needs a strategy for API design and implementation. Two fundamental approaches exist:
- **Code-first:** Write annotated Java controllers, generate OpenAPI spec from code (e.g., springdoc-openapi)
- **API-first (spec-first):** Write OpenAPI spec as YAML, generate server interfaces and client types from it
This report evaluates API-first for the fete stack (Spring Boot 3.5.x, Java 25, Vue 3, TypeScript).
## Why API-First
| Aspect | Code-First | API-First |
|--------|-----------|-----------|
| Source of truth | Java source code | OpenAPI YAML file |
| Parallel development | Backend must exist first | Frontend + backend from day one |
| Contract stability | Implicit, can drift | Explicit, version-controlled, reviewed |
| Spec review in PRs | Derived artifact | First-class reviewable diff |
| Runtime dependency | springdoc library at runtime | None (build-time only) |
| Hexagonal fit | Controllers define contract | Spec defines contract, controllers implement |
API-first aligns with the project statutes:
- **No vibe coding**: the spec forces deliberate API design before implementation.
- **Research → Spec → Test → Implement**: the OpenAPI spec IS the specification for the API layer.
- **Privacy**: no runtime documentation library needed (no springdoc serving endpoints).
- **KISS**: one YAML file is the single source of truth for both sides.
## Backend: openapi-generator-maven-plugin
### Tool Assessment
- **Project:** [OpenAPITools/openapi-generator](https://github.com/OpenAPITools/openapi-generator)
- **Current version:** 7.20.0 (released 2026-02-16)
- **GitHub stars:** ~22k
- **License:** Apache 2.0
- **Maintenance:** Active, frequent releases (monthly cadence)
- **Spring Boot 3.5.x compatibility:** Confirmed via `useSpringBoot3: true` (Jakarta EE namespace)
- **Java 25 compatibility:** No blocking issues reported for Java 21+
### Generator: `spring` with `interfaceOnly: true`
The `spring` generator offers two modes:
1. **`interfaceOnly: true`** — generates API interfaces and model classes only. You write controllers that implement the interfaces.
2. **`delegatePattern: true`** — generates controllers + delegate interfaces. You implement the delegates.
**Recommendation: `interfaceOnly: true`** — cleaner integration with hexagonal architecture. The generated interface is the port definition, the controller is the driving adapter.
### What Gets Generated
From a spec like:
```yaml
paths:
/events:
post:
operationId: createEvent
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEventRequest'
responses:
'201':
content:
application/json:
schema:
$ref: '#/components/schemas/EventResponse'
```
The generator produces:
- `EventsApi.java` — interface with `@RequestMapping` annotations
- `CreateEventRequest.java` — POJO with Jackson annotations + Bean Validation
- `EventResponse.java` — POJO with Jackson annotations
You then write:
```java
@RestController
public class EventController implements EventsApi {
private final CreateEventUseCase createEventUseCase;
@Override
public ResponseEntity<EventResponse> createEvent(CreateEventRequest request) {
// Map DTO → domain command
// Call use case
// Map domain result → DTO
}
}
```
### Recommended Plugin Configuration
```xml
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>7.20.0</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi/api.yaml</inputSpec>
<generatorName>spring</generatorName>
<apiPackage>de.fete.adapter.in.web.api</apiPackage>
<modelPackage>de.fete.adapter.in.web.model</modelPackage>
<generateSupportingFiles>true</generateSupportingFiles>
<supportingFilesToGenerate>ApiUtil.java</supportingFilesToGenerate>
<configOptions>
<interfaceOnly>true</interfaceOnly>
<useSpringBoot3>true</useSpringBoot3>
<useBeanValidation>true</useBeanValidation>
<performBeanValidation>true</performBeanValidation>
<openApiNullable>false</openApiNullable>
<skipDefaultInterface>true</skipDefaultInterface>
<useResponseEntity>true</useResponseEntity>
<documentationProvider>none</documentationProvider>
<annotationLibrary>none</annotationLibrary>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
```
Key options rationale:
| Option | Value | Why |
|--------|-------|-----|
| `interfaceOnly` | `true` | Only interfaces + models; controllers are yours |
| `useSpringBoot3` | `true` | Jakarta EE namespace (required for Spring Boot 3.x) |
| `useBeanValidation` | `true` | `@Valid`, `@NotNull` on parameters |
| `openApiNullable` | `false` | Avoids `jackson-databind-nullable` dependency |
| `skipDefaultInterface` | `true` | No default method stubs — forces full implementation |
| `documentationProvider` | `none` | No Swagger UI / springdoc annotations |
| `annotationLibrary` | `none` | Minimal annotations on generated code |
### Build Integration
- Runs in Maven's `generate-sources` phase (before compilation)
- Output: `target/generated-sources/openapi/` — already gitignored
- `mvn clean compile` always regenerates from spec
- No generated code in git — the spec is the source of truth
### Additional Dependencies
With `openApiNullable: false` and `annotationLibrary: none`, minimal additional dependencies are needed. `jakarta.validation-api` is already transitively provided by `spring-boot-starter-web`.
### Hexagonal Architecture Mapping
```
adapter.in.web/
├── api/ ← generated interfaces (EventsApi.java)
├── model/ ← generated DTOs (CreateEventRequest.java, EventResponse.java)
└── controller/ ← your implementations (EventController implements EventsApi)
application.port.in/
└── CreateEventUseCase.java
domain.model/
└── Event.java ← clean domain object (can be a record)
```
Rules:
1. Generated DTOs exist ONLY in `adapter.in.web.model`
2. Domain objects are never exposed to the web layer
3. Controllers map between generated DTOs and domain objects
4. Mapping is manual (project is small enough; no MapStruct needed)
## Frontend: openapi-typescript + openapi-fetch
### Tool Comparison
| Tool | npm Weekly DL | Approach | Runtime | Active |
|------|--------------|----------|---------|--------|
| **openapi-typescript** | ~2.5M | Types only (.d.ts) | 0 kb | Yes |
| **openapi-fetch** | ~1.2M | Type-safe fetch wrapper | 6 kb | Yes |
| orval | ~828k | Full client codegen | Varies | Yes |
| @hey-api/openapi-ts | ~200-400k | Full client codegen | Varies | Yes (volatile API) |
| openapi-generator TS | ~500k | Full codegen (Java needed) | Heavy | Yes |
| swagger-typescript-api | ~43 | Full codegen | Varies | Declining |
### Recommendation: openapi-typescript + openapi-fetch
**Why this combination wins for fete:**
1. **Minimal footprint.** Types-only generation = zero generated runtime code. The `.d.ts` file disappears after TypeScript compilation.
2. **No Axios.** Uses native `fetch` — no unnecessary dependency.
3. **No phone home, no CDN.** Pure TypeScript types + a 6 kb fetch wrapper.
4. **Vue 3 Composition API fit.** Composables wrap `api.GET()`/`api.POST()` calls naturally.
5. **Actively maintained.** High download counts, regular releases, OpenAPI 3.0 + 3.1 support.
6. **Compile-time safety.** Wrong paths, missing parameters, wrong body types = TypeScript errors.
**Why NOT the alternatives:**
- **orval / hey-api:** Generate full runtime code (functions, classes). More than needed. Additional abstraction layer.
- **openapi-generator TypeScript:** Requires Java for generation. Produces verbose classes. Heavyweight.
- **swagger-typescript-api:** Declining maintenance. Not recommended for new projects.
### How It Works
#### Step 1: Generate Types
```bash
npx openapi-typescript ../backend/src/main/resources/openapi/api.yaml -o src/api/schema.d.ts
```
Produces a `.d.ts` file with `paths` and `components` interfaces that mirror the OpenAPI spec exactly.
#### Step 2: Create Client
```typescript
// src/api/client.ts
import createClient from "openapi-fetch";
import type { paths } from "./schema";
export const api = createClient<paths>({ baseUrl: "/api" });
```
#### Step 3: Use in Composables
```typescript
// src/composables/useEvent.ts
import { ref } from "vue";
import { api } from "@/api/client";
export function useEvent(eventId: string) {
const event = ref(null);
const error = ref(null);
async function load() {
const { data, error: err } = await api.GET("/events/{eventId}", {
params: { path: { eventId } },
});
if (err) error.value = err;
else event.value = data;
}
return { event, error, load };
}
```
Type safety guarantees:
- Path must exist in spec → TypeScript error if not
- Path parameters enforced → TypeScript error if missing
- Request body must match schema → TypeScript error if wrong
- Response `data` is typed as the 2xx response schema
### Build Integration
```json
{
"scripts": {
"generate:api": "openapi-typescript ../backend/src/main/resources/openapi/api.yaml -o src/api/schema.d.ts",
"dev": "npm run generate:api && vite",
"build": "npm run generate:api && vue-tsc && vite build"
}
}
```
The generated `schema.d.ts` can be committed to git (it is a stable, deterministic output) or gitignored and regenerated on each build. For simplicity, committing it is pragmatic — it allows IDE support without running the generator first.
### Dependencies
```json
{
"devDependencies": {
"openapi-typescript": "^7.x"
},
"dependencies": {
"openapi-fetch": "^0.13.x"
}
}
```
Requirements: Node.js 20+, TypeScript 5.x, `"module": "ESNext"` + `"moduleResolution": "Bundler"` in tsconfig.
## End-to-End Workflow
```
1. WRITE/EDIT SPEC
backend/src/main/resources/openapi/api.yaml
├──── 2. BACKEND: mvnw compile
│ → target/generated-sources/openapi/
│ ├── de/fete/adapter/in/web/api/EventsApi.java
│ └── de/fete/adapter/in/web/model/*.java
│ → Compiler errors show what controllers need updating
└──── 3. FRONTEND: npm run generate:api
→ frontend/src/api/schema.d.ts
→ TypeScript errors show what composables/views need updating
```
On spec change, both sides get compile-time feedback. The spec is a **compile-time contract**.
## Open Questions
1. **Spec location sharing.** The spec lives in `backend/src/main/resources/openapi/`. The frontend references it via relative path (`../backend/...`). This works in a monorepo. Alternative: symlink or copy step. Relative path is simplest.
2. **Generated `schema.d.ts` — commit or gitignore?** Committing is pragmatic (IDE support without running generator). Gitignoring is purist (derived artifact). Recommend: commit it, regenerate during build to catch drift.
3. **Spec validation in CI.** The openapi-generator-maven-plugin validates the spec during build. Frontend side could add `openapi-typescript` as a build step. Both fail on invalid specs.
## Conclusion
API-first with `openapi-generator-maven-plugin` (backend) and `openapi-typescript` + `openapi-fetch` (frontend) is a strong fit for fete:
- Single source of truth (one YAML file)
- Compile-time contract enforcement on both sides
- Minimal dependencies (no Swagger UI, no Axios, no runtime codegen libraries)
- Clean hexagonal architecture integration
- Actively maintained, well-adopted tooling

View File

@@ -1,216 +0,0 @@
---
date: 2026-03-04T01:40:21+01:00
git_commit: a55174b32333d0f46a55d94a50604344d1ba33f6
branch: master
topic: "Backpressure for Agentic Coding"
tags: [research, backpressure, agentic-coding, quality, tooling, hooks, static-analysis, archunit]
status: complete
---
# Research: Backpressure for Agentic Coding
## Research Question
What tools, methodologies, and patterns exist for implementing backpressure in agentic coding workflows? Which are applicable to the fete tech stack (Java 25, Spring Boot 3.5, Maven, Vue 3, TypeScript, Vitest)?
## Summary
Backpressure in agentic coding means: **automated feedback mechanisms that reject wrong output deterministically**, forcing the agent to self-correct before a human ever sees the result. The concept is borrowed from distributed systems (reactive streams, flow control) and applied to AI-assisted development.
The key insight from the literature: **90% deterministic, 10% agentic.** Encode constraints in the type system, linting rules, architecture tests, and test suites — not in prose instructions. The agent runs verification on its own output, sees failures, and fixes itself. Humans review only code that has already passed all automated gates.
### Core Sources
| Source | Author | Key Contribution |
|--------|--------|-----------------|
| [Don't waste your back pressure](https://ghuntley.com/pressure/) | Geoffrey Huntley | Coined "backpressure for agents." Feedback-driven quality, progressive delegation. |
| [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure) | JW | Verification hierarchy: types → linting → tests → agentic review. 90/10 rule. |
| [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure) | HumanLayer | Output filtering, fail-fast, context window preservation. |
| [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks) | Anthropic | PostToolUse hooks for automated feedback after file edits. |
| [ArchUnit](https://www.archunit.org/) | TNG Technology Consulting | Architecture rules as unit tests. Hexagonal architecture enforcement. |
## Detailed Findings
### 1. The Backpressure Concept
In distributed systems, backpressure prevents upstream producers from overwhelming downstream consumers. Applied to agentic coding:
- **Producer:** The AI agent generating code
- **Consumer:** The quality gates (compiler, linter, tests, architecture rules)
- **Backpressure:** Automated rejection of output that doesn't pass gates
Geoffrey Huntley: *"If you aren't capturing your back-pressure then you are failing as a software engineer."*
The paradigm shift: instead of telling the agent what to do (prompt engineering), **engineer an environment where wrong outputs get rejected automatically** (backpressure engineering).
### 2. The Verification Hierarchy
JW's article establishes a strict ordering — deterministic first, agentic last:
```
Layer 1: Type System (hardest constraint, compile-time)
Layer 2: Static Analysis (linting rules, pattern enforcement)
Layer 3: Architecture Tests (dependency rules, layer violations)
Layer 4: Unit/Integration Tests (behavioral correctness)
Layer 5: Agentic Review (judgment calls — only after 1-4 pass)
```
**Critical rule:** If a constraint can be checked deterministically, it MUST be checked deterministically. Relying on agentic review for things a linter could catch is "building on sand."
**Context efficiency:** Don't dump rules into CLAUDE.md that could be expressed as type constraints, lint rules, or tests. Reserve documentation for architectural intent and domain knowledge that genuinely requires natural language.
### 3. Context-Efficient Output
HumanLayer's research on context window management for coding agents:
- **On success:** Show only `✓` — don't waste tokens on 200 lines of passing test output
- **On failure:** Show the full error — the agent needs the details to self-correct
- **Fail-fast:** Enable `--bail` / `-x` / `-failfast` — one failure at a time prevents context-switching between multiple bugs
- **Filter output:** Strip generic stack frames, timing info, and irrelevant details
**Anti-pattern:** Piping output to `/dev/null` or using `head -n 50` — this hides information the agent might need and can force repeated test runs.
### 4. Claude Code Hooks
Hooks are shell commands that execute automatically at specific points in Claude Code's lifecycle:
| Event | Trigger | Use Case |
|-------|---------|----------|
| `PreToolUse` | Before a tool runs | Block dangerous operations |
| `PostToolUse` | After a tool completes | Run compile/lint/test checks |
| `Stop` | Agent finishes response | Final validation |
| `UserPromptSubmit` | User sends a prompt | Inject context |
| `SessionStart` | Session begins | Setup checks |
**PostToolUse** is the primary backpressure mechanism: after every file edit, run deterministic checks and feed the result back to the agent.
**Configuration:** `.claude/settings.json` (project-level, committed) or `.claude/settings.local.json` (personal, gitignored).
**Hook format example:**
```json
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit:*.java",
"hooks": [
{
"type": "command",
"command": "cd backend && ./mvnw compile -q 2>&1 || true"
}
]
}
]
}
}
```
The hook output is fed back to the agent as context, enabling self-correction in the same conversation turn.
### 5. Applicable Tools for fete's Tech Stack
#### 5.1 Java / Maven Backend
**Checkstyle** (coding conventions)
- Maven plugin: `maven-checkstyle-plugin`
- Enforces formatting, naming, imports, Javadoc rules
- Rulesets: Google Style (most widely adopted), Sun Style (legacy)
- Fails build on violation when configured with `<failOnViolation>true</failOnViolation>`
- Actively maintained, open source (LGPL-2.1)
**SpotBugs** (bug detection)
- Maven plugin: `spotbugs-maven-plugin`
- Successor to FindBugs — finds null pointer dereferences, infinite loops, resource leaks, concurrency bugs
- Runs bytecode analysis (requires compilation first)
- Configurable effort/threshold levels
- Actively maintained, open source (LGPL-2.1)
**Error Prone** (compile-time bug detection)
- Google's javac plugin — catches errors during compilation
- Tighter feedback loop than SpotBugs (compile-time vs. post-compile)
- Requires `maven-compiler-plugin` configuration with annotation processor
- More invasive setup, Java version compatibility can lag
- Actively maintained, open source (Apache-2.0)
**ArchUnit** (architecture enforcement)
- Library for writing architecture rules as JUnit tests
- Built-in support for onion/hexagonal architecture via `onionArchitecture()`
- Dedicated hexagonal ruleset: [archunit-hexagonal](https://github.com/whiskeysierra/archunit-hexagonal)
- Rules: "domain must not depend on adapters", "ports are interfaces", "no Spring annotations in domain"
- Fails as a normal test — agent sees the failure and can fix it
- Actively maintained, open source (Apache-2.0)
#### 5.2 Vue 3 / TypeScript Frontend
**TypeScript strict mode** (already configured)
- `strict: true` via `@vue/tsconfig`
- `noUncheckedIndexedAccess: true` (already in `tsconfig.app.json`)
- `vue-tsc --build` for type-checking (already in `package.json` as `type-check`)
**ESLint + oxlint** (already configured)
- ESLint with `@vue/eslint-config-typescript` (recommended rules)
- oxlint as fast pre-pass (Rust-based, handles simple rules)
- Custom ESLint rules can encode repeated agent mistakes
**Vitest** (already configured)
- `--bail` flag available for fail-fast behavior
- `--reporter=verbose` for detailed output on failure
### 6. Current State Analysis (fete project)
| Layer | Backend | Frontend |
|-------|---------|----------|
| Type System | Java 25 (strong, but no extra strictness configured) | TypeScript strict + `noUncheckedIndexedAccess` ✓ |
| Static Analysis | **Nothing configured** | ESLint + oxlint + Prettier ✓ |
| Architecture Tests | **Nothing configured** | N/A (flat structure) |
| Unit Tests | JUnit 5 via `./mvnw test` ✓ | Vitest via `npm run test:unit` ✓ |
| Claude Code Hooks | **Not configured** | **Not configured** |
| Fail-fast | **Not configured** | **Not configured** |
**Gaps:** The backend has zero static analysis or architecture enforcement. Claude Code hooks don't exist yet. Neither side has fail-fast configured.
### 7. Evaluation: What to Implement
| Measure | Effort | Impact | Privacy OK | Maintained | Recommendation |
|---------|--------|--------|------------|------------|----------------|
| Claude Code Hooks (PostToolUse) | Low | High | Yes (local) | N/A (config) | **Immediate** |
| Fail-fast + output filtering | Low | Medium | Yes (local) | N/A (config) | **Immediate** |
| Checkstyle Maven plugin | Low | Medium | Yes (no network) | Yes (LGPL) | **Yes** |
| SpotBugs Maven plugin | Low | Medium | Yes (no network) | Yes (LGPL) | **Yes** |
| ArchUnit hexagonal tests | Medium | High | Yes (no network) | Yes (Apache) | **Yes** |
| Error Prone | Medium | Medium | Yes (no network) | Yes (Apache) | **Defer** — overlaps with SpotBugs, more invasive setup, Java 25 compatibility uncertain |
| Custom ESLint rules | Low | Low-Medium | Yes (local) | N/A (project rules) | **As needed** — add rules when recurring agent mistakes are observed |
| MCP LSP Server | High | Medium | Yes (local) | Varies | **Defer** — experimental, high setup cost, unclear benefit vs. hooks |
### 8. Tool Compatibility Notes
**Java 25 compatibility:**
- Checkstyle: Confirmed support for Java 21+, Java 25 should work (runs on source, not bytecode)
- SpotBugs: Bytecode analysis — needs ASM version that supports Java 25 classfiles. Latest SpotBugs (4.9.x) supports up to Java 24; Java 25 support may require a newer release. **Verify before adopting.**
- ArchUnit: Runs via JUnit, analyzes compiled classes. Similar ASM dependency concern as SpotBugs. **Verify before adopting.**
- Error Prone: Tightly coupled to javac internals. Java 25 compatibility typically lags. **Higher risk.**
**Privacy compliance:** All recommended tools are offline-only. None phone home, none require external services. All are open source with permissive or copyleft licenses compatible with GPL.
## Decisions Required
| # | Decision | Options | Recommendation |
|---|----------|---------|----------------|
| 1 | Hooks in which settings file? | `.claude/settings.json` (project, committed) vs. `.claude/settings.local.json` (personal, gitignored) | **Project-level** — every agent user benefits |
| 2 | Checkstyle ruleset | Google Style vs. Sun Style vs. custom | **Google Style** — most widely adopted, well-documented |
| 3 | Include Error Prone in plan? | Yes (more coverage) vs. defer (simpler, overlap with SpotBugs) | **Defer** — Java 25 compatibility uncertain, overlaps with SpotBugs |
## References
- Geoffrey Huntley: [Don't waste your back pressure](https://ghuntley.com/pressure/)
- JW: [If you don't engineer backpressure, you'll get slopped](https://jw.hn/engineering-backpressure)
- HumanLayer: [Context-Efficient Backpressure for Coding Agents](https://www.hlyr.dev/blog/context-efficient-backpressure)
- Anthropic: [Claude Code Hooks Reference](https://code.claude.com/docs/en/hooks)
- Anthropic: [2026 Agentic Coding Trends Report](https://resources.anthropic.com/hubfs/2026%20Agentic%20Coding%20Trends%20Report.pdf)
- ArchUnit: [User Guide](https://www.archunit.org/userguide/html/000_Index.html)
- ArchUnit Hexagonal: [GitHub](https://github.com/whiskeysierra/archunit-hexagonal)
- SpotBugs: [Documentation](https://spotbugs.github.io/)
- Checkstyle: [Documentation](https://checkstyle.sourceforge.io/)
- Claude Code Hooks Guide: [Luiz Tanure](https://www.letanure.dev/blog/2025-08-06--claude-code-part-8-hooks-automated-quality-checks)
- lsp-mcp: [GitHub](https://github.com/jonrad/lsp-mcp)

View File

@@ -1,107 +0,0 @@
---
date: 2026-03-04T21:15:50+00:00
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
branch: master
topic: "Date/Time Handling Best Practices for the fete Stack"
tags: [research, datetime, java, postgresql, openapi, typescript]
status: complete
---
# Research: Date/Time Handling Best Practices
## Research Question
What are the best practices for handling dates and times across the full fete stack (Java 25 / Spring Boot 3.5.x / PostgreSQL / OpenAPI 3.1 / Vue 3 / TypeScript)?
## Summary
The project has two distinct date/time concepts: **event date/time** (when something happens) and **expiry date** (after which data is deleted). These map to different types at every layer. The recommendations align Java types, PostgreSQL column types, OpenAPI formats, and TypeScript representations into a consistent stack-wide approach.
## Detailed Findings
### Type Mapping Across the Stack
| Concept | Java | PostgreSQL | OpenAPI | TypeScript | Example |
|---------|------|------------|---------|------------|---------|
| Event date/time | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-15T20:00:00+01:00` |
| Expiry date | `LocalDate` | `date` | `string`, `format: date` | `string` | `2026-06-15` |
| Audit timestamps (createdAt, etc.) | `OffsetDateTime` | `timestamptz` | `string`, `format: date-time` | `string` | `2026-03-04T14:22:00Z` |
### Event Date/Time: `OffsetDateTime` + `timestamptz`
**Why `OffsetDateTime`, not `LocalDateTime`:**
- PostgreSQL best practice explicitly recommends `timestamptz` over `timestamp` — the PostgreSQL wiki says ["don't use `timestamp`"](https://wiki.postgresql.org/wiki/Don't_Do_This). `timestamptz` maps naturally to `OffsetDateTime`.
- Hibernate 6 (Spring Boot 3.5.x) has native `OffsetDateTime``timestamptz` support. `LocalDateTime` requires extra care to avoid silent timezone bugs at the JDBC driver level.
- An ISO 8601 string with offset (`2026-03-15T20:00:00+01:00`) is unambiguous in the API. A bare `LocalDateTime` string forces the client to guess the timezone.
- The OpenAPI `date-time` format and `openapi-generator` default to `OffsetDateTime` in Java — no custom type mappings needed.
**Why not `ZonedDateTime`:** Carries IANA zone IDs (e.g. `Europe/Berlin`) which add complexity without value for this use case. Worse JDBC support than `OffsetDateTime`.
**How PostgreSQL stores it:** `timestamptz` does **not** store the timezone. It converts input to UTC and stores UTC. On retrieval, it converts to the session's timezone setting. The offset is preserved in the Java `OffsetDateTime` via the JDBC driver.
**Practical flow:** The frontend sends the offset based on the organizer's browser locale. The server stores UTC. Display-side conversion happens in the frontend.
### Expiry Date: `LocalDate` + `date`
The expiry date is a calendar-date concept ("after which day should data be deleted"), not a point-in-time. A cleanup job runs periodically and deletes events where `expiryDate < today`. Sub-day precision adds no value and complicates the UX.
### Jackson Serialization (Spring Boot 3.5.x)
Spring Boot 3.x auto-configures `jackson-datatype-jsr310` (JavaTimeModule) and disables `WRITE_DATES_AS_TIMESTAMPS` by default:
- `OffsetDateTime` serializes to `"2026-03-15T20:00:00+01:00"` (ISO 8601 string)
- `LocalDate` serializes to `"2026-06-15"`
No additional configuration needed. For explicitness, can add to `application.properties`:
```properties
spring.jackson.serialization.write-dates-as-timestamps=false
```
### Hibernate 6 Configuration
With Hibernate 6, `OffsetDateTime` maps to `timestamptz` using the `NATIVE` timezone storage strategy by default on PostgreSQL. Can be made explicit:
```properties
spring.jpa.properties.hibernate.timezone.default_storage=NATIVE
```
This tells Hibernate to use the database's native `TIMESTAMP WITH TIME ZONE` type directly.
### OpenAPI Schema Definitions
```yaml
# Event date/time
eventDateTime:
type: string
format: date-time
example: "2026-03-15T20:00:00+01:00"
# Expiry date
expiryDate:
type: string
format: date
example: "2026-06-15"
```
**Code-generation mapping (defaults, no customization needed):**
| OpenAPI format | Java type (openapi-generator) | TypeScript type (openapi-typescript) |
|---------------|-------------------------------|--------------------------------------|
| `date-time` | `java.time.OffsetDateTime` | `string` |
| `date` | `java.time.LocalDate` | `string` |
### Frontend (TypeScript)
`openapi-typescript` generates `string` for both `format: date-time` and `format: date`. This is correct — JSON has no native date type, so dates travel as strings. Parsing to `Date` objects happens explicitly at the application boundary when needed (e.g. for display formatting).
## Sources
- [PostgreSQL Wiki: Don't Do This](https://wiki.postgresql.org/wiki/Don't_Do_This) — recommends `timestamptz` over `timestamp`
- [PostgreSQL Docs: Date/Time Types](https://www.postgresql.org/docs/current/datatype-datetime.html)
- [Thorben Janssen: Hibernate 6 OffsetDateTime and ZonedDateTime](https://thorben-janssen.com/hibernate-6-offsetdatetime-and-zoneddatetime/)
- [Baeldung: OffsetDateTime Serialization With Jackson](https://www.baeldung.com/java-jackson-offsetdatetime)
- [Baeldung: Map Date Types With OpenAPI Generator](https://www.baeldung.com/openapi-map-date-types)
- [Baeldung: ZonedDateTime vs OffsetDateTime](https://www.baeldung.com/java-zoneddatetime-offsetdatetime)
- [Reflectoring: Handling Timezones in Spring Boot](https://reflectoring.io/spring-timezones/)
- [openapi-typescript documentation](https://openapi-ts.dev/)

View File

@@ -1,215 +0,0 @@
---
date: "2026-03-04T22:27:37.933286+00:00"
git_commit: 91e566efea0cbf53ba06a29b63317b7435609bd8
branch: master
topic: "Automatic OpenAPI Validation Pipelines for Backpressure Hooks"
tags: [research, openapi, validation, hooks, backpressure, linting]
status: complete
---
# Research: Automatic OpenAPI Validation Pipelines
## Research Question
What automatic validation pipelines exist for OpenAPI specs that can be integrated into the current Claude Code backpressure hook setup, running after the OpenAPI spec has been modified?
## Summary
The project already has a PostToolUse hook system that runs backend compile checks and frontend lint/type-checks after Edit/Write operations. Adding OpenAPI spec validation requires a new hook script that triggers specifically when `api.yaml` is modified. Several CLI tools support OpenAPI 3.1.0 validation — **Redocly CLI** is the strongest fit given the existing Node.js toolchain, MIT license, active maintenance, and zero-config baseline.
## Current Backpressure Setup
### Hook Architecture (`.claude/settings.json`)
The project uses Claude Code hooks for automated quality gates:
| Hook Event | Trigger | Scripts |
|---|---|---|
| `PostToolUse` | `Edit\|Write` tool calls | `backend-compile-check.sh`, `frontend-check.sh` |
| `Stop` | Agent attempts to stop | `run-tests.sh` |
### How Hooks Work
Each hook script:
1. Reads JSON from stdin containing `tool_input.file_path`
2. Pattern-matches the file path to decide if it should run
3. Executes validation (compile, lint, type-check, test)
4. Returns JSON with either success message or failure details
5. On failure: outputs `hookSpecificOutput` with error context (PostToolUse) or `{"decision":"block"}` (Stop)
### Existing Pattern for File Matching
```bash
# backend-compile-check.sh — matches Java files
case "$FILE_PATH" in
*/backend/src/*.java|backend/src/*.java) ;;
*) exit 0 ;;
esac
# frontend-check.sh — matches TS/Vue files
case "$FILE_PATH" in
*/frontend/src/*.ts|*/frontend/src/*.vue|frontend/src/*.ts|frontend/src/*.vue) ;;
*) exit 0 ;;
esac
```
An OpenAPI validation hook would use the same pattern:
```bash
case "$FILE_PATH" in
*/openapi/api.yaml|*/openapi/*.yaml) ;;
*) exit 0 ;;
esac
```
### Existing OpenAPI Tooling in the Project
- **Backend:** `openapi-generator-maven-plugin` v7.20.0 generates Spring interfaces from `api.yaml` (`pom.xml:149-178`)
- **Frontend:** `openapi-typescript` v7.13.0 generates TypeScript types; `openapi-fetch` v0.17.0 provides type-safe client
- **No validation/linting tools** currently installed — no Redocly, Spectral, or other linter config exists
## Tool Evaluation
### Redocly CLI (`@redocly/cli`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Full support |
| Install | `npm install -g @redocly/cli` or `npx @redocly/cli@latest` |
| CLI | `redocly lint api.yaml` |
| License | MIT |
| Maintenance | Very active — latest v2.20.3 (2026-03-03), daily/weekly releases |
| GitHub | ~1.4k stars (Redocly ecosystem: 24k+ combined) |
**Checks:** Structural validity against OAS schema, configurable linting rules (naming, descriptions, operation IDs, security), style/consistency enforcement. Built-in rulesets: `minimal`, `recommended`, `recommended-strict`. Zero-config baseline works immediately. Custom rules via `redocly.yaml`.
**Fit for this project:** Node.js already in the toolchain (frontend). `npx` form requires no permanent install. MIT license compatible with GPL-3.0. The `@redocly/openapi-core` package is already present as a transitive dependency of `openapi-typescript` in `node_modules`.
### Spectral (`@stoplight/spectral-cli`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Full support (since v6.x) |
| Install | `npm install -g @stoplight/spectral-cli` |
| CLI | `spectral lint api.yaml` |
| License | Apache 2.0 |
| Maintenance | Active — latest v6.15.0 (2025-04-22), slower cadence |
| GitHub | ~3k stars |
**Checks:** Schema compliance, missing descriptions/tags/operationIds, contact/license metadata. Highly extensible custom rulesets via YAML/JS. Configurable severity levels.
**Fit for this project:** Well-established industry standard. Apache 2.0 compatible with GPL. Less actively maintained than Redocly (10 months since last release). Heavier custom ruleset system may be over-engineered for current needs.
### Vacuum (`daveshanley/vacuum`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Full support (via libopenapi) |
| Install | `brew install daveshanley/vacuum/vacuum` or Go binary |
| CLI | `vacuum lint api.yaml` |
| License | MIT |
| Maintenance | Active — latest release 2025-12-22 |
| GitHub | ~1k stars |
**Checks:** Structural validation, Spectral-compatible rulesets, OWASP security checks, naming conventions, descriptions/examples/tags. Single Go binary — no runtime dependencies.
**Fit for this project:** Zero-dependency binary is appealing for CI. However, adds a non-Node.js tool dependency when the project already has Node.js. Spectral ruleset compatibility is a plus for portability.
### oasdiff (`oasdiff/oasdiff`)
| Attribute | Value |
|---|---|
| OpenAPI 3.1 | Beta |
| Install | `brew install oasdiff` or Go binary |
| CLI | `oasdiff breaking base.yaml revision.yaml` |
| License | Apache 2.0 |
| Maintenance | Active — latest v1.11.10 (2026-02-05) |
| GitHub | ~1.1k stars |
**Checks:** 300+ breaking change detection rules (paths, parameters, schemas, security, headers, enums). Requires two spec versions to compare — not a standalone validator.
**Fit for this project:** Different category — detects breaking changes between spec versions, not structural validity. Useful as a CI-only check comparing `HEAD~1` vs `HEAD`. OAS 3.1 support is still beta.
### Not Recommended
- **swagger-cli:** Abandoned, no OAS 3.1 support
- **IBM OpenAPI Validator:** Active but opinionated IBM-specific rules add configuration overhead for no benefit
## Tool Comparison Matrix
| Tool | OAS 3.1 | License | Last Release | Stars | Runtime | Category |
|---|---|---|---|---|---|---|
| **Redocly CLI** | Full | MIT | 2026-03-03 | ~1.4k | Node.js | Lint + validate |
| **Spectral** | Full | Apache 2.0 | 2025-04-22 | ~3k | Node.js | Lint |
| **Vacuum** | Full | MIT | 2025-12-22 | ~1k | Go binary | Lint + validate |
| **oasdiff** | Beta | Apache 2.0 | 2026-02-05 | ~1.1k | Go binary | Breaking changes |
## Integration Pattern
### Hook Script Structure
An OpenAPI validation hook would follow the existing pattern in `.claude/hooks/`:
```bash
#!/usr/bin/env bash
set -euo pipefail
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_input',{}).get('file_path',''))" 2>/dev/null || echo "")
# Only run for OpenAPI spec files
case "$FILE_PATH" in
*/openapi/*.yaml|*/openapi/*.yml) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/backend"
# Run validation
if OUTPUT=$(npx @redocly/cli@latest lint src/main/resources/openapi/api.yaml --format=stylish 2>&1); then
echo '{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"✓ OpenAPI spec validation passed."}}'
else
ESCAPED=$(echo "$OUTPUT" | python3 -c "import sys,json; print(json.dumps(sys.stdin.read()))")
echo "{\"hookSpecificOutput\":{\"hookEventName\":\"PostToolUse\",\"additionalContext\":$ESCAPED}}"
fi
```
### Registration in `.claude/settings.json`
The hook would be added to the existing `PostToolUse` array alongside the compile and lint hooks:
```json
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/openapi-validate.sh\"",
"timeout": 120
}
```
### Configuration (Optional)
A `redocly.yaml` in the project root or `backend/` directory can customize rules:
```yaml
extends:
- recommended
rules:
operation-operationId: error
tag-description: warn
no-ambiguous-paths: error
```
## Code References
- `.claude/settings.json:1-32` — Hook configuration (PostToolUse + Stop events)
- `.claude/hooks/backend-compile-check.sh` — Java file detection pattern + compile check
- `.claude/hooks/frontend-check.sh` — TS/Vue file detection pattern + type-check + lint
- `.claude/hooks/run-tests.sh` — Stop hook with test execution and block/approve logic
- `backend/pom.xml:149-178` — openapi-generator-maven-plugin configuration
- `backend/src/main/resources/openapi/api.yaml` — The OpenAPI 3.1.0 spec to validate
## Open Questions
- Should the validation use a pinned version (`npx @redocly/cli@1.x.x`) or latest? Pinned is more reproducible; latest gets rule updates automatically.
- Should a `redocly.yaml` config be added immediately with the `recommended` ruleset, or start with zero-config (structural validation only) and add rules incrementally?
- Is breaking change detection (oasdiff) desirable as a separate CI check, or is structural validation sufficient for now?

View File

@@ -1,202 +0,0 @@
---
date: 2026-03-04T21:15:50+00:00
git_commit: b8421274b47c6d1778b83c6b0acb70fd82891e71
branch: master
topic: "RFC 9457 Problem Details for HTTP API Error Responses"
tags: [research, error-handling, rfc9457, spring-boot, openapi]
status: complete
---
# Research: RFC 9457 Problem Details
## Research Question
How should the fete API structure error responses? What does RFC 9457 (Problem Details) specify, and how does it integrate with Spring Boot 3.5.x, OpenAPI 3.1, and openapi-fetch?
## Summary
RFC 9457 (successor to RFC 7807) defines a standard JSON format (`application/problem+json`) for machine-readable HTTP API errors. Spring Boot 3.x has first-class support via `ProblemDetail`, `ErrorResponseException`, and `ResponseEntityExceptionHandler`. The recommended approach is a single `@RestControllerAdvice` that handles all exceptions consistently — no `spring.mvc.problemdetails.enabled` property, no fallback to legacy error format.
## Detailed Findings
### RFC 9457 Format
Standard fields:
| Field | Type | Description |
|-------|------|-------------|
| `type` | URI | Identifies the problem type. Defaults to `about:blank`. |
| `title` | string | Short, human-readable summary. Should not change between occurrences. |
| `status` | int | HTTP status code. |
| `detail` | string | Human-readable explanation specific to this occurrence. |
| `instance` | URI | Identifies the specific occurrence (e.g. correlation ID). |
Extension members (additional JSON properties) are explicitly permitted. This is the mechanism for validation errors, error codes, etc.
**Key rule:** With `type: "about:blank"`, the `title` must match the HTTP status phrase exactly. Use a custom `type` URI when providing a custom `title`.
### Spring Boot 3.x Built-in Support
- **`ProblemDetail`** — container class for the five standard fields + a `properties` Map for extensions.
- **`ErrorResponseException`** — base class for custom exceptions that carry their own `ProblemDetail`.
- **`ResponseEntityExceptionHandler`** — `@ControllerAdvice` base class that handles all Spring MVC exceptions and renders them as `application/problem+json`.
- **`ProblemDetailJacksonMixin`** — automatically unwraps the `properties` Map as top-level JSON fields during serialization.
### Recommended Configuration
Use a single `@RestControllerAdvice` extending `ResponseEntityExceptionHandler`. Do **not** use the `spring.mvc.problemdetails.enabled` property.
```java
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
// All Spring MVC exceptions are handled automatically.
// Add @ExceptionHandler methods for domain exceptions here.
// Add a catch-all for Exception.class to prevent legacy error format.
}
```
Reasons to avoid the property-based approach:
1. No place to add custom `@ExceptionHandler` methods.
2. Having both the property AND a custom `ResponseEntityExceptionHandler` bean causes a conflict.
3. The property ignores `server.error.include-*` properties.
### Validation Errors (Field-Level)
Spring deliberately does **not** include field-level validation errors in `ProblemDetail` by default (security rationale). Override `handleMethodArgumentNotValid`:
```java
@Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
ProblemDetail problemDetail = ex.getBody();
problemDetail.setTitle("Validation Failed");
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
List<Map<String, String>> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fe -> Map.of(
"field", fe.getField(),
"message", fe.getDefaultMessage()
))
.toList();
problemDetail.setProperty("fieldErrors", fieldErrors);
return handleExceptionInternal(ex, problemDetail, headers, status, request);
}
```
Resulting response:
```json
{
"type": "urn:problem-type:validation-error",
"title": "Validation Failed",
"status": 400,
"detail": "Invalid request content.",
"instance": "/api/events",
"fieldErrors": [
{ "field": "title", "message": "must not be blank" },
{ "field": "expiryDate", "message": "must be a future date" }
]
}
```
### OpenAPI Schema Definition
```yaml
components:
schemas:
ProblemDetail:
type: object
properties:
type:
type: string
format: uri
default: "about:blank"
title:
type: string
status:
type: integer
detail:
type: string
instance:
type: string
format: uri
additionalProperties: true
ValidationProblemDetail:
allOf:
- $ref: '#/components/schemas/ProblemDetail'
- type: object
properties:
fieldErrors:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
required:
- field
- message
responses:
BadRequest:
description: Validation failed
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ValidationProblemDetail'
NotFound:
description: Resource not found
content:
application/problem+json:
schema:
$ref: '#/components/schemas/ProblemDetail'
```
Use media type `application/problem+json` in response definitions. Set `additionalProperties: true` on the base schema.
### Frontend Consumption (openapi-fetch)
openapi-fetch uses a discriminated union for responses:
```typescript
const { data, error } = await client.POST('/api/events', { body: eventData })
if (error) {
// `error` is typed from the OpenAPI error response schema
console.log(error.title) // "Validation Failed"
console.log(error.fieldErrors) // [{ field: "title", message: "..." }]
return
}
// `data` is the typed success response
```
The `error` object is already typed from the generated schema — no manual type assertions needed for defined error shapes.
### Known Pitfalls
| Pitfall | Description | Mitigation |
|---------|-------------|------------|
| **Inconsistent formats** | Exceptions escaping to Spring Boot's `BasicErrorController` return legacy format (`timestamp`, `error`, `path`), not Problem Details. | Add a catch-all `@ExceptionHandler(Exception.class)` in the `@RestControllerAdvice`. |
| **`server.error.include-*` ignored** | When Problem Details is active, these properties have no effect. | Control content via `ProblemDetail` directly. |
| **Validation errors hidden by default** | Spring returns only `"Invalid request content."` without field details. | Override `handleMethodArgumentNotValid` explicitly. |
| **Content negotiation** | `application/problem+json` is only returned when the client accepts it. `openapi-fetch` sends `Accept: application/json` which Spring considers compatible. | No action needed for SPA clients. |
| **`about:blank` semantics** | With `type: "about:blank"`, `title` must match the HTTP status phrase. Custom titles require a custom `type` URI. | Use `urn:problem-type:*` URIs for custom problem types. |
## Sources
- [RFC 9457 Full Text](https://www.rfc-editor.org/rfc/rfc9457.html)
- [Spring Framework Docs: Error Responses](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html)
- [Swagger Blog: Problem Details RFC 9457](https://swagger.io/blog/problem-details-rfc9457-doing-api-errors-well/)
- [Baeldung: Returning Errors Using ProblemDetail](https://www.baeldung.com/spring-boot-return-errors-problemdetail)
- [SivaLabs: Spring Boot 3 Error Reporting](https://www.sivalabs.in/blog/spring-boot-3-error-reporting-using-problem-details/)
- [Spring Boot Issue #43850: Render global errors as Problem Details](https://github.com/spring-projects/spring-boot/issues/43850)

View File

@@ -1,404 +0,0 @@
# Research: Modern Sans-Serif Fonts for Mobile-First PWA
**Date:** 2026-03-04
**Context:** Selecting a primary typeface for fete, a privacy-focused PWA for event announcements and RSVPs. The font must be open-source with permissive licensing, modern geometric/neo-grotesque style, excellent mobile readability, and strong weight range.
---
## Executive Summary
Based on research of 9 candidate fonts, **6 meet all requirements** for self-hosting and redistribution under permissive licenses. Two do not qualify:
- **General Sans**: Proprietary (ITF Free Font License, non-commercial personal use only)
- **Satoshi**: License ambiguity; sources conflict between full OFL and ITF restrictions
The remaining **6 fonts are fully open-source** and suitable for the project:
| Font | License | Design | Weights | Status |
|------|---------|--------|---------|--------|
| Inter | OFL-1.1 | Neo-grotesque, humanist | 9 (ThinBlack) | ✅ Recommended |
| Plus Jakarta Sans | OFL-1.1 | Geometric, modern | 7 (ExtraLightExtraBold) | ✅ Recommended |
| Outfit | OFL-1.1 | Geometric | 9 (ThinBlack) | ✅ Recommended |
| Space Grotesk | OFL-1.1 | Neo-grotesque, distinctive | 5 (LightBold) | ✅ Recommended |
| Manrope | OFL-1.1 | Geometric, humanist | 7 (ExtraLightExtraBold) | ✅ Recommended |
| DM Sans | OFL-1.1 | Geometric, low-contrast | 9 (ThinBlack) | ✅ Recommended |
| Sora | OFL-1.1 | Geometric | 8 (ThinExtraBold) | ✅ Recommended |
---
## Detailed Candidate Analysis
### 1. Inter
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official:** https://github.com/rsms/inter (releases page)
- **NPM:** `inter-ui` package
- **Homebrew:** `font-inter`
- **Official CDN:** https://rsms.me/inter/inter.css
**Design Character:** Neo-grotesque with humanist touches. High x-height for enhanced legibility on screens. Geometric letterforms with open apertures. Designed specifically for UI and on-screen use.
**Available Weights:** 9 weights from Thin (100) to Black (900), each with italic variant. Also available as a variable font with weight axis.
**Notable Apps/Products:**
- **UX/Design tools:** Figma, Notion, Pixar Presto
- **OS:** Elementary OS, GNOME
- **Web:** GitLab, ISO, Mozilla, NASA
- **Why:** Chosen by product teams valuing clarity and modern minimalism; default choice for UI designers
**Mobile Suitability:** Excellent. Specifically engineered for screen readability with high x-height and open apertures. Performs well at 1416px body text.
**Distinctive Strengths:**
- Purpose-built for digital interfaces
- Exceptional clarity in dense UI layouts
- Strong brand identity (recognizable across tech products)
- Extensive OpenType features
**Weakness:** Very widely used; less distinctive for a bold brand identity. Considered the "safe" choice.
---
### 2. Plus Jakarta Sans
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/tokotype/PlusJakartaSans
- **Source Files:** `sources/`, compiled fonts in `fonts/` directory
- **Designer Contact:** mail@tokotype.com (Gumpita Rahayu, Tokotype)
- **Latest Version:** 2.7.1 (May 2023)
- **Build Command:** `gftools builder sources/builder.yaml`
**Design Character:** Geometric sans-serif with modern, clean-cut forms. Inspired by Neuzeit Grotesk and Futura but with contemporary refinement. Slightly taller x-height for clear spacing between caps and lowercase. Open counters and balanced spacing for legibility across sizes. **Bold, distinctive look** with personality.
**Available Weights:** 7 weights from ExtraLight (200) to ExtraBold (800), with matching italics.
**Notable Apps/Products:**
- Original commission: Jakarta Provincial Government's "+Jakarta City of Collaboration" program (2020)
- Now widely used in: Branding projects, modern web design, UI design
- **Why:** Chosen for fresh, contemporary feel without generic blandness
**Mobile Suitability:** Excellent. Designed with mobile UI in mind. Clean letterforms render crisply on small screens.
**Distinctive Strengths:**
- **Stylistic sets:** Sharp, Straight, and Swirl variants add design flexibility
- Modern geometric with Indonesian design heritage (unique perspective)
- Excellent for branding (not generic like Inter)
- OpenType features for sophisticated typography
- Well-maintained, active development
**Weakness:** Less ubiquitous than Inter; smaller ecosystem of design tool integrations.
---
### 3. Outfit
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/Outfitio/Outfit-Fonts
- **Fonts Directory:** `/fonts` in repository
- **OFL Text:** `OFL.txt` in repository
- **Designer:** Rodrigo Fuenzalida (originally for Outfit.io)
- **Status:** Repository archived Feb 25, 2025 (read-only, downloads remain accessible)
**Design Character:** Geometric sans-serif with warm, friendly appearance. Generous x-height, balanced spacing, low contrast. Nine static weights plus variable font with weight axis.
**Available Weights:** 9 weights from Thin (100) to Black (900). No italics.
**Notable Apps/Products:**
- Originally created for Outfit.io platform
- Good readability for body text (≈16px) and strong headline presence
- Used in design tools (Figma integration)
**Mobile Suitability:** Good. Geometric forms and generous spacing work well on mobile, though low contrast may require careful pairing with sufficient color contrast.
**Distinctive Strengths:**
- Full weight range (ThinBlack)
- Variable font option for granular weight control
- Stylistic alternates and rare ligatures
- Accessible character set
**Weakness:** Archived repository; no active development. Low contrast design requires careful color/contrast pairing for accessibility.
---
### 4. Space Grotesk
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/floriankarsten/space-grotesk
- **Official Site:** https://fonts.floriankarsten.com/space-grotesk
- **Designer:** Florian Karsten
- **Variants:** Variable font with weight axis
**Design Character:** Neo-grotesque with distinctive personality. Proportional variant of Space Mono (Colophon Foundry, 2016). Retains Space Mono's idiosyncratic details while optimizing for improved readability. Bold, tech-forward aesthetic with monowidth heritage visible in character design.
**Available Weights:** 5 weights—Light (300), Regular (400), Medium (500), SemiBold (600), Bold (700). No italics.
**Notable Apps/Products:**
- Modern tech companies and startups seeking distinctive branding
- Popular in neo-brutalist web design
- Good for headlines and display use
**Mobile Suitability:** Good. Clean proportional forms with distinctive character. Works well for headlines; body text at 14px+ is readable.
**Distinctive Strengths:**
- **Bold, tech-forward personality** — immediately recognizable
- Heritage from Space Mono adds character without looking dated
- Excellent OpenType support (old-style figures, tabular figures, superscript, subscript, fractions, stylistic alternates)
- **Supports extended language coverage:** Latin, Vietnamese, Pinyin, Central/South-Eastern European
**Weakness:** Only 5 weights (lightest is 300, no Thin). Fewer weight options than Inter or DM Sans.
---
### 5. Manrope
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/sharanda/manrope
- **Designer:** Mikhail Sharanda (2018), converted to variable by Mirko Velimirovic (2019)
- **Alternative Sources:** Multiple community forks on GitHub, npm packages
- **NPM Package:** `@fontsource/manrope`, `@fontsource-variable/manrope`
**Design Character:** Modern geometric sans-serif blending geometric shapes with humanistic elements. Semi-condensed structure with clean, contemporary feel. Geometric digits, packed with OpenType features.
**Available Weights:** 7 weights from ExtraLight (200) to ExtraBold (800). Available as variable font.
**Notable Apps/Products:**
- Widely used in modern design systems
- Popular in product/SaaS design
- Good for both UI and branding
**Mobile Suitability:** Excellent. Clean geometric design with humanistic touches; balanced proportions work well on mobile.
**Distinctive Strengths:**
- Geometric + humanistic blend (best of both worlds)
- Well-maintained active project
- Variable font available
- Strong design community around the font
**Weakness:** None significant; solid all-around choice.
---
### 6. DM Sans
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/googlefonts/dm-fonts
- **Releases Page:** https://github.com/googlefonts/dm-fonts/releases
- **Google Fonts:** https://fonts.google.com/specimen/DM+Sans
- **Design:** Commissioned from Colophon Foundry; Creative Direction: MultiAdaptor & DeepMind
**Design Character:** Low-contrast geometric sans-serif optimized for text at smaller sizes. Part of the DM suite (DM Sans, DM Serif Text, DM Serif Display). Designed for clarity and efficiency in dense typography.
**Available Weights:** 9 weights from Thin (100) to Black (900), each with italic variant.
**Notable Apps/Products:**
- DeepMind products (by commission)
- Tech companies favoring geometric clarity
- Professional and commercial products requiring text legibility
**Mobile Suitability:** Excellent. Specifically optimized for small text sizes; low contrast minimizes visual noise on mobile screens.
**Distinctive Strengths:**
- **Optimized for small text** — superior at 1214px
- Full weight range (ThinBlack)
- Active Google Fonts maintenance
- Italic variants (unlike Outfit or Space Grotesk)
- Commissioned by reputable team (DeepMind)
**Weakness:** Low contrast may feel less bold on headlines without careful sizing/weight adjustment.
---
### 7. Sora
**License:** SIL Open Font License 1.1 (OFL-1.1)
**Download Location:**
- **Official Repository:** https://github.com/sora-xor/sora-font
- **GitHub Releases:** Direct TTF/OTF downloads available
- **NPM Packages:** `@fontsource/sora`, `@fontsource-variable/sora`
- **Original Purpose:** Custom typeface for SORA decentralized autonomous economy
**Design Character:** Geometric sans-serif with contemporary, clean aesthetic. Available as both static fonts and variable font. Designed as a branding solution for decentralized systems.
**Available Weights:** 8 weights from Thin (100) to ExtraBold (800), each with italic variant. Variable font available.
**Notable Apps/Products:**
- Sora (XOR) decentralized projects
- Crypto/blockchain projects using modern typography
- Web3 products seeking distinctive branding
**Mobile Suitability:** Good. Clean geometric forms render well on mobile; italics available for emphasis.
**Distinctive Strengths:**
- Full weight range with italics
- Variable font option
- Designed for digital-first branding
- GitHub-native distribution
**Weakness:** Less established than Inter or DM Sans in mainstream product design; smaller ecosystem.
---
## Rejected Candidates
### General Sans
**Status:** ❌ Does not meet licensing requirements
**License:** ITF Free Font License (proprietary, non-commercial personal use only)
**Why Rejected:** This is a **paid commercial font** distributed by the Indian Type Foundry (not open-source). The ITF Free Font License permits personal use only; commercial use requires a separate paid license. Does not meet the "open-source with permissive license" requirement.
**Designer:** Frode Helland (published by Indian Type Foundry)
---
### Satoshi
**Status:** ⚠️ License ambiguity — conflicting sources
**Documented License:**
- Some sources claim SIL Open Font License (OFL-1.1)
- Other sources indicate ITF Free Font License (personal use only) similar to General Sans
**Design:** Swiss-style modernist sans-serif (Light to Black, 510 weights)
**Download:** Fontshare (Indian Type Foundry's free font service)
**Why Not Recommended:** The license status is unclear. While Fontshare advertises "free for personal and commercial use," the font's origin (Indian Type Foundry) and conflicting license documentation create uncertainty. For a privacy-focused project with clear open-source requirements, Satoshi's ambiguous licensing creates unnecessary legal risk. Better alternatives with unambiguous OFL-1.1 licensing are available.
**Recommendation:** If clarity is needed, contact Fontshare/ITF directly. For now, exclude from consideration to reduce licensing complexity.
---
## Comparative Table: Qualified Fonts
| Metric | Inter | Plus Jakarta Sans | Outfit | Space Grotesk | Manrope | DM Sans | Sora |
|--------|-------|-------------------|--------|---------------|---------|---------|------|
| **License** | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 | OFL-1.1 |
| **Weights** | 9 | 7 | 9 | 5 | 7 | 9 | 8 |
| **Italics** | ✅ Yes | ✅ Yes | ❌ No | ❌ No | ❌ No | ✅ Yes | ✅ Yes |
| **Variable Font** | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
| **Design** | Neo-grotesque | Geometric | Geometric | Neo-grotesque | Geo + Humanist | Geometric | Geometric |
| **Personality** | Generic/Safe | Bold/Fresh | Warm/Friendly | Tech-Forward | Balanced | Efficient/Clean | Contemporary |
| **Mobile Text** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| **Distinctiveness** | Low | High | Medium | High | High | Medium | Medium |
| **Ecosystem** | Very Large | Growing | Medium | Growing | Growing | Large | Small |
| **Active Dev** | ✅ Yes | ✅ Yes | ❌ Archived | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
---
## Recommendations
### For Bold App-Native Branding
**Primary Choice: Plus Jakarta Sans**
**Rationale:**
- Fully open-source (OFL-1.1) with unambiguous licensing
- Bold, modern geometric aesthetic suitable for app branding
- Stylistic sets (Sharp, Straight, Swirl) provide design flexibility
- Well-maintained by Tokotype with clear development history
- Strong presence in modern UI/web design
- Excellent mobile readability with thoughtful character spacing
- Indonesian design heritage adds unique perspective (not generic)
**Alternative: Space Grotesk**
If you prefer **even more distinctive character:**
- Neo-grotesque with tech-forward personality
- Smaller weight range (5 weights) but strong identity
- Popular in contemporary design circles
- Good for headlines; pair with a more neutral font for body text if needed
---
### For Safe, Professional UI
**Primary Choice: Inter or DM Sans**
**Inter if:**
- Maximum ecosystem and tool support desired
- Designing for broad recognition and trust
- Team already familiar with Inter (widespread in tech)
**DM Sans if:**
- Emphasis on small text legibility (optimized for 1214px)
- Prefer italic variants
- Want active maintenance from Google Fonts community
---
### For Balanced Approach
**Manrope**
- Geometric + humanistic blend (versatile)
- Excellent mobile performance
- Strong weight range (7 weights)
- Underrated choice; often overlooked for bolder options but delivers polish
---
## Implementation Notes for Self-Hosting
All recommended fonts can be self-hosted:
1. **Download:** Clone repository or download from releases page
2. **Generate Web Formats:** Use FontForge, FontTools, or online converters to generate WOFF2 (required for modern browsers)
3. **CSS:** Include via `@font-face` with local file paths
4. **License:** Include `LICENSE.txt` or `OFL.txt` in the distribution
Example self-hosted CSS:
```css
@font-face {
font-family: 'Plus Jakarta Sans';
src: url('/fonts/PlusJakartaSans-Regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
```
---
## Privacy Considerations
All selected fonts are self-hosted open-source projects with no telemetry, no external CDN dependencies, and no tracking. Fully compliant with the project's privacy-first principles.
---
## Conclusion
**Inter, Plus Jakarta Sans, and Space Grotesk** are the strongest candidates. The choice depends on brand positioning:
- **Generic + Safe → Inter**
- **Bold + Modern → Plus Jakarta Sans**
- **Tech-Forward + Distinctive → Space Grotesk**
All seven recommended fonts meet the strict licensing, openness, mobile readability, and weight-range requirements. Any of them are viable; the decision is primarily aesthetic.
---
## Sources
- [Inter Font GitHub Repository](https://github.com/rsms/inter)
- [Plus Jakarta Sans GitHub Repository](https://github.com/tokotype/PlusJakartaSans)
- [Outfit Fonts GitHub Repository](https://github.com/Outfitio/Outfit-Fonts)
- [Space Grotesk GitHub Repository](https://github.com/floriankarsten/space-grotesk)
- [Manrope GitHub Repository](https://github.com/sharanda/manrope)
- [DM Fonts GitHub Repository](https://github.com/googlefonts/dm-fonts)
- [Sora Font GitHub Repository](https://github.com/sora-xor/sora-font)
- [SIL Open Font License](https://openfontlicense.org/)
- [Google Fonts (reference)](https://fonts.google.com)
- [Fontshare (reference)](https://www.fontshare.com)

View File

@@ -1,135 +0,0 @@
# Single-Container Deployment: Spring Boot + Vue SPA
**Date:** 2026-03-04
**Context:** T-2 research — how to serve a Spring Boot API and Vue SPA from one Docker container.
## The Three Approaches
### Approach A: Spring Boot Serves Static Files (Recommended for fete)
Frontend `dist/` is built by Vite and copied into Spring Boot's `classpath:/static/` during the Docker build. One JAR serves everything.
**Pros:** One process, one port, one health check. No process manager. The JAR is the single artifact.
**Cons:** Embedded Tomcat is not optimized for static files (no `sendfile`, no `gzip_static`). Irrelevant at fete's scale (tens to hundreds of users).
**Real-world examples:** [jonashackt/spring-boot-vuejs](https://github.com/jonashackt/spring-boot-vuejs), [georgwittberger/spring-boot-embedded-spa-example](https://github.com/georgwittberger/spring-boot-embedded-spa-example), [bootify-io/spring-boot-react-example](https://github.com/bootify-io/spring-boot-react-example).
### Approach B: nginx + Spring Boot (Two Processes)
nginx serves static files, proxies `/api` to Spring Boot. Needs supervisord or a wrapper script.
**Pros:** nginx is battle-tested for static files (gzip, caching, HTTP/2). Clean separation.
**Cons:** Two processes, supervisord complexity, partial-failure detection issues, larger image.
**When to use:** High-traffic apps where static file performance matters.
### Approach C: Separate Containers
Not relevant — violates the single-container requirement.
## The Context-Path Problem
Current config: `server.servlet.context-path=/api`. This scopes **everything** under `/api`, including static resources. Frontend at `classpath:/static/` would be served at `/api/index.html`, not `/`.
### Solution: Remove context-path, use `addPathPrefix`
Remove `server.servlet.context-path=/api`. Instead, use Spring's `PathMatchConfigurer`:
```java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("/api",
c -> c.isAnnotationPresent(RestController.class));
}
}
```
Result:
- API: `/api/...` (only `@RestController` classes)
- Static files: `/` (from `classpath:/static/`)
- Actuator: `/actuator/health` (outside `/api` — correct for infra health checks)
- SPA forwarding controller (`@Controller`, not `@RestController`) is not prefixed
OpenAPI spec keeps `servers: [{url: /api}]`. Frontend client keeps `baseUrl: "/api"`. No changes needed.
## SPA Routing (Vue Router History Mode)
Vue Router in history mode uses real URL paths. The server must return `index.html` for all paths that aren't API endpoints or actual static files.
### Recommended: Custom `PathResourceResolver`
```java
@Configuration
public class SpaConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath,
Resource location) throws IOException {
Resource requested = location.createRelative(resourcePath);
return (requested.exists() && requested.isReadable())
? requested
: new ClassPathResource("/static/index.html");
}
});
}
}
```
Requests flow: `@RestController` match → serve API. No match → resource handler → file exists → serve file. No file → serve `index.html` → Vue Router handles route.
## Multi-Stage Dockerfile Pattern
```dockerfile
# Stage 1: Build frontend
FROM node:24-alpine AS frontend-build
WORKDIR /app/frontend
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
COPY backend/src/main/resources/openapi/api.yaml ../backend/src/main/resources/openapi/api.yaml
RUN npm run build
# Stage 2: Build backend (with frontend assets)
FROM eclipse-temurin:25-jdk-alpine AS backend-build
WORKDIR /app
COPY backend/ backend/
COPY --from=frontend-build /app/frontend/dist backend/src/main/resources/static/
WORKDIR /app/backend
RUN ./mvnw -B -DskipTests package
# Stage 3: Runtime
FROM eclipse-temurin:25-jre-alpine
WORKDIR /app
COPY --from=backend-build /app/backend/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
```
Key decisions:
- Frontend built first (needs `api.yaml` for type generation)
- `npm ci` before `COPY frontend/` for Docker layer caching
- Frontend output copied into `backend/src/main/resources/static/` before Maven build → ends up in JAR
- `-DskipTests` in Docker build (tests run in CI)
- JRE-only final image (no JDK, no Node)
- Alpine-based for minimal size
## Open-Source Consensus
All surveyed Spring Boot + SPA projects use Approach A. None use supervisord + nginx for small-to-medium apps.
## Sources
- [jonashackt/spring-boot-vuejs](https://github.com/jonashackt/spring-boot-vuejs)
- [georgwittberger/spring-boot-embedded-spa-example](https://github.com/georgwittberger/spring-boot-embedded-spa-example)
- [Bundling React/Vite with Spring Boot (jessym.com)](https://www.jessym.com/articles/bundling-react-vite-with-spring-boot)
- [Spring Framework PathMatchConfigurer docs](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-config/path-matching.html)
- [Docker: Multi-service containers](https://docs.docker.com/engine/containers/multi-service_container/)

View File

@@ -1,477 +0,0 @@
---
date: 2026-03-04T00:19:03+01:00
git_commit: 7b460dd322359dc1fa3ca0dc950a91c607163977
branch: master
topic: "T-1: Initialize monorepo structure — Tech stack research"
tags: [research, codebase, t-1, scaffolding, spring-boot, vue, maven, vite, hexagonal-architecture]
status: complete
---
# Research: T-1 — Initialize Monorepo Structure
## Research Question
What are the current versions, scaffolding approaches, and architectural patterns needed to implement T-1 (Initialize monorepo structure) with the specified tech stack: Java (latest LTS), Spring Boot, Maven, hexagonal/onion architecture backend + Svelte with Vite frontend?
## Summary
This research covers all technical aspects needed for T-1. The spec requires a monorepo with `backend/` and `frontend/` directories, both building successfully as empty scaffolds. The key findings and open decisions are:
1. **Java 25** is the current LTS (Sep 2025). Spring Boot **4.0.3** is the latest stable — but **3.5.x** is more battle-tested. This is an architectural decision.
2. **Svelte 5** is stable (since Oct 2024). The SPA router ecosystem for plain Svelte 5 is weak — **SvelteKit in SPA mode** is the pragmatic alternative.
3. **Hexagonal architecture**: Single Maven module with package-level separation + ArchUnit enforcement. Base package `com.fete`.
4. **TypeScript** for the frontend is recommended but is a decision point.
## Detailed Findings
### 1. Java Version
**Java 25 (LTS)** — released September 16, 2025.
- Premier support through Sep 2030, extended support through Sep 2033.
- Supersedes Java 21 (Sep 2023) as the current LTS.
- Java 21 is still supported but free updates end Sep 2026.
- Next LTS: Java 29 (Sep 2027).
**Recommendation:** Java 25. Longest support runway, both Spring Boot 3.5 and 4.0 support it.
### 2. Spring Boot Version
Two actively supported lines as of March 2026:
| Version | Latest Patch | OSS Support Ends | Java Baseline | Key Dependencies |
|---------|-------------|-------------------|---------------|-----------------|
| **4.0.x** | 4.0.3 | Dec 2026 | Java 17+ (up to 25) | Spring Framework 7.0, Jakarta EE 11, Hibernate 7.1, Jackson 3.0, Tomcat 11.0 |
| **3.5.x** | 3.5.11 | Jun 2026 | Java 17+ (up to 25) | Spring Framework 6.x, Jakarta EE 10, Hibernate 6.x, Jackson 2.x, Tomcat 10.x |
**Trade-offs:**
| Factor | Spring Boot 4.0 | Spring Boot 3.5 |
|--------|----------------|-----------------|
| Support runway | Dec 2026 (longer) | Jun 2026 (shorter) |
| Ecosystem maturity | Jackson 3.0 + Hibernate 7.1 are new major versions; fewer community examples | Battle-tested, large ecosystem of examples |
| Migration burden | None (greenfield) | None (greenfield) |
| Forward-looking | Yes | Will need migration to 4.x eventually |
**Decision needed:** Spring Boot 4.0 (forward-looking) vs. 3.5 (more battle-tested). Both support Java 25.
### 3. Maven
- **Maven version:** 3.9.12 (latest stable; Maven 4.0.0 is still RC).
- **Maven Wrapper:** Yes, include it. Modern "only-script" distribution — no binary JAR in repo. Scripts `mvnw`/`mvnw.cmd` + `.mvn/wrapper/maven-wrapper.properties` are committed.
- Benefits: deterministic builds in Docker, no Maven pre-install requirement for contributors or CI.
**Minimal dependencies for Spring Boot + PostgreSQL:**
```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.0.3</version> <!-- or 3.5.11 -->
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
```
Note: For the empty scaffold (T-1), JPA autoconfig must be excluded or deferred since there's no DB yet. Either omit `spring-boot-starter-data-jpa` until T-4, or add `spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration` to `application.properties`.
### 4. Svelte + Vite Frontend
**Current versions:**
- Svelte: **5.53.6** (stable since Oct 2024, runes-based reactivity)
- Vite: **7.3.1** (stable; Vite 8 is in beta)
**Svelte 5 key changes from Svelte 4:**
- Reactive state: `let count = $state(0)` (explicit rune) instead of implicit `let count = 0`
- Derived values: `$derived()` replaces `$:` blocks
- Props: `$props()` replaces `export let`
- Reactivity works in `.svelte.ts` files too (not just components)
### 5. SvelteKit vs. Plain Svelte — Decision Point
The spec says "Svelte with Vite as bundler" for an SPA with a separate REST backend.
**Option A: Plain Svelte + Vite + third-party router**
| Pro | Con |
|-----|-----|
| Simpler mental model | Router ecosystem broken for Svelte 5: `svelte-spa-router` has no Svelte 5 support (issue #318) |
| Literal spec wording | Only unmaintained/low-adoption alternatives exist |
| No unused SSR concepts | Violates dependency statute ("actively maintained") |
**Option B: SvelteKit in SPA mode (adapter-static + `ssr: false`)**
| Pro | Con |
|-----|-----|
| Built-in file-based routing (maintained by Svelte team) | Has SSR concepts that exist but are unused |
| First-class Vitest integration (T-4 requirement) | Slightly larger framework footprint |
| SPA config is 3 lines of code | SEO concerns (irrelevant for this app) |
| Output is static HTML/JS/CSS — no Node.js runtime needed | |
| `npx sv create` scaffolds TS/ESLint/Vitest in one step | |
**SPA configuration (total effort):**
1. `npm i -D @sveltejs/adapter-static`
2. `svelte.config.js`: `adapter: adapter({ fallback: '200.html' })`
3. `src/routes/+layout.js`: `export const ssr = false;`
**Decision needed:** SvelteKit in SPA mode (pragmatic, solves the router problem) vs. plain Svelte + Vite (minimalist, but router ecosystem is a real problem).
### 6. TypeScript vs. JavaScript — Decision Point
**Arguments for TypeScript:**
- Java backend is strongly typed; TS catches API contract drift at compile time.
- Svelte 5 has first-class TS support, including TS in markup.
- The API client layer (T-4) benefits most from type safety.
- Zero-config in both SvelteKit and the `svelte-ts` Vite template.
- Rich Harris: "TypeScript for apps, JSDoc for libraries." This is an app.
**Arguments against:**
- KISS/grugbrain principle — TS adds cognitive overhead.
- Svelte's compiler already catches many errors.
**Verdict from research:** TS overhead in Svelte 5 is minimal (`let count: number = $state(0)` vs. `let count = $state(0)`). The real value is in the API client layer and shared types.
### 7. Node.js Version
| Version | Status | End of Life |
|---------|--------|-------------|
| Node.js 24 | Active LTS | ~April 2028 |
| Node.js 22 | Active LTS | April 2027 |
| Node.js 20 | Maintenance LTS | April 2026 (EOL imminent) |
**Recommendation:** Target Node.js 22 LTS as minimum. Docker image should use `node:22-alpine`.
### 8. Hexagonal Architecture — Package Structure
**Approach:** Single Maven module with package-level separation. Multi-module Maven is overkill for a small app. ArchUnit test enforces dependency rules.
**Package structure:**
```
com.fete
├── FeteApplication.java # @SpringBootApplication
├── domain/
│ ├── model/ # Entities, value objects (plain Java, no framework annotations)
│ └── port/
│ ├── in/ # Driving port interfaces (use cases)
│ └── out/ # Driven port interfaces (repositories)
├── application/
│ └── service/ # Use case implementations (@Service)
├── adapter/
│ ├── in/
│ │ └── web/ # REST controllers, DTOs, mappers
│ └── out/
│ └── persistence/ # JPA entities, Spring Data repos, mappers
└── config/ # @Configuration classes
```
**Dependency flow (strict):**
```
domain → nothing
application → domain only
adapter → application + domain
config → everything (wiring)
```
**Spring annotations by layer:**
| Layer | Annotations | Rationale |
|-------|-------------|-----------|
| `domain.model` | None | Plain Java — no framework coupling |
| `domain.port` | None | Plain Java interfaces |
| `application.service` | `@Service` only | Pragmatic compromise for component scanning |
| `adapter.in.web` | `@RestController`, `@GetMapping`, etc. | Framework adapter layer |
| `adapter.out.persistence` | `@Entity`, `@Repository`, `@Table`, etc. | Framework adapter layer |
| `config` | `@Configuration`, `@Bean` | Wiring layer |
**Domain purity:** Persistence has its own JPA entity classes (e.g., `EventJpaEntity`) separate from domain model classes. Mappers convert between them.
**Empty directory markers:** Use `package-info.java` in each leaf package. Documents package purpose, allows Git to track the directory, and aids component scanning.
**Base package:** `com.fete` (Maven convention, clean, short).
### 9. .gitignore
The existing `.gitignore` covers IDE files (`.idea/`, `.vscode/`, `*.iml`), OS files (`.DS_Store`, `Thumbs.db`), and Claude settings. The following sections need to be added:
**Java/Maven:**
- `*.class`, `*.jar`, `*.war`, `*.ear`, `*.nar` — compiled artifacts
- `target/` — Maven build output
- Maven release plugin files (`pom.xml.tag`, `pom.xml.releaseBackup`, etc.)
- `.mvn/wrapper/maven-wrapper.jar` — downloaded automatically
- Eclipse files (`.classpath`, `.project`, `.settings/`, `.factorypath`)
- Spring Boot (`.springBeans`, `.sts4-cache`)
- Java crash logs (`hs_err_pid*`, `replay_pid*`)
- `*.log`
**Node.js/Svelte/Vite:**
- `node_modules/`
- `dist/`, `build/` — build output
- `.svelte-kit/` — SvelteKit generated files
- `vite.config.js.timestamp-*`, `vite.config.ts.timestamp-*` — Vite temp files
- `.env`, `.env.*` (but NOT `.env.example`)
- `npm-debug.log*`
**Editor files:**
- `*.swp`, `*.swo`, `*~` — Vim/editor backup files
- `\#*\#`, `.#*` — Emacs
**Committed (NOT ignored):**
- `mvnw`, `mvnw.cmd`, `.mvn/wrapper/maven-wrapper.properties`
- `package-lock.json`
### 10. Existing Repository State
The repository currently contains:
- `CLAUDE.md` — Project statutes
- `README.md` — With tech stack docs and docker-compose example
- `LICENSE` — GPL
- `.gitignore` — Partial (IDE + OS only)
- `Ideen.md` — German idea document
- `spec/` — User stories, personas, setup tasks, implementation phases
- `.ralph/` — Ralph loop infrastructure
- `ralph.sh` — Ralph loop runner
- `review-findings.md` — Review notes
No `backend/` or `frontend/` directories exist yet. No `Dockerfile` exists yet (listed in README project structure, but deferred to T-2).
## Decisions Required Before Implementation
These are architectural decisions that require approval per CLAUDE.md governance statutes:
| # | Decision | Options | Recommendation |
|---|----------|---------|----------------|
| 1 | Spring Boot version | 4.0.3 (latest, longer support) vs. 3.5.11 (battle-tested, shorter support) | 4.0.3 — greenfield project, no migration burden, longer support |
| 2 | SvelteKit vs. plain Svelte | SvelteKit SPA mode vs. plain Svelte + third-party router | SvelteKit SPA mode — router ecosystem for plain Svelte 5 is broken |
| 3 | TypeScript vs. JavaScript | TypeScript (type safety on API boundary) vs. JavaScript (simpler) | TypeScript — minimal overhead in Svelte 5, catches API contract drift |
| 4 | Spring Boot JPA in T-1? | Include `spring-boot-starter-data-jpa` now (exclude autoconfig) vs. add it in T-4 | Defer to T-4 — T-1 is "empty scaffold", JPA needs a datasource |
## Code References
- `spec/setup-tasks.md` — T-1 acceptance criteria
- `spec/implementation-phases.md:9-14` — Phase 0 task order
- `CLAUDE.md:36-43` — Dependency statutes
- `Ideen.md:76-78` — Tech stack decisions (already made)
- `.gitignore` — Current state (needs extension)
- `README.md:112-119` — Documented project structure (target)
## Architecture Documentation
### Target Repository Layout (after T-1)
```
fete/
├── backend/
│ ├── pom.xml
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── .mvn/wrapper/maven-wrapper.properties
│ └── src/
│ ├── main/
│ │ ├── java/com/fete/
│ │ │ ├── FeteApplication.java
│ │ │ ├── domain/model/ (package-info.java)
│ │ │ ├── domain/port/in/ (package-info.java)
│ │ │ ├── domain/port/out/ (package-info.java)
│ │ │ ├── application/service/ (package-info.java)
│ │ │ ├── adapter/in/web/ (package-info.java)
│ │ │ ├── adapter/out/persistence/(package-info.java)
│ │ │ └── config/ (package-info.java)
│ │ └── resources/
│ │ └── application.properties
│ └── test/java/com/fete/
│ └── FeteApplicationTest.java
├── frontend/
│ ├── package.json
│ ├── package-lock.json
│ ├── svelte.config.js
│ ├── vite.config.ts
│ ├── tsconfig.json
│ ├── src/
│ │ ├── app.html
│ │ ├── routes/
│ │ │ ├── +layout.js (ssr = false)
│ │ │ └── +page.svelte
│ │ └── lib/
│ └── static/
├── spec/
├── .gitignore (extended)
├── CLAUDE.md
├── README.md
├── LICENSE
└── Ideen.md
```
### Build Commands (Target State)
| What | Command |
|------|---------|
| Backend build | `cd backend && ./mvnw package` |
| Backend test | `cd backend && ./mvnw test` |
| Frontend install | `cd frontend && npm install` |
| Frontend build | `cd frontend && npm run build` |
| Frontend test | `cd frontend && npm test` |
| Frontend dev | `cd frontend && npm run dev` |
## Open Questions
All resolved — see Follow-up Research below.
## Follow-up Research: Frontend Pivot to Vue 3 (2026-03-04)
### Context
During decision review, the developer raised concerns about the Svelte 5 ecosystem maturity (specifically the broken third-party router situation signaling a smaller, less mature ecosystem). After comparing Svelte 5 vs Vue 3 on ecosystem size, community support, team size, and stability, the decision was made to pivot from Svelte to **Vue 3**.
### Rationale
- Vue 3 has a significantly larger ecosystem and community
- Official, battle-tested packages for all needs (Vue Router, Pinia, Vitest)
- Vite was created by Evan You (Vue's creator) — first-class integration
- Vue 3 Composition API is modern and elegant while being mature (stable since 2020)
- Larger team, broader funding, more StackOverflow answers and tutorials
### Vue 3 Stack — Research Findings
**Current versions (March 2026):**
| Package | Version | Notes |
|---------|---------|-------|
| Vue | 3.5.29 | Stable. Vue 3.6 (Vapor Mode) is in beta |
| Vue Router | 5.0.3 | Includes file-based routing from unplugin-vue-router. Drop-in from v4 for manual routes |
| Vite | 7.3.1 | Stable. Vite 8 (Rolldown) is in beta |
| Vitest | 4.0.18 | Stable. Browser Mode graduated from experimental |
| @vue/test-utils | 2.4.6 | Official component testing utilities |
| create-vue | 3.22.0 | Official scaffolding tool |
| Node.js | 24 LTS | Latest LTS, support through ~April 2028 |
**Scaffolding:** `npm create vue@latest` (official Vue CLI scaffolding). Interactive prompts offer TypeScript, Vue Router, Pinia, Vitest, ESLint, Prettier out of the box.
**Selected options for fete:**
- TypeScript: **Yes**
- Vue Router: **Yes**
- Pinia: **No** — Composition API (`ref`/`reactive`) + localStorage is sufficient for this app's simple state
- Vitest: **Yes**
- ESLint: **Yes**
- Prettier: **Yes**
- E2E testing: **No** (not needed for T-1)
**Project structure (scaffolded by create-vue):**
```
frontend/
├── public/
│ └── favicon.ico
├── src/
│ ├── assets/ # Static assets (CSS, images)
│ ├── components/ # Reusable components
│ ├── composables/ # Composition API composables (added manually)
│ ├── router/ # Vue Router config (index.ts)
│ ├── views/ # Route-level page components
│ ├── App.vue # Root component
│ └── main.ts # Entry point
├── index.html
├── package.json
├── tsconfig.json
├── tsconfig.app.json
├── tsconfig.node.json
├── vite.config.ts
├── eslint.config.js
├── .prettierrc.json
├── env.d.ts
└── README.md
```
**Key conventions:**
- `src/views/` for route page components (not `src/pages/` — that's Nuxt)
- `src/components/` for reusable components
- `src/composables/` for Composition API composables (e.g., `useStorage.ts`)
- `src/router/index.ts` for route definitions
### Resolved Decisions
| # | Decision | Resolution |
|---|----------|------------|
| 1 | Spring Boot version | ~~**4.0.3**~~**3.5.11** — see addendum below |
| 2 | Frontend framework | **Vue 3** — pivot from Svelte due to ecosystem maturity concerns |
| 3 | TypeScript | **Yes** — confirmed by developer |
| 4 | Node.js version | **24 LTS** (latest LTS) |
| 5 | Base package | **`de.fete`** (not `com.fete`) |
| 6 | JPA in T-1 | **Defer to T-4** — T-1 is empty scaffold, JPA needs a datasource |
| 7 | State management | **No Pinia** — Composition API + localStorage sufficient |
### Addendum: Spring Boot 4.0 → 3.5 Pivot (2026-03-04)
During T-1 implementation, Spring Boot 4.0.3 proved unworkable for the scaffold phase. The 4.0 release massively reorganized internal packages — test infrastructure classes (`TestRestTemplate`, `AutoConfigureMockMvc`, etc.) were moved into new modules with different package paths. The Spring Boot 4.0 Migration Guide did not cover these changes adequately, and resolving the issues required extensive trial-and-error with undocumented class locations and missing transitive dependencies.
**Decision:** Pivot to **Spring Boot 3.5.11** (latest 3.5.x patch). This is the battle-tested line with OSS support through June 2026. Since this is a greenfield project, migrating to 4.x later (once the ecosystem and documentation have matured) is straightforward.
**Impact:** None on architecture or feature scope. The hexagonal package structure, dependency choices, and all other decisions remain unchanged. Only the Spring Boot parent version in `pom.xml` changed.
### Updated Target Repository Layout
```
fete/
├── backend/
│ ├── pom.xml
│ ├── mvnw
│ ├── mvnw.cmd
│ ├── .mvn/wrapper/maven-wrapper.properties
│ └── src/
│ ├── main/
│ │ ├── java/de/fete/
│ │ │ ├── FeteApplication.java
│ │ │ ├── domain/model/ (package-info.java)
│ │ │ ├── domain/port/in/ (package-info.java)
│ │ │ ├── domain/port/out/ (package-info.java)
│ │ │ ├── application/service/ (package-info.java)
│ │ │ ├── adapter/in/web/ (package-info.java)
│ │ │ ├── adapter/out/persistence/(package-info.java)
│ │ │ └── config/ (package-info.java)
│ │ └── resources/
│ │ └── application.properties
│ └── test/java/de/fete/
│ └── FeteApplicationTest.java
├── frontend/
│ ├── public/
│ ├── src/
│ │ ├── assets/
│ │ ├── components/
│ │ ├── composables/
│ │ ├── router/index.ts
│ │ ├── views/
│ │ ├── App.vue
│ │ └── main.ts
│ ├── index.html
│ ├── package.json
│ ├── package-lock.json
│ ├── tsconfig.json
│ ├── vite.config.ts
│ └── eslint.config.js
├── spec/
├── .gitignore (extended)
├── CLAUDE.md
├── README.md
├── LICENSE
└── Ideen.md
```

View File

@@ -1,213 +0,0 @@
---
date: "2026-03-04T18:19:10.241698+00:00"
git_commit: 316137bf1c391577e884ce525af780f45e34da86
branch: master
topic: "T-3: CI/CD Pipeline — Gitea Actions"
tags: [research, codebase, ci-cd, gitea, docker, pipeline]
status: complete
---
# Research: T-3 CI/CD Pipeline
## Research Question
What is the current state of the project relevant to implementing T-3 (CI/CD pipeline with Gitea Actions), what are its requirements, dependencies, and what infrastructure already exists?
## Summary
T-3 requires a Gitea Actions workflow in `.gitea/workflows/` that runs on every push: tests both backend and frontend, builds a Docker image, and publishes it to the Gitea container registry. The task is currently unstarted but all dependencies (T-1, T-2) are completed. The project already has a working multi-stage Dockerfile, comprehensive test suites for both backend and frontend, and clearly defined build commands. No CI/CD configuration files exist yet.
## Detailed Findings
### T-3 Specification
Defined in `spec/setup-tasks.md:41-55`.
**Acceptance Criteria (all unchecked):**
1. Gitea Actions workflow file in `.gitea/workflows/` runs on push: test, build, publish Docker image
2. Backend tests run via Maven
3. Frontend tests run via Vitest
4. Docker image is published to the Gitea container registry on the same instance
5. Pipeline fails visibly if any test fails or the build breaks
6. Docker image is only published if all tests pass and the build succeeds
**Dependencies:** T-1 (completed), T-2 (completed)
**Platform Decision (Q-5):** Per `.ralph/review-findings/questions.md:12-22`, Gitea is confirmed as the exclusive CI/CD platform. Only Gitea infrastructure will be used — Gitea Actions for pipelines, Gitea container registry for Docker image publishing.
### Dependencies — What Already Exists
#### Dockerfile (repo root)
A working 3-stage Dockerfile exists (`Dockerfile:1-26`):
| Stage | Base Image | Purpose |
|-------|-----------|---------|
| `frontend-build` | `node:24-alpine` | `npm ci` + `npm run build` (includes OpenAPI type generation) |
| `backend-build` | `eclipse-temurin:25-jdk-alpine` | Maven build with frontend assets baked into `static/` |
| `runtime` | `eclipse-temurin:25-jre-alpine` | `java -jar app.jar`, exposes 8080, HEALTHCHECK configured |
The Dockerfile skips tests and static analysis during build (`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip` at `Dockerfile:17`), with the explicit design rationale that quality gates belong in CI (T-3).
The OpenAPI spec is copied into the frontend build stage (`Dockerfile:8-9`) because `npm run build` triggers `generate:api` as a pre-step.
#### .dockerignore
`.dockerignore:18-19` already excludes `.gitea/` from the Docker build context, anticipating this directory's creation.
#### Backend Build & Test
- **Build:** `./mvnw package` (or `./mvnw -B package` for batch mode)
- **Test:** `./mvnw test`
- **Full verify:** `./mvnw verify` (includes Checkstyle, SpotBugs, ArchUnit, JUnit 5)
- **Config:** `backend/pom.xml` — Spring Boot 3.5.11, Java 25
- **Quality gates:** Checkstyle (Google Style, `validate` phase), SpotBugs (`verify` phase), ArchUnit (9 rules, runs with JUnit), Surefire (fail-fast at 1 failure)
#### Frontend Build & Test
- **Build:** `npm run build` (runs `generate:api` + parallel type-check and vite build)
- **Test:** `npm run test:unit` (Vitest)
- **Lint:** `npm run lint` (oxlint + ESLint)
- **Type check:** `vue-tsc --build`
- **Config:** `frontend/package.json` — Vue 3.5, Vite 7.3, TypeScript 5.9
- **Node engines:** `^20.19.0 || >=22.12.0`
### Scheduling and Parallelism
Per `spec/implementation-phases.md:15`, T-3 is at priority level 4* — parallelizable with T-4 (Development infrastructure). It can be implemented at any time now since T-1 and T-2 are done.
From `review-findings.md:105-107`: T-3 should be completed before the first user story is finished, otherwise code will exist without a pipeline.
### Gitea Actions Specifics
Gitea Actions uses the same YAML format as GitHub Actions with minor differences:
- Workflow files go in `.gitea/workflows/` (not `.github/workflows/`)
- Uses `runs-on: ubuntu-latest` or custom runner labels
- Container registry URL pattern: `{gitea-host}/{owner}/{repo}` (no separate registry domain)
- Supports `docker/login-action`, `docker/build-push-action`, and direct `docker` CLI
- Secrets are configured in the Gitea repository settings (e.g., `secrets.GITHUB_TOKEN` equivalent is typically `secrets.GITEA_TOKEN` or the built-in `gitea.token`)
### Build Commands for the Pipeline
The pipeline needs to execute these commands in order:
| Step | Command | Runs in |
|------|---------|---------|
| Backend test | `cd backend && ./mvnw -B verify` | JDK 25 environment |
| Frontend install | `cd frontend && npm ci` | Node 24 environment |
| Frontend lint | `cd frontend && npm run lint` | Node 24 environment |
| Frontend type-check | `cd frontend && npm run type-check` | Node 24 environment (needs OpenAPI spec) |
| Frontend test | `cd frontend && npm run test:unit -- --run` | Node 24 environment |
| Docker build | `docker build -t {registry}/{owner}/{repo}:{tag} .` | Docker-capable runner |
| Docker push | `docker push {registry}/{owner}/{repo}:{tag}` | Docker-capable runner |
Note: `npm run build` is implicitly tested by the Docker build stage (the Dockerfile runs `npm run build` in stage 1). Running it separately in CI is redundant but could provide faster feedback.
The `--run` flag on Vitest ensures it runs once and exits (non-watch mode).
### Container Registry
The Gitea container registry is built into Gitea. Docker images are pushed using the Gitea instance hostname as the registry. Authentication uses a Gitea API token or the built-in `GITHUB_TOKEN`-equivalent that Gitea Actions provides.
Push format: `docker push {gitea-host}/{owner}/{repo}:{tag}`
### What Does NOT Exist Yet
- No `.gitea/` directory
- No `.github/` directory
- No workflow YAML files anywhere
- No CI/CD configuration of any kind
- No Makefile or build orchestration script
- No documentation about the Gitea instance URL or registry configuration
## Code References
- `spec/setup-tasks.md:41-55` — T-3 specification and acceptance criteria
- `spec/implementation-phases.md:15` — T-3 scheduling (parallel with T-4)
- `.ralph/review-findings/questions.md:12-22` — Q-5 resolution (Gitea confirmed)
- `Dockerfile:1-26` — Multi-stage Docker build
- `Dockerfile:17` — Tests skipped in Docker build (deferred to CI)
- `.dockerignore:18-19``.gitea/` already excluded from Docker context
- `backend/pom.xml:56-168` — Maven build plugins (Checkstyle, Surefire, SpotBugs, OpenAPI generator)
- `frontend/package.json:6-17` — npm build and test scripts
- `CLAUDE.md` — Build commands reference table
## Architecture Documentation
### Pipeline Architecture Pattern
The project follows a "test in CI, skip in Docker" pattern:
- The Dockerfile is a pure build artifact — it produces a runnable image as fast as possible
- All quality gates (tests, linting, static analysis) are expected to run in the CI pipeline before the Docker build
- The Docker image is only published if all preceding steps pass
### Gitea Actions Workflow Pattern (GitHub Actions compatible)
Gitea Actions workflows follow the same `on/jobs/steps` YAML structure as GitHub Actions. The runner is a self-hosted instance with Docker available on the host, but the pipeline uses Buildah for container image builds to avoid Docker-in-Docker complexity.
### Image Tagging Strategy (resolved)
SemVer with rolling tags, following standard Docker convention. When a tag like `2.3.9` is pushed, the pipeline publishes the **same image** under all four tags:
| Tag | Type | Example | Updated when |
|-----|------|---------|-------------|
| `2.3.9` | Immutable | Exact version | Only once, on this release |
| `2.3` | Rolling | Latest `2.3.x` | Overwritten by any `2.3.x` release |
| `2` | Rolling | Latest `2.x.x` | Overwritten by any `2.x.x` release |
| `latest` | Rolling | Newest release | Overwritten by every release |
This means the pipeline does four pushes per release (one per tag). Users can pin `2` to get automatic minor and patch updates, or pin `2.3.9` for exact reproducibility.
Images are only published on tagged releases (Git tags matching a SemVer pattern), not on every push.
### Release Process (resolved)
Manual Git tags trigger releases. The workflow is:
```bash
git tag 1.2.3
git push --tags
```
The pipeline triggers on tags matching a SemVer pattern (e.g., `1.2.3`) and publishes the image with rolling SemVer tags. No `v` prefix — tags are pure SemVer. No automated versioning tools (release-please, semantic-release) — the developer decides the version.
### Container Build Tool (resolved)
Buildah is used instead of Docker for building and pushing container images. This avoids Docker-in-Docker issues entirely and works cleanly on self-hosted runners regardless of whether the runner process runs inside a container or on the host.
### Pipeline Quality Scope (resolved)
The pipeline runs the maximum quality gates available:
- **Backend:** `./mvnw verify` (full lifecycle — Checkstyle, JUnit 5, ArchUnit, SpotBugs)
- **Frontend:** Lint (`npm run lint`), type-check (`npm run type-check`), tests (`npm run test:unit -- --run`)
### Gitea Registry Authentication (researched)
Key findings from Gitea documentation and community:
- `GITHUB_TOKEN` / `GITEA_TOKEN` (the built-in runner token) does **not** have permissions to push to the Gitea container registry. It only has repository read access.
- A **Personal Access Token** (PAT) with package write permissions must be created and stored as a repository secret (e.g., `REGISTRY_TOKEN`).
- The registry URL is the Gitea instance hostname (e.g., `gitea.example.com`). `${{ github.server_url }}` provides it with protocol prefix — needs stripping or use a repository variable.
- Push URL format: `{registry-host}/{owner}/{repo}:{tag}`
### Reference Workflow (existing project)
An existing Gitea Actions workflow in the sibling project `../arr/.gitea/workflows/deploy.yaml` provides a reference:
- Runner label: `ubuntu-latest`
- Uses `${{ vars.DEPLOY_PATH }}` for repository variables
- Simple deploy pattern (git pull + docker compose up)
## Resolved Questions
1. **Gitea instance URL:** Configured via repository variable or derived from `${{ github.server_url }}`. The registry hostname is the same as the Gitea instance.
2. **Runner label:** `ubuntu-latest` (consistent with the existing `arr` project workflow).
3. **Runner setup:** Self-hosted runner on the host with Docker available. Buildah used for image builds to avoid DinD.
4. **Container build tool:** Buildah (no DinD needed).
5. **Image tagging strategy:** SemVer with rolling tags (`latest`, `2`, `2.3`, `2.3.9`). Published only on tagged releases.
6. **Branch protection / publish trigger:** Images are only published from tagged releases, not from branch pushes. Every push triggers test + build (without publish).
7. **Maven lifecycle scope:** `./mvnw verify` (full lifecycle including SpotBugs). Frontend also runs all available quality gates (lint, type-check, tests).
8. **Registry authentication:** Personal Access Token stored as repository secret (built-in `GITHUB_TOKEN` lacks package write permissions).
## Open Questions
None — all questions resolved.

View File

@@ -1,359 +0,0 @@
---
date: 2026-03-04T19:37:59.203261+00:00
git_commit: cb0bcad145b03fec63be0ee3c1fca46ee545329e
branch: master
topic: "T-4: Development Infrastructure Setup"
tags: [research, codebase, t4, database, liquibase, testcontainers, router, test-infrastructure, docker-compose]
status: complete
---
# Research: T-4 Development Infrastructure Setup
## Research Question
What is the current state of the codebase relative to T-4's acceptance criteria? What already exists, what is missing, and what are the technical considerations for each criterion?
## Summary
T-4 is the final infrastructure task before user story implementation can begin. It bridges the gap between the existing project scaffolds (T-1, T-2, T-3, T-5 — all complete) and actual feature development with TDD. The task covers six areas: database migration framework, database connectivity, environment variable configuration, SPA router, backend test infrastructure, frontend test infrastructure, docker-compose documentation, and container verification with PostgreSQL.
The codebase already has partial coverage: Vue Router exists with placeholder routes, frontend test infrastructure (Vitest + @vue/test-utils) is operational, and backend test infrastructure (JUnit 5 + Spring Boot Test + MockMvc) is partially in place. What's missing: JPA/Liquibase, Testcontainers, environment variable wiring, and docker-compose documentation.
## Detailed Findings
### AC 1: Database Migration Framework (Liquibase)
**Current state:** Not present. No Liquibase or Liquibase dependency in `pom.xml`. No migration files anywhere in the project. The CLAUDE.md explicitly states "No JPA until T-4."
**What's needed:**
- Add `spring-boot-starter-data-jpa` dependency to `backend/pom.xml`
- Add `liquibase-core` dependency (Spring Boot manages the version)
- Create changelog directory at `backend/src/main/resources/db/changelog/`
- Create master changelog: `db.changelog-master.xml` that includes individual changesets
- Create first empty/baseline changeset to prove the tooling works
- Spring Boot auto-configures Liquibase when it's on the classpath and a datasource is available — no explicit `@Bean` config needed
**Spring Boot + Liquibase conventions:**
- Default changelog location: `classpath:db/changelog/db.changelog-master.xml`
- Format: XML (chosen for schema validation and explicitness)
- Changelogs are DB-agnostic — Liquibase generates dialect-specific SQL at runtime
- Spring Boot 3.5.x ships Liquibase via its dependency management
- Liquibase runs automatically on startup before JPA entity validation
**Architectural note:** The hexagonal architecture has an existing `adapter.out.persistence` package (currently empty, with `package-info.java`). JPA repositories and entity classes will go here. Domain model classes remain in `domain.model` without JPA annotations — the persistence adapter maps between them. The existing ArchUnit tests already enforce this boundary.
### AC 2: Database Connectivity via Environment Variables
**Current state:** `application.properties` has no datasource configuration. Only `spring.application.name=fete` and actuator settings.
**What's needed:**
- Configure Spring datasource properties to read from environment variables via profile-based separation
**Chosen approach: Profile-based separation with generic env vars.**
The properties are split across three files with clear responsibilities:
**`application.properties`** — environment-independent, always active:
```properties
spring.application.name=fete
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.open-in-view=false
management.endpoints.web.exposure.include=health
management.endpoint.health.show-details=never
```
**`application-prod.properties`** — committed, production profile, activated in Docker via `ENV SPRING_PROFILES_ACTIVE=prod`:
```properties
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}
```
**`application-local.properties`** — gitignored, developer creates from `.example` template:
```properties
spring.datasource.url=jdbc:postgresql://localhost:5432/fete
spring.datasource.username=fete
spring.datasource.password=fete
```
**`application-local.properties.example`** — committed as template, never directly used.
**Dockerfile:**
```dockerfile
ENV SPRING_PROFILES_ACTIVE=prod
```
Key points:
- No datasource defaults in `application.properties` — if neither profile nor env vars are set, the app fails to start (intentional: no silent fallback to a nonexistent DB)
- Generic env var names (`DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`) — the container user never sees Spring property names
- `ddl-auto=validate` ensures Hibernate validates entities against the Liquibase-managed schema but never modifies it
- `open-in-view=false` prevents the anti-pattern of lazy-loading in views (also avoids Spring Boot's startup warning)
- PostgreSQL JDBC driver (`org.postgresql:postgresql`) is needed — Spring Boot manages the version
- Tests use `@ServiceConnection` (Testcontainers) which auto-configures the datasource — no profile or env vars needed for tests
### AC 3: All Runtime Configuration via Environment Variables
**Current state:** No environment-variable-driven configuration exists beyond Spring Boot defaults.
**What's needed beyond database:**
- Unsplash API key: optional, used by US-16
- Max active events: optional, used by US-13
**Implementation pattern:** `@ConfigurationProperties(prefix = "fete")` class (`FeteProperties`) in `de.fete.config`. Type-safe, validatable, testable.
These properties also go in `application-prod.properties` with generic env var mapping:
```properties
fete.unsplash.api-key=${UNSPLASH_API_KEY:}
fete.max-active-events=${MAX_ACTIVE_EVENTS:0}
```
Empty `UNSPLASH_API_KEY` = feature disabled. `MAX_ACTIVE_EVENTS=0` = unlimited.
**Note:** These properties are only scaffolded in T-4 (the `FeteProperties` class with fields and defaults). The business logic using them comes with US-13/US-16.
### AC 4: SPA Router Configuration
**Current state:** Vue Router IS configured and operational.
**File:** `frontend/src/router/index.ts`
- Uses `createWebHistory` (HTML5 History API — clean URLs, no hash)
- Two routes defined: `/` (HomeView, eager) and `/about` (AboutView, lazy-loaded)
- Router is registered in `main.ts` via `app.use(router)`
**Backend SPA support:** Already implemented in `WebConfig.java`:
- `PathResourceResolver` falls back to `index.html` for any path not matching a static file
- This enables client-side routing — the backend serves `index.html` for all non-API, non-static paths
**Assessment:** This AC is effectively already met. The router exists, uses history mode, and the backend supports it. What will change during user stories: routes will be added (e.g., `/event/:token`, `/event/:token/edit`), but the infrastructure is in place.
### AC 5: Backend Test Infrastructure
**Current state:** Partially in place.
**What exists:**
- JUnit 5 (via `spring-boot-starter-test`) — operational
- Spring Boot Test with `@SpringBootTest` — operational
- MockMvc for REST endpoint testing — operational (`FeteApplicationTest.java`, `WebConfigTest.java`)
- ArchUnit for architecture validation — operational (`HexagonalArchitectureTest.java`)
- Surefire configured with fail-fast (`skipAfterFailureCount=1`)
- Test logging configured (`logback-test.xml` at WARN level)
**What's missing:**
- **Testcontainers** — not in `pom.xml`, no test configuration for it
- **Integration test support with real PostgreSQL** — currently no database tests exist (because no database exists yet)
**What's needed:**
- Add `org.testcontainers:postgresql` dependency (test scope)
- Add `org.testcontainers:junit-jupiter` dependency (test scope) — JUnit 5 integration
- Add `spring-boot-testcontainers` dependency (test scope) — Spring Boot 3.1+ Testcontainers integration
- Create a test configuration class or use `@ServiceConnection` annotation (Spring Boot 3.1+) for automatic datasource wiring in tests
**Spring Boot 3.5 + Testcontainers pattern (TestApplication):**
A `TestFeteApplication.java` in `src/test/` registers Testcontainers beans. All `@SpringBootTest` tests automatically get a PostgreSQL instance — no per-test wiring needed. Existing tests (`FeteApplicationTest`, `WebConfigTest`) continue to work without modification.
```java
// src/test/java/de/fete/TestFeteApplication.java
@TestConfiguration(proxyBeanMethods = false)
public class TestcontainersConfig {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:17-alpine");
}
}
```
With `@ServiceConnection`, Spring Boot auto-configures the datasource to point at the Testcontainers-managed PostgreSQL — no manual URL/username/password wiring needed. Testcontainers starts one shared container per test suite run, not per test class.
**Important:** Once JPA is on the classpath, every `@SpringBootTest` needs a datasource. The TestApplication pattern ensures this globally. Without it, all existing `@SpringBootTest` tests would break immediately.
**Test categories after T-4:**
- Unit tests: no Spring context, no database — fast, test domain logic
- Integration tests: `@SpringBootTest` + Testcontainers — test full stack including database
- Architecture tests: ArchUnit — already in place
### AC 6: Frontend Test Infrastructure
**Current state:** Already in place and operational.
**What exists:**
- Vitest configured (`vitest.config.ts`): jsdom environment, bail=1, e2e excluded
- `@vue/test-utils` v2.4.6 installed — Vue component mounting and assertions
- TypeScript test config (`tsconfig.vitest.json`) with jsdom types
- Sample test exists: `components/__tests__/HelloWorld.spec.ts` — mounts component, asserts text
- Test command: `npm run test:unit` (runs Vitest in watch mode) / `npm run test:unit -- --run` (single run)
**Assessment:** This AC is already met. The test infrastructure is functional with a passing sample test.
### AC 7: Both Test Suites Executable
**Current state:** Both work.
- Backend: `cd backend && ./mvnw test` — runs JUnit 5 tests (3 tests in 3 classes)
- Frontend: `cd frontend && npm run test:unit -- --run` — runs Vitest (1 test in 1 file)
- CI pipeline (`ci.yaml`) already runs both in parallel
**Assessment:** Already met. Will remain met after adding Testcontainers (new tests use the same `./mvnw test` command).
### AC 8: README Docker-Compose Documentation
**Current state:** No docker-compose file or documentation exists. The README covers development setup and code quality but has no deployment section.
**What's needed:**
- A `docker-compose.yml` example (either in-repo or documented in README)
- Must include: app service (the fete container) + postgres service
- Must document required environment variables: `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD`
- Must document optional environment variables: `UNSPLASH_API_KEY`, `MAX_ACTIVE_EVENTS`
- Per CLAUDE.md: "A docker-compose example in the README is sufficient" — no separate file in repo
**Example structure:**
```yaml
services:
db:
image: postgres:17-alpine
environment:
POSTGRES_DB: fete
POSTGRES_USER: fete
POSTGRES_PASSWORD: changeme
volumes:
- fete-db:/var/lib/postgresql/data
app:
image: gitea.example.com/user/fete:latest
ports:
- "8080:8080"
environment:
DATABASE_URL: jdbc:postgresql://db:5432/fete
DATABASE_USERNAME: fete
DATABASE_PASSWORD: changeme
# MAX_ACTIVE_EVENTS: 100 # optional
# UNSPLASH_API_KEY: abc123 # optional
depends_on:
db:
condition: service_healthy
volumes:
fete-db:
```
### AC 9: Container Health Check with PostgreSQL
**Current state:** The Dockerfile has a HEALTHCHECK directive that queries `/actuator/health`. Currently the app starts without a database and the health check passes.
**After T-4:** With JPA and Liquibase on the classpath, Spring Boot will:
- Fail to start if no database is reachable (datasource auto-configuration fails)
- Include database health in `/actuator/health` automatically (via `DataSourceHealthIndicator`)
- Run Liquibase migrations on startup — if migrations fail, the app won't start
**What's needed for verification:**
- Start the app with docker-compose (app + postgres)
- Verify `/actuator/health` returns `{"status":"UP"}` (which now includes DB connectivity)
- Verify Liquibase ran the baseline migration (check `flyway_schema_history` table or app logs)
## Code References
### Existing Files (will be modified)
- `backend/pom.xml:1-170` — Add JPA, Liquibase, PostgreSQL driver, Testcontainers dependencies
- `backend/src/main/resources/application.properties:1-4` — Add datasource, JPA, Liquibase, app-specific config
- `README.md:1-134` — Add deployment section with docker-compose example
### Existing Files (relevant context, likely untouched)
- `backend/src/main/java/de/fete/config/WebConfig.java:1-40` — SPA routing already configured
- `backend/src/main/java/de/fete/FeteApplication.java` — Entry point, no changes needed
- `frontend/src/router/index.ts:1-23` — Router already configured
- `frontend/vitest.config.ts:1-15` — Test infra already configured
- `frontend/package.json:1-52` — Test dependencies already present
- `.gitea/workflows/ci.yaml` — CI pipeline, may need Testcontainers Docker access for backend tests
### New Files (to be created)
- `backend/src/main/resources/db/changelog/db.changelog-master.xml` — Liquibase master changelog
- `backend/src/main/resources/application-prod.properties` — Production profile with env var placeholders
- `backend/src/main/resources/application-local.properties.example` — Template for local development
- `backend/src/test/java/de/fete/TestFeteApplication.java` (or similar) — Testcontainers PostgreSQL bean via TestApplication pattern
- `de/fete/config/FeteProperties.java``@ConfigurationProperties` class for app-specific settings
- README deployment section — docker-compose example inline (no standalone file)
- `backend/src/main/resources/application-prod.properties` — Production profile with env var placeholders
- `backend/src/main/resources/application-local.properties.example` — Template for local development
### Package Structure (existing, will gain content)
- `de.fete.adapter.out.persistence` — JPA entities and Spring Data repositories (empty now)
- `de.fete.domain.model` — Domain entities (empty now, no JPA annotations here)
- `de.fete.config` — App configuration (WebConfig exists, may add `@ConfigurationProperties` class)
## Architecture Documentation
### Hexagonal Architecture and JPA
The existing ArchUnit tests (`HexagonalArchitectureTest.java`) enforce:
- Domain layer must not depend on Spring, adapters, application, or config
- Ports (in/out) must be interfaces
- Web adapter and persistence adapter must not cross-depend
This means JPA integration must follow the pattern:
1. Domain entities in `domain.model` — plain Java, no JPA annotations
2. JPA entities in `adapter.out.persistence` — annotated with `@Entity`, `@Table`, etc.
3. Mapping between domain and JPA entities in the persistence adapter
4. Repository interfaces (Spring Data) in `adapter.out.persistence`
5. Port interfaces in `domain.port.out` — define what the domain needs from persistence
6. Service implementations in `application.service` — use port interfaces, not repositories directly
This is a well-established hexagonal pattern. The ArchUnit tests will automatically validate any new code follows these boundaries.
### Test Architecture After T-4
```
Test Type | Context | Database | Speed | Purpose
-------------------|---------------|-----------------|---------|---------------------------
Unit tests | None | None | Fast | Domain logic, services
Integration tests | SpringBoot | Testcontainers | Medium | Full stack, DB queries
Architecture tests | None (static) | None | Fast | Structural validation
```
All test types run via `./mvnw test`. Testcontainers starts/stops PostgreSQL containers automatically — no external setup needed. The CI pipeline already has Docker available (runs on `ubuntu-latest` with Docker socket).
### CI Pipeline Considerations
The current CI pipeline runs `./mvnw -B verify` for backend tests. Testcontainers requires Docker socket access. On Gitea Actions with `ubuntu-latest` runners, Docker is typically available. If the runner uses a Docker-in-Docker setup, the Testcontainers `DOCKER_HOST` environment variable may need configuration — but this is a runtime concern, not a code concern.
### Spring Boot Profiles
Currently no profiles are configured. For T-4, a `test` profile may be useful to separate test-specific configuration (e.g., Testcontainers datasource) from production defaults. Spring Boot's `@ActiveProfiles("test")` on test classes or `application-test.properties` can handle this. However, with `@ServiceConnection`, Testcontainers auto-configures the datasource without profile-specific properties.
## Acceptance Criteria Status Matrix
| # | Criterion | Current Status | Work Required |
|---|-----------|----------------|---------------|
| 1 | Liquibase configured with first changelog | Not started | Add `liquibase-core`, create changelog dir and master XML |
| 2 | External PostgreSQL via env var | Not started | Add datasource properties with env var placeholders |
| 3 | All runtime config via env vars | Not started | Add datasource + app-specific properties |
| 4 | SPA router configured | **Done** | Vue Router with history mode already works |
| 5 | Backend test infra (Testcontainers) | Partial | JUnit 5 + MockMvc exist; add Testcontainers |
| 6 | Frontend test infra | **Done** | Vitest + @vue/test-utils operational |
| 7 | Both test suites executable | **Done** | Both `./mvnw test` and `npm run test:unit` work |
| 8 | README docker-compose documentation | Not started | Add deployment section with example |
| 9 | Container health with PostgreSQL | Not started | Verify after JPA/Liquibase are added |
## Resolved Decisions
1. **Liquibase** for database migrations, **XML** format. DB-agnostic changelogs — Liquibase generates dialect-specific SQL at runtime. XML chosen over YAML for schema validation and explicitness. The project intentionally avoids PostgreSQL-specific features in migrations to keep the database layer portable.
2. **Profile-based properties separation** with generic environment variable names. Three files: `application.properties` (environment-independent, always active), `application-prod.properties` (committed, maps `${DATABASE_URL}` etc. to Spring properties, activated in Docker via `ENV SPRING_PROFILES_ACTIVE=prod`), `application-local.properties` (gitignored, concrete local values, activated via `-Dspring-boot.run.profiles=local`). A committed `.example` template guides developers. The container user sets `DATABASE_URL`, `DATABASE_USERNAME`, `DATABASE_PASSWORD` — never sees Spring internals.
3. **`@ConfigurationProperties`** for app-specific settings (`FeteProperties` class). Type-safe, validatable, testable. Properties: `fete.unsplash.api-key` (from `UNSPLASH_API_KEY`) and `fete.max-active-events` (from `MAX_ACTIVE_EVENTS`). Both are only scaffolded in T-4; business logic using them comes with US-13/US-16.
4. **docker-compose example in README only** — no standalone `docker-compose.yml` in the repo. Per CLAUDE.md: "A docker-compose example in the README is sufficient." A local docker-compose for development may be added later separately.
5. **TestApplication pattern** for Testcontainers integration. A `TestFeteApplication.java` in `src/test/` registers a `@ServiceConnection` PostgreSQL container. All `@SpringBootTest` tests automatically get a database — existing tests continue to work without modification.
6. **README erweitern** with local development setup documentation (how to copy `application-local.properties.example`, start with profile, PostgreSQL prerequisites).
## Open Questions
1. **Testcontainers in CI:** The Gitea Actions runner needs Docker available for Testcontainers. This works out-of-the-box on `ubuntu-latest` but should be verified after implementation.
2. **Baseline changelog content:** The first Liquibase changeset should be a minimal, empty changeset that proves the tooling works. No schema needed yet — US-1 will create the first real table.

View File

@@ -1,195 +0,0 @@
---
date: 2026-03-04T21:04:31+00:00
git_commit: 747ed189456d2328147051bb8e7b3bbb43f47ea6
branch: master
topic: "US-1: Create an Event — Codebase Research"
tags: [research, codebase, us-1, event-creation, hexagonal-architecture]
status: complete
---
# Research: US-1 — Create an Event
## Research Question
What is the current state of the codebase relevant to implementing US-1 (Create an event)? What exists, what infrastructure is in place, and what needs to be built?
## Summary
US-1 is the first user story to be implemented. All setup tasks (T-1 through T-5) are complete. The codebase provides a hexagonal architecture skeleton with ArchUnit enforcement, an API-first workflow (OpenAPI spec → generated interfaces + TypeScript types), Liquibase migration tooling with an empty baseline, Testcontainers for integration tests, and a Vue 3 SPA frontend with typed API client. No domain models, use cases, persistence adapters, or controllers exist yet — the entire business logic layer is empty and waiting for US-1.
## US-1 Acceptance Criteria (from spec/userstories.md:21-40)
- [ ] Organizer fills in: title (required), description (optional), date/time (required), location (optional), expiry date (required)
- [ ] Server stores event, returns event token (UUID) + organizer token (UUID) in creation response
- [ ] Organizer redirected to event page after creation
- [ ] Organizer token stored in localStorage for organizer access on this device
- [ ] Event token, title, date stored in localStorage for local overview (US-7)
- [ ] No account, login, or personal data required
- [ ] Expiry date is mandatory, cannot be left blank
- [ ] Event not discoverable except via direct link
Dependencies: T-4 (complete).
## Detailed Findings
### 1. Backend Architecture Skeleton
The hexagonal architecture is fully scaffolded but empty. All business-logic packages contain only `package-info.java` documentation files:
| Package | Location | Status |
|---------|----------|--------|
| `de.fete.domain.model` | `backend/src/main/java/de/fete/domain/model/` | Empty — domain entities go here |
| `de.fete.domain.port.in` | `backend/src/main/java/de/fete/domain/port/in/` | Empty — use case interfaces go here |
| `de.fete.domain.port.out` | `backend/src/main/java/de/fete/domain/port/out/` | Empty — repository ports go here |
| `de.fete.application.service` | `backend/src/main/java/de/fete/application/service/` | Empty — use case implementations go here |
| `de.fete.adapter.in.web` | `backend/src/main/java/de/fete/adapter/in/web/` | Empty hand-written code — generated HealthApi interface exists in target/ |
| `de.fete.adapter.out.persistence` | `backend/src/main/java/de/fete/adapter/out/persistence/` | Empty — JPA entities + Spring Data repos go here |
Architecture constraints are enforced by ArchUnit (`HexagonalArchitectureTest.java:1-63`):
- Domain layer must not depend on adapters, application, config, or Spring
- Inbound and outbound ports must be interfaces
- Web adapter and persistence adapter must not depend on each other
- Onion architecture layers validated via `onionArchitecture()` rule
### 2. OpenAPI Spec — Current State and Extension Point
The OpenAPI spec at `backend/src/main/resources/openapi/api.yaml:1-38` currently defines only the health check endpoint. US-1 requires adding:
- **New path:** `POST /events` — create event endpoint
- **New schemas:** Request body (title, description, dateTime, location, expiryDate) and response (eventToken, organizerToken)
- **Error responses:** RFC 9457 Problem Details format (see `docs/agents/research/2026-03-04-rfc9457-problem-details.md`)
- **Server base:** Already set to `/api` (line 11), matching `WebConfig.java:19`
Generated code lands in `target/generated-sources/openapi/`:
- Interfaces: `de.fete.adapter.in.web.api` — controller must implement generated interface
- Models: `de.fete.adapter.in.web.model` — request/response DTOs
Frontend types are generated via `npm run generate:api` into `frontend/src/api/schema.d.ts`.
### 3. Web Configuration
`WebConfig.java:1-41` configures two things relevant to US-1:
1. **API prefix** (line 19): All `@RestController` beans are prefixed with `/api`. So the OpenAPI path `/events` becomes `/api/events` at runtime.
2. **SPA fallback** (lines 23-39): Any non-API, non-static-asset request falls through to `index.html`. This means Vue Router handles client-side routes like `/events/:token`.
### 4. Database Infrastructure
**Liquibase** is configured in `application.properties:8`:
```
spring.liquibase.change-log=classpath:db/changelog/db.changelog-master.xml
```
The master changelog (`db.changelog-master.xml:1-10`) includes a single empty baseline (`000-baseline.xml:1-13`). US-1 needs a new migration file (e.g. `001-create-event-table.xml`) added to the master changelog.
**JPA** is configured with `ddl-auto=validate` (`application.properties:4`), meaning Hibernate validates entity mappings against the schema but never auto-creates tables. Liquibase is the sole schema management tool.
**PostgreSQL** connection is externalized via environment variables in `application-prod.properties:1-4`:
```
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}
```
### 5. Test Infrastructure
**Backend:**
- JUnit 5 + Spring Boot Test + MockMvc (see `FeteApplicationTest.java`)
- Testcontainers PostgreSQL (`TestcontainersConfig.java:1-17`) — real database for integration tests
- ArchUnit for architecture validation
- Checkstyle (Google Checks) and SpotBugs configured as build plugins
**Frontend:**
- Vitest with jsdom environment (`vitest.config.ts`)
- `@vue/test-utils` for component testing
- Single placeholder test exists (`HelloWorld.spec.ts`)
- Test pattern: `src/**/__tests__/*.spec.ts`
### 6. Frontend — Router, API Client, and localStorage
**Router** (`frontend/src/router/index.ts:1-23`): Currently has two placeholder routes (`/` and `/about`). US-1 needs:
- A route for the event creation form (e.g. `/create`)
- A route for the event page (e.g. `/events/:token`) — needed for post-creation redirect
**API client** (`frontend/src/api/client.ts:1-4`): Singleton `openapi-fetch` client typed against generated schema. Base URL `/api`. Ready for use — just needs the new endpoints in the generated types.
**localStorage:** No utilities exist yet. The `composables/` directory contains only `.gitkeep`. US-1 needs:
- A composable or utility for storing/retrieving organizer tokens per event
- Storage of event token, title, and date for the local overview (US-7)
**Components:** Only Vue/Vite scaffold defaults (HelloWorld, TheWelcome, icons). All need to be replaced with the actual event creation form.
### 7. Token Model
The spec defines three token types (`userstories.md:12-18`):
- **Event token**: Public UUID v4 in the event URL. Used by guests to access event pages.
- **Organizer token**: Secret UUID v4 stored in localStorage. Used to authenticate organizer actions.
- **Internal DB ID**: Never exposed — implementation detail only.
UUID v4 (random) is used for both tokens. KISS — no time-ordering (v7) needed for this use case. Generated server-side via `java.util.UUID.randomUUID()`.
### 8. Cross-Cutting Concerns
- **Date/time handling:** See `docs/agents/research/2026-03-04-datetime-best-practices.md` for the full stack-wide type mapping. Event dateTime → `OffsetDateTime` / `timestamptz`. Expiry date → `LocalDate` / `date`.
- **Error responses:** RFC 9457 Problem Details format. See `docs/agents/research/2026-03-04-rfc9457-problem-details.md`.
- **Honeypot fields:** Removed from scope — overengineered for this project.
## Code References
- `spec/userstories.md:21-40` — US-1 full specification
- `spec/implementation-phases.md:7` — US-1 is first in implementation order
- `backend/src/main/resources/openapi/api.yaml:1-38` — OpenAPI spec (extension point)
- `backend/src/main/java/de/fete/config/WebConfig.java:19` — API prefix `/api`
- `backend/src/main/java/de/fete/config/WebConfig.java:23-39` — SPA fallback routing
- `backend/src/main/resources/application.properties:4` — JPA ddl-auto=validate
- `backend/src/main/resources/application.properties:8` — Liquibase changelog config
- `backend/src/main/resources/db/changelog/db.changelog-master.xml:8` — Single include, extend here
- `backend/src/main/resources/db/changelog/000-baseline.xml:8-10` — Empty baseline changeset
- `backend/src/main/resources/application-prod.properties:1-4` — DB env vars
- `backend/src/test/java/de/fete/HexagonalArchitectureTest.java:1-63` — Architecture constraints
- `backend/src/test/java/de/fete/TestcontainersConfig.java:1-17` — Test DB container
- `frontend/src/router/index.ts:1-23` — Vue Router (extend with event routes)
- `frontend/src/api/client.ts:1-4` — API client (ready to use with generated types)
- `frontend/src/composables/.gitkeep` — Empty composables directory
## Architecture Documentation
### Hexagonal Layer Mapping for US-1
| Layer | Package | US-1 Artifacts |
|-------|---------|----------------|
| **Domain Model** | `de.fete.domain.model` | `Event` entity (title, description, dateTime, location, expiryDate, eventToken, organizerToken, createdAt) |
| **Inbound Port** | `de.fete.domain.port.in` | `CreateEventUseCase` interface |
| **Outbound Port** | `de.fete.domain.port.out` | `EventRepository` interface (save, findByToken) |
| **Application Service** | `de.fete.application.service` | `EventService` implementing `CreateEventUseCase` |
| **Web Adapter** | `de.fete.adapter.in.web` | Controller implementing generated `EventsApi` interface |
| **Persistence Adapter** | `de.fete.adapter.out.persistence` | JPA entity + Spring Data repository implementing `EventRepository` port |
| **Config** | `de.fete.config` | (existing WebConfig sufficient) |
### API-First Flow
```
api.yaml (edit) → mvn compile → HealthApi.java + EventsApi.java (generated)
HealthResponse.java + CreateEventRequest.java + CreateEventResponse.java (generated)
→ npm run generate:api → schema.d.ts (generated TypeScript types)
```
The hand-written controller in `adapter.in.web` implements the generated interface. The frontend uses the generated types via `openapi-fetch`.
### Database Schema Required
US-1 needs a single `events` table with columns mapping to the domain model. The migration file goes into `db/changelog/` and must be included in `db.changelog-master.xml`.
### Frontend Data Flow
```
EventCreateForm.vue → api.post('/events', body) → backend
← { eventToken, organizerToken }
→ localStorage.setItem (organizer token, event meta)
→ router.push(`/events/${eventToken}`)
```
## Resolved Questions
- **Expiry date validation at creation:** Yes — the server enforces that the expiry date is in the future at creation time, not only at edit time (US-5). Rationale: an event should never exist in an invalid state. If it's never edited, a past expiry date would be nonsensical. This extends US-1 AC7 beyond "mandatory" to "mandatory and in the future".
- **Event page after creation:** Option A — create a minimal stub route (`/events/:token`) with a placeholder view (e.g. "Event created" confirmation). The full event page is built in US-2. This keeps story boundaries clean while satisfying US-1 AC3 (redirect after creation).

View File

@@ -1,273 +0,0 @@
---
date: 2026-03-05T10:14:52+00:00
git_commit: ffea279b54ad84be09bd0e82b3ed9c89a95fc606
branch: master
topic: "End-to-End Testing for Vue 3 with Playwright"
tags: [research, e2e, playwright, testing, frontend]
status: complete
---
# Research: End-to-End Testing for Vue 3 with Playwright
## Research Question
How to set up and structure end-to-end tests for the fete Vue 3 + Vite frontend using Playwright?
## Summary
Playwright is Vue 3's officially recommended E2E testing framework. It integrates with Vite projects through a `webServer` config block (no Vite plugin needed), supports Chromium/Firefox/WebKit under a single API, and is fully free including parallelism. The fete project's existing vitest.config.ts already excludes `e2e/**`, making the integration path clean.
## Detailed Findings
### 1. Current Frontend Test Infrastructure
The project uses **Vitest 4.0.18** with jsdom for unit/component tests:
- **Config:** `frontend/vitest.config.ts` — merges with vite.config, uses jsdom environment, bail on first failure
- **Exclusion:** Already excludes `e2e/**` from Vitest's test discovery (`vitest.config.ts:10`)
- **Existing tests:** 3 test files with ~25 tests total:
- `src/composables/__tests__/useEventStorage.spec.ts` (6 tests)
- `src/views/__tests__/EventCreateView.spec.ts` (11 tests)
- `src/views/__tests__/EventStubView.spec.ts` (8 tests)
- **No E2E framework** is currently configured
### 2. Why Playwright
Vue's official testing guide ([vuejs.org/guide/scaling-up/testing](https://vuejs.org/guide/scaling-up/testing)) positions Playwright as the primary E2E recommendation. Key advantages over Cypress:
| Dimension | Playwright | Cypress |
|---|---|---|
| Browser support | Chromium, Firefox, WebKit | Chrome-family, Firefox (WebKit experimental) |
| Parallelism | Free, native | Requires paid Cypress Cloud |
| Architecture | Out-of-process (CDP/BiDi) | In-browser (same process) |
| Speed | 35-45% faster in parallel | Slower at scale |
| Pricing | 100% free, Apache 2.0 | Cloud features cost money |
| Privacy | No account, no cloud dependency | Cloud service integration |
Playwright aligns with fete's privacy constraints (no cloud dependency, no account required).
### 3. Playwright + Vite Integration
Playwright does **not** use a Vite plugin. Integration is purely through process management:
1. Playwright reads `webServer.command` and spawns the Vite dev server
2. Polls `webServer.url` until ready
3. Runs tests against `use.baseURL`
4. Kills the server after all tests finish
The existing Vite dev proxy (`/api``localhost:8080`) works transparently — E2E tests can hit the real backend or intercept via `page.route()` mocks.
Note: `@playwright/experimental-ct-vue` exists for component-level testing (mounting individual Vue components without a server), but is still experimental and is a different category from E2E.
### 4. Installation
```bash
cd frontend
npm install --save-dev @playwright/test
npx playwright install --with-deps chromium
```
Using `npm init playwright@latest` generates scaffolding automatically, but for an existing project manual setup is cleaner.
### 5. Project Structure
```
frontend/
playwright.config.ts # Playwright config
e2e/ # E2E test directory
home.spec.ts
event-create.spec.ts
event-view.spec.ts
fixtures/ # shared test fixtures (optional)
helpers/ # page object models (optional)
playwright-report/ # generated HTML report (gitignored)
test-results/ # generated artifacts (gitignored)
```
The `e2e/` directory is already excluded from Vitest via `vitest.config.ts:10`.
### 6. Recommended playwright.config.ts
```typescript
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: process.env.CI ? 'github' : 'html',
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// Uncomment for cross-browser coverage:
// { name: 'firefox', use: { ...devices['Desktop Firefox'] } },
// { name: 'webkit', use: { ...devices['Desktop Safari'] } },
],
webServer: {
command: 'npm run dev',
url: 'http://localhost:5173',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
stdout: 'pipe',
},
})
```
Key decisions:
- `testDir: './e2e'` — separates E2E from Vitest unit tests
- `forbidOnly: !!process.env.CI` — prevents `test.only` from shipping to CI
- `workers: process.env.CI ? 1 : undefined` — single worker in CI avoids shared-state flakiness; locally uses all cores
- `reporter: 'github'` — GitHub Actions annotations in CI
- `command: 'npm run dev'` — runs `generate:api` first (via the existing npm script), then starts Vite
- `reuseExistingServer: !process.env.CI` — reuses running dev server locally for fast iteration
### 7. package.json Scripts
```json
"test:e2e": "playwright test",
"test:e2e:ui": "playwright test --ui",
"test:e2e:debug": "playwright test --debug"
```
### 8. .gitignore Additions
```
playwright-report/
test-results/
```
### 9. TypeScript Configuration
The existing `tsconfig.app.json` excludes `src/**/__tests__/*`. Since E2E tests live in `e2e/` (outside `src/`), they are already excluded from the app build.
A separate `tsconfig` for E2E tests is not strictly required — Playwright's own TypeScript support handles it. If needed, a minimal `e2e/tsconfig.json` can extend `tsconfig.node.json`.
### 10. Vue-Specific Testing Patterns
**Router navigation:**
```typescript
await page.goto('/events/abc-123')
await page.waitForURL('/events/abc-123') // confirms SPA router resolved
```
**Waiting for reactive content (auto-retry):**
```typescript
await expect(page.getByRole('heading', { name: 'My Event' })).toBeVisible()
// Playwright auto-retries assertions for up to the configured timeout
```
**URL assertions:**
```typescript
await expect(page).toHaveURL(/\/events\/.+/)
```
**API mocking (for isolated E2E tests):**
```typescript
await page.route('/api/events/**', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ title: 'Test Event', date: '2026-04-01' }),
})
})
```
**Locator strategy — prefer accessible locators:**
```typescript
page.getByRole('button', { name: 'RSVP' }) // best
page.getByLabel('Event Title') // form fields
page.getByTestId('event-card') // data-testid fallback
page.locator('.some-class') // last resort
```
### 11. CI Integration
**GitHub Actions workflow:**
```yaml
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
# --with-deps installs OS-level libraries (libglib, libnss, etc.)
# Specify 'chromium' to save ~2min vs installing all browsers
- name: Run E2E tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: frontend/playwright-report/
retention-days: 30
```
**Docker:** Use official images `mcr.microsoft.com/playwright:v1.x.x-noble` (Ubuntu 24.04). Alpine is unsupported (browsers need glibc). Key flag: `--ipc=host` prevents Chromium memory exhaustion. The Playwright Docker image version must match the `@playwright/test` package version exactly.
For the fete project, E2E tests run as a separate CI step, not inside the app's Dockerfile.
### 12. Integration with Existing Backend
Two approaches for E2E tests:
1. **Mocked backend** (via `page.route()`): Fast, isolated, no backend dependency. Good for frontend-only testing.
2. **Real backend**: Start Spring Boot alongside Vite. Tests hit `/api` through the Vite proxy. More realistic but requires Java in CI. Could use Docker Compose.
The Vite proxy config (`vite.config.ts:19-23`) already forwards `/api` to `localhost:8080`, so both approaches work without changes.
## Code References
- `frontend/vitest.config.ts:10` — E2E exclusion pattern already in place
- `frontend/vite.config.ts:19-23` — API proxy configuration for backend integration
- `frontend/package.json:8-9``dev` script runs `generate:api` before Vite
- `frontend/src/router/index.ts` — Route definitions (Home, Create, Event views)
- `frontend/src/api/client.ts` — openapi-fetch client using `/api` base URL
- `frontend/tsconfig.app.json` — App TypeScript config (excludes test files)
## Architecture Documentation
### Test Pyramid in fete
| Layer | Framework | Directory | Purpose |
|---|---|---|---|
| Unit | Vitest + jsdom | `src/**/__tests__/` | Composables, isolated logic |
| Component | Vitest + @vue/test-utils | `src/**/__tests__/` | Vue component behavior |
| E2E | Playwright (proposed) | `e2e/` | Full browser, user flows |
| Visual | browser-interactive-testing skill | `.agent-tests/` | Agent-driven screenshots |
### Decision Points for Implementation
1. **Start with Chromium only** — add Firefox/WebKit later if needed
2. **Use `npm run dev`** as webServer command (includes API type generation)
3. **API mocking by default** — use `page.route()` for E2E isolation; full-stack tests as a separate concern
4. **`data-testid` attributes** on key interactive elements for stable selectors
5. **Page Object Model** recommended once the test suite grows beyond 5-10 tests
## Sources
- [Testing | Vue.js](https://vuejs.org/guide/scaling-up/testing) — official E2E recommendation
- [Installation | Playwright](https://playwright.dev/docs/intro)
- [webServer | Playwright](https://playwright.dev/docs/test-webserver) — Vite integration
- [CI Intro | Playwright](https://playwright.dev/docs/ci-intro)
- [Docker | Playwright](https://playwright.dev/docs/docker)
- [Cypress vs Playwright 2026 | BugBug](https://bugbug.io/blog/test-automation-tools/cypress-vs-playwright/)
- [Playwright vs Cypress | Katalon](https://katalon.com/resources-center/blog/playwright-vs-cypress)
## Decisions (2026-03-05)
- **Mocked backend only** — E2E tests use `page.route()` to mock API responses. No real Spring Boot backend in E2E.
- **Mocking stack:** `@msw/playwright` + `@msw/source` — reads OpenAPI spec at runtime, generates MSW handlers, per-test overrides via `network.use()`.
- **US-1 flows first** — Event creation is the only implemented user story; E2E tests cover that flow.
- **No CI caching yet** — Playwright browser binaries are not cached; CI runner needs reconfiguration first.
- **E2E tests are part of frontend tasks** — every frontend user story includes E2E test coverage going forward.
- **OpenAPI examples mandatory** — all response schemas in the OpenAPI spec must include `example:` fields (required for `@msw/source` mock generation).