---
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>

<scope_note>
## Deferred Decisions — D-D01, D-D02, D-D03, D-D04 (User-Authoritative)

Phase 17 is a **restyle-only** phase. The following decisions from CONTEXT.md are explicitly deferred to a future phase when the real chat backend is built:

- **D-D01** — `DiscussionMessageView` gaining `IsOwn bool` set via `DiscussionMessagesFromRows(rows, currentUserID)`: deferred. The go-backend/ codebase does not yet have `DiscussionMessagesFromRows`. `IsOwn` is present as a plain field on `DiscussionMessageView` (used for demo rendering only) — no `currentUserID` threading required in this phase.
- **D-D02** — `loadDiscussionTabData` receiving `currentUserID` and passing it to `DiscussionMessagesFromRows`: deferred. go-backend/ has no `loadDiscussionTabData` function or equivalent yet.
- **D-D03** — HTMX POST path setting `IsOwn = true` vs. SSE broadcast setting `IsOwn = false`: deferred. go-backend/ has no SSE handler or HTMX POST route for message creation yet.
- **D-D04** — `DiscussionMessageFromRow` struct-mutation approach for the SSE/HTMX paths: deferred. There is no `DiscussionMessageFromRow` function in go-backend/ yet.

**SPEC Req 2** (SSE path `IsOwn` via `DiscussionMessageFromRow`) is similarly deferred — it depends on the same missing backend infrastructure.

**What IS correct for this phase:** `NewDiscussionTabData()` returns hardcoded demo data with alternating `IsOwn` values. This is intentional. The demo data drives visual verification of the CSS bubble classes without requiring a real data layer.

Executors: do not attempt to implement D-D01 through D-D04 in this phase. Implement only what is described in the tasks below.
</scope_note>

<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>
