docs(10): create phase plan

This commit is contained in:
Arthur Belleville 2026-05-16 00:19:03 +02:00
parent 0b894f494d
commit a15596cd09
No known key found for this signature in database
5 changed files with 625 additions and 12 deletions

View file

@ -2,15 +2,15 @@
gsd_state_version: 1.0 gsd_state_version: 1.0
milestone: v2.0 milestone: v2.0
milestone_name: Collaboration, planning, and social sign-in milestone_name: Collaboration, planning, and social sign-in
status: Phase 09 UAT passed; security review pending status: ready_to_execute
last_updated: "2026-05-15T22:12:20.689Z" last_updated: "2026-05-15T22:18:38.208Z"
last_activity: 2026-05-15 -- Phase 09 UAT gap resolved and Plan 09-04 closed last_activity: 2026-05-15 -- Phase 10 planning complete
progress: progress:
total_phases: 5 total_phases: 5
completed_phases: 2 completed_phases: 2
total_plans: 9 total_plans: 13
completed_plans: 9 completed_plans: 9
percent: 100 percent: 69
--- ---
# STATE # STATE
@ -24,15 +24,15 @@ progress:
See: `.planning/PROJECT.md` (updated 2026-05-15) See: `.planning/PROJECT.md` (updated 2026-05-15)
**Core value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider. **Core value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider.
**Current focus:** Phase 09 — etapes **Current focus:** Phase 10 — events
## Current Position ## Current Position
Phase: 09 (etapes) — EXECUTING Phase: 10 (events) — PLANNED
Plan: 4 of 4 Plan: 0 of 4
Status: Phase 09 UAT passed; security review pending Status: Ready to execute
Last activity: 2026-05-15 -- Phase 09 UAT gap resolved and Plan 09-04 closed Last activity: 2026-05-15 -- Phase 10 planning complete
Resume file: .planning/phases/10-events/10-UI-SPEC.md Resume file: .planning/phases/10-events/10-01-PLAN.md
## Phase Status ## Phase Status
@ -40,7 +40,7 @@ Resume file: .planning/phases/10-events/10-UI-SPEC.md
|---|-------|--------| |---|-------|--------|
| 8 | Social Sign-in | ✓ Complete | | 8 | Social Sign-in | ✓ Complete |
| 9 | Etapes | ◆ UAT passed; security pending | | 9 | Etapes | ◆ UAT passed; security pending |
| 10 | Events | ○ Pending | | 10 | Events | ◆ Ready to execute |
| 11 | Individual Planning | ○ Pending | | 11 | Individual Planning | ○ Pending |
| 12 | Native Tablo Chat | ○ Pending | | 12 | Native Tablo Chat | ○ Pending |

View file

