From 9bab2801d8f73f6379c02b9e3d3f95bdce42f811 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sat, 16 May 2026 00:09:52 +0200 Subject: [PATCH] docs(10): research events phase --- .planning/phases/10-events/10-RESEARCH.md | 207 ++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 .planning/phases/10-events/10-RESEARCH.md diff --git a/.planning/phases/10-events/10-RESEARCH.md b/.planning/phases/10-events/10-RESEARCH.md new file mode 100644 index 0000000..8f1c336 --- /dev/null +++ b/.planning/phases/10-events/10-RESEARCH.md @@ -0,0 +1,207 @@ +--- +phase: 10 +slug: events +status: complete +researched: 2026-05-16 +requirements: + - EVENT-01 + - EVENT-02 + - EVENT-03 + - EVENT-04 + - EVENT-05 +--- + +# Phase 10 - Events Research + +## Research Complete + +Phase 10 should add a first functional events slice to the Go + HTMX application: tablo-scoped scheduled events, a simple month calendar in a new Events tab, inline create/edit/delete flows, and a user-scoped query surface for Phase 11's individual planning page. + +The implementation should follow the recent Phase 9 etapes pattern: add the database model and SQLC queries first, add protected tablo child routes, render tab-local HTMX fragments, and refresh the whole tab after mutations. + +## Existing System Fit + +### Tablo Detail Tabs + +`backend/templates/tablos.templ` already has the main integration point. `TabloDetailPage` renders the tab navigation and a `#tab-content` target. Existing tabs use HTMX links with `hx-target="#tab-content"`, `hx-swap="innerHTML"`, and `hx-push-url`. Events should be added beside Overview, Tasks, and Files with the same behavior. + +`TasksTabFragment` and `FilesTabFragment` are the closest fragment patterns. Phase 10 should add an `EventsTabFragment` that can be returned by `/tablos/{id}/events` and can also be embedded in the full `TabloDetailPage` when `activeTab == "events"`. + +### Route and Handler Patterns + +`backend/internal/web/router.go` mounts protected tablo child resources below `/tablos/{id}/...`. Static child routes should remain before parametric routes. Events should follow this shape: + +- `GET /tablos/{id}/events` - render Events tab or full page fallback. +- `GET /tablos/{id}/events/new` - render inline create form, with optional `date=YYYY-MM-DD` and `month=YYYY-MM`. +- `POST /tablos/{id}/events` - create an event and return the refreshed Events tab. +- `GET /tablos/{id}/events/{event_id}/edit` - render inline edit form. +- `POST /tablos/{id}/events/{event_id}` - update an event and return the refreshed Events tab. +- `GET /tablos/{id}/events/{event_id}/delete` - render delete confirmation. +- `POST /tablos/{id}/events/{event_id}/delete` - hard delete and return the refreshed Events tab. + +The events handlers should use `loadOwnedTablo` as the parent guard and add a `loadOwnedEvent` helper that queries by both `event_id` and `tablo_id`. Missing or foreign resources should return 404, matching tasks, files, and etapes. + +### Form and Fragment Pattern + +Create/edit/delete should render inline inside the Events tab, likely in a dedicated `#event-form-slot`. Mutation responses should refresh the whole `#events-tab` rather than only a day cell. This matches the reliability pattern from etapes and avoids calendar count/day ordering drift after changes. + +Phase 9 exposed a bug where a URL/search-param filter could override an explicit selected form value. Events should avoid that class of issue: if both a query string date/month and submitted form fields exist, submitted form values are authoritative. Query params are only defaults for rendering a new form. + +## Data Model + +Add a migration after `0007_etapes.sql` for a tablo-owned `events` table. + +Recommended schema: + +```sql +CREATE TABLE events ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tablo_id UUID NOT NULL REFERENCES tablos(id) ON DELETE CASCADE, + title TEXT NOT NULL, + event_date DATE NOT NULL, + start_time TIME NOT NULL, + end_time TIME, + description TEXT, + location TEXT, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT events_title_not_blank CHECK (length(trim(title)) > 0), + CONSTRAINT events_end_after_start CHECK (end_time IS NULL OR end_time > start_time) +); + +CREATE INDEX events_tablo_month_idx + ON events (tablo_id, event_date, start_time, title); + +CREATE INDEX events_date_idx + ON events (event_date, start_time); +``` + +Notes: + +- `event_date`, `start_time`, and `end_time` preserve the Phase 10 local-date/local-time decision. +- `location` is optional because EVENT-01 and the discussion context include it. +- Hard delete means no `deleted_at`. +- The database check should reject same-time or backwards end times even if a handler bug slips through. +- If the project has an existing updated-at trigger convention, reuse it. Otherwise set `updated_at = now()` in the update query, as existing query files do. + +## SQLC Query Surface + +Create `backend/internal/db/queries/events.sql`. + +Recommended queries: + +- `CreateEvent` inserts a tablo event and returns the row. +- `GetEventByID` accepts `id` and `tablo_id` to support parent-scoped authorization. +- `ListEventsByTabloRange` accepts `tablo_id`, `start_date`, and `end_date`, ordered by `event_date`, `start_time`, `title`. +- `UpdateEvent` accepts `id`, `tablo_id`, and editable fields, sets `updated_at = now()`, and returns the row. +- `DeleteEvent` accepts `id` and `tablo_id`. +- `ListUserEventsRange` joins `events` to `tablos` where `tablos.user_id = $1` and `events.event_date` is inside a provided range. It should return event fields plus `tablo_title` and `tablo_color` for Phase 11 planning. + +Expected date/time Go types should be confirmed after generation. With pgx-backed SQLC, PostgreSQL `date` and `time` usually generate pgtype-backed values. Add small parse/format helpers in the web layer rather than scattering conversion logic across handlers and templates. + +## Calendar Rendering + +The Events tab should render a restrained month grid. The planner should keep it server-rendered with no client-side calendar dependency. + +Recommended model: + +- Parse `month=YYYY-MM`; default to the current local month when missing. +- Compute month start, month end, leading blank cells, and trailing blanks server-side. +- List events for the inclusive date range covering the visible month. +- Group events by `event_date` before passing data to the template. +- Day cells show titles only. Show the first few titles and then `+N more` when the cell has more events. +- Previous/next month links should target `/tablos/{id}/events?month=YYYY-MM`. +- The general `New event` button should open the create form without a date default. +- Day-cell create actions should open `/tablos/{id}/events/new?date=YYYY-MM-DD&month=YYYY-MM`. + +Use a small helper such as `parseCalendarMonth(raw string, now time.Time)` so tests can cover invalid and missing month values without depending on the wall clock. + +## Validation Rules + +Handler validation should cover: + +- Title required after trimming. +- Date required and parseable as `YYYY-MM-DD`. +- Start time required and parseable as `HH:MM`. +- End time optional, parseable as `HH:MM` when present. +- End time must be after start time when present. +- Description and location optional; apply conservative maximum lengths consistent with local form conventions. + +Templates should use existing field error patterns and clear copy for the end-time rule. The expected message does not need exact product polish, but it should be explicit that the end time must be after the start time. + +## Security Notes + +Threats to account for in the plan: + +- Cross-tablo event access: every event read/update/delete query must include the parent `tablo_id`, and the parent tablo must be loaded through `loadOwnedTablo`. +- Cross-user event aggregation: `ListUserEventsRange` must join through `tablos.user_id`, not trust event IDs or tablo IDs supplied by the client. +- CSRF: every create/update/delete form must include the existing CSRF token and run through the current middleware. +- Stored content display: titles, descriptions, and locations should be rendered through templ so HTML is escaped. +- Time validation bypass: enforce end-after-start both in handlers and with a DB check constraint. +- Route ambiguity: mount static event routes before parametric routes. + +## Testing Strategy + +Add DB-backed web tests in the existing style from `handlers_tasks_test.go` and recent Phase 9 tests. + +High-value automated tests: + +- Migration/query test creates a tablo event and lists it by month/range. +- Events tab renders in full page mode and HTMX fragment mode. +- `GET /tablos/{id}/events?month=YYYY-MM` shows month navigation and event titles. +- `GET /tablos/{id}/events/new?date=YYYY-MM-DD` pre-fills the date field. +- Creating an event refreshes the Events tab and shows the new title. +- Creating with `end_time <= start_time` returns 422 and a visible validation error. +- Updating an event changes title/date/time and refreshes the calendar. +- Deleting an event removes it from the calendar. +- Events owned by another user's tablo return 404 for edit/update/delete. +- User-scoped range query returns only events for tablos owned by the authenticated user and includes tablo title/color. + +Manual UAT should still check the calendar interaction flow in a browser because the first version is HTMX-heavy and layout-sensitive. + +## Implementation Risks + +- `NewRouter` signature changes will affect many tests and app wiring. Update all call sites in one task. +- SQLC generated files should be regenerated, never hand-edited. +- Date/time type conversion can be noisy. Keep helpers small and test them indirectly through handler tests. +- Month query params and form field precedence should be explicit to avoid repeating the Phase 9 selected-etape bug. +- The UI safety gate applies: this phase adds a visible Events tab and calendar. Planning should not continue to implementation until a `10-UI-SPEC.md` exists. + +## Validation Architecture + +Phase 10 has enough behavioral surface to need automated sampling from the first implementation wave. + +### Test Infrastructure + +- Framework: Go `testing` with existing DB-backed web handler tests. +- Quick command: `go test ./backend/internal/web ./backend/internal/db/sqlc` +- Full command: `go test ./...` +- Expected quick feedback latency: under 20 seconds on the local dev database if migrations are already available. + +### Reference Dataset + +Use generated UUID users/tablos/events in tests. The minimum dataset should include: + +- User A with Tablo A and two events in the selected month. +- User A with a second tablo and one event for user-scoped aggregation. +- User B with Tablo B and one event in the same date range to prove isolation. +- One event with no end time. +- One event with description and location. +- One invalid submission where end time is equal to start time. + +### Required Coverage Dimensions + +- Persistence: migration, SQLC create/list/update/delete, cascade behavior via tablo delete if practical. +- Authorization: parent-tablo ownership for all event mutation and read paths. +- Calendar query: month parsing, range boundaries, previous/next month links. +- Form behavior: create defaults from day click, submitted form values override query defaults, validation errors retain submitted values. +- HTMX integration: tab route returns fragment for HTMX and full page fallback for non-HTMX. +- Phase 11 handoff: user-scoped range query returns only accessible events and includes tablo title/color. + +### Sampling Plan + +- After schema/query task: run DB query tests. +- After handler task: run focused web handler tests. +- After template/tab integration task: run focused web handler tests plus `go test ./backend/templates` if template packages are separately testable; otherwise regenerate templates and run the web tests. +- Before UAT: run `go test ./...`. +