---
phase: 12-native-tablo-chat
plan: 03
type: execute
wave: 3
depends_on:
  - 12-01-PLAN.md
  - 12-02-PLAN.md
files_modified:
  - backend/internal/web/discussion_broker.go
  - backend/internal/web/handlers_discussion.go
  - backend/internal/web/handlers_discussion_test.go
  - backend/internal/web/router.go
  - backend/cmd/web/main.go
  - backend/templates/layout.templ
  - backend/templates/discussion.templ
  - backend/static/discussion-sse.js
autonomous: false
requirements: [CHAT-04, CHAT-05]

must_haves:
  truths:
    - "D-17/D-21: realtime target is SSE receive plus CSRF-protected HTMX POST send; no WebSocket."
    - "D-18: reconnect is silent; do not add visible connection status UI."
    - "D-19: stream newly created messages and enough unread badge updates for dashboard/tab cards."
    - "D-20: POST renders sender message immediately; SSE updates other open views without duplicates."
    - "CHAT-05: no managed chat provider, external realtime runtime, or CDN script is added."
  artifacts:
    - path: "backend/internal/web/discussion_broker.go"
      provides: "owned in-process SSE pub/sub"
    - path: "backend/internal/web/handlers_discussion.go"
      provides: "SSE stream endpoint and broadcast after insert"
    - path: "backend/static/discussion-sse.js"
      provides: "minimal local EventSource DOM integration"
---

<objective>
Vertical slice 3: add authenticated SSE realtime delivery and final browser/proxy verification for native discussion.
</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-VALIDATION.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/12-native-tablo-chat/12-01-PLAN.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/12-native-tablo-chat/12-02-PLAN.md
</context>

<threat_model>
T-12-10 Unauthorized stream: SSE endpoint must be protected and owner-only like history/send.
T-12-11 Stream resource leak: broker must unregister clients on request context cancellation and avoid blocking broadcast forever on slow clients.
T-12-12 Proxy buffering: stream must flush headers/events/keepalives and be manually verified behind the configured proxy path.
T-12-13 Duplicate messages: sender must not see both POST-rendered and SSE-rendered copies of the same message.
T-12-14 Provider creep: do not add managed realtime provider, WebSocket server, external worker runtime, or runtime CDN script.
</threat_model>

<tasks>

<task type="auto">
  <name>Task 1: Add RED SSE handler and broker tests</name>
  <files>
    - backend/internal/web/handlers_discussion_test.go
  </files>
  <read_first>
    - backend/internal/web/handlers_discussion.go
    - backend/internal/web/router.go
    - .planning/phases/12-native-tablo-chat/12-VALIDATION.md
  </read_first>
  <action>
    Add tests that initially fail:
    1. `TestDiscussionStreamRequiresAuth` expects unauthenticated stream requests to redirect like other protected routes.
    2. `TestDiscussionStreamOwnershipReturns404` expects a non-owner stream request to return 404.
    3. `TestDiscussionStreamHeaders` expects `Content-Type: text/event-stream`, `Cache-Control: no-cache`, and a flushed/stream-compatible response for an owner request.
    4. `TestDiscussionPostBroadcastsToBroker` uses an injectable broker/fake to prove a successful POST publishes the inserted message ID/tablo ID and unread update intent.
    5. `TestDiscussionBrokerUnregistersOnCancel` exercises the broker directly to prove clients are removed when context cancels.
  </action>
  <verify>
    <automated>cd backend && go test ./internal/web -run 'TestDiscussionStream|TestDiscussionBroker|TestDiscussionPostBroadcasts' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - Tests cover auth, ownership, stream headers, publish-after-insert, and cancellation cleanup.
    - Tests do not pretend to prove browser/proxy streaming delivery; that remains manual UAT.
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 2: Implement SSE broker, stream route, and local browser glue</name>
  <files>
    - backend/internal/web/discussion_broker.go
    - backend/internal/web/handlers_discussion.go
    - backend/internal/web/router.go
    - backend/cmd/web/main.go
    - backend/templates/layout.templ
    - backend/templates/discussion.templ
    - backend/static/discussion-sse.js
  </files>
  <read_first>
    - backend/embed.go
    - backend/templates/layout.templ
    - backend/templates/discussion.templ
    - backend/internal/web/handlers_discussion.go
    - .planning/phases/12-native-tablo-chat/12-RESEARCH.md
  </read_first>
  <action>
    Add a small `DiscussionBroker` with subscribe/unsubscribe and non-blocking or bounded publish behavior. Wire it through `DiscussionDeps` in `cmd/web/main.go` and test routers.
    Add protected `GET /tablos/{id}/discussion/stream`. The handler must call `loadOwnedTablo`, require `http.Flusher`, set SSE headers, send periodic keepalive comments, flush after writes, and stop on request context cancellation.
    After successful message insert, render/broadcast enough payload for other views to append the message and refresh unread badge targets. Keep DB history as the source of truth for reconnect/missed events.
    Add a tiny local `/static/discussion-sse.js` script that creates `EventSource` for discussion containers, appends incoming message fragments, applies OOB badge fragments if used, and suppresses duplicates by message ID. Do not show connection status or typing/presence UI.
    Include the local script from `Layout` or conditionally from the discussion template without adding a CDN or framework.
  </action>
  <verify>
    <automated>cd backend && just generate && go test ./internal/web -run 'TestDiscussionStream|TestDiscussionBroker|TestDiscussionPostBroadcasts|TestDiscussion' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - Owner stream requests return SSE-compatible headers and flush data/keepalives.
    - Non-owner and unauthenticated stream requests cannot subscribe.
    - Successful POST publishes a realtime update after DB insert.
    - Local JS is framework-free, silent on reconnect, and avoids sender duplicates.
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 3: Run full backend verification and fix regressions</name>
  <files>
    - backend/internal/web/handlers_discussion.go
    - backend/internal/web/discussion_broker.go
    - backend/internal/web/handlers_discussion_test.go
    - backend/templates/discussion.templ
    - backend/templates/tablos.templ
    - backend/static/discussion-sse.js
  </files>
  <read_first>
    - .planning/phases/12-native-tablo-chat/12-VALIDATION.md
    - .planning/phases/12-native-tablo-chat/12-UI-SPEC.md
  </read_first>
  <action>
    Run generated-code verification and the full backend suite. Fix compile breaks, route signature updates, templ generation issues, streaming handler regressions, or existing auth/tablo/task/file/event/planning regressions caused by Phase 12 changes.
  </action>
  <verify>
    <automated>cd backend && just generate && TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1 && git diff --check</automated>
  </verify>
  <acceptance_criteria>
    - `cd backend && just generate` exits 0.
    - Full backend tests pass with `TEST_DATABASE_URL`.
    - `git diff --check` exits 0.
  </acceptance_criteria>