@ -0,0 +1,156 @@
---
phase: 10-events
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- backend/migrations/0008_events.sql
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_events.go
- backend/internal/web/router.go
- backend/cmd/web/main.go
- backend/templates/events.templ
- backend/templates/events_forms.go
- backend/templates/tablos.templ
autonomous: true
requirements: [EVENT-01, EVENT-03, EVENT-04, EVENT-05]
must_haves:
truths:
- "GOAL: A user can schedule events that belong to tablos with the same authorization expectations as tasks and files"
- "EVENT-01: User can create a scheduled event attached to a tablo with title, start time, optional end time, optional description, and optional location"
- "EVENT-03: Tablo detail page includes an Events view listing that tablo's scheduled events"
- "EVENT-04: Event validation requires end_time to be empty or after start_time"
- "EVENT-05: Event create/list authorization follows tablo ownership via loadOwnedTablo"
- "D-01/D-05: Store local event_date, start_time, and nullable end_time columns, not timestamptz"
- "D-02/D-03/D-04: Date/start time are required; end time is optional and display omits end when blank"
- "D-06/D-07/D-10/D-17/D-18/D-19: Events tab renders a month grid showing past/future events as titles ordered by start_time then title"
- "D-12/D-13/D-15: Inline create form fields refresh the whole Events tab after create"
- "UI-SPEC: Events tab uses #events-tab, #event-form-slot, New event, Create event, Close form, and slate/blue/red local UI styling"
artifacts:
- path: "backend/migrations/0008_events.sql"
provides: "events table with local date/time columns, tablo FK, end-after-start check, and ordering indexes"
- path: "backend/internal/db/queries/events.sql"
provides: "CreateEvent, GetEventByID, and ListEventsByTabloRange"
- path: "backend/internal/web/handlers_events.go"
provides: "Events tab, create form, create validation, month data loading, and ownership checks"
- path: "backend/templates/events.templ"
provides: "Events tab month grid and inline create form rendering"
---
<objective>
Vertical slice 1: a signed-in user can open a tablo's Events tab, create an event with local date/time fields, and see the event title in that tablo's month calendar. This plan establishes the event schema, query surface, route wiring, and first working tab UI.
</objective>
<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-UI-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-VALIDATION.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-PATTERNS.md
</context>
<threat_model>
T-10-01 Cross-tablo event access: every list/create path must load the parent through `loadOwnedTablo`; child event queries must include `tablo_id`.
T-10-02 CSRF: create form must include `@ui.CSRFField(csrfToken)` and use the existing POST route behind CSRF middleware.
T-10-03 XSS: event title, description, and location must render through templ escaped expressions.
T-10-04 Time validation bypass: create handler and DB check must reject `end_time <= start_time`.
T-10-05 Route ambiguity: events static routes must be mounted before event parametric routes.
</threat_model>
<tasks>
<task type="auto">
<name>Task 1: Add RED coverage for create-and-list calendar slice</name>
<files>
- backend/internal/web/handlers_events_test.go
</files>
<read_first>
- backend/internal/web/handlers_tasks_test.go
- backend/internal/web/handlers_etapes_test.go
- backend/internal/web/router.go
- .planning/phases/10-events/10-VALIDATION.md
- .planning/phases/10-events/10-UI-SPEC.md
</read_first>
<action>
Add DB-backed handler tests in `handlers_events_test.go` using `setupTestDB`, `preInsertUser`, authenticated cookies, and `getCSRFToken`.
Add `TestEventsTabRendersMonthGrid` for `GET /tablos/{id}/events?month=2026-05` expecting `Events`, `May 2026`, weekday labels, and `New event`.
Add `TestEventCreateRendersTitleInCalendar` that POSTs `/tablos/{id}/events` with `title=Kickoff`, `event_date=2026-05-20`, `start_time=09:30`, empty `end_time`, `location=Office`, `description=Planning`, then expects the refreshed fragment to contain `Kickoff`.
Add `TestEventCreateRejectsEndTimeBeforeOrEqualStart` with `start_time=10:00` and `end_time=10:00`, expecting HTTP 422 and copy `End time must be after the start time.`.
Add `TestEventsTabOwnershipReturns404` proving another signed-in user cannot GET the first user's `/tablos/{id}/events`.
</action>
<verify>
<automated>cd backend && go test ./internal/web -run 'TestEventsTabRendersMonthGrid|TestEventCreateRendersTitleInCalendar|TestEventCreateRejectsEndTimeBeforeOrEqualStart|TestEventsTabOwnershipReturns404' -count=1</automated>
</verify>
<acceptance_criteria>
- `backend/internal/web/handlers_events_test.go` contains all four named tests.
- Tests fail before implementation because the events route/schema/templates do not exist.
- The validation test asserts exact copy `End time must be after the start time.`.
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Implement event creation and month listing</name>
<files>
- backend/migrations/0008_events.sql
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events.go
- backend/internal/web/router.go
- backend/cmd/web/main.go
- backend/templates/events.templ
- backend/templates/events_forms.go
- backend/templates/tablos.templ
</files>
<read_first>
- backend/migrations/0003_tablos.sql
- backend/migrations/0007_etapes.sql
- backend/internal/db/queries/tablos.sql
- backend/internal/web/handlers_files.go
- backend/internal/web/handlers_etapes.go
- backend/internal/web/router.go
- backend/templates/files.templ
- backend/templates/etapes.templ
- backend/templates/tablos.templ
- .planning/phases/10-events/10-RESEARCH.md
- .planning/phases/10-events/10-UI-SPEC.md
- .planning/phases/10-events/10-PATTERNS.md
</read_first>
<action>
Add `0008_events.sql` with `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())`, `events_title_not_blank`, `events_end_after_start`, `events_tablo_month_idx`, and `events_date_idx`.
Add `events.sql` queries: `CreateEvent`, `GetEventByID`, and `ListEventsByTabloRange`, all parent-scoped by `tablo_id` where applicable and ordered by `event_date`, `start_time`, `title`.
Add `EventsDeps{Queries *sqlc.Queries}` and wire it through `NewRouter` plus `backend/cmd/web/main.go` and test helpers. Mount `GET /tablos/{id}/events`, `GET /tablos/{id}/events/new`, and `POST /tablos/{id}/events` under authenticated routes with static event routes before future parametric routes.
Add `handlers_events.go` with month parsing for `YYYY-MM`, local current-month default, date/time parsing helpers for `YYYY-MM-DD` and `HH:MM`, create validation, and a `renderEventsTab` helper that returns `EventsTabFragment`.
Extend `TabloDetailPage` to include the `Events` nav link and render `EventsTabFragment` when `activeTab == "events"`.
Add `events.templ` and `events_forms.go` with `#events-tab`, `#event-form-slot`, month grid, weekday labels, `New event`, `Create event`, `Close form`, and escaped event title rows. Day cells show event titles only; no special empty-month copy.
Run `cd backend && just generate` after migration/query/template changes.
</action>
<verify>
<automated>cd backend && just generate && go test ./internal/web -run 'TestEventsTabRendersMonthGrid|TestEventCreateRendersTitleInCalendar|TestEventCreateRejectsEndTimeBeforeOrEqualStart|TestEventsTabOwnershipReturns404' -count=1</automated>
</verify>
<acceptance_criteria>
- `backend/migrations/0008_events.sql` contains `CREATE TABLE events`, `event_date DATE NOT NULL`, `start_time TIME NOT NULL`, `end_time TIME`, and `events_end_after_start`.
- `backend/internal/db/queries/events.sql` contains `CreateEvent`, `GetEventByID`, and `ListEventsByTabloRange`.
- `backend/internal/web/router.go` mounts `GET /tablos/{id}/events` and `POST /tablos/{id}/events`.
- `backend/templates/tablos.templ` contains tab label `Events` and active branch for `activeTab == "events"`.
- Targeted tests listed in verify pass.
</acceptance_criteria>
</task>
</tasks>
<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./internal/web -run 'TestEventsTabRendersMonthGrid|TestEventCreateRendersTitleInCalendar|TestEventCreateRejectsEndTimeBeforeOrEqualStart|TestEventsTabOwnershipReturns404' -count=1`
- `git diff --check`
</verification>
<success_criteria>
- EVENT-01 create path is delivered for all required fields.
- EVENT-03 Events tab displays created event titles in a month grid.
- EVENT-04 create validation and DB check reject invalid end times.
- EVENT-05 create/list access is scoped to the owned tablo.
</success_criteria>

View file

@ -0,0 +1,138 @@
---
phase: 10-events
plan: 02
type: execute
wave: 2
depends_on:
- 10-01-PLAN.md
files_modified:
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_events.go
- backend/internal/web/router.go
- backend/templates/events.templ
- backend/templates/events_forms.go
autonomous: true
requirements: [EVENT-01, EVENT-02, EVENT-04, EVENT-05]
must_haves:
truths:
- "EVENT-02: User can edit and delete tablo events"
- "EVENT-04: Update path also rejects end_time before or equal to start_time"
- "EVENT-05: Event edit/update/delete authorization follows parent tablo ownership and returns 404 for inaccessible events"
- "D-12/D-14/D-15: Clicking an event title opens inline edit; create/edit/delete refresh the whole Events tab"
- "D-13: Edit form exposes required title/date/start time and optional end time/description/location"
- "D-16: Delete is hard delete after confirmation; no deleted_at or restore behavior"
- "UI-SPEC: Edit submit copy is Save event changes; delete copy uses Delete event?, Delete event, Keep event"
artifacts:
- path: "backend/internal/web/handlers_events.go"
provides: "loadOwnedEvent plus edit, update, delete-confirm, and hard delete handlers"
- path: "backend/templates/events.templ"
provides: "event title edit triggers, edit form, and delete confirmation fragments"
- path: "backend/internal/web/handlers_events_test.go"
provides: "edit/delete/ownership and update validation coverage"
---
<objective>
Vertical slice 2: a user can click an event title in the Events tab, edit all event fields inline, and hard-delete the event after confirmation without leaking or mutating inaccessible events.
</objective>
<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-UI-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-PATTERNS.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-01-PLAN.md
</context>
<threat_model>
T-10-01 Cross-tablo event access: edit/update/delete must bind `event_id` to `tablo_id` through `GetEventByID`.
T-10-02 CSRF: update and delete forms must include `@ui.CSRFField(csrfToken)`.
T-10-03 XSS: edited title, description, and location remain escaped on re-render.
T-10-04 Time validation bypass: update handler must reject invalid end time and DB check remains in place.
T-10-06 Data retention mismatch: delete must be hard delete only; do not add `deleted_at`.
</threat_model>
<tasks>
<task type="auto">
<name>Task 1: Add RED coverage for edit, update, delete, and ownership</name>
<files>
- backend/internal/web/handlers_events_test.go
</files>
<read_first>
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_etapes_test.go
- backend/templates/events.templ
- .planning/phases/10-events/10-UI-SPEC.md
</read_first>
<action>
Add failing tests:
1. `TestEventEditRendersInlineForm` creates an event, GETs `/tablos/{id}/events/{event_id}/edit`, and expects `Save event changes`, current title, date, start time, location, and description.
2. `TestEventUpdateChangesCalendarPlacement` posts `/tablos/{id}/events/{event_id}` with changed title/date/time and verifies the refreshed calendar contains the new title and not the old title.
3. `TestEventUpdateRejectsInvalidEndTime` posts equal start/end times and expects 422 plus `End time must be after the start time.`.
4. `TestEventDeleteRemovesFromCalendarAndDatabase` confirms delete, verifies refreshed calendar omits the title, and verifies `GetEventByID` returns no row.
5. `TestEventMutationOwnershipReturns404` verifies non-owner edit/update/delete-confirm/delete attempts return 404.
</action>
<verify>
<automated>cd backend && go test ./internal/web -run 'TestEventEditRendersInlineForm|TestEventUpdateChangesCalendarPlacement|TestEventUpdateRejectsInvalidEndTime|TestEventDeleteRemovesFromCalendarAndDatabase|TestEventMutationOwnershipReturns404' -count=1</automated>
</verify>
<acceptance_criteria>
- `handlers_events_test.go` contains all five named tests.
- Tests fail before implementation because edit/update/delete routes or behavior are missing.
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Implement inline edit and hard delete</name>
<files>
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events.go
- backend/internal/web/router.go
- backend/templates/events.templ
- backend/templates/events_forms.go
</files>
<read_first>
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events.go
- backend/internal/web/handlers_etapes.go
- backend/internal/web/router.go
- backend/templates/events.templ
- backend/templates/etapes.templ
- .planning/phases/10-events/10-UI-SPEC.md
- .planning/phases/10-events/10-PATTERNS.md
</read_first>
<action>
Add `UpdateEvent` and `DeleteEvent` SQLC queries, each scoped by `id` and `tablo_id`, with `UpdateEvent` setting `updated_at = now()`.
Add `loadOwnedEvent` in `handlers_events.go` using `loadOwnedTablo`, `chi.URLParam(r, "event_id")`, `uuid.Parse`, `GetEventByID`, 404 on missing/foreign rows, and 500 on unexpected errors.
Mount `GET /tablos/{id}/events/{event_id}/edit`, `POST /tablos/{id}/events/{event_id}`, `GET /tablos/{id}/events/{event_id}/delete-confirm`, and `POST /tablos/{id}/events/{event_id}/delete` after static events routes.
Render event titles as HTMX controls targeting `#event-form-slot` with accessible label `Edit event: {title}`. Add edit form and delete confirmation fragments using UI-SPEC copy: `Save event changes`, `Delete event?`, `This removes the event from this tablo. This cannot be undone.`, `Delete event`, and `Keep event`.
Reuse create validation helpers for update. On update validation errors, return 422 and re-render the edit form with submitted values. After successful update/delete, refresh `#events-tab` with the currently selected `month` context.
Run `cd backend && just generate`.
</action>
<verify>
<automated>cd backend && just generate && go test ./internal/web -run 'TestEventEditRendersInlineForm|TestEventUpdateChangesCalendarPlacement|TestEventUpdateRejectsInvalidEndTime|TestEventDeleteRemovesFromCalendarAndDatabase|TestEventMutationOwnershipReturns404' -count=1</automated>
</verify>
<acceptance_criteria>
- `backend/internal/db/queries/events.sql` contains `UpdateEvent` and `DeleteEvent`.
- `backend/internal/web/handlers_events.go` contains `loadOwnedEvent`.
- `backend/templates/events.templ` contains `Save event changes`, `Delete event?`, `Delete event`, and `Keep event`.
- Delete test proves the row is removed from the database rather than marked with `deleted_at`.
- Targeted tests listed in verify pass.
</acceptance_criteria>
</task>
</tasks>
<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./internal/web -run 'TestEventEditRendersInlineForm|TestEventUpdateChangesCalendarPlacement|TestEventUpdateRejectsInvalidEndTime|TestEventDeleteRemovesFromCalendarAndDatabase|TestEventMutationOwnershipReturns404' -count=1`
- `git diff --check`
</verification>
<success_criteria>
- EVENT-02 is delivered for edit and hard delete.
- EVENT-04 is enforced on update.
- EVENT-05 is enforced on all event mutation routes.
</success_criteria>

View file

@ -0,0 +1,134 @@
---
phase: 10-events
plan: 03
type: execute
wave: 3
depends_on:
- 10-02-PLAN.md
files_modified:
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_events.go
- backend/templates/events.templ
- backend/templates/events_forms.go
autonomous: true
requirements: [EVENT-01, EVENT-03, EVENT-05]
must_haves:
truths:
- "D-08/D-21: Events tab supports previous/next month navigation with /tablos/{id}/events?month=YYYY-MM"
- "D-09: New event can be opened generally or from a day cell with the date prefilled"
- "D-10/D-11: Day cells show event titles only, first few titles, and +N more"
- "D-20/D-22/D-23: Phase 10 includes user-scoped date-range query surface returning event fields plus tablo title/color"
- "EVENT-05: User-scoped aggregation must filter through tablos.user_id and not leak other users' events"
- "Research pitfall: submitted form fields must win over date/month query defaults"
artifacts:
- path: "backend/internal/db/queries/events.sql"
provides: "ListUserEventsRange for Phase 11 planning handoff"
- path: "backend/internal/web/handlers_events.go"
provides: "month navigation, day prefill, and form/query precedence helpers"
- path: "backend/templates/events.templ"
provides: "previous/next controls, day create controls, and +N more rendering"
---
<objective>
Vertical slice 3: the Events tab supports month navigation and day-click date prefill, while the database query layer exposes the user-scoped date-range aggregation that Phase 11 needs for individual planning.
</objective>
<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-UI-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-PATTERNS.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-02-PLAN.md
</context>
<threat_model>
T-10-01 Cross-tablo event access: day prefill and month links must never bypass `loadOwnedTablo`.
T-10-07 Cross-user aggregation leak: `ListUserEventsRange` must join `tablos` and filter by `tablos.user_id = $1`.
T-10-08 Query-default confusion: date query params are defaults only; POST body fields must be authoritative.
T-10-03 XSS: `+N more`, titles, and month/day labels must render as escaped text.
</threat_model>
<tasks>
<task type="auto">
<name>Task 1: Add RED coverage for navigation, prefill, overflow, and aggregation</name>
<files>
- backend/internal/web/handlers_events_test.go
</files>
<read_first>
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_tasks_test.go
- backend/templates/events.templ
- .planning/phases/10-events/10-RESEARCH.md
- .planning/phases/10-events/10-UI-SPEC.md
</read_first>
<action>
Add failing tests:
1. `TestEventsMonthNavigationPushesMonthParam` GETs `/tablos/{id}/events?month=2026-05` and expects links for `month=2026-04` and `month=2026-06` plus accessible labels naming those months.
2. `TestEventNewFromDayPrefillsDate` GETs `/tablos/{id}/events/new?date=2026-05-20&month=2026-05` and expects the date input value `2026-05-20`.
3. `TestEventCreateSubmittedDateOverridesQueryDefault` POSTs `/tablos/{id}/events?date=2026-05-20&month=2026-05` with form `event_date=2026-05-21` and verifies the event is stored on `2026-05-21`.
4. `TestEventsCalendarShowsMoreIndicator` creates four events on one date and expects three titles plus `+1 more`.
5. `TestListUserEventsRangeReturnsOnlyOwnedTablos` creates events for two users in the same date range and verifies the user-scoped query returns only the authenticated user's events with `tablo_title` and `tablo_color`.
</action>
<verify>
<automated>cd backend && go test ./internal/web -run 'TestEventsMonthNavigationPushesMonthParam|TestEventNewFromDayPrefillsDate|TestEventCreateSubmittedDateOverridesQueryDefault|TestEventsCalendarShowsMoreIndicator|TestListUserEventsRangeReturnsOnlyOwnedTablos' -count=1</automated>
</verify>
<acceptance_criteria>
- `handlers_events_test.go` contains all five named tests.
- The submitted-date test fails before implementation if query defaults override form data.
- The user-scoped query test proves foreign-user events are not returned.
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Implement navigation, day prefill, overflow display, and planning query</name>
<files>
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events.go
- backend/templates/events.templ
- backend/templates/events_forms.go
</files>
<read_first>
- backend/internal/db/queries/events.sql
- backend/internal/web/handlers_events.go
- backend/templates/events.templ
- backend/templates/events_forms.go
- .planning/phases/10-events/10-CONTEXT.md
- .planning/phases/10-events/10-UI-SPEC.md
</read_first>
<action>
Add `ListUserEventsRange` to `events.sql`, joining `events` to `tablos`, filtering by `tablos.user_id`, `events.event_date >= $2`, and `events.event_date <= $3`, ordering by `event_date`, `start_time`, `title`, and returning event fields plus `tablo_title` and `tablo_color`.
Extend month model generation to calculate previous and next `YYYY-MM` links, in-month day metadata, and per-day event truncation at three titles with `+N more`.
Add day-cell create controls targeting `/tablos/{id}/events/new?date=YYYY-MM-DD&month=YYYY-MM` with `aria-label="Create event on YYYY-MM-DD"`.
Implement `EventNewFormHandler` so query `date` pre-populates the create form date and query `month` is carried as calendar context. Ensure POST create/update reads only submitted form fields for `event_date`, `start_time`, and `end_time`; month query controls the refresh view only.
Run `cd backend && just generate`.
</action>
<verify>
<automated>cd backend && just generate && go test ./internal/web -run 'TestEventsMonthNavigationPushesMonthParam|TestEventNewFromDayPrefillsDate|TestEventCreateSubmittedDateOverridesQueryDefault|TestEventsCalendarShowsMoreIndicator|TestListUserEventsRangeReturnsOnlyOwnedTablos' -count=1</automated>
</verify>
<acceptance_criteria>
- `backend/internal/db/queries/events.sql` contains `ListUserEventsRange` and `tablos.user_id`.
- `backend/templates/events.templ` contains `+` more rendering logic and day create labels.
- `EventNewFormHandler` pre-fills date from query only on GET.
- POST create/update handlers use submitted `event_date`, not query `date`, for persistence.
- Targeted tests listed in verify pass.
</acceptance_criteria>
</task>
</tasks>
<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./internal/web -run 'TestEventsMonthNavigationPushesMonthParam|TestEventNewFromDayPrefillsDate|TestEventCreateSubmittedDateOverridesQueryDefault|TestEventsCalendarShowsMoreIndicator|TestListUserEventsRangeReturnsOnlyOwnedTablos' -count=1`
- `git diff --check`
</verification>
<success_criteria>
- Month navigation and day-click create prefill are delivered.
- Calendar cells show titles only and handle overflow with `+N more`.
- Phase 11 can query the authenticated user's events across owned tablos by date range with tablo title/color.
</success_criteria>

