xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-RESEARCH.md
Arthur Belleville e18bf66dbf
docs(20): research phase — tablo detail page + kanban restyle
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 15:21:53 +02:00

616 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Phase 20: Tablo Detail & Kanban Restyle - Research
**Researched:** 2026-05-18
**Domain:** Go + HTMX + Tailwind + templ — new page creation + CSS restyle
**Confidence:** HIGH
---
## Summary
Phase 20 has two intertwined goals: (1) create a **tablo detail page** that does not yet exist in the codebase, and (2) deliver the Figma-matching visual restyle for that page's kanban board and task cards. The current Go backend has no `GET /tablos/{tabloID}` route — clicking a tablo in the list has no detail view. This phase must build the page from scratch, then style it to match the UI-SPEC.
The existing kanban implementation lives exclusively inside the global `/tasks` page (`tasks.templ` → `TasksKanbanLayout`). That layout uses a CSS grid (`grid-cols-1 md:grid-cols-2 lg:grid-cols-4`) and the `TaskCard` component. The tablo detail kanban is a separate surface: a flex-row of fixed-width columns (18rem each, `overflow-x: auto`) scoped to one tablo, with Sortable.js drag-and-drop. The Phase 4 code (task creation, reorder handler, Sortable.js init) for the tablo-detail kanban currently exists only in `router_test.go` integration tests referencing `ListTasksByTablo`, implying it was coded in a previous milestone's tablo detail handler that was later replaced by `go-backend/`. All task mutation infrastructure (CreateTask, GetTaskByID, UpdateTask, SoftDeleteTask, ListTasksByTablo) exists in the repository layer and in the InMemory test repository — it just has no route-level handler wiring for the detail page.
The primary recommendation: build `GET /tablos/{tabloID}` as a new handler + templ component, add `GET/POST /tablos/{tabloID}/tasks/...` for task CRU within the detail context, implement the tab-bar, and apply the full CSS restyle from `20-UI-SPEC.md` in `app.css`.
**Primary recommendation:** Create the tablo detail page as a new handler + view layer; reuse existing `ListTasksByTablo` and task mutation repo methods; apply all UI-SPEC CSS changes to `app.css` and the new templ components.
---
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| Tablo detail route (`GET /tablos/{tabloID}`) | Go handler (server) | — | Fetches tablo record + tasks, builds view model, renders templ |
| Tab bar (Overview/Tasks/Files/Discussion/Events) | templ template | — | Static HTML tabs, HTMX push-url for navigation |
| Kanban board layout | templ template (`tablos_detail.templ`) | CSS (`app.css`) | Column+card HTML; flex layout and card styles in CSS |
| Task CRUD within detail | Go handler (server) | — | CreateTask/UpdateTask/DeleteTask/ReorderTask scoped to tabloID |
| Drag-and-drop reorder | Browser (Sortable.js) | Go handler (reorder endpoint) | JS-side drag; server persists new order via existing handler pattern |
| Progress bar computation | Go handler (server) | — | doneTasks/totalTasks computed at render time from task list |
| CSS restyle (cards, columns, header) | CSS (`app.css`) | — | Token-based rules; no new tokens needed |
| Files tab (restyle only) | templ + CSS | — | Existing `ui.Table`; add border/radius wrapper and header style |
---
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| DETAIL-01 | Tablo detail page (header, tasks section, etapes section, files table) is restyled to match Figma | New `GET /tablos/{tabloID}` handler + `TabloDetailPage` templ component + CSS in `app.css` matching UI-SPEC tokens |
| TASK-01 | Kanban board columns, task cards, and drag-and-drop are restyled to match Figma | New tablo-scoped kanban templ + `.task-card` CSS block in `app.css`; Sortable.js init preserved verbatim |
</phase_requirements>
---
## Standard Stack
### Core (existing — no new packages)
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| `github.com/a-h/templ` | existing | HTML components | Entire view layer uses templ |
| Sortable.js | CDN (existing) | Drag-and-drop | Already wired in router_test; Phase 4 integration |
| HTMX | CDN v4.0.0-beta2 (existing) | Partial page swaps | Entire app uses HTMX for navigation |
| Tailwind CSS | existing build | Utility classes | Used alongside custom CSS classes |
### No new packages needed
The UI-SPEC explicitly confirms: "No external registries. All components are templ + custom CSS." All needed UI primitives (`ui.Button`, `ui.IconButton`, `ui.Badge`, `ui.Table`) exist in `backend/internal/web/ui/`.
**Installation:** none required.
---
## Package Legitimacy Audit
> No new packages to install in this phase. The phase is purely a restyle + new page creation using existing infrastructure.
**Packages removed due to slopcheck:** none
**Packages flagged as suspicious:** none
---
## Architecture Patterns
### System Architecture Diagram
```
Browser
│ GET /tablos/{tabloID}
Go handler: GetTabloDetailPage
│ repo.ListTablos (find single tablo by ID + owner check)
│ repo.ListTasksByTablo (fetch tasks for this tablo)
│ compute progress: doneTasks / totalTasks * 100
TabloDetailPageViewModel {tablo, tasks by status column, progress}
templ: TabloDetailPage
├── TabloDetailHeader (name h1, status badge, progress bar, owner row)
├── TabloTabBar (Overview | Tasks | Files | Discussion | Events)
└── tab: Tasks → KanbanBoardSection
├── KanbanColumn × 4 (Todo / In Progress / In Review / Done)
│ ├── ColumnHeader (title, count badge, "+ Add" link)
│ └── TaskCardList (sortable-column)
│ └── TaskCard × N (drag-handle, title, delete icon)
└── TaskCreateInlineForm (htmx fragment, within column)
Browser
Sortable.js init (DOMContentLoaded + htmx:afterSettle)
handles: .sortable-column containers
POST /tablos/{tabloID}/tasks/reorder (existing reorder pattern)
```
### Recommended Project Structure
New files to create:
```
go-backend/internal/web/
├── views/
│ ├── tablo_detail.templ # TabloDetailPage + all sub-components
│ └── tablo_detail_view.go # TabloDetailViewModel + helpers
├── handlers/
│ └── tablo_detail.go # GetTabloDetailPage handler
```
Modified files:
```
go-backend/
├── router.go # Add GET /tablos/{tabloID}
├── internal/web/ui/app.css # All CSS changes from UI-SPEC delta table
```
### Pattern 1: Tablo Detail Handler (new page + HTMX swap)
```go
// Source: existing handler pattern from handlers/auth.go + handlers/tablos.go
func (h *AuthHandler) GetTabloDetailPage() 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
}
tabloID, err := uuid.Parse(r.PathValue("tabloID"))
if err != nil {
http.Error(w, "invalid tablo id", http.StatusBadRequest)
return
}
// Reuse ListTablos to find the owned tablo (no GetTabloByID sqlc query exists)
tablos, err := h.repo.ListTablos(r.Context(), ListTablosInput{OwnerID: user.ID})
if err != nil {
http.Error(w, "failed to load tablos", http.StatusInternalServerError)
return
}
tablo, ok := findTabloByID(tablos, tabloID)
if !ok {
http.Error(w, "tablo not found", http.StatusNotFound)
return
}
// ListTasksByTablo is defined in the repo and InMemory repo
taskRepo := h.repo.(interface {
ListTasksByTablo(context.Context, ListTasksByTabloInput) ([]TaskRecord, error)
})
tasks, err := taskRepo.ListTasksByTablo(r.Context(), ListTasksByTabloInput{
OwnerID: user.ID,
TabloID: tabloID,
})
if err != nil {
http.Error(w, "failed to load tasks", http.StatusInternalServerError)
return
}
vm := views.NewTabloDetailViewModel(tablo, tasks)
content := views.TabloDetailPage(vm)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
var renderErr error
if isHXRequest(r) {
renderErr = views.DashboardContentSwap("/tablos/"+tabloID.String(), tablos, content).Render(r.Context(), w)
} else {
renderErr = views.DashboardPage("/tablos/"+tabloID.String(), tablos, content).Render(r.Context(), w)
}
if renderErr != nil {
http.Error(w, "failed to render tablo detail", http.StatusInternalServerError)
}
}
}
```
**[ASSUMED]** — GetTabloByID does not exist in sqlc queries; using ListTablos + findTabloByID is the current pattern from GetEditTabloModal. For a large tablo list this is an O(n) scan. At v1 scale this is acceptable.
### Pattern 2: Progress Computation
```go
// Source: [ASSUMED] — derived from existing tabloStatusPresentation pattern
func computeTabloProgress(tasks []TaskRecord) int {
total := len(tasks)
if total == 0 {
return 0
}
var done int
for _, t := range tasks {
if t.Status == TaskStatusDone {
done++
}
}
return (done * 100) / total
}
```
### Pattern 3: Kanban Column templ (new, scoped to tablo detail)
```templ
// Source: UI-SPEC + existing TasksKanbanLayout pattern
templ TabloDetailKanbanColumn(col TabloDetailColumnView) {
<div class="tablo-kanban-column" data-status={ col.Status }>
<div class="tablo-kanban-column-header">
<span class="tablo-kanban-column-title">{ col.Label }</span>
<span class="tablo-kanban-task-count">{ len(col.Tasks) }</span>
<a href="#" class="tablo-kanban-add-link" hx-get={ col.CreateHref }
hx-target={ "#create-zone-" + col.Status } hx-swap="innerHTML">
+ Ajouter
</a>
</div>
<div id={ "task-list-" + col.Status } class="task-list sortable-column"
data-status={ col.Status }>
for _, task := range col.Tasks {
@TabloDetailTaskCard(task)
}
</div>
<div id={ "create-zone-" + col.Status }></div>
</div>
}
```
### Pattern 4: Drag Handle (opacity-based hover)
```css
/* Source: UI-SPEC Component Delta section */
.task-drag-handle {
color: var(--color-text-faint);
cursor: grab;
font-size: 1rem;
opacity: 0;
transition: opacity 0.12s ease;
}
.task-card:hover .task-drag-handle {
opacity: 1;
}
```
### Pattern 5: Sortable.js init (preserved from Phase 4)
```javascript
// Source: [ASSUMED] — Sortable.js init pattern from STATE.md Phase 4 decisions
// Must fire on DOMContentLoaded AND htmx:afterSettle
function initSortable() {
document.querySelectorAll('.sortable-column').forEach(function(el) {
if (el._sortable) return; // idempotent
el._sortable = Sortable.create(el, {
group: 'tablo-tasks',
animation: 150,
handle: '.task-drag-handle',
draggable: '.task-card',
onEnd: function(evt) {
// Update hidden reorder form + submit
document.querySelector('#reorder-form').requestSubmit();
}
});
});
}
document.addEventListener('DOMContentLoaded', initSortable);
document.addEventListener('htmx:afterSettle', initSortable);
```
The UI-SPEC states: "Sortable.js initialization on `DOMContentLoaded` and `htmx:afterSettle` is PRESERVED verbatim."
### Anti-Patterns to Avoid
- **Using `display: none` for drag handle / delete icon:** UI-SPEC requires `opacity: 0 → 1` (not display-toggling) to preserve layout during hover.
- **Mixing global tasks kanban with tablo-detail kanban:** The `TasksKanbanLayout` in `tasks.templ` is for the global `/tasks` page and must remain unchanged. The tablo detail gets a new separate `TabloDetailKanbanLayout`.
- **Adding a task-view switcher (Board/List/Gantt):** UI-SPEC explicitly defers the inner task-view switcher to Phase 21. Do not add it.
- **Using inline Tailwind for the card restyle:** The UI-SPEC defines `.task-card` as a named CSS class in `app.css`. Inline Tailwind classes on these components would conflict with the CSS cascade.
- **Routing task create/delete to global `/tasks` endpoints:** Task mutations within the detail page must include `tabloID` context so the response re-renders the detail page, not the global tasks page.
---
## Don't Hand-Roll
| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Drag-and-drop column reorder | Custom JS drag | Sortable.js (already included) | Phase 4 already wired; re-implementing is regression risk |
| Status badge pill | Custom badge HTML | `ui.Badge` with `badgeVariantForTone` | Already has all required tone variants in `badge.css` |
| Modal dialog | Raw HTML `<dialog>` | `ui.Modal` | Already exists in `modal.templ` + `modal.css` |
| Progress bar math | Separate API endpoint | Compute inline in handler from task list | Task list already fetched; no extra query needed |
| Table wrapper | Custom `<table>` | `ui.Table` + wrapper div | `ui.Table` handles shell; Phase 20 adds `border/radius` wrapper div around it |
---
## Common Pitfalls
### Pitfall 1: No `GetTabloByID` sqlc query
**What goes wrong:** Code tries to call `h.repo.GetTabloByID(...)` which doesn't exist in the `AuthRepository` interface or sqlc.
**Why it happens:** The repository only exposes `ListTablos` (filtered list). No single-record lookup was added in earlier phases.
**How to avoid:** Use `findTabloByID(tablos, tabloID)` after `ListTablos`. This is the same pattern used in `GetEditTabloModal`.
**Warning signs:** Compiler error: `AuthRepository has no method GetTabloByID`.
### Pitfall 2: `ListTasksByTablo` not in `AuthRepository` interface
**What goes wrong:** `h.repo` is typed as `AuthRepository` which does not include `ListTasksByTablo`. The method is only on `taskPageRepository` interface (defined in `handlers/tasks.go`).
**Why it happens:** `AuthRepository` in `handlers/auth.go` was not extended with task methods.
**How to avoid:** Either (a) add a `TabloDetailRepository` interface combining the needed methods, or (b) type-assert `h.repo.(taskDetailRepository)` the same way `renderTasksPage` does. Pattern (b) requires the InMemoryAuthRepository to also implement `ListTasksByTablo` — it already does (`in_memory_auth_repository.go:168`).
**Warning signs:** Compiler error about interface satisfaction.
### Pitfall 3: HTMX active-path mismatch in sidebar
**What goes wrong:** The sidebar nav item for the current tablo is not highlighted when viewing `/tablos/{tabloID}`.
**Why it happens:** `sidebarNavItemID` uses the href to generate DOM IDs; tablo-detail is a sub-path that doesn't match any primary nav item's `Href`.
**How to avoid:** Pass `activePath` as `/tablos` (the parent) for all tablo detail pages so the sidebar "Tablos" nav item remains active.
### Pitfall 4: Sortable.js double-init after HTMX swap
**What goes wrong:** After an HTMX swap refreshes the kanban (e.g., after task create), Sortable.js is initialized again, creating a second instance on already-initialized containers, causing duplicate handlers.
**Why it happens:** `htmx:afterSettle` fires every time a swap completes; naive re-init doesn't check for existing instances.
**How to avoid:** Guard init with `if (el._sortable) return;` — set `el._sortable` on first init.
### Pitfall 5: `progressInlineStyle` uses `var(--project-color)`
**What goes wrong:** The progress bar fill color on the tablo detail page matches the project color instead of `var(--color-brand-primary)` as specified.
**Why it happens:** The existing `.project-progress-bar` rule uses `background: var(--project-color, var(--color-project-fallback))` — inherited from Phase 19 tablo cards.
**How to avoid:** UI-SPEC says "Change fill color from `var(--project-color)` to `var(--color-brand-primary)`". Apply a scoped override for the detail page header progress bar: `.tablo-detail-header .project-progress-bar { background: var(--color-brand-primary); }` — or use a separate CSS class `tablo-progress-bar` on the detail page's progress element.
### Pitfall 6: Tablo `Status` vs task `Status` confusion
**What goes wrong:** The tablo status field (`todo/in_progress/done`) is used to drive the progress bar value (0/50/100) on tablo cards — but this is NOT the task completion progress. The tablo detail header's progress must be computed from actual task statuses (done tasks / total tasks), not the tablo's own status field.
**Why it happens:** `buildTabloCardViews` uses `tabloStatusPresentation(tablo.Status)` to set `Progress: 50` for in-progress tablos — that's a placeholder.
**How to avoid:** In the detail handler, compute `progress = computeTabloProgress(tasks)` from real task records.
---
## Code Examples
### CSS: `.task-card` new block (from UI-SPEC)
```css
/* Source: 20-UI-SPEC.md Surfaces: Kanban Board → Task cards */
.task-card {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 8px;
cursor: pointer;
display: flex;
flex-direction: column;
gap: 8px;
padding-inline: 12px;
padding-block: 8px;
transition: box-shadow 0.12s ease, border-color 0.12s ease;
}
.task-card:hover {
border-color: var(--color-border-strong);
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.08);
}
```
### CSS: Kanban column border-radius change
```css
/* Source: 20-UI-SPEC.md Component Delta table — change 1rem → 0.75rem */
.tablo-kanban-column {
background: var(--color-surface-default);
border: 1px solid var(--color-border-default);
border-radius: 0.75rem; /* was 1rem in tasks-section */
overflow: hidden;
width: 18rem;
flex-shrink: 0;
}
```
### CSS: Task list gap
```css
/* Source: 20-UI-SPEC.md Component Delta — add gap: 8px; padding: 8px */
.task-list {
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px;
}
```
### CSS: Kanban board container
```css
/* Source: 20-UI-SPEC.md Surfaces: Kanban Board → Board container */
.tablo-kanban-board {
display: flex;
gap: 16px;
overflow-x: auto;
padding-bottom: 16px;
}
```
### CSS: Empty column state
```css
/* Source: 20-UI-SPEC.md Surfaces: Kanban Board → Empty column state */
.tablo-kanban-empty {
color: var(--color-text-faint);
font-size: 0.875rem;
padding: 24px 16px;
text-align: center;
}
```
### CSS: Files table wrapper restyle
```css
/* Source: 20-UI-SPEC.md Surfaces: Files Table */
.tablo-files-table-wrapper {
border: 1px solid var(--color-border-default);
border-radius: 12px;
overflow: hidden;
}
.tablo-files-table-wrapper thead tr {
background: var(--color-surface-muted);
}
.tablo-files-table-wrapper thead th {
color: var(--color-text-muted);
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.tablo-files-table-wrapper tbody tr {
border-bottom: 1px solid var(--color-border-default);
}
.tablo-files-table-wrapper tbody tr:hover {
background: var(--color-surface-subtle);
}
```
### templ: Tablo detail header layout
```templ
// Source: [ASSUMED] — pattern derived from UI-SPEC header spec
templ TabloDetailHeader(vm TabloDetailHeaderView) {
<header class="tablo-detail-header">
<div class="tablo-detail-title-row">
<div class="tablo-detail-avatar" style={ projectColorVariableStyle(vm.Color) }>
{ vm.Initial }
</div>
<h1 class="tablo-detail-title">{ vm.Name }</h1>
</div>
<div class="tablo-metadata-row">
<span class="tablo-meta-segment">
<!-- owner avatar 24×24 + name -->
</span>
<span class="tablo-meta-segment">
@ActionIcon("calendar")
if vm.DueDate != "" { vm.DueDate } else { "—" }
</span>
<span class="tablo-meta-segment">
@ui.Badge(ui.BadgeProps{Label: vm.StatusLabel, Variant: badgeVariantForTone(vm.StatusTone)})
</span>
<span class="tablo-meta-segment tablo-meta-progress">
<div class="project-progress-track" style="min-width:120px">
<div class="tablo-detail-progress-bar" style={ progressInlineStyle(vm.Progress) }></div>
</div>
<strong>{ vm.ProgressLabel }</strong>
</span>
</div>
</header>
}
```
---
## State of the Art
| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Tablo progress = tablo.Status mapped to 0/50/100 | Compute from actual task done/total ratio | Phase 20 | Detail page shows real progress |
| No tablo detail page | `GET /tablos/{tabloID}` with tab bar | Phase 20 | First time users can navigate into a tablo |
| Global kanban (flat grid, `grid-cols-4`) | Tablo-scoped kanban (flex row, `overflow-x: auto`) | Phase 20 | Detail page kanban vs global tasks page kanban |
**Deprecated/outdated:**
- `.project-progress-bar { background: var(--project-color) }` for detail page use: replaced by `var(--color-brand-primary)` per UI-SPEC.
- `.tasks-section { border-radius: 1rem }` in cards context: changed to `0.75rem` for kanban columns.
---
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | `GetTabloByID` sqlc query does not exist; must use `ListTablos + findTabloByID` | Pitfall 1 | If a GetTabloByID query was added via `just generate` after last codebase read, handler can be simplified — low risk either way |
| A2 | `ListTasksByTablo` is not in `AuthRepository` interface; needs type assertion or new interface | Pitfall 2 | If someone added it to the interface, no type-assert needed |
| A3 | Sortable.js CDN is already loaded on the tablo detail page | Pattern 5 | If Sortable.js is not in `DashboardPage` head, it must be added |
| A4 | The tablo detail page has no existing route and no existing templ component | Standard Stack | If a partial implementation exists in a branch, avoid duplication |
| A5 | Files tab shows files from the tablo's R2 bucket; no file listing query currently exists for tablo-scoped files | Files Table section | Phase 20 UI-SPEC says "restyle only" — implies files list already renders. If no file data available, the files tab renders with empty state only |
---
## Open Questions
1. **Tablo detail route navigation from tablo cards**
- What we know: `TabloGridCard` and `TabloListRow` in `tablos.templ` have no `hx-get` to the detail page — they have edit/delete buttons only.
- What's unclear: Should clicking the card title navigate to detail? Or is there a separate "View" button?
- Recommendation: Make the card title/avatar an `hx-get="/tablos/{id}"` link, consistent with the linear-style navigation pattern.
2. **Files tab data for tablo detail**
- What we know: The UI-SPEC says "restyle only" for the files table. No file listing sqlc query for tablo-scoped files was found.
- What's unclear: Are files accessible? Is there a `ListFilesByTablo` repo method?
- Recommendation: If no files query exists, render files tab with empty state (`ui.EmptyState`) only — no blocking dependency for TASK-01 or DETAIL-01.
3. **Owner display for tablo detail header**
- What we know: `tablo.OwnerID` exists; `GetPublicUserByID` exists in `AuthRepository`.
- What's unclear: Whether the handler should fetch the owner record for display.
- Recommendation: Fetch owner display name in handler via `GetPublicUserByID` for the metadata row. Falls back to "Propriétaire" if fetch fails.
---
## Environment Availability
> Phase is code/CSS only. No new external tools or services required.
**Step 2.6: SKIPPED** — No external dependencies beyond existing Go toolchain, templ, and Tailwind build already in place.
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Vitest (frontend not applicable) / Go stdlib `testing` + `net/http/httptest` |
| Config file | none (Go stdlib test runner) |
| Quick run command | `go test ./... -run TestTabloDetail -count=1` |
| Full suite command | `go test ./... -count=1` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| DETAIL-01 | `GET /tablos/{tabloID}` returns 200 with tablo name in response | unit/integration | `go test ./... -run TestGetTabloDetailPage -count=1` | ❌ Wave 0 |
| DETAIL-01 | Progress bar percentage rendered as `{N}%` in response | unit | `go test ./... -run TestTabloDetailProgress -count=1` | ❌ Wave 0 |
| DETAIL-01 | Status badge label rendered in metadata row | unit | `go test ./... -run TestTabloDetailStatusBadge -count=1` | ❌ Wave 0 |
| TASK-01 | Kanban columns render 4 status columns | unit | `go test ./... -run TestTabloDetailKanbanColumns -count=1` | ❌ Wave 0 |
| TASK-01 | Task cards contain `.task-card` class | unit | `go test ./... -run TestTabloDetailTaskCardMarkup -count=1` | ❌ Wave 0 |
| TASK-01 | Drag handle has `opacity:0` at rest (CSS class present) | unit | `go test ./... -run TestTabloDetailDragHandle -count=1` | ❌ Wave 0 |
### Sampling Rate
- **Per task commit:** `go test ./... -run TestTabloDetail -count=1`
- **Per wave merge:** `go test ./... -count=1`
- **Phase gate:** Full suite green before `/gsd:verify-work`
### Wave 0 Gaps
- [ ] `go-backend/internal/web/handlers/tablo_detail_test.go` — covers all DETAIL-01 + TASK-01 handler tests
- [ ] `go-backend/router_test.go` additions — integration-level smoke test for `GET /tablos/{tabloID}`
*(Existing test infrastructure in `handlers/tablos_test.go` provides the scaffold pattern to follow.)*
---
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | yes | `authenticatedUser(r.Context(), r)` — existing session check, same as all other handlers |
| V3 Session Management | yes | existing `xtablo_session` cookie pattern |
| V4 Access Control | yes | `findTabloByID` filters by `OwnerID` — prevents accessing other users' tablos |
| V5 Input Validation | yes | `uuid.Parse(r.PathValue("tabloID"))` guards invalid IDs |
| V6 Cryptography | no | No crypto in this phase |
### Known Threat Patterns for this stack
| Pattern | STRIDE | Standard Mitigation |
|---------|--------|---------------------|
| IDOR — access another user's tablo by guessing UUID | Elevation of Privilege | `findTabloByID` only returns tablos owned by authenticated user's ID |
| Task mutation across tablo boundary | Tampering | `ListTasksByTablo` includes `OwnerID` in query; UpdateTask/DeleteTask handlers enforce ownership |
---
## Sources
### Primary (HIGH confidence)
- `/go-backend/internal/web/ui/base.css` — all CSS custom property tokens confirmed
- `/go-backend/internal/web/ui/app.css` — existing `.task-row`, `.project-progress-bar`, `.tasks-section` CSS rules
- `/go-backend/internal/web/views/tasks.templ` — existing `TasksKanbanLayout`, `TaskCard`, `TasksKanbanColumnView` patterns
- `/go-backend/internal/web/views/tablos.templ` — existing `TabloGridCard`, header/progress patterns
- `/go-backend/internal/web/handlers/tablos.go``findTabloByID`, `tabloStatusPresentation`, existing handler patterns
- `/go-backend/internal/web/handlers/auth.go``AuthRepository` interface definition (no `ListTasksByTablo`)
- `/go-backend/router.go` — confirmed no `GET /tablos/{tabloID}` route exists
- `/go-backend/internal/tablos/model.go` — tablo model fields confirmed
- `/go-backend/internal/tasks/model.go` — task status enums, `ListByTabloInput`
- `.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md` — locked visual decisions
### Secondary (MEDIUM confidence)
- `.claude/skills/sketch-findings-xtablo-source/SKILL.md` — design direction and CSS patterns verified against existing `base.css` tokens
### Tertiary (LOW confidence)
- `[ASSUMED]` items in the Assumptions Log above — architectural inferences from existing handler patterns
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — confirmed from codebase; no new packages
- Architecture: HIGH — detail page gap confirmed, handler patterns confirmed, CSS tokens confirmed
- Pitfalls: HIGH — confirmed from direct code inspection (interface gaps, CSS overrides)
**Research date:** 2026-05-18
**Valid until:** 2026-06-17 (30-day window; stable stack)