From 27326f57d6becbccfbe43df0af8b553cb8b93599 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Mon, 18 May 2026 15:47:28 +0200 Subject: [PATCH] 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) --- .../20-01-SUMMARY.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 .planning/phases/20-tablo-detail-kanban-restyle/20-01-SUMMARY.md diff --git a/.planning/phases/20-tablo-detail-kanban-restyle/20-01-SUMMARY.md b/.planning/phases/20-tablo-detail-kanban-restyle/20-01-SUMMARY.md new file mode 100644 index 0000000..f1f575f --- /dev/null +++ b/.planning/phases/20-tablo-detail-kanban-restyle/20-01-SUMMARY.md @@ -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