10 Commits

Author SHA1 Message Date
c2bbb78b7b Add OpenAPI spec validation hook (Redocly CLI)
All checks were successful
CI / backend-test (push) Successful in 50s
CI / frontend-test (push) Successful in 17s
CI / build-and-publish (push) Has been skipped
PostToolUse hook triggers on openapi/*.yaml edits and runs
redocly lint with the recommended ruleset (security-defined
disabled since endpoints are intentionally public).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 00:01:17 +01:00
91e566efea Remove honeypot fields from US-1 and US-3
Honeypot spam protection is overengineered for this project's scope.
Removed the acceptance criteria from both stories and added addenda
documenting the decision. Updated implementation order and review
findings accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:24:23 +01:00
b8421274b4 Add API-first development methodology to project statutes
The OpenAPI spec is the single source of truth for the REST API
contract. Endpoints and schemas must be defined in the spec before
writing implementation code.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:08:19 +01:00
747ed18945 Replace implementation phases with sequential implementation order
Rework the implementation roadmap from parallelizable phases to a strict
sequential order optimized for earliest usable increments. Key changes:
- All 20 user stories in a single sequential queue (no parallelization)
- Progress tracker at the top for status tracking
- US-17 (dark mode) moved from Phase 5 to Increment 4 (before US-15)
- US-14 (PWA) moved to last position
- Deferred AC activation schedule added

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 22:02:13 +01:00
e9110ea143 Add recommended preset to Renovate config
All checks were successful
CI / backend-test (push) Successful in 49s
CI / frontend-test (push) Successful in 18s
CI / build-and-publish (push) Has been skipped
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:43:11 +01:00
bd84f4e355 Merge pull request 'Configure Renovate' (#1) from renovate/configure into master
All checks were successful
CI / backend-test (push) Successful in 51s
CI / frontend-test (push) Successful in 17s
CI / build-and-publish (push) Has been skipped
Reviewed-on: #1
2026-03-04 21:41:58 +01:00
23b264e66e T-4: add JPA, Liquibase, Testcontainers, and deployment docs
All checks were successful
CI / backend-test (push) Successful in 1m4s
CI / frontend-test (push) Successful in 18s
CI / build-and-publish (push) Has been skipped
Set up development infrastructure for TDD: JPA + Liquibase for
database migrations, Testcontainers for integration tests against
real PostgreSQL, profile-based configuration (prod/local), and
README deployment documentation with docker-compose example.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:40:06 +01:00
cb0bcad145 Add release skill for SemVer tagging workflow
Project-specific skill that validates version, checks for clean
working tree and pushed commits, then creates and pushes a SemVer
tag to trigger the CI/CD pipeline.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:35:50 +01:00
e8184be12f T-3: mark CI/CD pipeline complete, update spec and plan
All checks were successful
CI / backend-test (push) Successful in 46s
CI / frontend-test (push) Successful in 17s
CI / build-and-publish (push) Has been skipped
All manual verification passed:
- Branch push: tests only, no image build
- Non-SemVer tag: tests only, no image build
- SemVer tag (0.0.1): all jobs green, 4 tags in Gitea registry

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:27:57 +01:00
Renovate Bot
f5df2d7290 Add renovate.json 2026-03-04 02:01:38 +00:00
27 changed files with 1554 additions and 149 deletions

View File

@@ -0,0 +1,22 @@
#!/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 OpenAPI spec files
case "$FILE_PATH" in
*/openapi/*.yaml|*/openapi/*.yml) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR/backend"
# Run validation (zero-config: structural validity only)
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

View File

@@ -13,6 +13,11 @@
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/frontend-check.sh\"",
"timeout": 120
},
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR/.claude/hooks/openapi-validate.sh\"",
"timeout": 120
}
]
}

View File

@@ -0,0 +1,66 @@
---
name: release
description: Create a SemVer release tag and push it to trigger the CI/CD pipeline. Use this skill when the user says "release", "tag a release", "publish version X.Y.Z", "create a release", or mentions pushing a version tag. Also trigger when the user says "/release".
---
# Release
Create a SemVer git tag and push it to the Gitea remote, triggering the CI/CD pipeline which builds and publishes a Docker image.
## How it works
The project uses a Gitea Actions pipeline (`.gitea/workflows/ci.yaml`) with three jobs:
- **backend-test**: JDK 25, `./mvnw -B verify`
- **frontend-test**: Node 24, lint + type-check + tests + build
- **build-and-publish**: Docker image build + push (only on SemVer tags)
When a tag matching `X.Y.Z` is pushed, `build-and-publish` runs after both test jobs pass. It publishes the Docker image to the Gitea container registry with four rolling tags: `X.Y.Z`, `X.Y`, `X`, and `latest`.
## Arguments
The user provides a version string as argument, e.g. `/release 0.2.0`. If no version is provided, ask for one.
## Workflow
Execute these steps in order. Stop and report if any check fails.
### 1. Validate the version
The argument must be a valid SemVer string: `X.Y.Z` where X, Y, Z are non-negative integers. Reject anything else (no `v` prefix, no pre-release suffixes).
### 2. Check for existing tag
Run `git tag -l <version>`. If the tag already exists locally or on the remote, stop and tell the user.
### 3. Check working tree is clean
Run `git status --porcelain`. If there is any output, stop and warn the user about uncommitted changes. List what's dirty.
### 4. Check all commits are pushed
Compare `git rev-parse HEAD` with `git rev-parse @{upstream}`. If they differ, stop and warn the user that there are unpushed local commits. Show `git log @{upstream}..HEAD --oneline` so they can see what's pending.
### 5. Confirm with the user
Before creating the tag, show a summary and ask for confirmation:
- Version to tag: `<version>`
- Commit being tagged: short hash + subject line
- What will happen: pipeline runs tests, then builds and publishes `<registry>/<owner>/fete:<version>` (plus rolling tags)
### 6. Create and push the tag
```bash
git tag <version>
git push origin <version>
```
### 7. Report success
Tell the user:
- Tag `<version>` pushed successfully
- The CI/CD pipeline is now running
- They can watch the progress in the Gitea Actions UI
- Once complete, the image will be available as `docker pull <registry>/<owner>/fete:<version>`
Do not wait for the pipeline to finish or poll its status.

3
.gitignore vendored
View File

@@ -49,6 +49,9 @@ npm-debug.log*
.env.*
!.env.example
# Spring Boot local profile (developer-specific, not committed)
backend/src/main/resources/application-local.properties
# Editor swap files
*.swp
*.swo

View File

@@ -16,6 +16,7 @@ These are the non-negotiable principles of this project. Every decision — arch
### Methodology
- Follow Research → Spec → Test → Implement → Review. No shortcuts.
- API-first development: the OpenAPI spec (`backend/src/main/resources/openapi/api.yaml`) is the single source of truth for the REST API contract. Define endpoints and schemas in the spec first, then generate backend interfaces and frontend types before writing any implementation code.
- Never write implementation code without a specification.
- Always write tests before implementation (TDD). Red → Green → Refactor.
- Refactoring is permitted freely as long as it does not alter the fundamental architecture.

View File

@@ -21,6 +21,7 @@ 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"]

View File

@@ -47,6 +47,7 @@ A privacy-focused, self-hostable web app for event announcements and RSVPs. An a
- Java (latest LTS) + Maven wrapper (`./mvnw`, included)
- Node.js (latest LTS) + npm
- Docker (for running backend tests via Testcontainers)
### Project structure
@@ -68,6 +69,26 @@ cd backend && ./mvnw test
cd frontend && npm run test:unit
```
### Running the backend locally
**Option A: Without external PostgreSQL (Testcontainers)**
```bash
cd backend && ./mvnw spring-boot:test-run
```
Starts the app with a Testcontainers-managed PostgreSQL that is created and destroyed automatically. Requires Docker.
**Option B: With 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
```
### Building
```bash
@@ -129,6 +150,62 @@ ArchUnit enforces hexagonal boundaries: domain must not depend on adapters, appl
|---------------------|------------------|-------------------|
| Prettier | `npm run format` | Formatting issues |
### OpenAPI Spec (YAML)
**After editing an `openapi/*.yaml` file** (PostToolUse hook):
| What | Command | Fails on |
|---------------------|--------------------------|-----------------------------------|
| Redocly CLI | `redocly lint api.yaml` | Structural and ruleset violations |
Validates the OpenAPI 3.1 spec against the Redocly `recommended` ruleset (with `security-defined` disabled, since endpoints are intentionally public). Runs via `npx @redocly/cli@latest`.
## 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: git.bahamut.nitrix.one/nitrix/fete:latest
ports:
- "8080:8080"
environment:
DATABASE_URL: jdbc:postgresql://db:5432/fete
DATABASE_USERNAME: fete
DATABASE_PASSWORD: changeme
depends_on:
db:
condition: service_healthy
volumes:
fete-db:
```
### Environment variables
| Variable | Required | Default | Description |
|---------------------|----------|---------|-----------------------------------|
| `DATABASE_URL` | Yes | — | JDBC connection string |
| `DATABASE_USERNAME` | Yes | — | Database username |
| `DATABASE_PASSWORD` | Yes | — | Database password |
## License
GPL — see [LICENSE](LICENSE) for details.

View File

@@ -37,6 +37,22 @@
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
@@ -49,6 +65,24 @@
<version>1.4.1</version>
<scope>test</scope>
</dependency>
<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>
</dependencies>
<build>

5
backend/redocly.yaml Normal file
View File

@@ -0,0 +1,5 @@
extends:
- recommended
rules:
security-defined: off

View File

@@ -0,0 +1,6 @@
# 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

View File

@@ -0,0 +1,4 @@
# Database (required)
spring.datasource.url=${DATABASE_URL}
spring.datasource.username=${DATABASE_USERNAME}
spring.datasource.password=${DATABASE_PASSWORD}

View File

@@ -1,4 +1,12 @@
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

View File

@@ -0,0 +1,12 @@
<?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">
<changeSet id="000-baseline" author="nitrix">
<comment>Baseline changeset — Liquibase tooling verification</comment>
</changeSet>
</databaseChangeLog>

View File

@@ -0,0 +1,10 @@
<?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>

View File

@@ -8,10 +8,12 @@ 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.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestcontainersConfig.class)
class FeteApplicationTest {
@Autowired

View File

@@ -0,0 +1,14 @@
package de.fete;
import org.springframework.boot.SpringApplication;
/** Test entry point — starts the app with Testcontainers PostgreSQL. */
public class TestFeteApplication {
/** Starts the application with Testcontainers PostgreSQL. */
public static void main(String[] args) {
SpringApplication.from(FeteApplication::main)
.with(TestcontainersConfig.class)
.run(args);
}
}

