docs(17): create phase 17 plans — discussion view and planning page restyle

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 09:26:26 +02:00
parent f45f7c6010
commit 7e3b3af1e3
No known key found for this signature in database
5 changed files with 1158 additions and 1 deletions

View file

@ -139,13 +139,19 @@ Plans:
**Mode:** mvp **Mode:** mvp
**Status:** Pending **Status:** Pending
**Requirements:** CHAT-UI-01, PLAN-UI-01 **Requirements:** CHAT-UI-01, PLAN-UI-01
**Plans:** 2 plans
**Success Criteria:** **Success Criteria:**
1. Discussion view uses card/surface design; own messages vs. others are visually differentiated 1. Discussion view uses card/surface design; own messages vs. others are visually differentiated
2. Planning page uses overview-section layout with chronological event list 2. Planning page uses overview-section layout with chronological event list
3. All existing chat and planning handler tests pass unchanged 3. All existing chat and planning handler tests pass unchanged
4. Browser walkthrough confirms both views look consistent with the Phase 1516 restyled surfaces 4. Browser walkthrough confirms both views look consistent with the Phase 1516 restyled surfaces
**User-in-loop:** Approve chat bubble/row style and planning event row design before implementation. Plans:
**Wave 1** *(both plans parallel — no shared files)*
- [ ] 17-01-PLAN.md — Discussion view: CSS message-bubble classes + DiscussionTabData view model + ChatMainContent() component + handler wiring + browser verify
- [ ] 17-02-PLAN.md — Planning view: h1 selector fix + PlanningTabData view model + PlanningShowDaySeparator + PlanningMainContent() component + handler wiring + browser verify
**User-in-loop:** Browser verify checkpoints in both plans — approve visual result before considering the phase complete.
--- ---

View file

