docs(20-01): complete tablo detail handler + view model plan summary

- TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView exported
- computeTabloProgress excludes etape tasks
- GetTabloDetailPage handler with IDOR mitigation
- GET /tablos/{tabloID} route registered
- Full test suite green (13 packages)
This commit is contained in:
Arthur Belleville 2026-05-18 15:47:28 +02:00
parent 3fc8aae7a0
commit 27326f57d6
No known key found for this signature in database

View file

@ -0,0 +1,140 @@
---
phase: 20-tablo-detail-kanban-restyle
plan: "01"
subsystem: go-backend/handlers+views
tags: [tablo-detail, kanban, view-model, handler, router]
dependency_graph:
requires: []
provides:
- TabloDetailViewModel (views package)
- TabloDetailColumnView (views package)
- TabloDetailEtapeView (views package)
- NewTabloDetailViewModel (views package)
- computeTabloProgress (views package)
- GetTabloDetailPage handler
- GET /tablos/{tabloID} route
affects:
- go-backend/router.go
tech_stack:
added: []
patterns:
- tabloDetailRepository interface (type assertion pattern, mirrors taskPageRepository)
- TabloDetailPage stub templ.ComponentFunc (replaced by Plan 02 with real component)
- TDD RED/GREEN cycle across views + handlers packages
key_files:
created:
- go-backend/internal/web/views/tablo_detail_view.go
- go-backend/internal/web/views/tablo_detail_view_test.go
- go-backend/internal/web/handlers/tablo_detail.go
- go-backend/internal/web/handlers/tablo_detail_test.go
modified:
- go-backend/router.go
decisions:
- computeTabloProgress lives in views package (not handlers) — needed by NewTabloDetailViewModel; handlers already have tabloStatusPresentation duplicated in views for same reason (avoid import cycle)
- tabloStatusPresentation duplicated in views package — mirrors handlers/tablos.go to avoid import cycle; both functions are simple switch statements
- TabloDetailPage stub uses templ.ComponentFunc — avoids needing templ generate in Plan 01; Plan 02 replaces with real .templ component
- activePath="/tablos" in DashboardPage/DashboardContentSwap — sidebar Tablos nav item stays highlighted on detail page (Pitfall 3 from RESEARCH)
- projectInitialFromName defined in views package — mirrors projectInitial in handlers to avoid import cycle
metrics:
duration: "~20min"
completed_date: "2026-05-18"
tasks_completed: 2
files_created: 4
files_modified: 1
---
# Phase 20 Plan 01: Tablo Detail Handler + View Model Summary
GET /tablos/{tabloID} handler, view model, and route registration for the tablo detail page. Data contracts (TabloDetailViewModel, column views, etape views) are established that Plan 02 builds its templ component against.
## What Was Built
**views/tablo_detail_view.go**
- `TabloDetailViewModel` — full page view model with Columns, Etapes, Progress, StatusTone
- `TabloDetailColumnView` — kanban column with Tasks []TabloDetailTaskView and CreateHref
- `TabloDetailTaskView` — task card with DeleteHref and EditHref
- `TabloDetailEtapeView` — etape summary with TaskCount (child tasks count)
- `computeTabloProgress(tasks)` — excludes IsEtape==true tasks; returns (doneCount*100)/total
- `NewTabloDetailViewModel(tablo, tasks, ownerName)` — builds 4 ordered columns + etapes
- `TabloDetailPage(vm)` stub — emits tablo name, column status IDs, `initTabloDetailSortable` function
**handlers/tablo_detail.go**
- `tabloDetailRepository` interface exposing `ListTasksByTablo`
- `GetTabloDetailPage()` handler with: auth check → UUID parse (400) → ownership-scoped ListTablos+findTabloByID (404) → type assertion to tabloDetailRepository → ListTasksByTablo → owner lookup (best-effort) → render
**router.go**
- `mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage())` registered before `/tablos/{tabloID}/edit`
## Security (Threat Model)
| Threat | Mitigation | Status |
|--------|------------|--------|
| T-20-01: IDOR via guessed UUID | findTabloByID filters by OwnerID from authenticated session | Mitigated |
| T-20-02: Session spoofing | authenticatedUser validates session cookie via GetSessionByToken | Mitigated |
| T-20-03: Tampered OwnerID in ListTasksByTablo input | OwnerID comes from session, not request params | Mitigated |
## Test Coverage
**View tests (go-backend/internal/web/views/):**
- `TestComputeTabloProgress_Empty` — returns 0 for empty input
- `TestComputeTabloProgress_AllDone` — returns 100
- `TestComputeTabloProgress_Half` — returns 50
- `TestComputeTabloProgress_EtapesIgnored` — etape tasks excluded
- `TestNewTabloDetailViewModel_GroupsTasksByStatus` — 4 columns populated correctly
- `TestNewTabloDetailViewModel_EtapesExcludedFromColumns` — etapes not in columns
- `TestNewTabloDetailViewModel_EtapesPopulated` — etapes with correct TaskCount
**Handler tests (go-backend/internal/web/handlers/):**
- `TestGetTabloDetailPage_Returns200` — authenticated + owned tablo → 200 + name in body
- `TestGetTabloDetailPage_Returns404` — authenticated + unknown tablo → 404
- `TestGetTabloDetailPage_Returns400` — invalid UUID → 400
- `TestGetTabloDetailPage_Unauthenticated` — no cookie → 302 /login
- `TestTabloDetailKanbanColumns` — body contains todo/in_progress/in_review/done
- `TestGetTabloDetailPage_ContainsSortableScript` — body contains "initTabloDetailSortable"
## Commits
| Hash | Type | Description |
|------|------|-------------|
| f24e1c4 | test | RED: failing tests for TabloDetailViewModel + GetTabloDetailPage |
| 9713cbd | feat | GREEN: TabloDetailViewModel + computeTabloProgress + TabloDetailPage stub |
| 3fc8aae | feat | GetTabloDetailPage handler + GET /tablos/{tabloID} route registration |
## Deviations from Plan
**1. [Rule 2 - Critical] tabloStatusPresentation duplicated in views package**
- **Found during:** Task 1
- **Issue:** The plan specifies calling `tabloStatusPresentation(tablo.Status)` from the handler, but view model construction (NewTabloDetailViewModel) lives in the views package and cannot import from handlers (import cycle)
- **Fix:** Duplicated the small switch statement as `tabloStatusPresentation` in `tablo_detail_view.go` (views package). Same approach used elsewhere in the codebase (e.g. `projectInitialFromName` mirrors `projectInitial`).
- **Files modified:** go-backend/internal/web/views/tablo_detail_view.go
- **Impact:** Cosmetic code duplication only; no behavior divergence
**2. [Rule 2 - Critical] projectInitialFromName implemented in views package**
- **Found during:** Task 1
- **Issue:** `projectInitial` is a handler-package function; calling it from views would create import cycle
- **Fix:** `projectInitialFromName` added to views package with identical logic (uppercase first rune of name)
- **Files modified:** go-backend/internal/web/views/tablo_detail_view.go
## Known Stubs
| Stub | File | Line | Reason |
|------|------|------|--------|
| `TabloDetailPage(vm)` stub | go-backend/internal/web/views/tablo_detail_view.go | ~155 | Plan 02 replaces with real templ component; stub emits tablo name + column IDs + initTabloDetailSortable |
## Self-Check
Files created:
- go-backend/internal/web/views/tablo_detail_view.go — FOUND
- go-backend/internal/web/views/tablo_detail_view_test.go — FOUND
- go-backend/internal/web/handlers/tablo_detail.go — FOUND
- go-backend/internal/web/handlers/tablo_detail_test.go — FOUND
Commits:
- f24e1c4 — FOUND
- 9713cbd — FOUND
- 3fc8aae — FOUND
go build ./... — PASSES
go test ./... -count=1 — ALL PASS (13 packages, 0 failures)
## Self-Check: PASSED