docs(12): create phase plan
This commit is contained in:
parent
164fa1133c
commit
f848c42e54
7 changed files with 792 additions and 11 deletions
|
|
@ -2,15 +2,15 @@
|
|||
gsd_state_version: 1.0
|
||||
milestone: v2.0
|
||||
milestone_name: Collaboration, planning, and social sign-in
|
||||
status: planning
|
||||
last_updated: "2026-05-16T07:55:44.531Z"
|
||||
last_activity: 2026-05-16
|
||||
status: executing
|
||||
last_updated: "2026-05-16T08:03:11.927Z"
|
||||
last_activity: 2026-05-16 -- Phase 12 planning complete
|
||||
progress:
|
||||
total_phases: 5
|
||||
completed_phases: 4
|
||||
total_plans: 15
|
||||
total_plans: 18
|
||||
completed_plans: 15
|
||||
percent: 100
|
||||
percent: 83
|
||||
---
|
||||
|
||||
# STATE
|
||||
|
|
@ -24,15 +24,15 @@ progress:
|
|||
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.
|
||||
**Current focus:** Phase 11 — Individual Planning
|
||||
**Current focus:** Phase 12 — Native Tablo Chat
|
||||
|
||||
## Current Position
|
||||
|
||||
Phase: 12
|
||||
Plan: Not started
|
||||
Status: Ready to plan
|
||||
Last activity: 2026-05-16
|
||||
Resume file: .planning/phases/12-native-tablo-chat/12-UI-SPEC.md
|
||||
Status: Ready to execute
|
||||
Last activity: 2026-05-16 -- Phase 12 planning complete
|
||||
Resume file: .planning/phases/12-native-tablo-chat/12-01-PLAN.md
|
||||
|
||||
## Phase Status
|
||||
|
||||
|
|
@ -41,8 +41,8 @@ Resume file: .planning/phases/12-native-tablo-chat/12-UI-SPEC.md
|
|||
| 8 | Social Sign-in | ✓ Complete |
|
||||
| 9 | Etapes | ◆ UAT passed; security pending |
|
||||
| 10 | Events | ✓ Complete |
|
||||
| 11 | Individual Planning | ◆ Ready to execute |
|
||||
| 12 | Native Tablo Chat | ○ Pending |
|
||||
| 11 | Individual Planning | ✓ Complete |
|
||||
| 12 | Native Tablo Chat | ◆ Ready to execute |
|
||||
|
||||
## Verification Record
|
||||
|
||||
|
|
|
|||
170
.planning/phases/12-native-tablo-chat/12-01-PLAN.md
Normal file
170
.planning/phases/12-native-tablo-chat/12-01-PLAN.md
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
---
|
||||
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>
|
||||
129
.planning/phases/12-native-tablo-chat/12-02-PLAN.md
Normal file
129
.planning/phases/12-native-tablo-chat/12-02-PLAN.md
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
---
|
||||
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>
|
||||
197
.planning/phases/12-native-tablo-chat/12-03-PLAN.md
Normal file
197
.planning/phases/12-native-tablo-chat/12-03-PLAN.md
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
---
|
||||
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>
|
||||
53
.planning/phases/12-native-tablo-chat/12-PATTERNS.md
Normal file
53
.planning/phases/12-native-tablo-chat/12-PATTERNS.md
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
# Phase 12: Native Tablo Chat - Patterns
|
||||
|
||||
**Mapped:** 2026-05-16
|
||||
|
||||
## Closest Existing Analogs
|
||||
|
||||
| New Concern | Closest Existing File | Reuse Pattern |
|
||||
|-------------|-----------------------|---------------|
|
||||
| Discussion routes and deps | `backend/internal/web/router.go` | Add explicit deps to `NewRouter`; mount protected static segments before parametric children. |
|
||||
| Owned tablo child resource | `backend/internal/web/handlers_events.go` | Call `loadOwnedTablo`, return 404 for invalid/non-owned, render HTMX fragment or full detail page. |
|
||||
| DB-backed handler tests | `backend/internal/web/handlers_events_test.go` | Use `setupTestDB`, `sessionCookieForUser`, `getCSRFToken`, full router requests, and response/DB assertions. |
|
||||
| Tab content integration | `backend/templates/tablos.templ` | Add fifth tab link and active-tab branch inside `TabloDetailPage`. |
|
||||
| Child tab template | `backend/templates/events.templ` | Keep a single tab fragment with form/list subcomponents and restrained slate/blue styling. |
|
||||
| Form errors | `backend/templates/auth_form_errors.templ` | Reuse `FieldError` and `GeneralError` for textarea validation. |
|
||||
| Nullable text params | `backend/internal/web/handlers_events.go` | Use `pgtype.Text{String: s, Valid: s != ""}`. |
|
||||
| Local static assets | `backend/templates/layout.templ`, `backend/embed.go` | Serve script from `/static/...`; embedding includes all static files. |
|
||||
| Dashboard card extension | `backend/templates/tablos.templ` | Keep badge small inside the existing card header; no layout rewrite. |
|
||||
|
||||
## File Mapping
|
||||
|
||||
| File | Role | Notes |
|
||||
|------|------|-------|
|
||||
| `backend/migrations/0009_discussion.sql` | Schema | Add `discussion_messages` and `discussion_read_states`; include constraints/indexes. |
|
||||
| `backend/internal/db/queries/discussion.sql` | sqlc queries | History, insert, read-state upsert, unread counts, message lookup after insert. |
|
||||
| `backend/internal/web/handlers_discussion.go` | HTTP handlers | Discussion tab, send, stream, read-state helpers. |
|
||||
| `backend/internal/web/discussion_broker.go` | SSE broker | Small per-process pub/sub with context cleanup and keepalive support. |
|
||||
| `backend/internal/web/handlers_discussion_test.go` | Tests | DB-backed contracts for all discussion behavior. |
|
||||
| `backend/templates/discussion.templ` | UI | Message list, rows, day separators, composer, empty state, SSE container targets. |
|
||||
| `backend/templates/discussion_forms.go` | View models | Form/errors, message row formatting, date grouping, unread badge helpers. |
|
||||
| `backend/templates/tablos.templ` | Integration | Add Discussion tab and dashboard unread badge support. |
|
||||
| `backend/static/discussion-sse.js` | Browser EventSource glue | Local, minimal, no framework; append fragments and apply OOB badge updates. |
|
||||
|
||||
## Required Local Conventions
|
||||
|
||||
- Keep sqlc-generated Go files uncommitted; execution runs `cd backend && just generate`.
|
||||
- Keep all discussion routes inside `auth.RequireAuth`.
|
||||
- Use `loadOwnedTablo` before any discussion query or stream registration.
|
||||
- Keep non-owner behavior as 404, not 403.
|
||||
- Use templ escaped expressions for message body and metadata.
|
||||
- Keep all added JS local and small; no CDN, no client framework.
|
||||
- Preserve non-HTMX fallback for history page and send redirect/refresh behavior.
|
||||
|
||||
## Decision Coverage Map
|
||||
|
||||
| Decisions | Implementation Surface |
|
||||
|-----------|------------------------|
|
||||
| D-01/D-02/D-03/D-04 | `TabloDetailPage` fifth tab, `/tablos/{id}/discussion` handler. |
|
||||
| D-05/D-26 | `discussion_read_states`, dashboard unread queries, badge template. |
|
||||
| D-06/D-07/D-08/D-09/D-10 | `discussion.templ` message list, day grouping, composer, empty state. |
|
||||
| D-11/D-12/D-13/D-14 | Schema nullable edit/delete metadata; no UI controls. |
|
||||
| D-15/D-16 | Composer textarea/button only. |
|
||||
| D-17/D-18/D-19/D-20/D-21 | HTMX POST send, SSE stream, local script, no WebSockets. |
|
||||
| D-22/D-23/D-24/D-25 | Owner-only handlers, email fallback author label, `1 participant`. |
|
||||
165
.planning/phases/12-native-tablo-chat/12-RESEARCH.md
Normal file
165
.planning/phases/12-native-tablo-chat/12-RESEARCH.md
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
# Phase 12: Native Tablo Chat - Research
|
||||
|
||||
**Researched:** 2026-05-16
|
||||
**Domain:** Go + HTMX persisted discussion, Postgres, SSE
|
||||
**Confidence:** HIGH
|
||||
|
||||
<user_constraints>
|
||||
## User Constraints
|
||||
|
||||
Locked decisions from `12-CONTEXT.md` and `12-UI-SPEC.md`:
|
||||
|
||||
- D-01/D-02/D-03/D-04: Discussion is the fifth tablo detail tab, label `Discussion`, after Events, with direct full-page fallback at `/tablos/{id}/discussion`.
|
||||
- D-05/D-26: Dashboard tablo cards need persistent per-user/tablo unread badges.
|
||||
- D-06/D-07/D-08/D-09/D-10: Message rows show author, absolute timestamp, text, oldest first, composer at bottom, day separators, and the specified empty state copy.
|
||||
- D-11/D-12/D-13/D-14: No edit/delete UI in Phase 12, but nullable edit/delete metadata must exist in the message schema without forcing future delete semantics.
|
||||
- D-15/D-16: Composer is a plain textarea plus send button; no Enter-to-send behavior.
|
||||
- D-17/D-18/D-19/D-20/D-21: Transport is SSE receive plus CSRF-protected HTMX POST send; reconnect is silent; POST renders the sender message immediately; SSE updates other open views; no WebSocket unless a hard blocker appears.
|
||||
- D-22/D-23/D-24/D-25: Owner-only access under the current tablo model; author label uses display name if available and email fallback; current schema has email only; header can show `1 participant`.
|
||||
- UI-SPEC: Use existing Layout, tab shell, local templ/ui patterns, restrained white/slate/blue styling, no JS framework, no rich chat controls, no presence/typing/status UI.
|
||||
|
||||
Deferred/out of scope:
|
||||
|
||||
- Typing, presence, read receipts, reactions, replies, rich media, attachments, search, pinned messages, edit UI, delete UI, membership/invitations, and exact future delete behavior.
|
||||
</user_constraints>
|
||||
|
||||
<architectural_responsibility_map>
|
||||
## Architectural Responsibility Map
|
||||
|
||||
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||
|------------|--------------|----------------|-----------|
|
||||
| Persisted message history | Database | Backend | Postgres owns message durability; handlers only authorize and render. |
|
||||
| Discussion tab rendering | Frontend Server | Browser | templ renders the full page and HTMX fragments; browser only swaps/appends HTML. |
|
||||
| Message send | Backend | Database | A normal POST validates CSRF/body, inserts, updates read state, renders the sender fragment, and broadcasts. |
|
||||
| Realtime receive | Backend | Browser | Go owns authenticated SSE streams; browser EventSource receives fragments and applies DOM updates with a tiny local script. |
|
||||
| Unread counts | Database | Backend | Persistent per-user/tablo read state is needed across reloads and future sharing. |
|
||||
| Authorization | Backend | Database | `loadOwnedTablo` and ownership-filtered queries preserve current 404-for-non-owner behavior. |
|
||||
</architectural_responsibility_map>
|
||||
|
||||
<research_summary>
|
||||
## Summary
|
||||
|
||||
The existing Go backend already has the right architecture for Phase 12: protected chi routes, `loadOwnedTablo` for owner-only access, sqlc/goose for schema and query changes, templ fragments for full-page fallback, HTMX for normal form submission, and local embedded static assets. Phase 12 should add a native `discussion_messages` table, a persistent `discussion_read_states` table, a `DiscussionDeps` dependency, handlers/templates for the fifth detail tab, and a small Go SSE broker for live append/unread updates.
|
||||
|
||||
The safest first version is not the historical Cloudflare worker/WebSocket chat architecture. Browser-to-server sends should remain CSRF-protected POST requests. Receive-only realtime should be SSE from protected routes, with a per-process broker and database-backed history/read state as source of truth. This satisfies the phase requirement without managed providers and keeps degraded non-JS sends/history usable.
|
||||
|
||||
**Primary recommendation:** Build the feature in three slices: persisted Discussion tab and POST send, unread badge/read-state integration, then SSE realtime with browser/proxy UAT.
|
||||
</research_summary>
|
||||
|
||||
<standard_stack>
|
||||
## Standard Stack
|
||||
|
||||
| Component | Existing Version/Pattern | Purpose | Recommendation |
|
||||
|-----------|--------------------------|---------|----------------|
|
||||
| Go HTTP | stdlib + chi v5.2.5 | Routes, streaming responses | Use `http.Flusher` and request context cancellation for SSE. |
|
||||
| templ | v0.3.1020 | Server-rendered HTML | Add discussion templates; rely on templ escaping for message text. |
|
||||
| HTMX | v2 local static asset | POST send and fragment swaps | Keep composer as HTMX POST; do not add htmx SSE extension unless vendored locally. |
|
||||
| Browser EventSource | Native browser API | Receive SSE | Add one small local script under `backend/static/` for EventSource append/OOB behavior. |
|
||||
| Postgres/sqlc/goose | Existing backend pattern | Durable schema and typed queries | Add `0009_discussion.sql` and `discussion.sql`; generated Go remains gitignored. |
|
||||
|
||||
No new managed service, frontend framework, CDN runtime, or WebSocket infrastructure is needed.
|
||||
</standard_stack>
|
||||
|
||||
<architecture_patterns>
|
||||
## Architecture Patterns
|
||||
|
||||
### System Architecture
|
||||
|
||||
```
|
||||
GET /tablos/{id}/discussion
|
||||
-> RequireAuth -> loadOwnedTablo -> ListDiscussionMessages + MarkDiscussionRead
|
||||
-> templ full page or tab fragment
|
||||
|
||||
POST /tablos/{id}/discussion/messages
|
||||
-> RequireAuth + CSRF -> loadOwnedTablo -> validate body
|
||||
-> InsertDiscussionMessage -> MarkDiscussionRead
|
||||
-> render sender fragment immediately
|
||||
-> broadcast message + unread badge refresh to SSE broker
|
||||
|
||||
GET /tablos/{id}/discussion/stream
|
||||
-> RequireAuth -> loadOwnedTablo -> register SSE client
|
||||
-> keepalive comments + message/unread events until context cancels
|
||||
|
||||
GET /
|
||||
-> RequireAuth -> ListTablosWithDiscussionUnreadCounts
|
||||
-> dashboard cards render numeric badge when count > 0
|
||||
```
|
||||
|
||||
### Recommended Project Structure
|
||||
|
||||
```
|
||||
backend/
|
||||
├── migrations/0009_discussion.sql
|
||||
├── internal/db/queries/discussion.sql
|
||||
├── internal/web/handlers_discussion.go
|
||||
├── internal/web/handlers_discussion_test.go
|
||||
├── internal/web/discussion_broker.go
|
||||
├── templates/discussion.templ
|
||||
├── templates/discussion_forms.go
|
||||
└── static/discussion-sse.js
|
||||
```
|
||||
|
||||
### Pattern 1: Ownership Gate First
|
||||
|
||||
Use `loadOwnedTablo(w, r, TablosDeps{Queries: deps.Queries})` before listing, inserting, marking read, or opening SSE. Preserve the established behavior where invalid UUIDs, missing tablos, and non-owner access return 404.
|
||||
|
||||
### Pattern 2: POST Response Is Sender Source Of Truth
|
||||
|
||||
The POST handler should render the sender's new message immediately, ideally as an appendable message fragment plus any OOB updates needed for composer/errors. SSE should broadcast to other open views; the browser-side receiver must suppress duplicates for the sender if it is also subscribed.
|
||||
|
||||
### Pattern 3: Database-Backed Read Cursor
|
||||
|
||||
Use a read-state row keyed by `(tablo_id, user_id)` with `last_read_message_id` and `last_read_at`. Unread count can be computed by joining messages for the owned tablo and counting rows newer than the read cursor/after `last_read_at`. This is durable across reloads and future-compatible with sharing.
|
||||
|
||||
### Pattern 4: Per-Process SSE Broker, DB Is Source Of Truth
|
||||
|
||||
A small in-memory broker is acceptable for the current single-process Go app because persisted history covers missed events on reconnect. The broker should never be the only source of message history or unread state.
|
||||
</architecture_patterns>
|
||||
|
||||
<common_pitfalls>
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Forgetting Full-Page Fallback
|
||||
**What goes wrong:** `/tablos/{id}/discussion` only works as an HTMX fragment and direct loads lose the detail shell.
|
||||
**How to avoid:** Match `TabloEventsTabHandler`: return `DiscussionTabFragment` for `HX-Request`, otherwise render `TabloDetailPage(..., activeTab="discussion")`.
|
||||
|
||||
### Pitfall 2: Relying On SSE For The Sender's Own Message
|
||||
**What goes wrong:** The sender sees delay, duplicate messages, or no update if the stream is not connected.
|
||||
**How to avoid:** POST inserts and returns the sender message fragment immediately; SSE is for other open views and badge refreshes.
|
||||
|
||||
### Pitfall 3: In-Memory Read State
|
||||
**What goes wrong:** Dashboard badges reset or become stale after reload/restart.
|
||||
**How to avoid:** Store read state in Postgres and compute badge counts from messages/read-state.
|
||||
|
||||
### Pitfall 4: Buffered SSE In Reverse Proxies
|
||||
**What goes wrong:** Events do not arrive until buffers fill or the connection closes.
|
||||
**How to avoid:** Set `Content-Type: text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`, flush after each event/keepalive, and manually verify the local/prod reverse proxy path.
|
||||
|
||||
### Pitfall 5: CSRF Expectations On EventSource
|
||||
**What goes wrong:** Trying to send custom CSRF headers with native EventSource creates unnecessary complexity.
|
||||
**How to avoid:** Keep SSE as authenticated GET with session cookie and no mutation. Keep mutations as CSRF-protected POST.
|
||||
|
||||
### Pitfall 6: Unescaped Message Rendering
|
||||
**What goes wrong:** Chat messages become an XSS surface.
|
||||
**How to avoid:** Render body through templ escaped expressions, preserve line breaks with CSS (`whitespace-pre-wrap`) rather than raw HTML, and test with HTML-looking input.
|
||||
|
||||
### Pitfall 7: Adding Rich Chat UI Too Early
|
||||
**What goes wrong:** Phase expands into presence, typing, replies, delete/edit UX, attachments, and client state.
|
||||
**How to avoid:** Enforce UI-SPEC: rows, day separators, composer, unread badge only.
|
||||
</common_pitfalls>
|
||||
|
||||
## Validation Architecture
|
||||
|
||||
Phase 12 needs DB-backed integration tests plus one manual/browser streaming checkpoint.
|
||||
|
||||
- Unit/DB integration tests should cover migration/query behavior, owner-only history, POST validation, CSRF, max body length, escaped rendering, full-page fallback, and unread count/read-state behavior.
|
||||
- Handler tests can verify SSE response headers and that unauthorized/non-owner streams are blocked. Full end-to-end delivery with two browsers and reverse proxy buffering requires manual UAT because `httptest.ResponseRecorder` is not a faithful streaming/proxy environment.
|
||||
- Final verification should run `cd backend && just generate`, targeted `go test ./internal/web -run 'TestDiscussion|TestTablosListDiscussionUnread' -count=1`, full backend tests with `TEST_DATABASE_URL`, and `git diff --check`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
Resolved during research:
|
||||
|
||||
- **Realtime transport:** Use SSE receive + HTMX POST send. No hard blocker found.
|
||||
- **Author display name:** Do not add user display-name schema in Phase 12; email fallback satisfies D-23/D-24 and avoids unrelated account profile scope.
|
||||
- **Unread model:** Use persistent `discussion_read_states`, even owner-only, to satisfy D-05/D-26 and future sharing.
|
||||
67
.planning/phases/12-native-tablo-chat/12-VALIDATION.md
Normal file
67
.planning/phases/12-native-tablo-chat/12-VALIDATION.md
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
phase: 12
|
||||
slug: native-tablo-chat
|
||||
status: draft
|
||||
nyquist_compliant: true
|
||||
wave_0_complete: true
|
||||
created: 2026-05-16
|
||||
---
|
||||
|
||||
# Phase 12 - Validation Strategy
|
||||
|
||||
> Per-phase validation contract for native tablo discussion.
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | Go test with existing DB-backed integration harness |
|
||||
| **Config file** | `backend/sqlc.yaml`, `backend/justfile` |
|
||||
| **Quick run command** | `cd backend && go test ./internal/web -run 'TestDiscussion|TestTablosListDiscussionUnread' -count=1` |
|
||||
| **Full suite command** | `cd backend && TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1` |
|
||||
| **Estimated runtime** | ~60 seconds for full backend suite with local Postgres |
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run the targeted discussion test command for touched behavior.
|
||||
- **After every plan wave:** Run `cd backend && just generate` plus the full backend suite.
|
||||
- **Before `$gsd-verify-work`:** Full suite and browser/SSE UAT must be green.
|
||||
- **Max feedback latency:** 60 seconds for automated checks.
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 12-01-01 | 01 | 1 | CHAT-03 | T-12-01/T-12-02 | Message schema carries tablo/author/body/timestamps/edit/delete metadata | integration | `cd backend && just generate` | Yes | pending |
|
||||
| 12-01-02 | 01 | 1 | CHAT-01/CHAT-02/CHAT-06 | T-12-03/T-12-04/T-12-05 | Owner-only history and POST validation with escaped output | integration | `cd backend && go test ./internal/web -run 'TestDiscussion' -count=1` | Yes | pending |
|
||||
| 12-01-03 | 01 | 1 | CHAT-01/CHAT-06 | T-12-05 | UI contract renders escaped history, day separators, composer, and fallback | integration | `cd backend && go test ./internal/web -run 'TestDiscussion' -count=1` | Yes | pending |
|
||||
| 12-02-01 | 02 | 2 | CHAT-01 | T-12-06 | Unread count comes from persistent owned read state | integration | `cd backend && go test ./internal/web -run 'TestTablosListDiscussionUnread|TestDiscussionReadState' -count=1` | Yes | pending |
|
||||
| 12-02-02 | 02 | 2 | CHAT-01 | T-12-06/T-12-07 | Dashboard badge shows only accurate unread counts | integration | `cd backend && go test ./internal/web -run 'TestTablosListDiscussionUnread' -count=1` | Yes | pending |
|
||||
| 12-03-01 | 03 | 3 | CHAT-04/CHAT-05 | T-12-08/T-12-09 | Authenticated SSE streams flush owned message events without external provider | integration/manual | `cd backend && go test ./internal/web -run 'TestDiscussionStream' -count=1` | Yes | pending |
|
||||
| 12-03-02 | 03 | 3 | CHAT-04/CHAT-05 | T-12-08/T-12-10 | Two browser views receive messages without refresh through local/prod proxy path | manual | Browser UAT | N/A | pending |
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
Existing infrastructure covers all phase requirements:
|
||||
|
||||
- `backend/internal/web/testdb_test.go` provides DB-backed schema-isolated tests.
|
||||
- `backend/internal/web/handlers_events_test.go` provides full-router authenticated child-resource patterns.
|
||||
- `backend/justfile` provides `just generate`, `just test`, and local dev server workflows.
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Two open browser views receive new discussion messages without manual refresh | CHAT-04 | Requires real browser EventSource behavior and DOM updates | Open the same tablo discussion in two tabs/sessions, send a message in one, confirm the other appends it without refresh and sender does not duplicate it. |
|
||||
| Streaming through reverse proxy path is not buffered | CHAT-04/CHAT-05 | `httptest` cannot prove proxy buffering/keepalive behavior | Run the app behind the local/prod reverse proxy path, keep the stream open, send a message, confirm delivery and keep-alive behavior. |
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [x] All tasks have `<automated>` verify or manual-only justification.
|
||||
- [x] Sampling continuity: no 3 consecutive tasks without automated verify.
|
||||
- [x] Wave 0 covers all missing test infrastructure references.
|
||||
- [x] No watch-mode flags.
|
||||
- [x] Feedback latency target < 60s.
|
||||
- [x] `nyquist_compliant: true` set in frontmatter.
|
||||
|
||||
**Approval:** pending execution
|
||||
Loading…
Reference in a new issue