---
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 ./...`.