</task>

<task type="checkpoint:human-verify">
  <name>Task 4: Browser and proxy UAT for realtime discussion</name>
  <files></files>
  <read_first>
    - .planning/phases/12-native-tablo-chat/12-UI-SPEC.md
    - .planning/phases/12-native-tablo-chat/12-VALIDATION.md
  </read_first>
  <action>
    Start the app through the normal local dev/proxy path and verify:
    1. Direct `/tablos/{id}/discussion` renders the full detail page with Discussion active.
    2. Existing messages load oldest-first with day separators, author email, absolute timestamps, and escaped text.
    3. Empty discussion shows `No messages yet` and `Start the discussion for this tablo.` while composer remains visible.
    4. Posting a valid message renders it immediately for the sender.
    5. Empty and too-long messages show the specified validation copy.
    6. Dashboard unread badge appears for unread discussion messages and clears after opening the discussion.
    7. A second open browser/tab receives a new message without manual refresh.
    8. Sender view does not duplicate the POST-rendered message.
    9. Reconnect remains silent; no status/presence/typing UI appears.
    10. Streaming works through the local/prod reverse proxy path with keepalives or buffering disabled enough for prompt delivery.
  </action>
  <verify>
    <automated>cd backend && TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1</automated>
    <manual>Browser confirms CHAT-01..06, D-01..D-26, UI-SPEC, and proxy streaming behavior.</manual>
  </verify>
  <acceptance_criteria>
    - Browser UAT confirms two-view realtime delivery without external provider.
    - Reverse proxy streaming behavior is explicitly checked.
    - User approves final realtime transport choice as SSE receive plus HTMX POST send.
  </acceptance_criteria>
</task>

</tasks>

<verification>
Run:
- `cd backend && just generate`
- `cd backend && go test ./internal/web -run 'TestDiscussionStream|TestDiscussionBroker|TestDiscussionPostBroadcasts|TestDiscussion' -count=1`
- `cd backend && TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`
- `git diff --check`
- Browser/proxy UAT from Task 4
</verification>

<success_criteria>
- CHAT-04: open discussion views receive messages without manual refresh.
- CHAT-05: realtime uses only Xtablo-owned Go/Postgres/static infrastructure.
- D-17..D-21 are implemented with SSE receive, HTMX POST send, silent reconnect, message/badge updates, and no WebSocket.
- Phase 12 is ready for final verification after browser/proxy UAT.
</success_criteria>
