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:
parent
f45f7c6010
commit
7e3b3af1e3
5 changed files with 1158 additions and 1 deletions
|
|
@ -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 15–16 restyled surfaces
|
4. Browser walkthrough confirms both views look consistent with the Phase 15–16 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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
271
.planning/phases/17-chat-planning/17-01-PLAN.md
Normal file
271
.planning/phases/17-chat-planning/17-01-PLAN.md
Normal 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>
|
||||||
335
.planning/phases/17-chat-planning/17-02-PLAN.md
Normal file
335
.planning/phases/17-chat-planning/17-02-PLAN.md
Normal 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>
|
||||||
470
.planning/phases/17-chat-planning/17-PATTERNS.md
Normal file
470
.planning/phases/17-chat-planning/17-PATTERNS.md
Normal 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 123–133) | 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 1–13):
|
||||||
|
```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 61–68 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 100–110):
|
||||||
|
```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 238–257)
|
||||||
|
|
||||||
|
**Overview-section heading pattern** (lines 238–256):
|
||||||
|
```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 1–6):
|
||||||
|
```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 95–112):
|
||||||
|
```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 162–168):
|
||||||
|
```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 318–336), 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 123–133 (the stubs being updated)
|
||||||
|
|
||||||
|
**renderAppPage helper pattern** (lines 178–206):
|
||||||
|
```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 123–133):
|
||||||
|
```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 135–143).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### `go-backend/internal/web/views/discussion_view_test.go` (test)
|
||||||
|
|
||||||
|
**Analog:** `go-backend/internal/web/views/dashboard_components_test.go`
|
||||||
|
|
||||||
|
**renderViewToString helper** (lines 143–151) — 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 44–65):
|
||||||
|
```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 3–13):
|
||||||
|
```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 875–892) — token-only CSS, no hardcoded hex
|
||||||
|
|
||||||
|
**Existing heading selector to extend** (lines 886–892):
|
||||||
|
```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 875–892, `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 196–204
|
||||||
|
**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
|
||||||
75
.planning/phases/17-chat-planning/17-VALIDATION.md
Normal file
75
.planning/phases/17-chat-planning/17-VALIDATION.md
Normal 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 15–16 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
|
||||||
Loading…
Reference in a new issue