---
phase: 12-native-tablo-chat
plan: 02
type: execute
wave: 2
depends_on:
  - 12-01-PLAN.md
files_modified:
  - backend/internal/db/queries/tablos.sql
  - backend/internal/db/queries/discussion.sql
  - backend/internal/web/handlers_tablos.go
  - backend/internal/web/handlers_discussion.go
  - backend/internal/web/handlers_discussion_test.go
  - backend/templates/tablos.templ
  - backend/templates/discussion_forms.go
autonomous: true
requirements: [CHAT-01]

must_haves:
  truths:
    - "D-05/D-26: dashboard tablo cards show durable unread badges from persistent per-user/tablo read state."
    - "D-19: unread-badge updates must be computable for later SSE refreshes."
    - "D-22: unread queries remain owner-only under current tablo ownership."
    - "Unread badges are numeric only and omitted when count is zero."
  artifacts:
    - path: "backend/internal/db/queries/discussion.sql"
      provides: "mark-read and unread count queries"
    - path: "backend/internal/db/queries/tablos.sql"
      provides: "dashboard tablo rows with unread counts or a companion query"
    - path: "backend/templates/tablos.templ"
      provides: "small dashboard unread badge"
---

<objective>
Vertical slice 2: wire persistent read state and dashboard unread badges so the discussion surface has durable unread behavior before realtime events are added.
</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-01-PLAN.md
</context>

<threat_model>
T-12-07 Cross-user unread leak: unread counts must be computed only for tablos owned by the authenticated user.
T-12-08 Stale badges: opening a discussion must mark messages read for that user/tablo so dashboard counts clear after reload.
T-12-09 Layout regression: unread badge must not overlap title/color/delete controls or resize cards unpredictably.
</threat_model>

<tasks>

<task type="auto">
  <name>Task 1: Add RED unread badge and read-state 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.go
    - backend/templates/tablos.templ
    - .planning/phases/12-native-tablo-chat/12-UI-SPEC.md
  </read_first>
  <action>
    Add tests that initially fail:
    1. `TestTablosListDiscussionUnreadBadge` creates two tablos, messages in one unread discussion, and expects `/` to show a numeric badge with accessible label such as `3 unread discussion messages` only on the matching card.
    2. `TestTablosListDiscussionUnreadDoesNotLeakOtherUsers` creates another user's messages and expects they do not affect the authenticated user's dashboard.
    3. `TestDiscussionGetMarksMessagesRead` creates unread messages, opens `/tablos/{id}/discussion`, then expects `/` no longer shows that unread badge.
    4. `TestDiscussionPostMarksSenderRead` posts as the authenticated user and verifies the sender does not see their own message as unread after redirect/dashboard reload.
  </action>
  <verify>
    <automated>cd backend && go test ./internal/web -run 'TestTablosListDiscussionUnread|TestDiscussion.*Read' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - Tests prove unread counts are scoped to the authenticated owner.
    - Tests prove reading the discussion clears the badge through persistent state.
    - Tests prove sender-created messages are not counted unread for the sender.
  </acceptance_criteria>
</task>

<task type="auto">
  <name>Task 2: Implement unread query integration and dashboard badge</name>
  <files>
    - backend/internal/db/queries/discussion.sql
    - backend/internal/db/queries/tablos.sql
    - backend/internal/web/handlers_tablos.go
    - backend/internal/web/handlers_discussion.go
    - backend/templates/tablos.templ
    - backend/templates/discussion_forms.go
  </files>
  <read_first>
    - backend/internal/db/queries/tablos.sql
    - backend/internal/db/queries/discussion.sql
    - backend/internal/web/handlers_tablos.go
    - backend/templates/tablos.templ
    - .planning/phases/12-native-tablo-chat/12-PATTERNS.md
  </read_first>
  <action>
    Add sqlc support for unread counts. Prefer either `ListTablosByUserWithDiscussionUnread` or a companion query keyed by `user_id`; avoid N+1 per-card queries if practical.
    Update `TablosListHandler` and create-tablo HTMX paths to pass a dashboard view model that includes unread counts. If a newly created tablo has no unread state, badge count is zero and omitted.
    Add a `DiscussionUnreadBadge` template/helper that renders numeric copy only when count > 0, caps display at `99+` if needed, and includes an accessible label.
    Ensure `TabloDiscussionTabHandler` marks messages read after successfully loading history, and `DiscussionMessageCreateHandler` marks the sender's read state after insert.
    Run `cd backend && just generate`.
  </action>
  <verify>
    <automated>cd backend && just generate && go test ./internal/web -run 'TestTablosListDiscussionUnread|TestDiscussion.*Read' -count=1</automated>
  </verify>
  <acceptance_criteria>
    - Dashboard cards show unread badges only when count > 0.
    - Opening Discussion clears the current user's unread badge durably.
    - Sender's own posts do not create unread count for the sender.
    - Targeted unread/read-state tests pass.
  </acceptance_criteria>
</task>

</tasks>

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

<success_criteria>
- D-05/D-26 are satisfied with persistent unread/read-state support.
- Dashboard badge behavior is ready for later SSE OOB refresh.
- Existing dashboard tablos behavior still works for users with zero unread messages.
</success_criteria>
