---
phase: 12-native-tablo-chat
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - backend/migrations/0009_discussion.sql
  - backend/internal/db/queries/discussion.sql
  - backend/internal/web/handlers_discussion_test.go
  - backend/internal/web/handlers_discussion.go
  - backend/internal/web/router.go
  - backend/cmd/web/main.go
  - backend/templates/tablos.templ
  - backend/templates/discussion.templ
  - backend/templates/discussion_forms.go
autonomous: true
requirements: [CHAT-01, CHAT-02, CHAT-03, CHAT-06]

must_haves:
  truths:
    - "D-01/D-02/D-03/D-04: Discussion is a fifth tablo detail tab after Events, with direct full-page fallback."
    - "D-06/D-07/D-08/D-09/D-10: rows show author, absolute timestamp, text, oldest first, day separators, bottom composer, and specified empty state."
    - "D-11/D-12/D-13/D-14: schema carries nullable edit/delete metadata; no edit/delete UI appears."
    - "D-15/D-16: composer is textarea plus Send message; no Enter-to-send behavior."
    - "D-22/D-23/D-24/D-25: owner-only; author label uses email fallback; header may show 1 participant."
    - "CHAT-06: message body is length-limited server-side and rendered escaped through templ."
  artifacts:
    - path: "backend/migrations/0009_discussion.sql"
      provides: "message and read-state schema foundation"
    - path: "backend/internal/db/queries/discussion.sql"
      provides: "typed sqlc discussion history/send/read queries"
    - path: "backend/internal/web/handlers_discussion.go"
      provides: "protected discussion tab and send handlers"
    - path: "backend/templates/discussion.templ"
      provides: "server-rendered discussion UI"
---

<objective>
Vertical slice 1: add persisted discussion history and CSRF-protected message posting inside the tablo detail Discussion tab, without realtime or dashboard unread badges yet.
</objective>

<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/12-native-tablo-chat/12-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/12-native-tablo-chat/12-UI-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/12-native-tablo-chat/12-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/12-native-tablo-chat/12-PATTERNS.md
</context>

<threat_model>
T-12-01 Schema bypass: message rows must reference real tablos/users with cascading foreign keys.
T-12-02 Future lifecycle ambiguity: include nullable edit/delete metadata but do not expose edit/delete UI or decide tombstone copy.
T-12-03 Unauthenticated access: discussion routes must live inside `auth.RequireAuth`.
T-12-04 Cross-user leak/mutation: all handlers must call `loadOwnedTablo`; non-owner access returns 404.
T-12-05 XSS/body abuse: body is trimmed, non-empty, max-length checked, and rendered through templ escaped expressions.
T-12-06 CSRF bypass: message send remains normal POST with gorilla/csrf hidden token.
</threat_model>

<tasks>

