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