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 <noreply@anthropic.com>
44 KiB
date, git_commit, branch, topic, tags, status
| date | git_commit | branch | topic | tags | status | ||||
|---|---|---|---|---|---|---|---|---|---|
| 2026-03-04T21:41:13+00:00 | 91e566efea |
master | US-1: Create an Event |
|
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
/healthendpoint (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/apiapplied to all@RestControllerbeansWebConfig.java:23-39— SPA fallback routes non-API requests toindex.htmlapplication.properties:4— JPAddl-auto=validate(Liquibase manages schema)- OpenAPI generator:
interfaceOnly=true,useBeanValidation=true, packagesde.fete.adapter.in.web.api/.model - Frontend types generated via
npm run generate:apiintosrc/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:
POST /api/eventsaccepts a JSON body with title, description, dateTime, location, expiryDate- Server validates input (Bean Validation), creates the event with two UUIDs (event token, organizer token), persists to PostgreSQL, returns both tokens
- Error responses follow RFC 9457 Problem Details format with field-level validation errors
- Frontend shows a mobile-first event creation form at
/create - On success, localStorage stores organizer token + event metadata, browser redirects to
/events/:token(stub page) - Root page
/shows a minimal landing with "Create Event" button - The scaffolding health endpoint is removed from the OpenAPI spec
- Vue scaffold defaults are cleaned up
- 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:
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:
- OpenAPI Spec & Cleanup — define the contract, remove scaffolding health endpoint
- Database Migration — create the
eventstable - Domain & Ports —
Eventmodel,CreateEventUseCase,EventRepository - Application Service —
EventServiceimplementing the use case - Persistence Adapter — JPA entity, Spring Data repository
- Web Adapter & Error Handling — Controller,
GlobalExceptionHandler - 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.
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 compilesucceeds — generatesEventsApiinterface, request/response modelscd frontend && npm run generate:apisucceeds —schema.d.tscontains event typescd backend && ./mvnw testpasses — no test references removed health endpoint- No
HealthApi.javaorHealthResponse.javain generated output
Manual Verification:
- OpenAPI spec is valid (no syntax errors)
- Generated
EventsApiinterface hascreateEventmethod 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 version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="001-create-events-table" author="fete">
<createTable tableName="events">
<column name="id" type="bigserial" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="event_token" type="uuid">
<constraints nullable="false" unique="true"/>
</column>
<column name="organizer_token" type="uuid">
<constraints nullable="false" unique="true"/>
</column>
<column name="title" type="varchar(200)">
<constraints nullable="false"/>
</column>
<column name="description" type="varchar(2000)"/>
<column name="date_time" type="timestamptz">
<constraints nullable="false"/>
</column>
<column name="location" type="varchar(500)"/>
<column name="expiry_date" type="date">
<constraints nullable="false"/>
</column>
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
<constraints nullable="false"/>
</column>
</createTable>
<createIndex tableName="events" indexName="idx_events_event_token">
<column name="event_token"/>
</createIndex>
<createIndex tableName="events" indexName="idx_events_expiry_date">
<column name="expiry_date"/>
</createIndex>
</changeSet>
</databaseChangeLog>
[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.
<include file="db/changelog/001-create-events-table.xml"/>
Success Criteria:
Automated Verification:
cd backend && ./mvnw testpasses — Testcontainers spins up PostgreSQL, Liquibase runs migration, Hibernate validates schema- Index on
event_tokenexists (primary lookup path for public access) - Index on
expiry_dateexists (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.
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.
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).
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<Event> findByEventToken(UUID eventToken);
}
Success Criteria:
Automated Verification:
cd backend && ./mvnw testpasses — ArchUnit validates:Eventindomain.modelhas no Spring/adapter dependenciesCreateEventUseCaseandEventRepositoryare interfaces- No dependency violations between layers
cd backend && ./mvnw checkstyle:checkpasses
Manual Verification:
- Domain model fields match the database schema from Phase 2
CreateEventCommandrecord 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.
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".
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
saveis called exactly once
Success Criteria:
Automated Verification:
cd backend && ./mvnw testpasses — allEventServiceTesttests greencd backend && ./mvnw checkstyle:checkpasses- ArchUnit:
EventServiceinapplication.servicemay depend ondomain.modelanddomain.portbut 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.
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.
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<EventJpaEntity, Long> {
Optional<EventJpaEntity> 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.
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<Event> 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 →
findByEventTokenreturns same event findByEventTokenwith unknown UUID → empty Optional- All fields round-trip correctly (especially
OffsetDateTime↔timestamptzandLocalDate↔date)
Success Criteria:
Automated Verification:
cd backend && ./mvnw testpasses — persistence integration tests greencd backend && ./mvnw checkstyle:checkpasses- 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.
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<Object> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatusCode status,
WebRequest request) {
ProblemDetail problemDetail = ex.getBody();
problemDetail.setTitle("Validation Failed");
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
List<Map<String, String>> fieldErrors = ex.getBindingResult()
.getFieldErrors()
.stream()
.map(fe -> Map.of(
"field", fe.getField(),
"message", fe.getDefaultMessage()
))
.toList();
problemDetail.setProperty("fieldErrors", fieldErrors);
return handleExceptionInternal(ex, problemDetail, headers, status, request);
}
@ExceptionHandler(ExpiryDateInPastException.class)
public ResponseEntity<ProblemDetail> 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<ProblemDetail> 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.
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<CreateEventResponse> 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 testpasses — all controller integration tests greencd backend && ./mvnw checkstyle:checkpassescd backend && ./mvnw verifypasses (full verification including SpotBugs)- ArchUnit: web adapter does not depend on persistence adapter
- Error responses use
application/problem+jsoncontent type
Manual Verification:
POST /api/eventswith valid body returns 201 with both tokens- Validation errors return field-level details in RFC 9457 format
- No
/api/healthendpoint exists anymore (404) /actuator/healthstill 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.vuefrontend/src/components/TheWelcome.vuefrontend/src/components/WelcomeItem.vuefrontend/src/components/icons/*.vue(all icon components)frontend/src/views/AboutView.vuefrontend/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 shellfrontend/src/views/HomeView.vue— replace with minimal landing (see mockup)frontend/src/router/index.ts— remove/aboutroutefrontend/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 <router-view>. 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.
// 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:
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 testsfrontend/src/views/__tests__/EventCreateView.spec.ts— form rendering, validation, submit flow
Test cases for localStorage composable:
saveCreatedEventstores data retrievable bygetStoredEventsgetOrganizerTokenreturns token for known event, undefined for unknown- Multiple events stored independently
Test cases for EventCreateView:
- Renders all form fields
- Required fields have
requiredattribute - 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:unitpasses — all new tests greencd frontend && npm run buildsucceeds — no TypeScript errors, no build errorscd 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 validationuseEventStorage.spec.ts— localStorage operationsEventCreateView.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:
- Start backend + frontend in dev mode
- Navigate to
/— see landing page with "Create Event" button - Navigate to
/create— fill out form, submit - Verify redirect to
/events/:tokenstub page - Check localStorage for stored event data
- Submit invalid form — verify error messages
- Check Network tab — no external requests (fonts, CDNs, etc.)
- Test on mobile viewport — verify app-native feel
Performance Considerations
- UUID indexes on
event_tokenandorganizer_tokenfor O(log n) lookups expiry_dateindex 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 researchdocs/agents/research/2026-03-04-rfc9457-problem-details.md— Error handling researchdocs/agents/research/2026-03-04-datetime-best-practices.md— Date/time type mappingspec/design-system.md— Permanent design spec (palette, font, component patterns)spec/userstories.md:21-41— US-1 specificationspec/implementation-phases.md— Implementation order