@ -0,0 +1,271 @@
---
phase: 17-chat-planning
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- go-backend/internal/web/ui/app.css
- go-backend/internal/web/views/discussion_view.go
- go-backend/internal/web/views/discussion_view_test.go
- go-backend/internal/web/views/dashboard_components.templ
- go-backend/internal/web/handlers/auth.go
autonomous: false
requirements:
- CHAT-UI-01
must_haves:
truths:
- "A user visiting /chat sees a message list with own messages right-aligned (brand tint) and others left-aligned (white)"
- "The message container uses the .ui-card surface (matching Phase 16 tab panels)"
- "go test ./... -count=1 exits 0 after all changes"
artifacts:
- path: "go-backend/internal/web/views/discussion_view.go"
provides: "DiscussionMessageView, DiscussionTabData, NewDiscussionTabData"
exports: ["DiscussionMessageView", "DiscussionTabData", "NewDiscussionTabData"]
- path: "go-backend/internal/web/views/discussion_view_test.go"
provides: "render assertions for .ui-card, .message-own, .message-other, .message-bubble"
contains: "TestChat"
- path: "go-backend/internal/web/ui/app.css"
provides: ".message-row, .message-own, .message-other, .message-bubble, .message-meta CSS classes"
contains: "message-bubble"
key_links:
- from: "go-backend/internal/web/handlers/auth.go"
to: "views.ChatMainContent(data)"
via: "GetChatPage passes views.NewDiscussionTabData()"
pattern: "NewDiscussionTabData"
- from: "go-backend/internal/web/views/dashboard_components.templ"
to: "DiscussionTabData"
via: "ChatMainContent(data DiscussionTabData)"
pattern: "ChatMainContent"
---
## Phase Goal
**As a** signed-in user, **I want to** see the discussions page styled with card surfaces and own-vs-others message bubbles, **so that** the chat view is visually consistent with the Phase 15-16 restyled tablo surfaces.
<objective>
Replace the ChatMainContent() placeholder stub with a real discussion view: styled message list with own-vs-others bubble alignment inside a .ui-card container, driven by a static view model.
Purpose: CHAT-UI-01 requires a card/surface design with visually differentiated own vs. others messages. This is a restyling-only slice — no new routes, no SSE handler, no real data layer.
Output: discussion_view.go (view model), discussion_view_test.go (render tests), CSS additions to app.css, ChatMainContent() replacement in dashboard_components.templ, handler call-site update in auth.go.
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-PATTERNS.md
<interfaces>
<!-- Key contracts the executor needs. Extracted from codebase. -->
From go-backend/internal/web/views/dashboard_components.templ (lines 162-168):
Current stubs being replaced:
templ PlanningMainContent() { @AppSectionMainContent("Planning", ...) }
templ ChatMainContent() { @AppSectionMainContent("Discussions", ...) }
From go-backend/internal/web/handlers/auth.go (lines 123-134):
func (h *AuthHandler) GetChatPage() http.HandlerFunc {
return h.renderAppPage("/chat", func(user PublicUser) templ.Component {
return views.ChatMainContent() // <-- becomes views.ChatMainContent(views.NewDiscussionTabData())
})
}
From go-backend/internal/web/views/home.go (pattern for demo data factory):
type dashboardTask struct { Title, Project, ProjectKey, ProjectHue, Date, Status, StatusTone string; Completed bool }
func overviewTasks() []dashboardTask { return []dashboardTask{...} }
From go-backend/internal/web/views/dashboard_components_test.go (lines 143-151):
// Already defined — do NOT redefine in new test files:
func renderViewToString(t *testing.T, component templ.Component) string { ... }
From go-backend/internal/web/ui/card.css:
.ui-card { background: var(--color-surface-default); border: 1px solid var(--color-border-default); border-radius: 1rem; box-shadow: var(--shadow-surface-md); }
From go-backend/internal/web/views/tablos.templ (EmptyState usage pattern):
@ui.EmptyState(ui.EmptyStateProps{ Title: "...", Description: "...", Icon: ui.UIIcon("...") })
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: CSS message classes + view model + render tests (RED → GREEN)</name>
<files>go-backend/internal/web/ui/app.css, go-backend/internal/web/views/discussion_view.go, go-backend/internal/web/views/discussion_view_test.go</files>
<read_first>
- go-backend/internal/web/ui/app.css — read lines 875-895 (existing .overview-section-heading h3 rule at line 886; file ends at 1896 — new block appended after)
- go-backend/internal/web/views/home.go — demo data factory pattern (overviewTasks, dashboardTask struct)
- go-backend/internal/web/views/dashboard_components_test.go — renderViewToString helper (lines 143-151, do NOT redeclare)
- go-backend/internal/web/ui/base.css — token names: --color-brand-primary, --color-surface-default, --color-border-subtle, --color-text-muted
</read_first>
<behavior>
- Test: renderViewToString(ChatMainContent(data)) where data has one IsOwn=true message → rendered HTML contains class="ui-card"
- Test: rendered HTML contains message-own somewhere in it for the IsOwn=true message
- Test: rendered HTML contains message-other somewhere in it for an IsOwn=false message
- Test: rendered HTML contains message-bubble for both message types
- Test: PlanningShowDaySeparator is NOT tested here — that belongs to Plan 02
</behavior>
<action>
Step 1 — Append to go-backend/internal/web/ui/app.css (after the last line, currently 1896):
Add a comment block "/* -- Message bubbles -- */" followed by these CSS rules using var(--...) tokens only (per D-M04):
- .message-row: padding 0.75rem 1rem
- .message-own: display flex, flex-direction column, align-items flex-end
- .message-other: display flex, flex-direction column, align-items flex-start
- .message-bubble: border 1px solid var(--color-border-subtle), border-radius 0.25rem 0.75rem 0.75rem 0.75rem (others: bottom-right heavy), max-width 70%, padding 0.75rem 1rem, white-space pre-wrap, word-break break-word
- .message-own .message-bubble: background rgba(128, 78, 236, 0.10) (brand tint per D-M02; spelled out intentionally — single-use value), border-radius 0.75rem 0.75rem 0.25rem 0.75rem (own: bottom-right tight)
- .message-other .message-bubble: background var(--color-surface-default)
- .message-meta: display flex, gap 0.5rem, margin-bottom 0.25rem, font-size 0.875rem, color var(--color-text-muted)
- .message-own .message-meta: justify-content flex-end
- .message-meta .message-author: font-weight 600
Step 2 — Create go-backend/internal/web/views/discussion_view.go with package views:
Define DiscussionMessageView struct with Author string, Timestamp string, Body string, IsOwn bool.
Define DiscussionTabData struct with Messages []DiscussionMessageView.
Define NewDiscussionTabData() DiscussionTabData that returns a populated DiscussionTabData with 4 hardcoded demo messages alternating IsOwn (true, false, true, false). Use realistic strings like Author "you@xtablo.com" / "other@xtablo.com", Timestamp "09:14" / "09:17", Body as short conversational text.
Step 3 — Create go-backend/internal/web/views/discussion_view_test.go with package views:
Import bytes, context, strings, testing. Do NOT redeclare renderViewToString — it is already in dashboard_components_test.go in the same package.
Write TestChatMainContentRendersBubbleClasses: construct DiscussionTabData with one IsOwn=true and one IsOwn=false message, call renderViewToString(ChatMainContent(data)), assert all four strings are present: `class="ui-card"`, `message-own`, `message-other`, `message-bubble`. Use t.Fatalf with the full expected/got pattern.
Run the test first (it will fail because ChatMainContent does not yet accept a parameter) — this is the RED phase.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./internal/web/views/ -run TestChat -count=1 2>&1 | head -30</automated>
</verify>
<acceptance_criteria>
- go-backend/internal/web/ui/app.css contains the string "message-bubble" after line 1896
- go-backend/internal/web/ui/app.css contains "rgba(128, 78, 236, 0.10)" for own bubble background
- go-backend/internal/web/ui/app.css contains "var(--color-border-subtle)" for message-bubble border
- go-backend/internal/web/views/discussion_view.go exists with exported types DiscussionMessageView, DiscussionTabData, and function NewDiscussionTabData
- discussion_view.go does NOT import any external packages beyond "package views"
- discussion_view_test.go does NOT declare renderViewToString (it reuses the existing one from dashboard_components_test.go)
- After RED: `go test ./internal/web/views/ -run TestChat -count=1` exits non-zero with a compile error or test failure
</acceptance_criteria>
<done>CSS classes appended; view model file created; failing test written (RED confirmed)</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: ChatMainContent() templ component + handler wiring (GREEN)</name>
<files>go-backend/internal/web/views/dashboard_components.templ, go-backend/internal/web/handlers/auth.go</files>
<read_first>
- go-backend/internal/web/views/dashboard_components.templ — lines 162-186 (PlanningMainContent and ChatMainContent stubs + AppSectionMainContent pattern)
- go-backend/internal/web/views/dashboard_components.templ — lines 238-260 (OverviewProjectsSection — the overview-section heading pattern to follow for any future planning work; not needed here but confirms ui.EmptyState import is already in scope)
- go-backend/internal/web/handlers/auth.go — lines 123-134 (GetChatPage current call site)
- go-backend/internal/web/views/discussion_view.go — just created in Task 1
- go-backend/internal/web/views/tablos.templ — lines 95-112 (EmptyState call pattern with ui.EmptyStateProps)
</read_first>
<behavior>
- After changes: TestChatMainContentRendersBubbleClasses passes (GREEN)
- go build ./... compiles without error
- go test ./... -count=1 exits 0
</behavior>
<action>
Step 1 — Replace ChatMainContent() stub in dashboard_components.templ:
Change the signature from `templ ChatMainContent()` to `templ ChatMainContent(data DiscussionTabData)`.
The body renders:
- Outer container: div with id="discussion-messages" and class="ui-card" (per D-C01; replaces the inline Tailwind border/bg classes that existed in the old stub — the stub had none, just apply .ui-card)
- If len(data.Messages) == 0: render @ui.EmptyState with Title="No messages yet", Description="Start the discussion.", Icon=ui.UIIcon("message-circle") or similar available icon
- If messages exist: for each message render a div with classes "message-row" + either "message-own" (if msg.IsOwn) or "message-other"
- Inside the row: first a div.message-meta containing a span.message-author with { msg.Author } and a span with { msg.Timestamp }
- Then a div.message-bubble containing { msg.Body }
- Do NOT use templ.Raw() for any field — all content via { expr } for auto-escaping
- Drop any divide-y divide-slate-100 divider from the container (per Claude's Discretion note in CONTEXT.md)
Step 2 — Update GetChatPage in go-backend/internal/web/handlers/auth.go:
Change `return views.ChatMainContent()` to `return views.ChatMainContent(views.NewDiscussionTabData())`.
No other handler logic changes — user parameter is available but unused (same pattern as GetFilesPage and GetFeedbackPage).
Step 3 — Run templ generate (required after any .templ file change):
Execute `cd go-backend && templ generate` to regenerate the *_templ.go files. If templ is not on PATH, try `go run github.com/a-h/templ/cmd/templ generate`.
Step 4 — Run tests (GREEN confirmation):
Run `cd go-backend && go test ./internal/web/views/ -run TestChat -count=1` — must exit 0.
Run `cd go-backend && go test ./... -count=1` — must exit 0 (full regression gate).
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -count=1 2>&1</automated>
</verify>
<acceptance_criteria>
- dashboard_components.templ line containing "templ ChatMainContent" reads "templ ChatMainContent(data DiscussionTabData)" (not zero-arg)
- handlers/auth.go GetChatPage contains "views.NewDiscussionTabData()" in the call
- `go build ./...` exits 0 from go-backend/
- `go test ./internal/web/views/ -run TestChat -count=1` exits 0 with output "PASS"
- `go test ./... -count=1` exits 0
- The .ui-card class string appears in the rendered ChatMainContent output (proven by TestChatMainContentRendersBubbleClasses)
- templ generate produced an updated dashboard_components_templ.go (timestamp newer than before)
</acceptance_criteria>
<done>ChatMainContent wired with real view, tests GREEN, full suite passes</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Browser verify — discussion view visual result</name>
<what-built>
- .message-* CSS classes in app.css
- ChatMainContent() component with .ui-card container and own/other bubble alignment
- Handler wired with demo data (4 alternating IsOwn messages)
</what-built>
<how-to-verify>
1. Start the dev server: `cd go-backend && go run ./cmd/web` (or `just dev` if a justfile target exists)
2. Navigate to http://localhost:8080/chat (sign in first if redirected)
3. Confirm: the message list container has card surface treatment (rounded corners, subtle shadow) — matching the tablo detail tab panels from Phase 16
4. Confirm: the first and third demo messages (IsOwn=true) appear right-aligned with a light purple/brand tint background
5. Confirm: the second and fourth demo messages (IsOwn=false) appear left-aligned with a white background
6. Confirm: author email + timestamp appear ABOVE each bubble, aligned to match the bubble side
7. Compare visually with the tablo detail files tab — the discussion card surface should match
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe any visual issues to fix</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| template render → HTML output | templ { expr } interpolations for Author, Timestamp, Body fields |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-17-01-01 | Tampering | discussion_view.go — Author/Body/Timestamp string fields rendered in template | mitigate | Use templ { expr } syntax (auto-escapes HTML) — never templ.Raw() for these fields. Demo data is hardcoded strings; future real data will flow through the same escaping path. |
| T-17-01-02 | Information Disclosure | /chat route serving demo data | accept | Phase 17 serves hardcoded demo messages only — no real user data. Auth is enforced by renderAppPage → authenticatedUser() — unchanged from existing handler. |
</threat_model>
<verification>
Full regression gate: `cd go-backend && go test ./... -count=1` must exit 0.
Targeted chat test: `cd go-backend && go test ./internal/web/views/ -run TestChat -count=1` must output "PASS".
Build check: `cd go-backend && go build ./...` must exit 0.
</verification>
<success_criteria>
- .message-row, .message-own, .message-other, .message-bubble, .message-meta, .message-own .message-bubble, .message-other .message-bubble, .message-own .message-meta, .message-meta .message-author CSS rules present in app.css after line 1896
- discussion_view.go exports DiscussionMessageView (with IsOwn bool), DiscussionTabData, NewDiscussionTabData
- ChatMainContent(data DiscussionTabData) renders .ui-card container + .message-own / .message-other rows
- handlers/auth.go GetChatPage passes views.NewDiscussionTabData() to views.ChatMainContent()
- go test ./... -count=1 exits 0
- Browser: own messages right-aligned with brand tint; others left-aligned with white background
</success_criteria>
<output>
After completion, create `.planning/phases/17-chat-planning/17-01-SUMMARY.md`
</output>

View file

@ -0,0 +1,335 @@
---
phase: 17-chat-planning
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- go-backend/internal/web/ui/app.css
- go-backend/internal/web/views/planning_view.go
- go-backend/internal/web/views/planning_view_test.go
- go-backend/internal/web/views/dashboard_components.templ
- go-backend/internal/web/handlers/auth.go
autonomous: false
requirements:
- PLAN-UI-01
must_haves:
truths:
- "A user visiting /planning sees a heading wrapped in .overview-section / .overview-section-heading with an h1 styled to match the dashboard section headings"
- "Events in the planning list are grouped under date-separator headers — the date label appears once per day group, not in every event row"
- "go test ./... -count=1 exits 0 after all changes"
artifacts:
- path: "go-backend/internal/web/views/planning_view.go"
provides: "PlanningEventRow, PlanningTabData, NewPlanningTabData, PlanningShowDaySeparator"
exports: ["PlanningEventRow", "PlanningTabData", "NewPlanningTabData", "PlanningShowDaySeparator"]
- path: "go-backend/internal/web/views/planning_view_test.go"
provides: "logic test for PlanningShowDaySeparator + render assertions for .overview-section, .overview-section-heading, separator div"
contains: "TestPlanning"
- path: "go-backend/internal/web/ui/app.css"
provides: "h1 added to .overview-section-heading heading selector"
contains: "overview-section-heading h1"
key_links:
- from: "go-backend/internal/web/handlers/auth.go"
to: "views.PlanningMainContent(data)"
via: "GetPlanningPage passes views.NewPlanningTabData()"
pattern: "NewPlanningTabData"
- from: "go-backend/internal/web/views/dashboard_components.templ"
to: "PlanningTabData"
via: "PlanningMainContent(data PlanningTabData)"
pattern: "PlanningMainContent"
- from: "dashboard_components.templ PlanningMainContent"
to: "PlanningShowDaySeparator"
via: "for i, event := range data.Events conditional"
pattern: "PlanningShowDaySeparator"
---
## Phase Goal
**As a** signed-in user, **I want to** see the planning page styled with the overview-section layout and date-grouped event separators, **so that** the planning view is visually consistent with the Phase 15-16 restyled tablo surfaces.
<objective>
Replace the PlanningMainContent() placeholder stub with a real planning view: overview-section heading, day-separated event list using PlanningShowDaySeparator, and empty-state via @ui.EmptyState.
Purpose: PLAN-UI-01 requires the planning page to use the overview-section layout for event aggregation. This is a restyling-only slice — no new routes, no schema changes, no real data layer.
Output: planning_view.go (view model + day separator helper), planning_view_test.go (logic + render tests), h1 selector extension in app.css, PlanningMainContent() replacement in dashboard_components.templ, handler call-site update in auth.go.
</objective>
<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-SPEC.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/17-chat-planning/17-PATTERNS.md
<interfaces>
<!-- Key contracts the executor needs. Extracted from codebase. -->
From go-backend/internal/web/views/dashboard_components.templ (lines 162-165):
Current stubs being replaced:
templ PlanningMainContent() { @AppSectionMainContent("Planning", "Visualisez le rythme...") }
From go-backend/internal/web/handlers/auth.go (lines 123-127):
func (h *AuthHandler) GetPlanningPage() http.HandlerFunc {
return h.renderAppPage("/planning", func(user PublicUser) templ.Component {
return views.PlanningMainContent() // <-- becomes views.PlanningMainContent(views.NewPlanningTabData())
})
}
From go-backend/internal/web/views/dashboard_components.templ (lines 238-257) — OverviewProjectsSection pattern to follow:
<section id="overview-projects-section" class="overview-section">
<div class="overview-section-heading">
<h3>Mes Projets</h3>
</div>
...
</section>
Planning variant uses <h1>Planning</h1> instead of h3 — requires app.css selector extension (see Task 1).
From go-backend/internal/web/ui/app.css (lines 886-892) — selector to extend:
.overview-section-heading h3,
.tasks-section-header h3 {
color: var(--color-surface-muted-inverse);
font-size: 1.6rem;
font-weight: 600;
margin: 0;
}
Change to add h1: ".overview-section-heading h1," as first line of the selector list.
From go-backend/internal/web/views/home.go — demo data factory pattern:
type dashboardTask struct { Title, Project, ... string; Completed bool }
func overviewTasks() []dashboardTask { return []dashboardTask{...} }
From go-backend/internal/web/views/dashboard_components_test.go (lines 143-151):
// Already defined — do NOT redefine in new test files:
func renderViewToString(t *testing.T, component templ.Component) string { ... }
From go-backend/internal/web/views/tablos.templ (EmptyState usage pattern):
@ui.EmptyState(ui.EmptyStateProps{
Title: "Aucun projet trouvé",
Description: "Créez votre premier projet",
Icon: ui.UIIcon("grid3x3"),
})
For planning: no Action field needed — omit it.
</interfaces>
</context>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: CSS h1 selector fix + view model + render and logic tests (RED → write)</name>
<files>go-backend/internal/web/ui/app.css, go-backend/internal/web/views/planning_view.go, go-backend/internal/web/views/planning_view_test.go</files>
<read_first>
- go-backend/internal/web/ui/app.css — lines 884-895 (the .overview-section-heading h3 selector that needs h1 added; confirm exact line numbers before editing)
- go-backend/internal/web/views/home.go — full file for demo data factory pattern (dashboardTask struct, overviewTasks function)
- go-backend/internal/web/views/dashboard_components_test.go — lines 143-151 (renderViewToString, do NOT redeclare)
- go-backend/internal/web/views/tablos.templ — lines 95-112 (EmptyState call with ui.EmptyStateProps — confirm exact field names Icon, Title, Description, Action)
</read_first>
<behavior>
- Logic test: PlanningShowDaySeparator(events, 0) returns true regardless of date
- Logic test: PlanningShowDaySeparator(events, 1) returns false when events[1].DateLabel == events[0].DateLabel
- Logic test: PlanningShowDaySeparator(events, 2) returns true when events[2].DateLabel != events[1].DateLabel
- Render test: renderViewToString(PlanningMainContent(data)) with 3 events spanning 2 dates → HTML contains "overview-section"
- Render test: same render → HTML contains "overview-section-heading"
- Render test: same render → HTML contains "bg-slate-50" (day separator element)
- Note: These tests will fail (RED) until PlanningMainContent is replaced in Task 2
</behavior>
<action>
Step 1 — Edit go-backend/internal/web/ui/app.css:
Find the selector block at approximately line 886 that reads:
.overview-section-heading h3,
.tasks-section-header h3 {
Prepend ".overview-section-heading h1," as a new first line of the selector list so it becomes:
.overview-section-heading h1,
.overview-section-heading h3,
.tasks-section-header h3 {
The body of the rule (color, font-size 1.6rem, font-weight 600, margin 0) stays unchanged.
No other changes to app.css in this task — the .message-* block is handled by Plan 01 Task 1.
Step 2 — Create go-backend/internal/web/views/planning_view.go with package views:
Define PlanningEventRow struct with fields: DateLabel string, TimeRange string, Title string, TabloTitle string, Location string.
Define PlanningTabData struct with fields: Events []PlanningEventRow, DateRange string (e.g. "May 17 May 30, 2026").
Define NewPlanningTabData() PlanningTabData that returns a PlanningTabData with DateRange set to a static demo range and 5 hardcoded demo events spanning 2 distinct DateLabel values:
- 3 events with DateLabel "May 17, 2026" — different TimeRange/Title/TabloTitle values
- 2 events with DateLabel "May 18, 2026" — different TimeRange/Title/TabloTitle values
This ensures PlanningShowDaySeparator will render 2 separator headers in the demo view.
Define PlanningShowDaySeparator(events []PlanningEventRow, index int) bool:
if index == 0 { return true }
return events[index].DateLabel != events[index-1].DateLabel
No imports needed — all fields are plain string and bool.
Step 3 — Create go-backend/internal/web/views/planning_view_test.go with package views:
Import bytes, context, strings, testing. Do NOT redeclare renderViewToString.
Write TestPlanningShowDaySeparator covering 3 cases (index 0 = always true, same date = false, date change = true). Use a table-driven test with t.Run subtests.
Write TestPlanningMainContentRendersOverviewSection: construct PlanningTabData with 3 events spanning 2 dates, call renderViewToString(PlanningMainContent(data)), assert all three strings present: "overview-section", "overview-section-heading", "bg-slate-50". Use t.Fatalf with the missing-string pattern (see PATTERNS.md render assertion template).
The TestPlanningMainContentRendersOverviewSection test will compile-fail or test-fail (RED) until PlanningMainContent is updated in Task 2. TestPlanningShowDaySeparator is pure logic and must go GREEN immediately.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./internal/web/views/ -run TestPlanningShowDaySeparator -count=1 2>&1</automated>
</verify>
<acceptance_criteria>
- go-backend/internal/web/ui/app.css line ~886 contains ".overview-section-heading h1," immediately before ".overview-section-heading h3,"
- go-backend/internal/web/views/planning_view.go exists, package views, exports PlanningEventRow, PlanningTabData, NewPlanningTabData, PlanningShowDaySeparator
- NewPlanningTabData() returns a PlanningTabData where len(Events) == 5 and Events contain exactly 2 distinct DateLabel values
- PlanningShowDaySeparator: grep for "func PlanningShowDaySeparator" in planning_view.go returns 1 match
- planning_view_test.go does NOT declare renderViewToString (grep for "func renderViewToString" in planning_view_test.go returns 0 matches)
- `go test ./internal/web/views/ -run TestPlanningShowDaySeparator -count=1` exits 0 with output "PASS" — logic tests are GREEN immediately
- `go test ./internal/web/views/ -run TestPlanningMainContentRendersOverviewSection -count=1` exits non-zero (RED — PlanningMainContent still has old stub signature)
</acceptance_criteria>
<done>CSS selector extended; view model + helper created; logic tests GREEN; render test written and RED (expected)</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: PlanningMainContent() templ component + handler wiring (GREEN)</name>
<files>go-backend/internal/web/views/dashboard_components.templ, go-backend/internal/web/handlers/auth.go</files>
<read_first>
- go-backend/internal/web/views/dashboard_components.templ — lines 162-168 (PlanningMainContent and ChatMainContent stubs) and lines 238-260 (OverviewProjectsSection — exact heading/section HTML pattern to follow)
- go-backend/internal/web/handlers/auth.go — lines 123-134 (GetPlanningPage current call site)
- go-backend/internal/web/views/planning_view.go — just created in Task 1 (confirm PlanningTabData fields and NewPlanningTabData signature)
- go-backend/internal/web/views/tablos.templ — lines 95-112 (exact @ui.EmptyState call site with ui.EmptyStateProps field names)
</read_first>
<behavior>
- After changes: TestPlanningMainContentRendersOverviewSection passes (GREEN)
- TestPlanningShowDaySeparator remains GREEN (no regression)
- `go build ./...` exits 0
- `go test ./... -count=1` exits 0
</behavior>
<action>
Step 1 — Replace PlanningMainContent() stub in dashboard_components.templ:
Change the signature from `templ PlanningMainContent()` to `templ PlanningMainContent(data PlanningTabData)`.
The body renders:
Outer wrapper: <div class="p-6"> (matches the padding used by other app sections)
Heading section (per D-P01):
<section class="overview-section">
<div class="overview-section-heading">
<div> containing <h1>Planning</h1>
Right side div: date range label as <span class="text-sm text-slate-500">{ data.DateRange }</span> + any nav button placeholders (can be empty divs for now — the planning page navigation is pre-existing; this plan does not add new controls)
</div>
</section>
Event list section below the heading:
If len(data.Events) == 0:
@ui.EmptyState(ui.EmptyStateProps{
Title: "No events in this range",
Description: "Use the navigation controls to browse another 14-day window.",
Icon: ui.UIIcon("calendar"),
})
If events exist: render a <ul> or <div> with the day-separated event list using a for loop:
for i, event := range data.Events {
if PlanningShowDaySeparator(data.Events, i) {
<div class="bg-slate-50 px-4 py-2 text-center text-sm text-slate-500">
{ event.DateLabel }
</div>
}
Render an event row: a <div class="flex items-start gap-4 py-3 border-b border-slate-100"> or similar simple row containing:
- TimeRange: <span class="text-sm text-slate-500 w-20 shrink-0">{ event.TimeRange }</span>
- Title: <span class="font-medium text-slate-800 grow">{ event.Title }</span>
- TabloTitle + Location right column: <span class="text-sm text-slate-500">{ event.TabloTitle } if event.Location != "" then " · " + event.Location </span>
DateLabel is NOT rendered inside the event row — it appears only in the day separator (per D-P04).
}
Important: Do NOT use templ.Raw() for any field. All content via { expr }.
Step 2 — Update GetPlanningPage in go-backend/internal/web/handlers/auth.go:
Change `return views.PlanningMainContent()` to `return views.PlanningMainContent(views.NewPlanningTabData())`.
No other handler logic changes.
Step 3 — Run templ generate:
Execute `cd go-backend && templ generate` (or `go run github.com/a-h/templ/cmd/templ generate`) to regenerate dashboard_components_templ.go.
Step 4 — Run tests (GREEN confirmation):
`cd go-backend && go test ./internal/web/views/ -run TestPlanning -count=1` — must exit 0.
`cd go-backend && go test ./... -count=1` — must exit 0 (full regression gate).
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -count=1 2>&1</automated>
</verify>
<acceptance_criteria>
- dashboard_components.templ line containing "templ PlanningMainContent" reads "templ PlanningMainContent(data PlanningTabData)" (not zero-arg)
- handlers/auth.go GetPlanningPage contains "views.NewPlanningTabData()" in the call
- `go build ./...` exits 0 from go-backend/
- `go test ./internal/web/views/ -run TestPlanning -count=1` exits 0 with output "PASS"
- `go test ./... -count=1` exits 0
- The rendered PlanningMainContent HTML contains "overview-section" (proven by TestPlanningMainContentRendersOverviewSection)
- The rendered PlanningMainContent HTML contains "bg-slate-50" (day separator element present)
- templ generate produced an updated dashboard_components_templ.go
</acceptance_criteria>
<done>PlanningMainContent wired with real view, tests GREEN, full suite passes</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 3: Browser verify — planning view visual result + combined walkthrough</name>
<what-built>
- .overview-section-heading h1 selector extension in app.css (planning h1 now styled at 1.6rem 600 weight)
- PlanningMainContent() component with overview-section heading, day-separated event list, empty state via @ui.EmptyState
- Handler wired with demo data (5 events across 2 dates)
</what-built>
<how-to-verify>
1. Start the dev server: `cd go-backend && go run ./cmd/web` (or `just dev`)
2. Navigate to http://localhost:8080/planning (sign in first if redirected)
3. Confirm the "Planning" heading: large bold text (1.6rem, 600 weight) in the overview-section-heading style — visually matches section headings on the dashboard and tablo-detail pages
4. Confirm the date range label appears to the right of the heading (or below, depending on heading layout)
5. Confirm 2 date separator bars appear: one reading "May 17, 2026" and one reading "May 18, 2026", each with the gray-tinted strip appearance (bg-slate-50)
6. Confirm 3 events appear under the first separator and 2 events under the second
7. Confirm no DateLabel text appears inside the individual event rows
8. Navigate to http://localhost:8080/chat (if Plan 01 has been executed — or verify Plan 01 is also complete)
9. Compare both pages side-by-side against the tablo detail page (/tablos/[any-tablo-id]) — confirm visual consistency: card surfaces, heading styles, and section layout match across all three surfaces
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe any visual issues to fix</resume-signal>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| template render → HTML output | templ { expr } interpolations for Title, TimeRange, TabloTitle, Location, DateRange fields |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-17-02-01 | Tampering | planning_view.go — event field strings rendered in template | mitigate | Use templ { expr } syntax (auto-escapes HTML) — never templ.Raw() for any PlanningEventRow fields. Demo data is hardcoded strings; future real data will flow through the same escaping path. |
| T-17-02-02 | Information Disclosure | /planning route serving demo data | accept | Phase 17 serves hardcoded demo events only — no real user data. Auth is enforced by renderAppPage → authenticatedUser() — unchanged from existing handler. |
</threat_model>
<verification>
Logic gate: `cd go-backend && go test ./internal/web/views/ -run TestPlanningShowDaySeparator -count=1` exits 0.
Render gate: `cd go-backend && go test ./internal/web/views/ -run TestPlanningMainContentRendersOverviewSection -count=1` exits 0.
Full regression gate: `cd go-backend && go test ./... -count=1` exits 0.
Build check: `cd go-backend && go build ./...` exits 0.
</verification>
<success_criteria>
- ".overview-section-heading h1," added to the selector in app.css (h1 now inherits 1.6rem 600 weight heading style)
- planning_view.go exports PlanningEventRow, PlanningTabData, NewPlanningTabData (5 demo events across 2 dates), PlanningShowDaySeparator
- PlanningMainContent(data PlanningTabData) renders .overview-section heading + day separators + event rows without DateLabel in row body
- handlers/auth.go GetPlanningPage passes views.NewPlanningTabData() to views.PlanningMainContent()
- go test ./... -count=1 exits 0
- Browser: planning heading styled to match dashboard section headings; 2 date separators visible; events grouped correctly
</success_criteria>
<output>
After completion, create `.planning/phases/17-chat-planning/17-02-SUMMARY.md`
</output>

View file

@ -0,0 +1,470 @@
# Phase 17: Chat & Planning - Pattern Map
**Mapped:** 2026-05-17
**Files analyzed:** 6 new/modified files
**Analogs found:** 6 / 6
## File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|---|---|---|---|---|
| `go-backend/internal/web/views/discussion_view.go` | model/view-helper | transform | `go-backend/internal/web/views/home.go` | exact |
| `go-backend/internal/web/views/planning_view.go` | model/view-helper | transform | `go-backend/internal/web/views/home.go` | exact |
| `go-backend/internal/web/views/dashboard_components.templ` | component | request-response | self (existing stubs being replaced) | exact |
| `go-backend/internal/web/views/discussion_view_test.go` | test | — | `go-backend/internal/web/views/dashboard_components_test.go` | exact |
| `go-backend/internal/web/views/planning_view_test.go` | test | — | `go-backend/internal/web/views/dashboard_components_test.go` | exact |
| `go-backend/internal/web/ui/app.css` | config/style | — | self (existing `.overview-section` block at line 875) | exact |
| `go-backend/internal/web/handlers/auth.go` | controller | request-response | self (existing `GetPlanningPage` / `GetChatPage` at lines 123133) | exact |
---
## Pattern Assignments
### `go-backend/internal/web/views/discussion_view.go` (model, transform)
**Analog:** `go-backend/internal/web/views/home.go`
**Package and imports pattern** (lines 113):
```go
package views
import (
"fmt"
"strings"
"time"
tablomodel "xtablo-backend/internal/tablos"
"xtablo-backend/internal/web/dates"
"xtablo-backend/internal/web/tabloicons"
"github.com/a-h/templ"
)
```
For `discussion_view.go` no external imports are needed beyond `package views` — the struct fields are all plain `string` and `bool`.
**Struct + constructor pattern** (home.go lines 6168 for `dashboardTask`, adapted):
```go
type dashboardTask struct {
Title string
Project string
ProjectKey string
ProjectHue string
Date string
Status string
StatusTone string
Completed bool
}
```
Apply same pattern: named struct with plain value fields, constructor returning a populated slice.
**Demo data factory pattern** (home.go lines 100110):
```go
func overviewTasks() []dashboardTask {
return []dashboardTask{
{Title: "yo", Project: "Hello", ProjectKey: "H", ProjectHue: "blue", ...},
...
}
}
```
**New file to create** — copy the struct + factory pattern:
```go
package views
type DiscussionMessageView struct {
Author string
Timestamp string
Body string
IsOwn bool
}
type DiscussionTabData struct {
Messages []DiscussionMessageView
}
func NewDiscussionTabData() DiscussionTabData {
return DiscussionTabData{Messages: []DiscussionMessageView{
// 3-5 hardcoded demo messages alternating IsOwn true/false
}}
}
```
---
### `go-backend/internal/web/views/planning_view.go` (model, transform)
**Analog:** `go-backend/internal/web/views/home.go`
Same struct + factory pattern as `discussion_view.go`. Also contains the `PlanningShowDaySeparator` helper:
**Day separator helper pattern** (pure logic, no analog — implement fresh):
```go
func PlanningShowDaySeparator(events []PlanningEventRow, index int) bool {
if index == 0 {
return true
}
return events[index].DateLabel != events[index-1].DateLabel
}
```
**New file to create:**
```go
package views
type PlanningEventRow struct {
DateLabel string
TimeRange string
Title string
TabloTitle string
Location string
}
type PlanningTabData struct {
Events []PlanningEventRow
DateRange string
}
func NewPlanningTabData() PlanningTabData {
return PlanningTabData{Events: []PlanningEventRow{
// 3-5 hardcoded demo events spanning 2 dates
}}
}
func PlanningShowDaySeparator(events []PlanningEventRow, index int) bool {
if index == 0 {
return true
}
return events[index].DateLabel != events[index-1].DateLabel
}
```
---
### `go-backend/internal/web/views/dashboard_components.templ``ChatMainContent` and `PlanningMainContent` stubs (component, request-response)
**Analog:** `OverviewProjectsSection` in `go-backend/internal/web/views/dashboard_components.templ` (lines 238257)
**Overview-section heading pattern** (lines 238256):
```go
templ OverviewProjectsSection(projects []TabloCardView) {
<section id="overview-projects-section" class="overview-section">
<div class="overview-section-heading">
<h3>Mes Projets</h3>
</div>
<div class="project-grid">
for _, project := range visibleOverviewProjects(projects) {
@TabloGridCard(project)
}
</div>
...
</section>
}
```
Planning heading uses the same `<section class="overview-section">` + `<div class="overview-section-heading">` wrapper. Replace `<h3>` with `<h1>` (requires extending the CSS selector — see Shared Patterns below). Add date-range label and nav buttons as right-side content inside `.overview-section-heading`.
**ui-card usage pattern** (card.css, lines 16):
```css
.ui-card {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 1rem;
box-shadow: var(--shadow-surface-md);
}
```
Apply `class="ui-card"` to the `#discussion-messages` container div — no inline border/bg classes needed.
**EmptyState usage pattern** (`go-backend/internal/web/views/tablos.templ` lines 95112):
```go
@ui.EmptyState(ui.EmptyStateProps{
Title: "Aucun projet trouvé",
Description: "Créez votre premier projet",
Icon: ui.UIIcon("grid3x3"),
Action: ui.Button(ui.ButtonProps{
Label: "Nouveau projet",
Variant: ui.ButtonVariantDefault,
Size: ui.SizeMD,
Type: "button",
Icon: "plus",
...
}),
})
```
For planning empty state: no `Action` needed — omit that field. For chat empty state: also no `Action`.
**Current stubs being replaced** (dashboard_components.templ lines 162168):
```go
templ PlanningMainContent() {
@AppSectionMainContent("Planning", "Visualisez le rythme...")
}
templ ChatMainContent() {
@AppSectionMainContent("Discussions", "Retrouvez les conversations...")
}
```
Replace with real components accepting a data parameter:
```go
templ PlanningMainContent(data PlanningTabData) { ... }
templ ChatMainContent(data DiscussionTabData) { ... }
```
**For-range with conditional separator pattern** — closest analog is the tasks list in `OverviewTasks` (lines 318336), but the day-separator variant is new. Use `for i, event := range data.Events` with `if PlanningShowDaySeparator(data.Events, i)` preceding each row.
---
### `go-backend/internal/web/handlers/auth.go``GetPlanningPage` and `GetChatPage` (controller, request-response)
**Analog:** `go-backend/internal/web/handlers/auth.go` lines 123133 (the stubs being updated)
**renderAppPage helper pattern** (lines 178206):
```go
func (h *AuthHandler) renderAppPage(activePath string, content func(user PublicUser) templ.Component) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
user, ok := h.authenticatedUser(r.Context(), r)
if !ok {
http.Redirect(w, r, "/login", http.StatusSeeOther)
return
}
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{OwnerID: user.ID})
if err != nil {
http.Error(w, "failed to load projects", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
pageContent := content(user)
var renderErr error
if isHXRequest(r) {
renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w)
} else {
renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w)
}
if renderErr != nil {
http.Error(w, "failed to render app page", http.StatusInternalServerError)
}
}
}
```
**Current call sites to update** (lines 123133):
```go
func (h *AuthHandler) GetPlanningPage() http.HandlerFunc {
return h.renderAppPage("/planning", func(user PublicUser) templ.Component {
return views.PlanningMainContent() // <-- add views.NewPlanningTabData()
})
}
func (h *AuthHandler) GetChatPage() http.HandlerFunc {
return h.renderAppPage("/chat", func(user PublicUser) templ.Component {
return views.ChatMainContent() // <-- add views.NewDiscussionTabData()
})
}
```
After Phase 17 the `user PublicUser` parameter is available but not used — that is fine, consistent with how `GetFilesPage` and `GetFeedbackPage` also ignore the user (lines 135143).
---
### `go-backend/internal/web/views/discussion_view_test.go` (test)
**Analog:** `go-backend/internal/web/views/dashboard_components_test.go`
**renderViewToString helper** (lines 143151) — already defined in the same package; do NOT redefine it:
```go
func renderViewToString(t *testing.T, component templ.Component) string {
t.Helper()
var buf bytes.Buffer
if err := component.Render(context.Background(), &buf); err != nil {
t.Fatalf("render component: %v", err)
}
return buf.String()
}
```
**Render assertion pattern** (lines 4465):
```go
func TestOverviewProjectsSectionRendersColorAndEditAction(t *testing.T) {
record := tablomodel.Record{ ... }
html := renderViewToString(t, OverviewProjectsSection(OverviewProjectsFromTablos([]tablomodel.Record{record})))
for _, want := range []string{
`style="--project-color:#22C55E;"`,
`aria-label="Modifier le projet"`,
} {
if !strings.Contains(html, want) {
t.Fatalf("expected %q in %q", want, html)
}
}
}
```
**Package declaration** (line 1): `package views` — same package as the components under test.
**Imports pattern** (lines 313):
```go
import (
"bytes"
"context"
"strings"
"testing"
"time"
"github.com/a-h/templ"
"github.com/google/uuid"
tablomodel "xtablo-backend/internal/tablos"
)
```
For discussion and planning tests, the minimal imports are: `"bytes"`, `"context"`, `"strings"`, `"testing"`.
---
### `go-backend/internal/web/views/planning_view_test.go` (test)
Same pattern as `discussion_view_test.go` above. Additionally needs a logic test for `PlanningShowDaySeparator`:
```go
func TestPlanningShowDaySeparator(t *testing.T) {
events := []PlanningEventRow{
{DateLabel: "May 17, 2026", ...},
{DateLabel: "May 17, 2026", ...},
{DateLabel: "May 18, 2026", ...},
}
// index 0 → true (always)
// index 1 → false (same date as 0)
// index 2 → true (date change)
}
```
---
### `go-backend/internal/web/ui/app.css` — new `.message-*` block and selector extension (config/style)
**Analog:** `.overview-section` block (app.css lines 875892) — token-only CSS, no hardcoded hex
**Existing heading selector to extend** (lines 886892):
```css
.overview-section-heading h3,
.tasks-section-header h3 {
color: var(--color-surface-muted-inverse);
font-size: 1.6rem;
font-weight: 600;
margin: 0;
}
```
Add `h1` to this rule:
```css
.overview-section-heading h1,
.overview-section-heading h3,
.tasks-section-header h3 { ... }
```
**New CSS block to append at end of file** (after line 1897):
```css
/* ── Message bubbles ── */
.message-row {
padding: 0.75rem 1rem;
}
.message-own {
display: flex;
flex-direction: column;
align-items: flex-end;
}
.message-other {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.message-bubble {
border: 1px solid var(--color-border-subtle);
border-radius: 0.25rem 0.75rem 0.75rem 0.75rem;
max-width: 70%;
padding: 0.75rem 1rem;
white-space: pre-wrap;
word-break: break-word;
}
.message-own .message-bubble {
background: rgba(128, 78, 236, 0.10);
border-radius: 0.75rem 0.75rem 0.25rem 0.75rem;
}
.message-other .message-bubble {
background: var(--color-surface-default);
}
.message-meta {
display: flex;
gap: 0.5rem;
margin-bottom: 0.25rem;
font-size: 0.875rem;
color: var(--color-text-muted);
}
.message-own .message-meta {
justify-content: flex-end;
}
.message-meta .message-author {
font-weight: 600;
}
```
---
## Shared Patterns
### Token-only CSS rule
**Source:** `go-backend/internal/web/ui/app.css` lines 875892, `go-backend/internal/web/ui/base.css`
**Apply to:** All new `.message-*` rules in `app.css`
All `var(--...)` token names verified in `base.css`:
- `--color-brand-primary` (for rgba tint value `128, 78, 236`)
- `--color-surface-default`
- `--color-border-subtle`
- `--color-text-muted`
- `--color-surface-muted-inverse`
- `--shadow-surface-md`
Exception: the rgba brand tint `rgba(128, 78, 236, 0.10)` is spelled out intentionally (single-use value; wrapping in a token adds indirection without benefit).
### templ auto-escaping
**Source:** Project-wide convention; verified in all existing templ files
**Apply to:** All `{ variable }` interpolations in message templates (Author, Body, Timestamp)
Use `{ expr }` — never `templ.Raw()` for user-supplied or demo data fields.
### HTMX page swap pattern
**Source:** `go-backend/internal/web/handlers/auth.go` lines 196204
**Apply to:** `GetPlanningPage` and `GetChatPage` handler updates
```go
if isHXRequest(r) {
renderErr = views.DashboardContentSwap(activePath, tablos, pageContent).Render(r.Context(), w)
} else {
renderErr = views.DashboardPage(activePath, tablos, pageContent).Render(r.Context(), w)
}
```
No changes to this branching logic — only the `content(user)` call site changes to pass a view model.
### Day separator visual style
**Source:** `CONTEXT.md` D-P03 (no existing analog to copy from — use inline Tailwind directly in the `.templ` file, consistent with other Tailwind-in-templ usage across the codebase):
```go
<div class="bg-slate-50 px-4 py-2 text-center text-sm text-slate-500">
{ event.DateLabel }
</div>
```
These Tailwind classes (`bg-slate-50`, `text-slate-500`) already exist in the generated `tailwind.css` (used by existing pages), so no purge issue.
---
## No Analog Found
All files have close analogs. No entries needed here.
---
## Metadata
**Analog search scope:** `go-backend/internal/web/views/`, `go-backend/internal/web/ui/`, `go-backend/internal/web/handlers/`
**Files scanned:** 7 (dashboard_components.templ, home.go, dashboard_components_test.go, auth.go, card.css, empty_state.templ, app.css)
**Pattern extraction date:** 2026-05-17

View file

@ -0,0 +1,75 @@
---
phase: 17
slug: chat-planning
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-05-17
---
# Phase 17 — Validation Strategy
> Per-phase validation contract for feedback sampling during execution.
---
## Test Infrastructure
| Property | Value |
|----------|-------|
| **Framework** | go test |
| **Config file** | go-backend/go.mod |
| **Quick run command** | `cd go-backend && go test ./internal/web/... -count=1` |
| **Full suite command** | `cd go-backend && go test ./... -count=1` |
| **Estimated runtime** | ~5 seconds |
---
## Sampling Rate
- **After every task commit:** Run `cd go-backend && go test ./internal/web/... -count=1`
- **After every plan wave:** Run `cd go-backend && go test ./... -count=1`
- **Before `/gsd-verify-work`:** Full suite must be green
- **Max feedback latency:** 10 seconds
---
## Per-Task Verification Map
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
| 17-01-01 | 01 | 1 | CHAT-UI-01 | — | N/A (UI-only) | unit | `cd go-backend && go test ./internal/web/... -count=1` | ✅ | ⬜ pending |
| 17-01-02 | 01 | 1 | CHAT-UI-01 | — | N/A (UI-only) | unit | `cd go-backend && go test ./internal/web/... -count=1` | ✅ | ⬜ pending |
| 17-02-01 | 02 | 1 | PLAN-UI-01 | — | N/A (UI-only) | unit | `cd go-backend && go test ./internal/web/... -count=1` | ✅ | ⬜ pending |
| 17-02-02 | 02 | 1 | PLAN-UI-01 | — | N/A (UI-only) | unit | `cd go-backend && go test ./internal/web/... -count=1` | ✅ | ⬜ pending |
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
---
## Wave 0 Requirements
Existing infrastructure covers all phase requirements. No new test files needed — `go-backend/internal/web/handlers/dashboard_components_test.go` is the model for any new component tests.
---
## Manual-Only Verifications
| Behavior | Requirement | Why Manual | Test Instructions |
|----------|-------------|------------|-------------------|
| Own vs. others message visual differentiation | CHAT-UI-01 | Visual design — right/left alignment, bubble color | Load discussion page, confirm own messages appear right-aligned with brand tint, others left-aligned with white bubble |
| Planning event chronological list | PLAN-UI-01 | Visual layout check | Load planning page, confirm overview-section layout with events listed chronologically |
| Visual consistency with Phase 1516 restyled surfaces | CHAT-UI-01, PLAN-UI-01 | Cross-page aesthetic check | Compare dashboard, tablo detail, discussion, and planning views side-by-side |
---
## Validation Sign-Off
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references
- [ ] No watch-mode flags
- [ ] Feedback latency < 10s
- [ ] `nyquist_compliant: true` set in frontmatter
**Approval:** pending