View File

@@ -0,0 +1,17 @@
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");
}
}

View File

@@ -4,14 +4,17 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import de.fete.TestcontainersConfig;
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.context.annotation.Import;
import org.springframework.test.web.servlet.MockMvc;
@SpringBootTest
@AutoConfigureMockMvc
@Import(TestcontainersConfig.class)
class WebConfigTest {
@Autowired

View File

@@ -218,10 +218,10 @@ jobs:
- [x] Authentication uses `secrets.REGISTRY_TOKEN` (not the built-in token)
#### Manual Verification:
- [ ] Push a commit to a branch → pipeline runs `backend-test` and `frontend-test` only — no image build
- [ ] 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
- [ ] Push a non-SemVer tag → pipeline runs tests only, no image build
- [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.
@@ -251,7 +251,11 @@ jobs:
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. **Buildah** must be installed on the runner (standard on most Linux runners)
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

View File

@@ -0,0 +1,567 @@
---
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

View File

@@ -0,0 +1,215 @@
---
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

@@ -0,0 +1,359 @@
---
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.

4
renovate.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"]
}

View File

@@ -44,7 +44,7 @@ US-1 hat 8 ACs, aber implizit beinhaltet sie den gesamten Erstaufbau des Stacks:
| Schicht | Was US-1 implizit verlangt |
|---------|---------------------------|
| DB | Event-Tabelle, Migration, Flyway-Setup |
| Backend | Entity, Repository, Service, REST Controller, UUID-Generation, Honeypot-Validation, JSON-Serialisierung |
| Backend | Entity, Repository, Service, REST Controller, UUID-Generation, JSON-Serialisierung |
| Frontend | Formular, Validierung, API-Call, localStorage-Management, Routing zur Erstellungsseite, Redirect zur Event-Page |
| Integration | Frontend↔Backend Verbindung, CORS-Konfiguration |

View File

@@ -1,160 +1,123 @@
# Implementation Phases
# Implementation Order
A recommended implementation order based on the dependency graph across all stories and setup tasks.
Sequential implementation order for all user stories. No parallelization — one story at a time.
## Phase 0: Project Infrastructure
## Progress Tracker
All setup tasks must complete before any user story work begins. T-3 can run in parallel with Phase 1.
- [ ] US-1 Create event
- [ ] US-2 View event page
- [ ] US-3 RSVP
- [ ] US-5 Edit event
- [ ] US-4 Manage guest list
- [ ] US-18 Cancel event
- [ ] US-19 Delete event
- [ ] US-12 Auto-cleanup after expiry
- [ ] US-13 Limit active events
- [ ] US-6 Bookmark event
- [ ] US-7 Local event overview
- [ ] US-17 Dark/light mode
- [ ] US-8 Calendar integration
- [ ] US-11 QR code
- [ ] US-9 Change highlights
- [ ] US-10a Update messages
- [ ] US-10b New-update indicator
- [ ] US-15 Color themes
- [ ] US-16 Unsplash header images
- [ ] US-14 PWA install
| Order | Task | Depends on | Notes |
|-------|------|------------|-------|
| 1 | T-1: Initialize monorepo structure | — | Scaffolds empty backend + frontend projects |
| 2 | T-5: API-first tooling setup | T-1 | OpenAPI spec, codegen plugins, generated types |
| 3 | T-2: Docker deployment setup | T-1, T-5 | Multi-stage Dockerfile — builds backend + frontend into one container |
| 4 | T-4: Development infrastructure | T-2, T-5 | Migrations, DB wiring, router, test infra, docker-compose docs — gates all user stories |
| 4* | T-3: CI/CD pipeline | T-1, T-2 | Parallelizable with T-4. Uses Gitea Actions (per Q-5 resolution) |
## Prerequisites
## Phase 1: Core Event Flow (Vertical Slice)
All setup tasks (T-1 through T-5) are complete.
The end-to-end journey from creating an event to viewing it to RSVPing. US-1 is the "Durchstich" that bootstraps the full stack (DB table, backend endpoint, frontend form, localStorage, routing). It will take significantly longer than subsequent stories.
## Order Rationale
| Order | Story | Depends on | Parallelizable |
|-------|-------|------------|----------------|
| 1 | US-1: Create an event | T-4 | — |
| 2 | US-2: View event landing page | US-1 | — |
| 3 | US-3: RSVP to an event | US-2 | — |
### Increment 1: Minimal Viable Event — US-1, US-2, US-3
These three stories are strictly sequential. No parallelization within this phase.
The vertical slice. After these three stories, the app is usable: an organizer creates an event, shares the link, guests view it and RSVP.
## Phase 2: Organizer Management & Event Lifecycle
| # | Story | Depends on | Delivers |
|---|-------|------------|----------|
| 1 | US-1: Create event | T-4 | Event creation with tokens, localStorage |
| 2 | US-2: View event page | US-1 | Public event page with attendee list, expired state |
| 3 | US-3: RSVP | US-2 | Attend/decline flow, localStorage dedup |
All stories in this phase depend on US-1 (and T-4 transitively). They can be implemented in parallel since they are independent of each other. Some are more useful after Phase 1 completes (e.g. US-4 needs RSVPs to manage), but they are structurally implementable after US-1.
### Increment 2: Organizer Toolset — US-5, US-4
| Story | Depends on | Notes |
|-------|------------|-------|
| US-4: Manage guest list | US-1 | Most useful after US-3 provides RSVPs to manage |
| US-5: Edit event details | US-1 | Required by US-9 in Phase 3 |
| US-18: Cancel an event | US-1 | Enables deferred ACs in US-2, US-3, US-8 |
| US-19: Delete an event | US-1 | Enables deferred AC in US-2 |
| US-12: Automatic data deletion after expiry | US-1 | Enables deferred AC in US-2; server-side scheduled job |
| US-13: Limit active events | US-1 | Server-side config; independent of all other stories |
The organizer needs to correct mistakes and moderate spam before the app goes to real users.
**Recommended order within phase:** US-5, US-4, US-18, US-19, US-12, US-13 — starting with US-5 because US-9 (Phase 3) depends on it, and US-4 because it completes the organizer toolset around RSVPs.
| # | Story | Depends on | Delivers |
|---|-------|------------|----------|
| 4 | US-5: Edit event | US-1 | Edit all fields, expiry-must-be-future constraint |
| 5 | US-4: Manage guest list | US-1 | View RSVPs, delete spam entries |
## Phase 3: Enhanced Event Page Features
US-5 before US-4: US-9 (change highlights) depends on US-5, so getting it done early unblocks Phase 3 work.
Features that enrich the event page for guests. Most depend on US-2 (event page exists). US-9 additionally requires US-5 (editing). US-10b requires US-10a.
### Increment 3: Event Lifecycle — US-18, US-19, US-12, US-13
| Story | Depends on | Parallelizable with |
|-------|------------|---------------------|
| US-6: Bookmark an event | US-2 | US-8, US-10a, US-11 |
| US-8: Add event to calendar (.ics / webcal) | US-2 | US-6, US-10a, US-11 |
| US-9: Highlight changed event details | US-2, US-5 | US-6, US-8, US-10a, US-11 (if US-5 is done) |
| US-10a: Post update messages | US-1, US-2 | US-6, US-8, US-11 |
| US-10b: New-update indicator | US-10a | Must follow US-10a |
| US-11: Generate QR code | US-2 | US-6, US-8, US-10a |
Complete lifecycle management. After this increment, the privacy guarantee is enforced and abuse prevention is in place.
**Recommended order within phase:** US-6, US-8, US-11 (simple, independent), then US-10a → US-10b (sequential pair), then US-9 (requires US-5 from Phase 2).
| # | Story | Depends on | Delivers | Activates deferred ACs |
|---|-------|------------|----------|----------------------|
| 6 | US-18: Cancel event | US-1 | One-way cancellation with optional message, expiry adjustment | US-2 AC5, US-3 AC11 |
| 7 | US-19: Delete event | US-1 | Immediate permanent deletion, localStorage cleanup | US-2 AC6 (partial) |
| 8 | US-12: Auto-cleanup | US-1 | Scheduled deletion after expiry, silent logging | US-2 AC6 (complete) |
| 9 | US-13: Event limit | US-1 | `MAX_ACTIVE_EVENTS` env var, server-side enforcement | — |
## Phase 4: Visual Customization
When implementing US-18, US-19, and US-12: immediately activate their deferred ACs in US-2 and US-3 (cancelled state display, RSVP blocking, event-not-found handling). These stories exist at this point — no reason to defer further.
Event-level theming and image selection. Both depend on US-1 and US-2. US-15 and US-16 are independent of each other but share the event creation/editing form surface area, so coordinating them is beneficial.
### Increment 4: App Shell — US-6, US-7, US-17
| Story | Depends on | Notes |
|-------|------------|-------|
| US-15: Choose event color theme | US-1, US-2 | Predefined theme picker in creation/edit forms |
| US-16: Select header image from Unsplash | US-1, US-2 | Optional feature gated by API key config |
The app gets a home screen. Users can find their events without the original link.
**Recommended order:** US-15 first (simpler, no external dependency), then US-16. Consider the interaction between event themes and dark/light mode (US-17) — implement US-17 before or alongside US-15 if possible.
| # | Story | Depends on | Delivers |
|---|-------|------------|----------|
| 10 | US-6: Bookmark event | US-2 | Client-only bookmark, no server contact |
| 11 | US-7: Local event overview | — | Root page `/` with all tracked events from localStorage |
| 12 | US-17: Dark/light mode | — | System preference detection, manual toggle, localStorage persistence |
## Phase 5: App Shell & PWA
US-6 before US-7: bookmarking populates localStorage entries that the overview displays. Without US-6, the overview only shows created and RSVPed events.
Client-side infrastructure and app-level UX features. These have no or minimal structural dependencies but are only meaningfully testable after earlier phases provide content and data.
US-17 here (not in a late phase): event color themes (US-15) must account for dark/light mode. Having it in place before US-15 avoids rework.
| Story | Depends on | Practically useful after |
|-------|------------|------------------------|
| US-7: Local event overview | None (structural) | US-1, US-3, US-6 populate localStorage |
| US-14: Install as PWA | T-4 (structural) | US-2, US-7 provide pages to cache |
| US-17: Dark/light mode | None (structural) | T-4 provides frontend scaffold |
### Increment 5: Rich Event Page — US-8, US-11, US-9, US-10a, US-10b
**Recommended order:** US-17 (can be started early once the frontend scaffold exists — consider implementing alongside Phase 2 or 3), then US-7 (after localStorage-populating stories are available), then US-14 (after the app has real pages and assets).
Features that enrich the event page for guests and organizers.
**Note on US-17 timing:** US-17 is listed in Phase 5 for logical grouping, but it can be implemented as early as Phase 2 since it only needs the frontend scaffold. Implementing it earlier is recommended because US-15 (Phase 4) must consider the interaction between event color themes and dark/light mode. Having dark/light mode in place before US-15 simplifies that work.
| # | Story | Depends on | Delivers |
|---|-------|------------|----------|
| 13 | US-8: Calendar .ics + webcal | US-2 | RFC 5545 download, webcal subscription, STATUS:CANCELLED support |
| 14 | US-11: QR code | US-2 | Server-generated QR, SVG/PNG download |
| 15 | US-9: Change highlights | US-2, US-5 | Field-level change indicators, localStorage-based read tracking |
| 16 | US-10a: Update messages | US-1, US-2 | Organizer posts, reverse-chronological display, delete capability |
| 17 | US-10b: New-update indicator | US-10a | localStorage-based unread badge |
## Deferred Acceptance Criteria
US-8 benefits from US-18 being complete: `STATUS:CANCELLED` in .ics can be implemented directly instead of deferred.
Several stories contain ACs that reference features from later phases. These are marked `[deferred until US-X is implemented]` in the story text:
US-9 benefits from US-5 being complete (increment 2): no dependency waiting.
| Story | AC | Deferred until | Phase unlocked |
|-------|-----|---------------|----------------|
| US-2 AC 5 | Cancelled state display | US-18 | Phase 2 |
| US-2 AC 6 | Event not found (expiry deletion) | US-12 | Phase 2 |
| US-2 AC 6 | Event not found (organizer deletion) | US-19 | Phase 2 |
| US-3 AC 11 | RSVP blocked on cancelled event | US-18 | Phase 2 |
| US-8 AC 9 | STATUS:CANCELLED in .ics | US-18 | Phase 2 |
| US-12 AC 2 | Delete stored header images | US-16 | Phase 4 |
### Increment 6: Visual Polish & PWA — US-15, US-16, US-14
Once the referenced story is implemented, revisit the deferring story to activate the deferred AC.
Final layer: visual customization and native app feel.
## Dependency Graph
| # | Story | Depends on | Delivers |
|---|-------|------------|----------|
| 18 | US-15: Color themes | US-1, US-2 | Predefined theme picker, event-scoped styling |
| 19 | US-16: Unsplash images | US-1, US-2 | Server-proxied search, local storage, attribution |
| 20 | US-14: PWA | T-4 | Manifest, service worker, installability |
Render this diagram at [mermaid.live](https://mermaid.live) or view it directly in Gitea (which renders `mermaid` blocks natively).
US-15 before US-16: themes are self-contained, Unsplash adds external API complexity.
```mermaid
graph TD
classDef infra fill:#4a90d9,stroke:#2c5f8a,color:#fff
classDef core fill:#e8a838,stroke:#b07c1e,color:#fff
classDef organizer fill:#50b86c,stroke:#2d8043,color:#fff
classDef enhanced fill:#9b59b6,stroke:#6c3483,color:#fff
classDef visual fill:#e74c3c,stroke:#a93226,color:#fff
classDef shell fill:#7f8c8d,stroke:#566566,color:#fff
US-14 last: PWA caching is most effective when the app has all its pages and assets. Service worker strategy can cover everything in one pass.
%% Phase 0: Infrastructure
T1(["T-1: Monorepo"]):::infra --> T5(["T-5: API-First Tooling"]):::infra
T1 --> T2(["T-2: Docker"]):::infra
T5 --> T2
T2 --> T4(["T-4: Dev Infra + DB"]):::infra
T5 --> T4
T2 --> T3(["T-3: CI/CD"]):::infra
Note: US-12 AC2 (delete stored header images on expiry) remains deferred until US-16 is implemented. When implementing US-16, activate this AC in US-12.
%% Phase 1: Core Event Flow
T4 --> US1["US-1: Create Event"]:::core
US1 --> US2["US-2: View Event"]:::core
US2 --> US3["US-3: RSVP"]:::core
## Deferred AC Activation Schedule
%% Phase 2: Organizer & Lifecycle (branch from US-1)
US1 --> US4["US-4: Guest List"]:::organizer
US1 --> US5["US-5: Edit Event"]:::organizer
US1 --> US18["US-18: Cancel"]:::organizer
US1 --> US19["US-19: Delete"]:::organizer
US1 --> US12["US-12: Auto-Cleanup"]:::organizer
US1 --> US13["US-13: Event Limit"]:::organizer
%% Phase 3: Enhanced Features (branch from US-2)
US2 --> US6["US-6: Bookmark"]:::enhanced
US2 --> US8["US-8: Calendar .ics"]:::enhanced
US2 --> US10a["US-10a: Messages"]:::enhanced
US2 --> US11["US-11: QR Code"]:::enhanced
US5 --> US9["US-9: Change Highlights"]:::enhanced
US2 --> US9
US10a --> US10b["US-10b: New-Update Badge"]:::enhanced
%% Phase 4: Visual Customization (branch from US-2)
US2 --> US15["US-15: Color Themes"]:::visual
US2 --> US16["US-16: Unsplash Images"]:::visual
%% Phase 5: App Shell & PWA
T4 --> US14["US-14: PWA"]:::shell
US7["US-7: Local Overview"]:::shell
US17["US-17: Dark/Light Mode"]:::shell
```
**Legend:**
- 🔵 Infrastructure (T-1 T-5)
- 🟠 Core Event Flow (US-1 US-3)
- 🟢 Organizer & Lifecycle (US-4, US-5, US-12, US-13, US-18, US-19)
- 🟣 Enhanced Features (US-6, US-8 US-11)
- 🔴 Visual Customization (US-15, US-16)
- ⚪ App Shell & PWA (US-7, US-14, US-17)
US-7 and US-17 appear as isolated nodes — they have no structural dependencies but are only practically useful after earlier phases provide content (see Phase 5 notes above).
| When implementing | Activate deferred AC in | AC description |
|-------------------|------------------------|----------------|
| US-18 (#6) | US-2 AC5 | Cancelled state display |
| US-18 (#6) | US-3 AC11 | RSVP blocked on cancelled event |
| US-18 (#6) | US-8 AC9 | STATUS:CANCELLED in .ics (if US-8 not yet done — in this order, US-8 comes later, so implement directly) |
| US-19 (#7) | US-2 AC6 | Event not found (organizer deletion) |
| US-12 (#8) | US-2 AC6 | Event not found (expiry deletion) |
| US-16 (#19) | US-12 AC2 | Delete stored header images on expiry |

View File

@@ -43,12 +43,12 @@
**Description:** Set up a Gitea Actions CI/CD pipeline that runs on every push, ensuring code quality before deployment.
**Acceptance Criteria:**
- [ ] Gitea Actions workflow file in `.gitea/workflows/` runs on push: test, build, publish Docker image
- [ ] Backend tests run via Maven
- [ ] Frontend tests run via Vitest
- [ ] Docker image is published to the Gitea container registry on the same instance
- [ ] Pipeline fails visibly if any test fails or the build breaks
- [ ] Docker image is only published if all tests pass and the build succeeds
- [x] Gitea Actions workflow file in `.gitea/workflows/` runs on push: test, build, publish Docker image
- [x] Backend tests run via Maven
- [x] Frontend tests run via Vitest
- [x] Docker image is published to the Gitea container registry on the same instance
- [x] Pipeline fails visibly if any test fails or the build breaks
- [x] Docker image is only published if all tests pass and the build succeeds
**Dependencies:** T-1, T-2
@@ -81,15 +81,15 @@
**Description:** Set up the development foundation needed before the first user story can be implemented with TDD (as required by CLAUDE.md). This bridges the gap between project scaffolds and actual feature development. Also includes the database and environment variable configuration deferred from T-2.
**Acceptance Criteria:**
- [ ] Database migration framework (Flyway or Liquibase) is configured in the backend with a first empty migration that runs successfully against a PostgreSQL instance
- [ ] App connects to external PostgreSQL via environment variable (e.g. `DATABASE_URL` or Spring-native `SPRING_DATASOURCE_*`)
- [ ] All runtime configuration via environment variables: database connection, optional Unsplash API key, optional max active events
- [ ] SPA router is configured in the Vue frontend (Vue Router) so pages can be navigated by URL path
- [ ] Backend test infrastructure is set up: JUnit 5 with Spring Boot Test, plus integration test support using Testcontainers (PostgreSQL) so tests can run against a real database without external setup
- [ ] Frontend test infrastructure is set up: Vitest with @vue/test-utils configured and a sample test runs successfully
- [ ] Both test suites (backend and frontend) can be executed via their respective build tools (`mvn test` and `npm test` / `npx vitest`)
- [ ] README documents deployment setup with a docker-compose example (app + postgres)
- [ ] Container starts and responds to health checks with a running PostgreSQL (migrations run on startup)
- [x] Database migration framework (Flyway or Liquibase) is configured in the backend with a first empty migration that runs successfully against a PostgreSQL instance
- [x] App connects to external PostgreSQL via environment variable (e.g. `DATABASE_URL` or Spring-native `SPRING_DATASOURCE_*`)
- [x] All runtime configuration via environment variables: database connection, optional Unsplash API key, optional max active events
- [x] SPA router is configured in the Vue frontend (Vue Router) so pages can be navigated by URL path
- [x] Backend test infrastructure is set up: JUnit 5 with Spring Boot Test, plus integration test support using Testcontainers (PostgreSQL) so tests can run against a real database without external setup
- [x] Frontend test infrastructure is set up: Vitest with @vue/test-utils configured and a sample test runs successfully
- [x] Both test suites (backend and frontend) can be executed via their respective build tools (`mvn test` and `npm test` / `npx vitest`)
- [x] README documents deployment setup with a docker-compose example (app + postgres)
- [x] Container starts and responds to health checks with a running PostgreSQL (migrations run on startup)
**Dependencies:** T-2, T-5

View File

@@ -32,13 +32,14 @@ The following terms are used consistently across all stories:
- [ ] The event token, title, and date are also stored in localStorage alongside the organizer token, so the local event overview (US-7) can display the event without additional server contact
- [ ] No account, login, or personal data is required to create an event
- [ ] The expiry date field is mandatory and cannot be left blank
- [ ] A honeypot field is present in the event creation form: hidden from real users; any submission with the field populated is silently discarded server-side
- [ ] The event is not discoverable except via its direct link
**Dependencies:** T-4
**Notes:** Non-guessable tokens (UUIDs) are specified in Ideen.md under security. Expiry date is mandatory per Ideen.md. No registration required per core principles. Per Q-4 resolution: organizer authentication uses the organizer token stored in localStorage on the device where the event was created. The organizer token is separate from the event token — since the event link is designed to be shared in group chats, using the same token for both public access and organizer auth would allow any guest to manage the event.
**Addendum (2026-03-04):** Honeypot field removed — overengineered for this project's scope. Expiry date must be in the future at creation time — an event should never exist in an invalid state (resolved during US-1 research).
---
### US-2: View event landing page
@@ -78,14 +79,15 @@ The following terms are used consistently across all stories:
- [ ] The event token, title, and date are also stored in localStorage alongside the RSVP data, so the local event overview (US-7) can display the event and link to it without server contact
- [ ] If a prior RSVP exists in localStorage for this event, the form pre-fills with the previous choice and name
- [ ] Re-submitting from the same device updates the existing RSVP entry rather than creating a duplicate
- [ ] A honeypot field is present in the RSVP form: hidden from real users; any submission with the field populated is silently discarded server-side
- [ ] RSVP submission is not possible after the event's expiry date
- [ ] RSVP submission is not possible if the event has been cancelled (US-18) [deferred until US-18 is implemented]
- [ ] No account, login, or data beyond the optionally entered name is required
**Dependencies:** US-2, T-4
**Notes:** RSVP flow specified in Ideen.md: "Ich komme" (with name) / "Ich komme nicht" (optional with name). LocalStorage device binding is the explicit duplicate-prevention mechanism — not a hard guarantee, but sufficient against accidental duplicates. Ideen.md acknowledges that malicious spam without accounts is an acceptable risk. Honeypot fields are listed under Ideen.md security measures.
**Notes:** RSVP flow specified in Ideen.md: "Ich komme" (with name) / "Ich komme nicht" (optional with name). LocalStorage device binding is the explicit duplicate-prevention mechanism — not a hard guarantee, but sufficient against accidental duplicates. Ideen.md acknowledges that malicious spam without accounts is an acceptable risk.
**Addendum (2026-03-04):** Honeypot field removed — overengineered for this project's scope.
---
@@ -464,3 +466,4 @@ The following terms are used consistently across all stories:
**Dependencies:** US-1, T-4
**Notes:** The overseer identified that using the expiry date as a deletion mechanism (setting it to today or a past date in US-5) was unintuitive and conflated two different actions. US-5 now enforces that the expiry date can only be set to a future date. If the organizer wants the event gone immediately, they use this explicit deletion feature. Unlike cancellation (US-18), which keeps the event visible with a cancellation notice until the expiry date, deletion removes the event entirely and immediately. This is the organizer's "nuclear option" — useful when the event was created by mistake, contains wrong information, or is no longer needed at all. The deletion behavior is identical to what US-12 does automatically after expiry, but triggered manually and immediately by the organizer.