From e3ca613210a68e516e543b5188bcabe99e765c4d Mon Sep 17 00:00:00 2001 From: nitrix Date: Thu, 5 Mar 2026 10:57:44 +0100 Subject: [PATCH] Add agent research and implementation plan docs for US-1 Research reports on datetime handling, RFC 9457, font selection. Implementation plans for US-1 create event and post-review fixes. Co-Authored-By: Claude Opus 4.6 --- .../plan/2026-03-04-us1-create-event.md | 1152 +++++++++++++++++ .../plan/2026-03-05-us1-post-review-fixes.md | 199 +++ .../plan/2026-03-05-us1-review-fixes.md | 109 ++ .../2026-03-04-datetime-best-practices.md | 107 ++ .../2026-03-04-rfc9457-problem-details.md | 202 +++ .../research/2026-03-04-sans-serif-fonts.md | 404 ++++++ .../research/2026-03-04-us1-create-event.md | 195 +++ 7 files changed, 2368 insertions(+) create mode 100644 docs/agents/plan/2026-03-04-us1-create-event.md create mode 100644 docs/agents/plan/2026-03-05-us1-post-review-fixes.md create mode 100644 docs/agents/plan/2026-03-05-us1-review-fixes.md create mode 100644 docs/agents/research/2026-03-04-datetime-best-practices.md create mode 100644 docs/agents/research/2026-03-04-rfc9457-problem-details.md create mode 100644 docs/agents/research/2026-03-04-sans-serif-fonts.md create mode 100644 docs/agents/research/2026-03-04-us1-create-event.md diff --git a/docs/agents/plan/2026-03-04-us1-create-event.md b/docs/agents/plan/2026-03-04-us1-create-event.md new file mode 100644 index 0000000..4542c41 --- /dev/null +++ b/docs/agents/plan/2026-03-04-us1-create-event.md @@ -0,0 +1,1152 @@ +--- +date: 2026-03-04T21:41:13+00:00 +git_commit: 91e566efea0cbf53ba06a29b63317b7435609bd8 +branch: master +topic: "US-1: Create an Event" +tags: [plan, us-1, event-creation, full-stack] +status: approved +--- + +# US-1: Create an Event — Implementation Plan + +## Overview + +Implement the first user story end-to-end: an organizer creates an event with title, description, date/time, location, and expiry date. The server stores the event, returns event token + organizer token. The frontend stores tokens in localStorage and redirects to a stub event page. This is the first vertical slice through all layers of the hexagonal architecture. + +Additionally: remove the scaffolding health endpoint from the OpenAPI spec (replaced by Spring Actuator), establish RFC 9457 error handling, set the app's visual design foundation (mobile-first, app-native feel), and clean up Vue scaffold defaults. + +## Current State Analysis + +- All setup tasks (T-1 through T-5) complete +- Hexagonal architecture skeleton with ArchUnit enforcement — all business packages empty +- OpenAPI spec contains only a scaffolding `/health` endpoint (to be removed) +- Liquibase with empty baseline migration +- Testcontainers PostgreSQL for integration tests +- Vue 3 SPA with placeholder routes and openapi-fetch client +- Dockerfile HEALTHCHECK correctly uses `/actuator/health` (not the custom endpoint) +- No domain code, no persistence, no controllers exist yet + +### Key Discoveries: +- `WebConfig.java:19` — API prefix `/api` applied to all `@RestController` beans +- `WebConfig.java:23-39` — SPA fallback routes non-API requests to `index.html` +- `application.properties:4` — JPA `ddl-auto=validate` (Liquibase manages schema) +- OpenAPI generator: `interfaceOnly=true`, `useBeanValidation=true`, packages `de.fete.adapter.in.web.api` / `.model` +- Frontend types generated via `npm run generate:api` into `src/api/schema.d.ts` +- Dockerfile HEALTHCHECK (`Dockerfile:25-26`) uses `/actuator/health` — safe to remove custom health endpoint + +## Desired End State + +After this plan is complete: + +1. `POST /api/events` accepts a JSON body with title, description, dateTime, location, expiryDate +2. Server validates input (Bean Validation), creates the event with two UUIDs (event token, organizer token), persists to PostgreSQL, returns both tokens +3. Error responses follow RFC 9457 Problem Details format with field-level validation errors +4. Frontend shows a mobile-first event creation form at `/create` +5. On success, localStorage stores organizer token + event metadata, browser redirects to `/events/:token` (stub page) +6. Root page `/` shows a minimal landing with "Create Event" button +7. The scaffolding health endpoint is removed from the OpenAPI spec +8. Vue scaffold defaults are cleaned up +9. All tests pass (backend unit + integration, frontend unit, ArchUnit, Checkstyle) + +### UI Mockups + +#### Root Page `/` (Mobile) +``` +┌─────────────────────────────┐ +│ │ +│ ┌─────────────────────┐ │ +│ │ fete │ │ +│ └─────────────────────┘ │ +│ │ +│ │ +│ No events yet. │ +│ Create your first one! │ +│ │ +│ ┌─────────────────────┐ │ +│ │ + Create Event │ │ +│ └─────────────────────┘ │ +│ │ +│ │ +└─────────────────────────────┘ +``` + +#### Create Event `/create` (Mobile) +``` +┌─────────────────────────────┐ +│ ← Create │ +│ │ +│ ┌─────────────────────────┐│ +│ │ Title * ││ +│ │ ││ +│ └─────────────────────────┘│ +│ │ +│ ┌─────────────────────────┐│ +│ │ Description ││ +│ │ ││ +│ │ ││ +│ └─────────────────────────┘│ +│ │ +│ ┌─────────────────────────┐│ +│ │ 📅 Date & Time * ││ +│ └─────────────────────────┘│ +│ │ +│ ┌─────────────────────────┐│ +│ │ 📍 Location ││ +│ └─────────────────────────┘│ +│ │ +│ ┌─────────────────────────┐│ +│ │ 📆 Expiry Date * ││ +│ └─────────────────────────┘│ +│ │ +│ ┌─────────────────────────┐│ +│ │ Create Event ││ +│ └─────────────────────────┘│ +│ │ +└─────────────────────────────┘ + +* = required +Card-style inputs, rounded corners, +generous padding, gradient background +``` + +#### Event Stub `/events/:token` (Mobile) +``` +┌─────────────────────────────┐ +│ ← fete │ +│ │ +│ │ +│ ✓ Event created! │ +│ │ +│ Share this link: │ +│ ┌─────────────────────────┐│ +│ │ https://…/events/abc123 ││ +│ │ 📋 Copy ││ +│ └─────────────────────────┘│ +│ │ +│ │ +└─────────────────────────────┘ +``` + +#### Desktop Layout +``` +┌──────────────────────────────────────────────┐ +│ ┌──────────────────┐ │ +│ │ │ │ +│ │ (mobile view │ │ +│ │ centered, │ │ +│ │ max ~480px) │ │ +│ │ │ │ +│ └──────────────────┘ │ +│ │ +│ ← gradient background fills full width → │ +└──────────────────────────────────────────────┘ +``` + +## What We're NOT Doing + +- Full event page (US-2) — only a stub with "Event created" confirmation +- RSVP functionality (US-3) +- Organizer view / event editing (US-4, US-5) +- Dark/light mode toggle (US-17) — but we design CSS with future theming in mind +- Concrete color palette selection — researched during implementation with browser tools +- PWA manifest / service worker (US-14) +- Event image or color theme (US-15, US-16) + +## Design Principles (Persistent — Applies to All Future Stories) + +These design decisions apply to US-1 and all subsequent frontend work: + +- **Mobile-first / App-native feel** — not a classic website. Think installed app, not browser page. +- **Desktop:** centered narrow column (max ~480px), gradient background fills the rest +- **Gradient backgrounds** as primary design language (subtle 2-3 color gradients) +- **Card-style form fields** — rounded corners, generous padding, elevated look +- **Typography: Sora** — contemporary geometric sans-serif with slightly rounded terminals. Self-hosted WOFF2, OFL 1.1 licensed. Source: github.com/sora-xor/sora-font. No Google Fonts CDN. +- **Generous whitespace** — elements breathe, nothing cramped +- **WCAG AA contrast** as baseline for all color choices + +### Color Palette: Electric Dusk + +Chosen for best balance of style, broad appeal, and accessibility (white text readable across almost the entire gradient). + +| Role | Hex | Description | +|------|-----|-------------| +| Gradient Start | `#F06292` | Pink | +| Gradient Mid | `#AB47BC` | Purple | +| Gradient End | `#5C6BC0` | Indigo blue | +| Accent (CTAs/buttons) | `#FF7043` | Deep orange | +| Text (light mode) | `#1C1C1E` | Near black | +| Text (dark mode) | `#FFFFFF` | White | +| Surface (light) | `#FFF5F8` | Pinkish white | +| Surface (dark) | `#1B1730` | Deep indigo-black | +| Card (light) | `#FFFFFF` | White | +| Card (dark) | `#2A2545` | Muted indigo | + +**Primary gradient:** +```css +background: linear-gradient(135deg, #F06292 0%, #AB47BC 50%, #5C6BC0 100%); +``` + +**Usage rules:** +- Gradient for hero/splash areas and page backgrounds — not as direct text background for body copy +- Cards and content areas use solid surface colors with high-contrast text +- Accent color (`#FF7043`) for primary action buttons with dark text (`#1C1C1E`) +- White text on gradient mid/end passes WCAG AA (4.82:1 and 4.86:1) +- White text on gradient start passes AA-large (3.06:1) — use for headings 18px+ only + +## Implementation Approach + +API-first, TDD, inside-out through the hexagonal layers. Seven phases: + +1. **OpenAPI Spec & Cleanup** — define the contract, remove scaffolding health endpoint +2. **Database Migration** — create the `events` table +3. **Domain & Ports** — `Event` model, `CreateEventUseCase`, `EventRepository` +4. **Application Service** — `EventService` implementing the use case +5. **Persistence Adapter** — JPA entity, Spring Data repository +6. **Web Adapter & Error Handling** — Controller, `GlobalExceptionHandler` +7. **Frontend** — design foundation, form, localStorage, routing, stub page + +--- + +## Phase 1: OpenAPI Spec & Cleanup + +### Overview +Define the `POST /events` contract in the OpenAPI spec. Remove the scaffolding `/health` endpoint and `HealthResponse` schema. Verify that nothing depends on the removed endpoint. Regenerate backend interfaces and frontend types. + +### Changes Required: + +#### [x] 1.1 Remove health endpoint from OpenAPI spec +**File**: `backend/src/main/resources/openapi/api.yaml` +**Changes**: Remove the `/health` path and `HealthResponse` schema. Add the `POST /events` path with request/response schemas and RFC 9457 error response schemas. + +```yaml +openapi: 3.1.0 +info: + title: fete API + description: Privacy-focused event announcements and RSVPs + version: 0.1.0 + license: + name: GPL-3.0-or-later + identifier: GPL-3.0-or-later + +servers: + - url: /api + +paths: + /events: + post: + operationId: createEvent + summary: Create a new event + tags: + - events + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/CreateEventRequest" + responses: + "201": + description: Event created successfully + content: + application/json: + schema: + $ref: "#/components/schemas/CreateEventResponse" + "400": + description: Validation failed + content: + application/problem+json: + schema: + $ref: "#/components/schemas/ValidationProblemDetail" + +components: + schemas: + CreateEventRequest: + type: object + required: + - title + - dateTime + - expiryDate + properties: + title: + type: string + minLength: 1 + maxLength: 200 + description: + type: string + maxLength: 2000 + dateTime: + type: string + format: date-time + description: Event date and time with UTC offset (ISO 8601) + example: "2026-03-15T20:00:00+01:00" + location: + type: string + maxLength: 500 + expiryDate: + type: string + format: date + description: Date after which event data is deleted. Must be in the future. + example: "2026-06-15" + + CreateEventResponse: + type: object + required: + - eventToken + - organizerToken + - title + - dateTime + - expiryDate + properties: + eventToken: + type: string + format: uuid + description: Public token for the event URL + organizerToken: + type: string + format: uuid + description: Secret token for organizer access + title: + type: string + dateTime: + type: string + format: date-time + expiryDate: + type: string + format: date + + 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 + required: + - field + - message + properties: + field: + type: string + message: + type: string +``` + +#### [x] 1.2 Remove health endpoint test reference +**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java` +**Changes**: Verify that tests only reference `/actuator/health`, not `/api/health`. Remove or update any test that calls the generated `HealthApi` interface. The test `apiPrefixNotAccessibleWithoutIt` may need to be adapted to use the new `/events` endpoint instead. + +#### [x] 1.3 Regenerate backend and frontend types +**Action**: Run `cd backend && ./mvnw compile` and `cd frontend && npm run generate:api` to verify generation succeeds with the new spec. The old `HealthApi.java` and `HealthResponse.java` in `target/` will be replaced by `EventsApi.java`, `CreateEventRequest.java`, `CreateEventResponse.java`, etc. + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd backend && ./mvnw compile` succeeds — generates `EventsApi` interface, request/response models +- [ ] `cd frontend && npm run generate:api` succeeds — `schema.d.ts` contains event types +- [ ] `cd backend && ./mvnw test` passes — no test references removed health endpoint +- [ ] No `HealthApi.java` or `HealthResponse.java` in generated output + +#### Manual Verification: +- [ ] OpenAPI spec is valid (no syntax errors) +- [ ] Generated `EventsApi` interface has `createEvent` method with correct signature + +**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: Database Migration + +### Overview +Create the `events` table via a Liquibase migration. The table stores all event fields plus both UUID tokens and a `created_at` audit timestamp. + +### Changes Required: + +#### [x] 2.1 Create Liquibase migration +**File**: `backend/src/main/resources/db/changelog/001-create-events-table.xml` +**Changes**: New migration file. + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +#### [x] 2.2 Include migration in master changelog +**File**: `backend/src/main/resources/db/changelog/db.changelog-master.xml` +**Changes**: Add include for the new migration file after the baseline. + +```xml + +``` + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd backend && ./mvnw test` passes — Testcontainers spins up PostgreSQL, Liquibase runs migration, Hibernate validates schema +- [ ] Index on `event_token` exists (primary lookup path for public access) +- [ ] Index on `expiry_date` exists (cleanup job query path, US-12) + +#### Manual Verification: +- [ ] Migration file uses correct PostgreSQL types (`timestamptz`, `date`, `uuid`, `bigserial`) +- [ ] Column constraints match domain rules (title not null, description nullable, etc.) + +**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: Domain Model & Ports + +### Overview +Create the `Event` domain entity and the inbound/outbound port interfaces. The domain layer has zero dependencies on Spring or adapters (enforced by ArchUnit). + +### Changes Required: + +#### [x] 3.1 Event domain model +**File**: `backend/src/main/java/de/fete/domain/model/Event.java` +**Changes**: New file. Plain Java class — no JPA annotations, no Spring dependencies. + +```java +package de.fete.domain.model; + +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.UUID; + +public class Event { + + private Long id; + private UUID eventToken; + private UUID organizerToken; + private String title; + private String description; + private OffsetDateTime dateTime; + private String location; + private LocalDate expiryDate; + private OffsetDateTime createdAt; + + // Constructor, getters, setters (or builder pattern if warranted) +} +``` + +#### [x] 3.2 CreateEventUseCase inbound port +**File**: `backend/src/main/java/de/fete/domain/port/in/CreateEventUseCase.java` +**Changes**: New file. Interface with a command object. + +```java +package de.fete.domain.port.in; + +import de.fete.domain.model.Event; +import java.time.LocalDate; +import java.time.OffsetDateTime; + +public interface CreateEventUseCase { + + Event createEvent(CreateEventCommand command); + + record CreateEventCommand( + String title, + String description, + OffsetDateTime dateTime, + String location, + LocalDate expiryDate + ) {} +} +``` + +#### [x] 3.3 EventRepository outbound port +**File**: `backend/src/main/java/de/fete/domain/port/out/EventRepository.java` +**Changes**: New file. Interface — only the `save` method needed for US-1. `findByEventToken` added for the stub page route (minimal, but needed for redirect verification in later stories). + +```java +package de.fete.domain.port.out; + +import de.fete.domain.model.Event; +import java.util.Optional; +import java.util.UUID; + +public interface EventRepository { + + Event save(Event event); + + Optional findByEventToken(UUID eventToken); +} +``` + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd backend && ./mvnw test` passes — ArchUnit validates: + - `Event` in `domain.model` has no Spring/adapter dependencies + - `CreateEventUseCase` and `EventRepository` are interfaces + - No dependency violations between layers +- [ ] `cd backend && ./mvnw checkstyle:check` passes + +#### Manual Verification: +- [ ] Domain model fields match the database schema from Phase 2 +- [ ] `CreateEventCommand` record contains only the fields the organizer provides (no tokens, no id, no createdAt) + +**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: Application Service + +### Overview +Implement `EventService` — the use case implementation that validates the expiry date, generates UUID tokens, builds the domain model, and delegates to the repository. + +### Changes Required: + +#### [x] 4.1 EventService +**File**: `backend/src/main/java/de/fete/application/service/EventService.java` +**Changes**: New file. Implements `CreateEventUseCase`. Spring `@Service` annotation. + +```java +package de.fete.application.service; + +import de.fete.domain.model.Event; +import de.fete.domain.port.in.CreateEventUseCase; +import de.fete.domain.port.out.EventRepository; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.UUID; +import org.springframework.stereotype.Service; + +@Service +public class EventService implements CreateEventUseCase { + + private final EventRepository eventRepository; + + public EventService(EventRepository eventRepository) { + this.eventRepository = eventRepository; + } + + @Override + public Event createEvent(CreateEventCommand command) { + if (!command.expiryDate().isAfter(LocalDate.now())) { + throw new ExpiryDateInPastException(command.expiryDate()); + } + + var event = new Event(); + event.setEventToken(UUID.randomUUID()); + event.setOrganizerToken(UUID.randomUUID()); + event.setTitle(command.title()); + event.setDescription(command.description()); + event.setDateTime(command.dateTime()); + event.setLocation(command.location()); + event.setExpiryDate(command.expiryDate()); + event.setCreatedAt(OffsetDateTime.now()); + + return eventRepository.save(event); + } +} +``` + +#### [x] 4.2 ExpiryDateInPastException +**File**: `backend/src/main/java/de/fete/application/service/ExpiryDateInPastException.java` +**Changes**: New file. Domain-level exception for the business rule "expiry date must be in the future". + +```java +package de.fete.application.service; + +import java.time.LocalDate; + +public class ExpiryDateInPastException extends RuntimeException { + + private final LocalDate expiryDate; + + public ExpiryDateInPastException(LocalDate expiryDate) { + super("Expiry date must be in the future: " + expiryDate); + this.expiryDate = expiryDate; + } + + public LocalDate getExpiryDate() { + return expiryDate; + } +} +``` + +#### [x] 4.3 Unit tests for EventService +**File**: `backend/src/test/java/de/fete/application/service/EventServiceTest.java` +**Changes**: New file. Unit tests with a mocked `EventRepository`. + +Test cases: +- Happy path: valid command → event saved with generated tokens, createdAt set +- Expiry date today → `ExpiryDateInPastException` +- Expiry date in the past → `ExpiryDateInPastException` +- Expiry date tomorrow → succeeds +- Event token and organizer token are different UUIDs +- Repository `save` is called exactly once + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd backend && ./mvnw test` passes — all `EventServiceTest` tests green +- [ ] `cd backend && ./mvnw checkstyle:check` passes +- [ ] ArchUnit: `EventService` in `application.service` may depend on `domain.model` and `domain.port` but not on adapters + +#### Manual Verification: +- [ ] Business rule enforced: expiry date must be strictly after today +- [ ] UUID generation uses `UUID.randomUUID()` (v4, non-guessable) + +**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: Persistence Adapter + +### Overview +Implement the JPA entity and Spring Data repository that fulfill the `EventRepository` outbound port. The adapter translates between the domain model and the JPA entity. + +### Changes Required: + +#### [x] 5.1 JPA Entity +**File**: `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaEntity.java` +**Changes**: New file. JPA entity mapping to the `events` table. + +```java +package de.fete.adapter.out.persistence; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.UUID; + +@Entity +@Table(name = "events") +public class EventJpaEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "event_token", nullable = false, unique = true) + private UUID eventToken; + + @Column(name = "organizer_token", nullable = false, unique = true) + private UUID organizerToken; + + @Column(nullable = false, length = 200) + private String title; + + @Column(length = 2000) + private String description; + + @Column(name = "date_time", nullable = false) + private OffsetDateTime dateTime; + + @Column(length = 500) + private String location; + + @Column(name = "expiry_date", nullable = false) + private LocalDate expiryDate; + + @Column(name = "created_at", nullable = false) + private OffsetDateTime createdAt; + + // Getters and setters +} +``` + +#### [x] 5.2 Spring Data Repository +**File**: `backend/src/main/java/de/fete/adapter/out/persistence/EventJpaRepository.java` +**Changes**: New file. Spring Data JPA interface. + +```java +package de.fete.adapter.out.persistence; + +import java.util.Optional; +import java.util.UUID; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface EventJpaRepository extends JpaRepository { + + Optional findByEventToken(UUID eventToken); +} +``` + +#### [x] 5.3 Persistence Adapter (Port Implementation) +**File**: `backend/src/main/java/de/fete/adapter/out/persistence/EventPersistenceAdapter.java` +**Changes**: New file. Implements `EventRepository` port, translates between domain model and JPA entity. + +```java +package de.fete.adapter.out.persistence; + +import de.fete.domain.model.Event; +import de.fete.domain.port.out.EventRepository; +import java.util.Optional; +import java.util.UUID; +import org.springframework.stereotype.Repository; + +@Repository +public class EventPersistenceAdapter implements EventRepository { + + private final EventJpaRepository jpaRepository; + + public EventPersistenceAdapter(EventJpaRepository jpaRepository) { + this.jpaRepository = jpaRepository; + } + + @Override + public Event save(Event event) { + EventJpaEntity entity = toEntity(event); + EventJpaEntity saved = jpaRepository.save(entity); + return toDomain(saved); + } + + @Override + public Optional findByEventToken(UUID eventToken) { + return jpaRepository.findByEventToken(eventToken).map(this::toDomain); + } + + private EventJpaEntity toEntity(Event event) { /* field mapping */ } + private Event toDomain(EventJpaEntity entity) { /* field mapping */ } +} +``` + +#### [x] 5.4 Integration test +**File**: `backend/src/test/java/de/fete/adapter/out/persistence/EventPersistenceAdapterTest.java` +**Changes**: New file. Integration test with Testcontainers. + +Test cases: +- Save event → returns event with generated ID +- Save event → `findByEventToken` returns same event +- `findByEventToken` with unknown UUID → empty Optional +- All fields round-trip correctly (especially `OffsetDateTime` ↔ `timestamptz` and `LocalDate` ↔ `date`) + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd backend && ./mvnw test` passes — persistence integration tests green +- [ ] `cd backend && ./mvnw checkstyle:check` passes +- [ ] ArchUnit: persistence adapter does not depend on web adapter +- [ ] Hibernate schema validation passes (JPA entity matches Liquibase migration) + +#### Manual Verification: +- [ ] JPA entity column mappings match Liquibase migration exactly +- [ ] Domain ↔ JPA entity mapping is complete (no fields missed) + +**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 6: Web Adapter & Error Handling + +### Overview +Implement the REST controller (generated `EventsApi` interface) and the `GlobalExceptionHandler` for RFC 9457 Problem Details. Remove or update any remaining health endpoint references in tests. + +### Changes Required: + +#### [x] 6.1 GlobalExceptionHandler +**File**: `backend/src/main/java/de/fete/adapter/in/web/GlobalExceptionHandler.java` +**Changes**: New file. Handles all exceptions consistently as RFC 9457 Problem Details. + +```java +package de.fete.adapter.in.web; + +import de.fete.application.service.ExpiryDateInPastException; +import java.net.URI; +import java.util.List; +import java.util.Map; +import org.springframework.http.*; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.context.request.WebRequest; +import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler; + +@RestControllerAdvice +public class GlobalExceptionHandler extends ResponseEntityExceptionHandler { + + @Override + protected ResponseEntity 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> 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); + } + + @ExceptionHandler(ExpiryDateInPastException.class) + public ResponseEntity handleExpiryDateInPast( + ExpiryDateInPastException ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( + HttpStatus.BAD_REQUEST, ex.getMessage()); + problemDetail.setTitle("Invalid Expiry Date"); + problemDetail.setType(URI.create("urn:problem-type:expiry-date-in-past")); + return ResponseEntity.badRequest() + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .body(problemDetail); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity handleAll(Exception ex) { + ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail( + HttpStatus.INTERNAL_SERVER_ERROR, + "An unexpected error occurred."); + problemDetail.setTitle("Internal Server Error"); + return ResponseEntity.internalServerError() + .contentType(MediaType.APPLICATION_PROBLEM_JSON) + .body(problemDetail); + } +} +``` + +#### [x] 6.2 EventController +**File**: `backend/src/main/java/de/fete/adapter/in/web/EventController.java` +**Changes**: New file. Implements generated `EventsApi` interface. + +```java +package de.fete.adapter.in.web; + +import de.fete.adapter.in.web.api.EventsApi; +import de.fete.adapter.in.web.model.CreateEventRequest; +import de.fete.adapter.in.web.model.CreateEventResponse; +import de.fete.domain.model.Event; +import de.fete.domain.port.in.CreateEventUseCase; +import de.fete.domain.port.in.CreateEventUseCase.CreateEventCommand; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class EventController implements EventsApi { + + private final CreateEventUseCase createEventUseCase; + + public EventController(CreateEventUseCase createEventUseCase) { + this.createEventUseCase = createEventUseCase; + } + + @Override + public ResponseEntity createEvent( + CreateEventRequest request) { + var command = new CreateEventCommand( + request.getTitle(), + request.getDescription(), + request.getDateTime(), + request.getLocation(), + request.getExpiryDate() + ); + + Event event = createEventUseCase.createEvent(command); + + var response = new CreateEventResponse(); + response.setEventToken(event.getEventToken()); + response.setOrganizerToken(event.getOrganizerToken()); + response.setTitle(event.getTitle()); + response.setDateTime(event.getDateTime()); + response.setExpiryDate(event.getExpiryDate()); + + return ResponseEntity.status(HttpStatus.CREATED).body(response); + } +} +``` + +#### [x] 6.3 Update WebConfigTest +**File**: `backend/src/test/java/de/fete/config/WebConfigTest.java` +**Changes**: Replace health endpoint references with the new `/api/events` endpoint. Keep the `/actuator/health` test. Adapt `apiPrefixNotAccessibleWithoutIt` to use `/events` instead of `/health`. + +#### [x] 6.4 Integration test for EventController +**File**: `backend/src/test/java/de/fete/adapter/in/web/EventControllerIntegrationTest.java` +**Changes**: New file. Full Spring Boot integration test with MockMvc + Testcontainers. + +Test cases: +- POST valid event → 201, response contains eventToken, organizerToken, title, dateTime, expiryDate +- POST missing title → 400, Problem Details with fieldErrors +- POST missing dateTime → 400, Problem Details with fieldErrors +- POST missing expiryDate → 400, Problem Details with fieldErrors +- POST expiry date in the past → 400, Problem Details with "expiry-date-in-past" type +- POST expiry date today → 400 +- POST with all optional fields null → 201 (description and location are optional) +- Response Content-Type for errors is `application/problem+json` + +#### [x] 6.5 Remove HealthController test (if any exists) +**File**: Verify no test file references a `HealthController` or `HealthApi` implementation. The only health tests should reference `/actuator/health`. + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd backend && ./mvnw test` passes — all controller integration tests green +- [ ] `cd backend && ./mvnw checkstyle:check` passes +- [ ] `cd backend && ./mvnw verify` passes (full verification including SpotBugs) +- [ ] ArchUnit: web adapter does not depend on persistence adapter +- [ ] Error responses use `application/problem+json` content type + +#### Manual Verification: +- [ ] `POST /api/events` with valid body returns 201 with both tokens +- [ ] Validation errors return field-level details in RFC 9457 format +- [ ] No `/api/health` endpoint exists anymore (404) +- [ ] `/actuator/health` still works + +**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 7: Frontend + +### Overview +Establish the visual design foundation (mobile-first app feel, gradients, card-style inputs, self-hosted font). Clean up Vue scaffold defaults. Implement the event creation form, localStorage composable, routing, and stub event page. + +### Changes Required: + +#### [x] 7.1 Clean up Vue scaffold defaults +**Files to remove**: +- `frontend/src/components/HelloWorld.vue` +- `frontend/src/components/TheWelcome.vue` +- `frontend/src/components/WelcomeItem.vue` +- `frontend/src/components/icons/*.vue` (all icon components) +- `frontend/src/views/AboutView.vue` +- `frontend/src/assets/base.css` (replaced by our own styles) +- `frontend/src/assets/logo.svg` (if exists) +- `frontend/src/components/__tests__/HelloWorld.spec.ts` + +**Files to update**: +- `frontend/src/App.vue` — strip to minimal app shell +- `frontend/src/views/HomeView.vue` — replace with minimal landing (see mockup) +- `frontend/src/router/index.ts` — remove `/about` route +- `frontend/src/assets/main.css` — replace with our design foundation + +#### [x] 7.2 Self-hosted font: Sora +**Directory**: `frontend/src/assets/fonts/` +**Changes**: Download Sora font from github.com/sora-xor/sora-font as WOFF2 files. Include weights: 400 (Regular), 500 (Medium), 600 (SemiBold), 700 (Bold), 800 (ExtraBold). Reference via `@font-face` in CSS. No external CDN. License: OFL 1.1. + +#### [x] 7.3 Design foundation CSS (per `spec/design-system.md`) +**File**: `frontend/src/assets/main.css` +**Changes**: Establish the design system: +- CSS custom properties for colors, gradients, spacing, border-radius, font +- Mobile-first base styles +- Gradient background +- Card-style input components +- Desktop centered-column layout (max-width ~480px) +- Responsive breakpoints +- WCAG AA contrast (verified during implementation with color tools) + +#### [x] 7.4 App shell component +**File**: `frontend/src/App.vue` +**Changes**: Minimal app shell with ``. Gradient background wrapper. Centered column for desktop. + +#### [x] 7.5 Home page (minimal landing) +**File**: `frontend/src/views/HomeView.vue` +**Changes**: Empty state with "Create Event" button linking to `/create`. Placeholder for future US-7 event overview. + +#### [x] 7.6 Event creation form +**File**: `frontend/src/views/EventCreateView.vue` +**Changes**: New file. Form with card-style inputs for all fields: +- Title (required, text input) +- Description (optional, textarea) +- Date & Time (required, datetime-local input) +- Location (optional, text input) +- Expiry Date (required, date input, min=tomorrow) + +Client-side validation: +- Required fields enforced (HTML5 `required` + JS check) +- Expiry date must be in the future (JS check) +- Show validation errors inline on the card fields + +On submit: +- Call `api.POST('/events', { body })` via openapi-fetch client +- On success: store tokens in localStorage, redirect to `/events/:token` +- On error: display field errors from RFC 9457 response + +#### [x] 7.7 localStorage composable +**File**: `frontend/src/composables/useEventStorage.ts` +**Changes**: New file. Composable for managing event data in localStorage. + +```typescript +// Stores per event token: +// - organizerToken (if created from this device) +// - title, dateTime, expiryDate (for local overview, US-7) + +interface StoredEvent { + eventToken: string + organizerToken?: string + title: string + dateTime: string + expiryDate: string +} + +export function useEventStorage() { + function saveCreatedEvent(event: StoredEvent): void { /* ... */ } + function getStoredEvents(): StoredEvent[] { /* ... */ } + function getOrganizerToken(eventToken: string): string | undefined { /* ... */ } + // ... +} +``` + +Storage key pattern: `fete:events` → JSON array of `StoredEvent`. + +#### [x] 7.8 Stub event page +**File**: `frontend/src/views/EventStubView.vue` +**Changes**: New file. Minimal confirmation page after event creation. Shows: +- "Event created!" confirmation +- Shareable event URL with copy-to-clipboard button +- Back link to home + +This is a temporary stub — US-2 replaces it with the full event page. + +#### [x] 7.9 Router update +**File**: `frontend/src/router/index.ts` +**Changes**: Replace scaffold routes with: + +```typescript +const routes = [ + { path: '/', name: 'home', component: HomeView }, + { path: '/create', name: 'create-event', component: () => import('../views/EventCreateView.vue') }, + { path: '/events/:token', name: 'event', component: () => import('../views/EventStubView.vue') }, +] +``` + +#### [x] 7.10 Frontend tests +**Files**: +- `frontend/src/composables/__tests__/useEventStorage.spec.ts` — localStorage composable tests +- `frontend/src/views/__tests__/EventCreateView.spec.ts` — form rendering, validation, submit flow + +Test cases for localStorage composable: +- `saveCreatedEvent` stores data retrievable by `getStoredEvents` +- `getOrganizerToken` returns token for known event, undefined for unknown +- Multiple events stored independently + +Test cases for EventCreateView: +- Renders all form fields +- Required fields have `required` attribute +- Submit button exists +- Form validation prevents submit with empty required fields +- (API call mocking for submit flow) + +### Success Criteria: + +#### Automated Verification: +- [ ] `cd frontend && npm run test:unit` passes — all new tests green +- [ ] `cd frontend && npm run build` succeeds — no TypeScript errors, no build errors +- [ ] `cd frontend && npx eslint src/` passes — no lint errors +- [ ] No references to removed scaffold components remain + +#### Manual Verification: +- [ ] Home page shows "Create Event" button on mobile viewport +- [ ] Create form renders with card-style inputs, gradient background +- [ ] Desktop view centers content in narrow column +- [ ] Submitting a valid form creates the event and redirects to stub page +- [ ] Stub page shows shareable link with copy button +- [ ] localStorage contains event data after creation +- [ ] Validation errors display inline on the form (both client-side and server-side) +- [ ] Self-hosted font loads (no external requests in Network tab) + +**Implementation Note**: After completing this phase, run the full verification suite, then use the `browser-interactive-testing` skill for visual verification of the form, desktop/mobile layouts, and the complete creation flow. + +--- + +## Testing Strategy + +### Unit Tests: +- `EventServiceTest` — business logic, token generation, expiry validation +- `useEventStorage.spec.ts` — localStorage operations +- `EventCreateView.spec.ts` — form rendering, validation + +### Integration Tests: +- `EventPersistenceAdapterTest` — JPA ↔ PostgreSQL round-trip (Testcontainers) +- `EventControllerIntegrationTest` — full HTTP request/response cycle (MockMvc + Testcontainers) + +### Architecture Tests: +- Existing ArchUnit tests validate all hexagonal layer boundaries automatically + +### Manual Testing Steps: +1. Start backend + frontend in dev mode +2. Navigate to `/` — see landing page with "Create Event" button +3. Navigate to `/create` — fill out form, submit +4. Verify redirect to `/events/:token` stub page +5. Check localStorage for stored event data +6. Submit invalid form — verify error messages +7. Check Network tab — no external requests (fonts, CDNs, etc.) +8. Test on mobile viewport — verify app-native feel + +## Performance Considerations + +- UUID indexes on `event_token` and `organizer_token` for O(log n) lookups +- `expiry_date` index prepares for US-12 cleanup queries +- Lazy-loaded route components (`EventCreateView`, `EventStubView`) for code splitting +- Self-hosted font: WOFF2 format for minimal file size + +## References + +- `docs/agents/research/2026-03-04-us1-create-event.md` — US-1 codebase research +- `docs/agents/research/2026-03-04-rfc9457-problem-details.md` — Error handling research +- `docs/agents/research/2026-03-04-datetime-best-practices.md` — Date/time type mapping +- `spec/design-system.md` — Permanent design spec (palette, font, component patterns) +- `spec/userstories.md:21-41` — US-1 specification +- `spec/implementation-phases.md` — Implementation order diff --git a/docs/agents/plan/2026-03-05-us1-post-review-fixes.md b/docs/agents/plan/2026-03-05-us1-post-review-fixes.md new file mode 100644 index 0000000..75b97a0 --- /dev/null +++ b/docs/agents/plan/2026-03-05-us1-post-review-fixes.md @@ -0,0 +1,199 @@ +# 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 `{{ errors.title }}` 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 +{{ errors.title }} +``` + +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 + +{{ errors.title }} +``` + +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 diff --git a/docs/agents/plan/2026-03-05-us1-review-fixes.md b/docs/agents/plan/2026-03-05-us1-review-fixes.md new file mode 100644 index 0000000..d8ae353 --- /dev/null +++ b/docs/agents/plan/2026-03-05-us1-review-fixes.md @@ -0,0 +1,109 @@ +# 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 (01–08) +- **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 `Vite App`. + +**Fix:** Change to `fete`. Also set `lang="en"` on the `` 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 `` 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 diff --git a/docs/agents/research/2026-03-04-datetime-best-practices.md b/docs/agents/research/2026-03-04-datetime-best-practices.md new file mode 100644 index 0000000..17fc1cd --- /dev/null +++ b/docs/agents/research/2026-03-04-datetime-best-practices.md @@ -0,0 +1,107 @@ +--- +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/) diff --git a/docs/agents/research/2026-03-04-rfc9457-problem-details.md b/docs/agents/research/2026-03-04-rfc9457-problem-details.md new file mode 100644 index 0000000..cba555c --- /dev/null +++ b/docs/agents/research/2026-03-04-rfc9457-problem-details.md @@ -0,0 +1,202 @@ +--- +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 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> 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) diff --git a/docs/agents/research/2026-03-04-sans-serif-fonts.md b/docs/agents/research/2026-03-04-sans-serif-fonts.md new file mode 100644 index 0000000..b26d120 --- /dev/null +++ b/docs/agents/research/2026-03-04-sans-serif-fonts.md @@ -0,0 +1,404 @@ +# 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 (Thin–Black) | ✅ Recommended | +| Plus Jakarta Sans | OFL-1.1 | Geometric, modern | 7 (ExtraLight–ExtraBold) | ✅ Recommended | +| Outfit | OFL-1.1 | Geometric | 9 (Thin–Black) | ✅ Recommended | +| Space Grotesk | OFL-1.1 | Neo-grotesque, distinctive | 5 (Light–Bold) | ✅ Recommended | +| Manrope | OFL-1.1 | Geometric, humanist | 7 (ExtraLight–ExtraBold) | ✅ Recommended | +| DM Sans | OFL-1.1 | Geometric, low-contrast | 9 (Thin–Black) | ✅ Recommended | +| Sora | OFL-1.1 | Geometric | 8 (Thin–ExtraBold) | ✅ 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 14–16px 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 (Thin–Black) +- 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 12–14px +- Full weight range (Thin–Black) +- 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, 5–10 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 12–14px) +- 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) diff --git a/docs/agents/research/2026-03-04-us1-create-event.md b/docs/agents/research/2026-03-04-us1-create-event.md new file mode 100644 index 0000000..006b424 --- /dev/null +++ b/docs/agents/research/2026-03-04-us1-create-event.md @@ -0,0 +1,195 @@ +--- +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).