Migrate project artifacts to spec-kit format
- Move cross-cutting docs (personas, design system, implementation phases, Ideen.md) to .specify/memory/ - Move cross-cutting research and plans to .specify/memory/research/ and .specify/memory/plans/ - Extract 5 setup tasks from spec/setup-tasks.md into individual specs/001-005/spec.md files with spec-kit template format - Extract 20 user stories from spec/userstories.md into individual specs/006-026/spec.md files with spec-kit template format - Relocate feature-specific research and plan docs into specs/[feature]/ - Add spec-kit constitution, templates, scripts, and slash commands - Slim down CLAUDE.md to Claude-Code-specific config, delegate principles to .specify/memory/constitution.md - Update ralph.sh with stream-json output and per-iteration logging - Delete old spec/ and docs/agents/ directories - Gitignore Ralph iteration JSONL logs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
199
specs/006-create-event/plan-post-review-fixes.md
Normal file
199
specs/006-create-event/plan-post-review-fixes.md
Normal file
@@ -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 `<span class="field-error" role="alert">{{ errors.title }}</span>` that is always in the DOM, even when empty. Screen readers may announce empty `role="alert"` elements.
|
||||
|
||||
**File:** `frontend/src/views/EventCreateView.vue`
|
||||
|
||||
**Fix:** Use `v-if` to conditionally render error spans:
|
||||
```html
|
||||
<span v-if="errors.title" class="field-error" role="alert">{{ errors.title }}</span>
|
||||
```
|
||||
|
||||
Apply to all 5 field error spans (title, description, dateTime, location, expiryDate).
|
||||
|
||||
**Note:** This removes the `min-height: 1.2em` layout reservation. Accept the layout shift as a trade-off for accessibility, OR add a wrapper div with `min-height` that doesn't carry `role="alert"`.
|
||||
|
||||
**Verification:** `cd frontend && npm run test:unit` — existing tests use `.querySelector('[role="alert"]')` so they may need adjustment since empty alerts will no longer be in the DOM.
|
||||
|
||||
## Task 3: Frontend A11y — aria-invalid and aria-describedby on fields [x]
|
||||
|
||||
**Problem:** When a field fails validation, there is no `aria-invalid="true"` or `aria-describedby` linking the input to its error message. Assistive technologies cannot associate errors with fields.
|
||||
|
||||
**File:** `frontend/src/views/EventCreateView.vue`
|
||||
|
||||
**Fix:**
|
||||
1. Add unique `id` to each error span (e.g., `id="title-error"`)
|
||||
2. Add `:aria-describedby="errors.title ? 'title-error' : undefined"` to each input
|
||||
3. Add `:aria-invalid="!!errors.title"` to each input
|
||||
|
||||
Example for title:
|
||||
```html
|
||||
<input
|
||||
id="title"
|
||||
v-model="form.title"
|
||||
type="text"
|
||||
class="form-field"
|
||||
required
|
||||
maxlength="200"
|
||||
placeholder="What's the event?"
|
||||
:aria-invalid="!!errors.title"
|
||||
:aria-describedby="errors.title ? 'title-error' : undefined"
|
||||
/>
|
||||
<span v-if="errors.title" id="title-error" class="field-error" role="alert">{{ errors.title }}</span>
|
||||
```
|
||||
|
||||
Apply the same pattern to all 5 fields (title, description, dateTime, location, expiryDate).
|
||||
|
||||
**Verification:** `cd frontend && npm run test:unit`
|
||||
|
||||
## Task 4: Frontend A11y — Error text contrast [x]
|
||||
|
||||
**Problem:** White (`#fff`) error text on the pink gradient start (`#F06292`) has a contrast ratio of only 3.06:1, which fails WCAG AA for small text (0.8rem). The project statute requires WCAG AA compliance.
|
||||
|
||||
**File:** `frontend/src/assets/main.css`
|
||||
|
||||
**Fix options (pick one):**
|
||||
- **Option A:** Use a light yellow/cream color like `#FFF9C4` or `#FFECB3` that has higher contrast on the gradient
|
||||
- **Option B:** Add a subtle dark text-shadow to the error text: `text-shadow: 0 1px 2px rgba(0,0,0,0.3)`
|
||||
- **Option C:** Make error text slightly larger/bolder to qualify for WCAG AA-large (18px+ or 14px+ bold)
|
||||
|
||||
**Recommended:** Option C — bump `.field-error` to `font-size: 0.85rem; font-weight: 600;` which at 600 weight qualifies for AA-large text at 14px+ (0.85rem ≈ 13.6px — close but may not quite qualify). Alternatively combine with option B for safety.
|
||||
|
||||
**Note:** Verify the final choice against the design system spec in `spec/design-system.md`. The spec notes that gradient start only passes AA-large. The error text must work across the full gradient.
|
||||
|
||||
**Verification:** Manual contrast check with a tool like WebAIM contrast checker.
|
||||
|
||||
## Task 5: Test — Happy-path submission in EventCreateView [x]
|
||||
|
||||
**Problem:** No test verifies successful form submission (the most important behavior).
|
||||
|
||||
**File:** `frontend/src/views/__tests__/EventCreateView.spec.ts`
|
||||
|
||||
**Fix:** Add a test that:
|
||||
1. Mocks `api.POST` to return `{ data: { eventToken: 'abc', organizerToken: 'xyz', title: 'Test', dateTime: '...', expiryDate: '...' } }`
|
||||
2. Fills all required fields
|
||||
3. Submits the form
|
||||
4. Asserts `api.POST` was called with the correct body
|
||||
5. Asserts navigation to `/events/abc` occurred
|
||||
6. Asserts `saveCreatedEvent` was called (need to mock `useEventStorage`)
|
||||
|
||||
**Note:** `useEventStorage` must be mocked. Use `vi.mock('@/composables/useEventStorage')`.
|
||||
|
||||
**Verification:** `cd frontend && npm run test:unit`
|
||||
|
||||
## Task 6: Test — EventStubView component tests [x]
|
||||
|
||||
**Problem:** No test file exists for `EventStubView.vue`.
|
||||
|
||||
**New file:** `frontend/src/views/__tests__/EventStubView.spec.ts`
|
||||
|
||||
**Fix:** Create tests covering:
|
||||
1. Renders the event URL based on route param `:token`
|
||||
2. Shows the correct share URL (`window.location.origin + /events/:token`)
|
||||
3. Copy button exists
|
||||
4. Back link navigates to home
|
||||
|
||||
**Note:** Read `frontend/src/views/EventStubView.vue` first to understand the component structure.
|
||||
|
||||
**Verification:** `cd frontend && npm run test:unit`
|
||||
|
||||
## Task 7: Test — Server-side field errors in EventCreateView [x]
|
||||
|
||||
**Problem:** The `fieldErrors` handling branch (lines 184-196 of EventCreateView.vue) is untested.
|
||||
|
||||
**File:** `frontend/src/views/__tests__/EventCreateView.spec.ts`
|
||||
|
||||
**Fix:** Add a test that:
|
||||
1. Mocks `api.POST` to return `{ error: { fieldErrors: [{ field: 'title', message: 'Title already taken' }] } }`
|
||||
2. Fills all required fields and submits
|
||||
3. Asserts the title field error shows "Title already taken"
|
||||
4. Asserts other field errors are empty
|
||||
|
||||
**Verification:** `cd frontend && npm run test:unit`
|
||||
|
||||
## Task 8: Fix border-radius on EventStubView copy button [x]
|
||||
|
||||
**Problem:** `border-radius: 10px` is hardcoded instead of using the design token `var(--radius-button)` (14px).
|
||||
|
||||
**File:** `frontend/src/views/EventStubView.vue`
|
||||
|
||||
**Fix:** Replace `border-radius: 10px` with `border-radius: var(--radius-button)` in the `.stub__copy` CSS class.
|
||||
|
||||
**Verification:** Visual check.
|
||||
|
||||
## Task 9: Add 404 catch-all route user story [x]
|
||||
|
||||
**Problem:** Navigating to an unknown path shows a blank page.
|
||||
|
||||
**File:** `spec/userstories.md`
|
||||
|
||||
**Fix:** Add a new user story for a 404/catch-all route. Something like:
|
||||
|
||||
```
|
||||
### US-X: 404 Page
|
||||
|
||||
As a user who navigates to a non-existent URL, I want to see a helpful error page so I can find my way back.
|
||||
|
||||
Acceptance Criteria:
|
||||
- [ ] Unknown routes show a "Page not found" message
|
||||
- [ ] The page includes a link back to the home page
|
||||
- [ ] The page follows the design system
|
||||
```
|
||||
|
||||
Read the existing user stories first to match the format.
|
||||
|
||||
**Verification:** N/A (spec only).
|
||||
|
||||
## Task 10: EventStubView silent clipboard failure [x]
|
||||
|
||||
**Problem:** In `EventStubView.vue`, the `catch` block on `navigator.clipboard.writeText()` is empty. If clipboard is unavailable (HTTP, older browser), the user gets no feedback.
|
||||
|
||||
**File:** `frontend/src/views/EventStubView.vue`
|
||||
|
||||
**Fix:** In the catch block, show a fallback message (e.g., set `copied` text to "Copy failed" or select the URL text for manual copying).
|
||||
|
||||
**Verification:** `cd frontend && npm run test:unit`
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Task 1 (Clock injection — backend, independent)
|
||||
2. Tasks 2 + 3 (A11y fixes — can be done together since they touch the same file)
|
||||
3. Task 4 (Contrast fix — CSS only)
|
||||
4. Tasks 5 + 7 (EventCreateView tests — same test file)
|
||||
5. Task 6 (EventStubView tests — new file)
|
||||
6. Tasks 8 + 10 (EventStubView fixes — same file)
|
||||
7. Task 9 (User story — spec only)
|
||||
8. Run all tests: `cd backend && ./mvnw test` and `cd frontend && npm run test:unit`
|
||||
|
||||
## Constraints
|
||||
|
||||
- TDD: write/update tests first, then fix (where applicable)
|
||||
- Follow existing code style and patterns
|
||||
- Do not refactor unrelated code
|
||||
- Do not add dependencies
|
||||
- Update design system spec if contrast solution changes the spec
|
||||
109
specs/006-create-event/plan-review-fixes.md
Normal file
109
specs/006-create-event/plan-review-fixes.md
Normal file
@@ -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 `<title>Vite App</title>`.
|
||||
|
||||
**Fix:** Change to `<title>fete</title>`. Also set `lang="en"` on the `<html>` tag (line 2 currently has `lang=""`).
|
||||
|
||||
**File:** `frontend/index.html`
|
||||
|
||||
### Fix 4: Favicon (Minor — Low)
|
||||
|
||||
**Problem:** The favicon is the Vite default. The project should either have its own favicon or remove the link entirely.
|
||||
|
||||
**Fix:** For now, remove the `<link rel="icon" href="/favicon.ico">` line and delete `frontend/public/favicon.ico` if it exists. A proper favicon can be added later as part of branding work.
|
||||
|
||||
**File:** `frontend/index.html`, `frontend/public/favicon.ico`
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. Fix 3 + Fix 4 (trivial, `index.html` + favicon cleanup)
|
||||
2. Fix 1 (reactive error clearing + test)
|
||||
3. Fix 2 (try-catch + test)
|
||||
4. Run all frontend tests: `cd frontend && npm run test:unit`
|
||||
5. Verify visually with `browser-interactive-testing` skill:
|
||||
- Start dev server, open `/create`
|
||||
- Submit empty → errors appear
|
||||
- Fill title → title error clears, others remain
|
||||
- Fill all fields → all errors gone
|
||||
- Submit with no backend → "Could not reach the server" message appears
|
||||
|
||||
## Constraints
|
||||
|
||||
- Follow existing code style and patterns in `EventCreateView.vue`
|
||||
- Do not refactor unrelated code
|
||||
- Do not add dependencies
|
||||
- Tests must follow existing test patterns in `EventCreateView.spec.ts`
|
||||
- TDD: write/update tests first, then fix
|
||||
1152
specs/006-create-event/plan.md
Normal file
1152
specs/006-create-event/plan.md
Normal file
File diff suppressed because it is too large
Load Diff
195
specs/006-create-event/research.md
Normal file
195
specs/006-create-event/research.md
Normal file
@@ -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).
|
||||
97
specs/006-create-event/spec.md
Normal file
97
specs/006-create-event/spec.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Feature Specification: Create an Event
|
||||
|
||||
**Feature**: `006-create-event`
|
||||
**Created**: 2026-03-06
|
||||
**Status**: Approved
|
||||
**Source**: Migrated from spec/userstories.md
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 - Create Event with Required Fields (Priority: P1)
|
||||
|
||||
An event organizer fills in the event creation form with a title, date/time, and mandatory expiry date, submits it, and is redirected to the new event page. The server returns both an event token and an organizer token. The organizer token is stored in localStorage on the current device.
|
||||
|
||||
**Why this priority**: Core action of the entire application. All other stories depend on event creation existing. Without it, there is nothing to view, RSVP to, or manage.
|
||||
|
||||
**Independent Test**: Can be fully tested by submitting the creation form and verifying the redirect to the event page, localStorage state, and the server-side persistence.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the organizer opens the event creation form, **When** they fill in title, date/time, and expiry date and submit, **Then** the server stores the event and returns a UUID event token and a separate UUID organizer token in the response.
|
||||
2. **Given** the event is created successfully, **When** the organizer is redirected, **Then** they land on the event page identified by the event token.
|
||||
3. **Given** the event is created successfully, **When** the organizer token is received, **Then** it is stored in localStorage to grant organizer access on this device.
|
||||
4. **Given** the event is created successfully, **When** the response is processed, **Then** the event token, title, and date are also stored in localStorage so the local event overview (US-7) can display the event without server contact.
|
||||
5. **Given** the event creation form, **When** it is opened, **Then** no account, login, or personal data is required.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Optional Fields (Priority: P2)
|
||||
|
||||
An organizer can optionally provide a description and location when creating an event. These fields are not required but are stored alongside the event when provided.
|
||||
|
||||
**Why this priority**: Enriches the event page but does not block the core creation flow.
|
||||
|
||||
**Independent Test**: Can be tested by creating an event with and without description/location, verifying both cases result in a valid event.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the creation form, **When** the organizer leaves description and location blank and submits, **Then** the event is created successfully without those fields.
|
||||
2. **Given** the creation form, **When** the organizer fills in description and location and submits, **Then** the event is created and those fields are stored.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Expiry Date Validation (Priority: P2)
|
||||
|
||||
The expiry date field is mandatory and must be set to a date in the future. The organizer cannot submit the form without providing it, and cannot set a past date.
|
||||
|
||||
**Why this priority**: Mandatory expiry is a core privacy guarantee (linked to US-12 data deletion). The field must always be valid at creation time.
|
||||
|
||||
**Independent Test**: Can be tested by attempting to submit the form without an expiry date, or with a past expiry date, and verifying the form is rejected with a clear validation message.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the creation form, **When** the organizer attempts to submit without an expiry date, **Then** the submission is rejected and the expiry date field is flagged as required.
|
||||
2. **Given** the creation form, **When** the organizer enters a past date as the expiry date and submits, **Then** the submission is rejected with a clear validation message.
|
||||
3. **Given** the creation form, **When** the organizer enters a future date as the expiry date and submits, **Then** the event is created successfully.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- What happens when the organizer submits the form with only whitespace in the title?
|
||||
- How does the system handle the expiry date set to exactly today (midnight boundary)?
|
||||
- What if localStorage is unavailable or full when storing the organizer token?
|
||||
- What happens if the server returns an error during event creation (network failure, server error)?
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST accept event creation with: title (required), description (optional), date and time (required), location (optional), expiry date (required).
|
||||
- **FR-002**: System MUST reject event creation if title is missing.
|
||||
- **FR-003**: System MUST reject event creation if date/time is missing.
|
||||
- **FR-004**: System MUST reject event creation if expiry date is missing or is not in the future.
|
||||
- **FR-005**: System MUST generate a unique, non-guessable UUID event token upon successful event creation.
|
||||
- **FR-006**: System MUST generate a separate unique, non-guessable UUID organizer token upon successful event creation.
|
||||
- **FR-007**: System MUST return both tokens in the creation response.
|
||||
- **FR-008**: Frontend MUST store the organizer token in localStorage to grant organizer access on the current device.
|
||||
- **FR-009**: Frontend MUST store the event token, title, and date in localStorage alongside the organizer token.
|
||||
- **FR-010**: Frontend MUST redirect the organizer to the event page after successful creation.
|
||||
- **FR-011**: System MUST NOT require any account, login, or personal data to create an event.
|
||||
- **FR-012**: The event MUST NOT be discoverable except via its direct link (no public listing).
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **Event**: Represents a scheduled gathering. Key attributes: event token (UUID, public), organizer token (UUID, secret), title, description, date/time, location, expiry date, creation timestamp.
|
||||
- **Organizer Token**: A secret UUID stored in localStorage on the device where the event was created. Used to authenticate organizer actions on that device.
|
||||
- **Event Token**: A public UUID embedded in the event URL. Used by guests to access the event page.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: An organizer can complete the event creation form and be redirected to the new event page in a single form submission.
|
||||
- **SC-002**: After creation, the organizer token and event metadata are present in localStorage on the current device.
|
||||
- **SC-003**: An event created without description or location renders correctly on the event page without errors.
|
||||
- **SC-004**: Submitting the form with a missing or past expiry date displays a clear, user-readable validation error.
|
||||
- **SC-005**: The event is not accessible via any URL other than the one containing the event token.
|
||||
Reference in New Issue
Block a user