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 <noreply@anthropic.com>
This commit is contained in:
1152
docs/agents/plan/2026-03-04-us1-create-event.md
Normal file
1152
docs/agents/plan/2026-03-04-us1-create-event.md
Normal file
File diff suppressed because it is too large
Load Diff
199
docs/agents/plan/2026-03-05-us1-post-review-fixes.md
Normal file
199
docs/agents/plan/2026-03-05-us1-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
docs/agents/plan/2026-03-05-us1-review-fixes.md
Normal file
109
docs/agents/plan/2026-03-05-us1-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
|
||||||
107
docs/agents/research/2026-03-04-datetime-best-practices.md
Normal file
107
docs/agents/research/2026-03-04-datetime-best-practices.md
Normal file
@@ -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/)
|
||||||
202
docs/agents/research/2026-03-04-rfc9457-problem-details.md
Normal file
202
docs/agents/research/2026-03-04-rfc9457-problem-details.md
Normal file
@@ -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<Object> handleMethodArgumentNotValid(
|
||||||
|
MethodArgumentNotValidException ex,
|
||||||
|
HttpHeaders headers,
|
||||||
|
HttpStatusCode status,
|
||||||
|
WebRequest request) {
|
||||||
|
|
||||||
|
ProblemDetail problemDetail = ex.getBody();
|
||||||
|
problemDetail.setTitle("Validation Failed");
|
||||||
|
problemDetail.setType(URI.create("urn:problem-type:validation-error"));
|
||||||
|
|
||||||
|
List<Map<String, String>> fieldErrors = ex.getBindingResult()
|
||||||
|
.getFieldErrors()
|
||||||
|
.stream()
|
||||||
|
.map(fe -> Map.of(
|
||||||
|
"field", fe.getField(),
|
||||||
|
"message", fe.getDefaultMessage()
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
problemDetail.setProperty("fieldErrors", fieldErrors);
|
||||||
|
return handleExceptionInternal(ex, problemDetail, headers, status, request);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Resulting response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"type": "urn:problem-type:validation-error",
|
||||||
|
"title": "Validation Failed",
|
||||||
|
"status": 400,
|
||||||
|
"detail": "Invalid request content.",
|
||||||
|
"instance": "/api/events",
|
||||||
|
"fieldErrors": [
|
||||||
|
{ "field": "title", "message": "must not be blank" },
|
||||||
|
{ "field": "expiryDate", "message": "must be a future date" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### OpenAPI Schema Definition
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
components:
|
||||||
|
schemas:
|
||||||
|
ProblemDetail:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
type:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
default: "about:blank"
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
type: integer
|
||||||
|
detail:
|
||||||
|
type: string
|
||||||
|
instance:
|
||||||
|
type: string
|
||||||
|
format: uri
|
||||||
|
additionalProperties: true
|
||||||
|
|
||||||
|
ValidationProblemDetail:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/components/schemas/ProblemDetail'
|
||||||
|
- type: object
|
||||||
|
properties:
|
||||||
|
fieldErrors:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
field:
|
||||||
|
type: string
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- field
|
||||||
|
- message
|
||||||
|
|
||||||
|
responses:
|
||||||
|
BadRequest:
|
||||||
|
description: Validation failed
|
||||||
|
content:
|
||||||
|
application/problem+json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ValidationProblemDetail'
|
||||||
|
NotFound:
|
||||||
|
description: Resource not found
|
||||||
|
content:
|
||||||
|
application/problem+json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ProblemDetail'
|
||||||
|
```
|
||||||
|
|
||||||
|
Use media type `application/problem+json` in response definitions. Set `additionalProperties: true` on the base schema.
|
||||||
|
|
||||||
|
### Frontend Consumption (openapi-fetch)
|
||||||
|
|
||||||
|
openapi-fetch uses a discriminated union for responses:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const { data, error } = await client.POST('/api/events', { body: eventData })
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
// `error` is typed from the OpenAPI error response schema
|
||||||
|
console.log(error.title) // "Validation Failed"
|
||||||
|
console.log(error.fieldErrors) // [{ field: "title", message: "..." }]
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// `data` is the typed success response
|
||||||
|
```
|
||||||
|
|
||||||
|
The `error` object is already typed from the generated schema — no manual type assertions needed for defined error shapes.
|
||||||
|
|
||||||
|
### Known Pitfalls
|
||||||
|
|
||||||
|
| Pitfall | Description | Mitigation |
|
||||||
|
|---------|-------------|------------|
|
||||||
|
| **Inconsistent formats** | Exceptions escaping to Spring Boot's `BasicErrorController` return legacy format (`timestamp`, `error`, `path`), not Problem Details. | Add a catch-all `@ExceptionHandler(Exception.class)` in the `@RestControllerAdvice`. |
|
||||||
|
| **`server.error.include-*` ignored** | When Problem Details is active, these properties have no effect. | Control content via `ProblemDetail` directly. |
|
||||||
|
| **Validation errors hidden by default** | Spring returns only `"Invalid request content."` without field details. | Override `handleMethodArgumentNotValid` explicitly. |
|
||||||
|
| **Content negotiation** | `application/problem+json` is only returned when the client accepts it. `openapi-fetch` sends `Accept: application/json` which Spring considers compatible. | No action needed for SPA clients. |
|
||||||
|
| **`about:blank` semantics** | With `type: "about:blank"`, `title` must match the HTTP status phrase. Custom titles require a custom `type` URI. | Use `urn:problem-type:*` URIs for custom problem types. |
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
- [RFC 9457 Full Text](https://www.rfc-editor.org/rfc/rfc9457.html)
|
||||||
|
- [Spring Framework Docs: Error Responses](https://docs.spring.io/spring-framework/reference/web/webmvc/mvc-ann-rest-exceptions.html)
|
||||||
|
- [Swagger Blog: Problem Details RFC 9457](https://swagger.io/blog/problem-details-rfc9457-doing-api-errors-well/)
|
||||||
|
- [Baeldung: Returning Errors Using ProblemDetail](https://www.baeldung.com/spring-boot-return-errors-problemdetail)
|
||||||
|
- [SivaLabs: Spring Boot 3 Error Reporting](https://www.sivalabs.in/blog/spring-boot-3-error-reporting-using-problem-details/)
|
||||||
|
- [Spring Boot Issue #43850: Render global errors as Problem Details](https://github.com/spring-projects/spring-boot/issues/43850)
|
||||||
404
docs/agents/research/2026-03-04-sans-serif-fonts.md
Normal file
404
docs/agents/research/2026-03-04-sans-serif-fonts.md
Normal file
@@ -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)
|
||||||
195
docs/agents/research/2026-03-04-us1-create-event.md
Normal file
195
docs/agents/research/2026-03-04-us1-create-event.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).
|
||||||
Reference in New Issue
Block a user