<task type="auto">
  <name>Task 1: Add discussion schema and sqlc queries</name>
  <files>
    - backend/migrations/0009_discussion.sql
    - backend/internal/db/queries/discussion.sql
  </files>
  <read_first>
    - backend/migrations/0003_tablos.sql
    - backend/migrations/0008_events.sql
    - backend/internal/db/queries/tablos.sql
    - backend/internal/db/queries/events.sql
    - .planning/phases/12-native-tablo-chat/12-RESEARCH.md
  </read_first>
  <action>
    Create `discussion_messages` with `id uuid default gen_random_uuid()`, `tablo_id uuid not null references tablos(id) on delete cascade`, `author_user_id uuid not null references users(id) on delete restrict`, `body text not null`, `created_at timestamptz not null default now()`, `updated_at timestamptz not null default now()`, nullable `edited_at`, `edited_by_user_id`, `deleted_at`, and `deleted_by_user_id`. Add checks for non-blank trimmed body and maximum body length; use 10000 characters unless implementation finds a stronger local convention.
    Create `discussion_read_states` keyed by `(tablo_id, user_id)` with nullable `last_read_message_id`, `last_read_at timestamptz not null default now()`, `created_at`, and `updated_at`.
    Add indexes for `(tablo_id, created_at, id)` and read-state lookup. Add sqlc queries for list history oldest-first with author email, insert message, get message row with author email, and upsert read state.
  </action>
  <verify>
    <automated>cd backend && just generate</automated>
  </verify>
  <acceptance_criteria>
    - Migration includes message lifecycle metadata fields required by CHAT-03.
    - Migration includes persistent read-state table for later D-05/D-26 work.
    - `discussion.sql` exposes typed queries needed by history and send handlers.
    - `cd backend && just generate` exits 0.
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 2: Add RED discussion handler tests</name>
  <files>
    - backend/internal/web/handlers_discussion_test.go
  </files>
  <read_first>
    - backend/internal/web/handlers_events_test.go
    - backend/internal/web/handlers_tablos_test.go
    - backend/internal/web/router.go
    - .planning/phases/12-native-tablo-chat/12-UI-SPEC.md
  </read_first>
  <action>
    Add DB-backed full-router tests that initially fail before implementation:
    1. `TestDiscussionTabRendersHistoryAndComposer` creates two messages with HTML-looking body text, requests `GET /tablos/{id}/discussion`, and expects Discussion, `1 participant`, oldest-first ordering, author email, absolute timestamp content, escaped text, day separator, textarea `Message`, placeholder `Write a message...`, and `Send message`.
    2. `TestDiscussionTabFullPageFallback` sends a non-HTMX direct GET and expects the full tablo detail shell with Discussion active.
    3. `TestDiscussionPostCreatesMessage` obtains CSRF, posts a valid body with `HX-Request:true`, expects 200, the new message fragment, and a DB row for the authenticated author/tablo.
    4. `TestDiscussionPostRejectsEmptyAndTooLong` expects 422 plus `Message is required.` and `Message is too long.`.
    5. `TestDiscussionOwnershipReturns404` verifies another user cannot GET or POST to the owner's discussion.
    6. `TestDiscussionRequiresCSRF` posts without CSRF and expects gorilla/csrf rejection.
  </action>
  <verify>
    <automated>cd backend && go test ./internal/web -run 'TestDiscussion' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - Tests use the existing session/CSRF/full-router helper patterns.
    - Tests prove body escaping by asserting raw `<script>` is absent and escaped text appears.
    - Tests assert no edit/delete controls appear in the Discussion tab.
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 3: Implement discussion tab, templates, and POST send</name>
  <files>
    - backend/internal/web/handlers_discussion.go
    - backend/internal/web/router.go
    - backend/cmd/web/main.go
    - backend/templates/tablos.templ
    - backend/templates/discussion.templ
    - backend/templates/discussion_forms.go
  </files>
  <read_first>
    - backend/internal/web/handlers_events.go
    - backend/templates/events.templ
    - backend/templates/tablos.templ
    - backend/templates/auth_form_errors.templ
    - backend/internal/web/router.go
    - .planning/phases/12-native-tablo-chat/12-PATTERNS.md
  </read_first>
  <action>
    Add `DiscussionDeps{Queries *sqlc.Queries}` and route wiring in `NewRouter` and `cmd/web/main.go`. Mount `GET /tablos/{id}/discussion` and `POST /tablos/{id}/discussion/messages` inside the protected route group after Events static routes and before later parametric child routes.
    Add `TabloDiscussionTabHandler` using `loadOwnedTablo`, `ListDiscussionMessagesByTablo`, and read-state upsert. Return `DiscussionTabFragment` for HTMX and `TabloDetailPage(..., activeTab="discussion")` for full-page fallback.
    Add `DiscussionMessageCreateHandler` that trims body, rejects whitespace-only and max-length violations, inserts with the authenticated user ID, marks sender read state, and renders a message fragment for HTMX. For non-HTMX valid POST, redirect to `/tablos/{id}/discussion` with 303.
    Extend `TabloDetailPage` signature carefully for discussion data or a view model; update all call sites. Add Discussion tab link after Events and render `DiscussionTabFragment` when `activeTab == "discussion"`.
    Add templates for heading, participant count, day separators, message rows, empty state, and composer following `12-UI-SPEC.md`.
  </action>
  <verify>
    <automated>cd backend && just generate && go test ./internal/web -run 'TestDiscussion' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - `GET /tablos/{id}/discussion` works as HTMX fragment and full page.
    - `POST /tablos/{id}/discussion/messages` is CSRF-protected, validates body, persists message, and renders sender message immediately.
    - Discussion UI contains no edit/delete/reaction/reply/presence/typing/attachment controls.
    - Targeted `TestDiscussion...` tests pass.
  </acceptance_criteria>
</task>

</tasks>

<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./internal/web -run 'TestDiscussion' -count=1`
- `git diff --check`
</verification>

<success_criteria>
- CHAT-01: authorized users can load persisted discussion history.
- CHAT-02: users can post text messages with CSRF and validation.
- CHAT-03: message schema includes required core and lifecycle metadata.
- CHAT-06: message body is max-length checked and escaped in rendered HTML.
</success_criteria>