View file

@ -0,0 +1,185 @@
---
phase: 10-events
plan: 04
type: execute
wave: 4
depends_on:
- 10-03-PLAN.md
files_modified:
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_tablos_test.go
- backend/internal/web/handlers_tasks_test.go
- backend/templates/events.templ
- backend/templates/tablos.templ
autonomous: false
requirements: [EVENT-01, EVENT-02, EVENT-03, EVENT-04, EVENT-05]
must_haves:
truths:
- "All EVENT-01..05 requirements are covered by automated tests or the final browser checkpoint"
- "D-01 through D-23 are represented by source behavior, tests, or the final browser checkpoint"
- "UI-SPEC: Events tab, toolbar, month grid, inline forms, copy, colors, typography, spacing, and accessibility labels are visible"
- "Validation Architecture: final full suite and browser UAT run before verify-work"
artifacts:
- path: "backend/internal/web/handlers_events_test.go"
provides: "final regression coverage for events"
- path: "backend/templates/events.templ"
provides: "final UI contract adjustments"
- path: "backend/templates/tablos.templ"
provides: "final tab integration adjustments"
---
<objective>
Vertical slice 4: harden the completed Events feature with regression tests, full generated-code verification, and browser UAT against all event requirements, context decisions, UI contract, and security expectations.
</objective>
<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-UI-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-VALIDATION.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/10-events/10-03-PLAN.md
</context>
<threat_model>
T-10-01 Cross-tablo event access remains covered by owner-only handler tests.
T-10-02 CSRF remains covered by mutation tests using real CSRF tokens.
T-10-03 XSS remains covered by templ escaped rendering and should be spot-checked with HTML-looking event content.
T-10-04 Time validation remains covered on create and update plus DB check.
T-10-07 Cross-user aggregation leak remains covered by `ListUserEventsRange` tests.
T-10-09 Regression risk: NewRouter signature changes can silently break existing tablo/task/file/auth tests if not run together.
</threat_model>
<source_audit>
SOURCE | ID | Feature/Requirement | Plan | Status | Notes
GOAL | - | Tablo-owned scheduled events with task/file-level authorization | 10-01,10-02,10-03,10-04 | COVERED | Schema, handlers, ownership tests, final UAT
REQ | EVENT-01 | Create event with title, start time, optional end time, description, location | 10-01,10-03,10-04 | COVERED | Create form, validation, submitted-value precedence
REQ | EVENT-02 | Edit and delete events | 10-02,10-04 | COVERED | Inline edit and hard delete
REQ | EVENT-03 | Tablo detail events view listing scheduled events | 10-01,10-03,10-04 | COVERED | Events tab month grid
REQ | EVENT-04 | End time empty or after start time | 10-01,10-02,10-04 | COVERED | Handler validation and DB check
REQ | EVENT-05 | Authorization follows tablo access rules | 10-01,10-02,10-03,10-04 | COVERED | Parent ownership and aggregation filtering
CONTEXT | D-01..D-05 | Local date/time storage and display assumptions | 10-01,10-02,10-04 | COVERED | date/time columns and helpers
CONTEXT | D-06..D-11 | Events tab month grid, navigation, day create, title-only cells, +N more | 10-01,10-03,10-04 | COVERED | Calendar UI tasks
CONTEXT | D-12..D-15 | Inline create/edit flow and full tab refresh | 10-01,10-02,10-04 | COVERED | `#event-form-slot`, `#events-tab`
CONTEXT | D-16..D-19 | Hard delete, past visibility, ordering, no special empty state | 10-01,10-02,10-04 | COVERED | Query/order/delete tasks
CONTEXT | D-20..D-23 | Phase 11 user-scoped date-range query with tablo metadata | 10-03,10-04 | COVERED | `ListUserEventsRange`
RESEARCH | Data model | `events` table with local date/time columns and indexes | 10-01 | COVERED | Migration task
RESEARCH | Security | Parent-scoped queries, CSRF, escaping, DB check | 10-01,10-02,10-03,10-04 | COVERED | Threat models and tests
RESEARCH | Testing | DB-backed create/list/update/delete/ownership/aggregation tests | 10-01,10-02,10-03,10-04 | COVERED | Per-plan RED tests
UI-SPEC | Contract | Events tab, calendar, forms, copy, a11y, registry safety | 10-01,10-02,10-03,10-04 | COVERED | Implemented and UAT checked
</source_audit>
<tasks>
<task type="auto">
<name>Task 1: Add final regression coverage for tab integration and escaping</name>
<files>
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_tablos_test.go
</files>
<read_first>
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_tablos_test.go
- backend/templates/events.templ
- backend/templates/tablos.templ
- .planning/phases/10-events/10-UI-SPEC.md
- .planning/phases/10-events/10-VALIDATION.md
</read_first>
<action>
Add final regression tests:
1. `TestTabloDetailEventsTabFullPageFallback` verifies non-HTMX `/tablos/{id}/events?month=2026-05` renders the full detail shell with Events active and includes Overview, Tasks, Files, and Events tabs.
2. `TestEventContentIsEscaped` creates an event with title `<script>alert(1)</script>` and verifies rendered HTML does not contain a raw script tag while still displaying escaped title text.
3. `TestEventWithoutEndTimeDisplaysOnlyStartTime` verifies an event with blank end time displays `09:30` and does not render an inferred range.
4. `TestEventsOrderedByStartTimeThenTitle` creates same-day events and verifies calendar title order follows start time ascending, then title.
</action>
<verify>
<automated>cd backend && go test ./internal/web -run 'TestTabloDetailEventsTabFullPageFallback|TestEventContentIsEscaped|TestEventWithoutEndTimeDisplaysOnlyStartTime|TestEventsOrderedByStartTimeThenTitle' -count=1</automated>
</verify>
<acceptance_criteria>
- `handlers_events_test.go` contains escaping, no-end-time display, and ordering tests.
- `handlers_tablos_test.go` or `handlers_events_test.go` contains the full-page fallback test.
- Targeted tests listed in verify pass.
</acceptance_criteria>
</task>
<task type="auto">
<name>Task 2: Run full backend verification and fix regressions</name>
<files>
- backend/internal/web/handlers_events.go
- backend/internal/web/router.go
- backend/templates/events.templ
- backend/templates/tablos.templ
- backend/internal/web/handlers_events_test.go
- backend/internal/web/handlers_tasks_test.go
- backend/internal/web/handlers_tablos_test.go
</files>
<read_first>
- backend/internal/web/router.go
- backend/internal/web/handlers_events.go
- backend/templates/events.templ
- backend/templates/tablos.templ
- .planning/phases/10-events/10-RESEARCH.md
- .planning/phases/10-events/10-UI-SPEC.md
- .planning/phases/10-events/10-VALIDATION.md
</read_first>
<action>
Run generated code and full backend tests. Fix any regressions caused by the new `EventsDeps` router signature, generated SQLC types, templ signatures, route ordering, or tab rendering.
Keep fixes scoped to Phase 10 files and affected test helpers. Do not alter Phase 8 social sign-in behavior, Phase 9 etape behavior, or existing task/file route semantics except where router call sites require the new dependency parameter.
</action>
<verify>
<automated>cd backend && just generate && go test ./... -count=1</automated>
</verify>
<acceptance_criteria>
- `cd backend && just generate` exits 0.
- `cd backend && go test ./... -count=1` exits 0.
- `git diff --check` exits 0.
- Existing task, etape, file, auth, and tablo tests still pass with the new router signature.
</acceptance_criteria>
</task>
<task type="checkpoint:human-verify">
<name>Task 3: Browser UAT checkpoint for Events tab</name>
<files></files>
<read_first>
- .planning/phases/10-events/10-UI-SPEC.md
- .planning/phases/10-events/10-VALIDATION.md
- backend/templates/events.templ
</read_first>
<action>
Start the app with `just dev` from the repository root or `cd backend && just dev`, sign in, open an existing tablo, and verify the Events tab manually:
1. Open Events and confirm the month grid, `Previous month`, `Next month`, and `New event` controls render without overlap.
2. Create an event from `New event` with title, date, start time, optional end time, description, and location.
3. Create another event from a day cell and confirm the date is prefilled.
4. Edit an event title/date/time and confirm the calendar refreshes to the correct month.
5. Try equal start/end times and confirm the visible error copy is `End time must be after the start time.`.
6. Delete an event and confirm it disappears after `Delete event`.
7. Navigate to a past month with events and confirm past events remain visible.
</action>
<verify>
<automated>cd backend && go test ./... -count=1</automated>
<manual>Browser confirms all EVENT-01..05 success criteria and the 10-UI-SPEC interaction contract.</manual>
</verify>
<acceptance_criteria>
- Full backend tests pass before the browser checkpoint.
- Browser verification covers create, edit, delete, invalid end time, month navigation, day-cell prefill, and past-month visibility.
- User approves the checkpoint before Phase 10 is marked complete.
</acceptance_criteria>
</task>
</tasks>
<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./... -count=1`
- `git diff --check`
- Browser UAT from Task 3
</verification>
<success_criteria>
- All EVENT-01..05 requirements are complete.
- All D-01..D-23 context decisions are implemented or verified.
- UI-SPEC is satisfied by the first functional Events tab UI.
- Security expectations are covered by parent-scoped queries, CSRF forms, escaped rendering, and owner-only tests.
</success_criteria>