- TestComputeTabloProgress_{Empty,AllDone,Half,EtapesIgnored}
- TestNewTabloDetailViewModel_{GroupsTasksByStatus,EtapesExcludedFromColumns,EtapesPopulated}
- TestGetTabloDetailPage_{Returns200,Returns404,Returns400,Unauthenticated}
- TestTabloDetailKanbanColumns
- TestGetTabloDetailPage_ContainsSortableScript
325 lines
22 KiB
Markdown
325 lines
22 KiB
Markdown
---
|
|
phase: 20-tablo-detail-kanban-restyle
|
|
plan: 01
|
|
type: execute
|
|
wave: 1
|
|
depends_on: []
|
|
files_modified:
|
|
- go-backend/internal/web/handlers/tablo_detail.go
|
|
- go-backend/internal/web/views/tablo_detail_view.go
|
|
- go-backend/internal/web/handlers/tablo_detail_test.go
|
|
- go-backend/router.go
|
|
autonomous: true
|
|
requirements:
|
|
- DETAIL-01
|
|
- TASK-01
|
|
|
|
must_haves:
|
|
truths:
|
|
- "GET /tablos/{tabloID} returns 200 with the tablo name in the HTML response body"
|
|
- "Accessing a tablo owned by a different user returns 404"
|
|
- "Accessing with an invalid UUID returns 400"
|
|
- "Progress is computed as doneTasks/totalTasks*100 (integer), not from tablo.Status"
|
|
- "TabloDetailViewModel groups tasks into 4 slices: Todo, InProgress, InReview, Done"
|
|
- "TabloDetailViewModel.Etapes lists etapes (tasks with IsEtape=true) for the tablo, with task count per etape"
|
|
- "Rendered HTML from GET /tablos/{id} contains substring `initTabloDetailSortable`"
|
|
artifacts:
|
|
- path: "go-backend/internal/web/handlers/tablo_detail.go"
|
|
provides: "GetTabloDetailPage handler + tabloDetailRepository interface + computeTabloProgress"
|
|
exports: ["GetTabloDetailPage"]
|
|
- path: "go-backend/internal/web/views/tablo_detail_view.go"
|
|
provides: "TabloDetailViewModel + TabloDetailColumnView + TabloDetailEtapeView + NewTabloDetailViewModel"
|
|
exports: ["TabloDetailViewModel", "TabloDetailColumnView", "TabloDetailEtapeView", "NewTabloDetailViewModel"]
|
|
- path: "go-backend/internal/web/handlers/tablo_detail_test.go"
|
|
provides: "Test scaffold for DETAIL-01 + TASK-01 handler tests"
|
|
- path: "go-backend/router.go"
|
|
provides: "GET /tablos/{tabloID} route registered"
|
|
contains: "mux.Get(\"/tablos/{tabloID}\""
|
|
key_links:
|
|
- from: "go-backend/router.go"
|
|
to: "go-backend/internal/web/handlers/tablo_detail.go"
|
|
via: "authHandler.GetTabloDetailPage()"
|
|
pattern: "GetTabloDetailPage"
|
|
- from: "go-backend/internal/web/handlers/tablo_detail.go"
|
|
to: "h.repo.(tabloDetailRepository)"
|
|
via: "type assertion for ListTasksByTablo"
|
|
pattern: "tabloDetailRepository"
|
|
---
|
|
|
|
<objective>
|
|
Create the GET /tablos/{tabloID} handler, view model, and route registration. This is the foundation plan: it creates the data contracts (TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView) that the templ plan will build against, and the test scaffold that covers DETAIL-01 + TASK-01 handler behavior.
|
|
|
|
Purpose: The tablo detail page does not exist. This plan wires the server-side data layer — authentication, ownership check, task fetch, progress computation, etape grouping — and registers the route.
|
|
|
|
Output: Handler file, view model file, test scaffold, updated router.go.
|
|
</objective>
|
|
|
|
<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/20-tablo-detail-kanban-restyle/20-RESEARCH.md
|
|
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/20-tablo-detail-kanban-restyle/20-UI-SPEC.md
|
|
|
|
<interfaces>
|
|
<!-- Key types the executor needs. Extracted from codebase. -->
|
|
|
|
From go-backend/internal/web/handlers/auth.go:
|
|
```go
|
|
// AuthRepository interface — does NOT include ListTasksByTablo
|
|
type AuthRepository interface {
|
|
CreateAuthUser(ctx context.Context, input CreateAuthUserInput) (uuid.UUID, error)
|
|
GetAuthUserByEmail(ctx context.Context, email string) (AuthUser, error)
|
|
GetPublicUserByID(ctx context.Context, id uuid.UUID) (PublicUser, error)
|
|
CreateSession(ctx context.Context, token string, userID uuid.UUID, expiresAt time.Time) error
|
|
GetSessionByToken(ctx context.Context, token string) (Session, error)
|
|
DeleteSessionByToken(ctx context.Context, token string) error
|
|
CreateTablo(ctx context.Context, input CreateTabloInput) (TabloRecord, error)
|
|
UpdateTablo(ctx context.Context, input UpdateTabloInput) error
|
|
ListTablos(ctx context.Context, input ListTablosInput) ([]TabloRecord, error)
|
|
SoftDeleteTablo(ctx context.Context, tabloID uuid.UUID, ownerID uuid.UUID) error
|
|
}
|
|
|
|
type AuthHandler struct { repo AuthRepository }
|
|
func (h *AuthHandler) authenticatedUser(ctx context.Context, r *http.Request) (PublicUser, bool)
|
|
```
|
|
|
|
From go-backend/internal/web/handlers/tasks.go:
|
|
```go
|
|
// taskPageRepository — separate interface for task reads; ListTasksByTablo NOT in AuthRepository
|
|
type taskPageRepository interface {
|
|
ListTasksByOwner(ctx context.Context, ownerID uuid.UUID) ([]TaskRecord, error)
|
|
}
|
|
|
|
// taskMutationRepository — separate interface for task writes
|
|
type taskMutationRepository interface {
|
|
CreateTask(ctx context.Context, input CreateTaskInput) (TaskRecord, error)
|
|
GetTaskByID(ctx context.Context, taskID uuid.UUID, ownerID uuid.UUID) (TaskRecord, error)
|
|
UpdateTask(ctx context.Context, input UpdateTaskInput) (TaskRecord, error)
|
|
SoftDeleteTask(ctx context.Context, taskID uuid.UUID, ownerID uuid.UUID) error
|
|
}
|
|
|
|
type ListTasksByTabloInput = taskmodel.ListByTabloInput // {OwnerID uuid.UUID, TabloID uuid.UUID}
|
|
type TaskRecord = taskmodel.Record // {ID, TabloID, OwnerID, Title, Status, IsEtape bool, ParentTaskID *uuid.UUID, ...}
|
|
type TaskStatus = taskmodel.Status
|
|
const TaskStatusDone = taskmodel.StatusDone // "done"
|
|
const TaskStatusTodo = taskmodel.StatusTodo
|
|
const TaskStatusInProgress = taskmodel.StatusInProgress
|
|
const TaskStatusInReview = taskmodel.StatusInReview
|
|
```
|
|
|
|
From go-backend/internal/tasks/model.go:
|
|
```go
|
|
// TaskRecord.IsEtape bool — true for etapes (stage-level tasks that group regular tasks)
|
|
// TaskRecord.ParentTaskID *uuid.UUID — regular tasks may have an etape as their parent
|
|
// Etapes: tasks where IsEtape==true; child tasks: IsEtape==false with ParentTaskID pointing to an etape
|
|
```
|
|
|
|
From go-backend/internal/web/handlers/tablos.go:
|
|
```go
|
|
// findTabloByID — existing helper, available within handlers package
|
|
func findTabloByID(tablos []TabloRecord, targetID uuid.UUID) (TabloRecord, bool)
|
|
func tabloStatusPresentation(status TabloStatus) (string, string, int, string)
|
|
// returns: (statusLabel, statusClass, progress, statusTone)
|
|
|
|
type TabloRecord = tablomodel.Record // {ID, OwnerID, Name, Color, Status, CreatedAt, UpdatedAt}
|
|
type ListTablosInput = tablomodel.ListInput // {OwnerID, Status*}
|
|
```
|
|
|
|
From go-backend/internal/web/handlers/in_memory_auth_repository.go (line 168):
|
|
```go
|
|
func (r *InMemoryAuthRepository) ListTasksByTablo(_ context.Context, input ListTasksByTabloInput) ([]TaskRecord, error)
|
|
// InMemoryAuthRepository already implements this — safe to use in tests
|
|
```
|
|
|
|
From go-backend/router.go:
|
|
```go
|
|
func newRouterWithHandler(authHandler *handlers.AuthHandler) http.Handler {
|
|
mux := chi.NewRouter()
|
|
// ... existing routes ...
|
|
mux.Get("/tablos/{tabloID}/edit", authHandler.GetEditTabloModal())
|
|
mux.Post("/tablos/{tabloID}", authHandler.PostTabloUpdate())
|
|
mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
|
|
// ADD: mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage())
|
|
// Must be BEFORE the edit/post/delete routes to avoid conflicts — chi uses first-match
|
|
}
|
|
```
|
|
|
|
From go-backend/internal/web/views (render pattern from tablos.go renderTablosResponse):
|
|
```go
|
|
// DashboardPageWithMainClass and DashboardContentSwapWithMainClass are used for full-width content
|
|
views.DashboardContentSwapWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w)
|
|
views.DashboardPageWithMainClass(activePath, tablos, "flex-1 overflow-auto", content).Render(r.Context(), w)
|
|
// activePath for tablo detail: "/tablos" (parent, so sidebar Tablos item stays highlighted per Pitfall 3)
|
|
```
|
|
</interfaces>
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 1: Define TabloDetailViewModel (with etapes) and test scaffold</name>
|
|
<files>go-backend/internal/web/views/tablo_detail_view.go, go-backend/internal/web/handlers/tablo_detail_test.go</files>
|
|
<read_first>
|
|
- go-backend/internal/web/views/tablos_view.go (TabloCardView struct pattern, normalizedView helper)
|
|
- go-backend/internal/web/views/tasks_view.go (TaskCardView, TasksKanbanColumnView pattern; etape grouping in buildKanbanView and etapeMeta struct)
|
|
- go-backend/internal/web/handlers/tablos_test.go (test scaffold pattern for handlers)
|
|
- go-backend/internal/web/handlers/in_memory_auth_repository.go (InMemoryAuthRepository method list, lines 168+)
|
|
</read_first>
|
|
<behavior>
|
|
- TabloDetailViewModel has fields: TabloID string, TabloName string, Color string, Initial string, OwnerName string, DueDate string, StatusLabel string, StatusTone string, Progress int, ProgressLabel string, Columns []TabloDetailColumnView, Etapes []TabloDetailEtapeView
|
|
- TabloDetailColumnView has fields: ID string, Label string, Tasks []TabloDetailTaskView, CreateHref string
|
|
- TabloDetailTaskView has fields: ID string, Title string, DeleteHref string, EditHref string
|
|
- TabloDetailEtapeView has fields: ID string, Name string, TaskCount int
|
|
- NewTabloDetailViewModel(tablo TabloRecord, tasks []TaskRecord, ownerName string) builds 4 columns in order: "todo", "in_progress", "in_review", "done". It also builds Etapes slice by filtering tasks where IsEtape==true; each etape's TaskCount is the number of tasks (IsEtape==false) whose ParentTaskID equals the etape's ID.
|
|
- computeTabloProgress(tasks []TaskRecord) int returns 0 when total of non-etape tasks==0, else (doneCount*100)/total where doneCount counts non-etape tasks with Status==TaskStatusDone
|
|
- Test: TestComputeTabloProgress_Empty — computeTabloProgress([]) == 0
|
|
- Test: TestComputeTabloProgress_AllDone — computeTabloProgress([done, done]) == 100
|
|
- Test: TestComputeTabloProgress_Half — computeTabloProgress([done, todo]) == 50
|
|
- Test: TestComputeTabloProgress_EtapesIgnored — etape tasks (IsEtape=true) are excluded from progress computation
|
|
- Test: TestNewTabloDetailViewModel_Groups tasks into correct columns by status (excludes etapes from columns)
|
|
- Test: TestNewTabloDetailViewModel_EtapesPopulated — Etapes slice contains etape tasks with correct TaskCount
|
|
- Test: TestGetTabloDetailPage_Returns200 — GET /tablos/{validTabloID} with session cookie returns 200 and tablo name in body
|
|
- Test: TestGetTabloDetailPage_Returns404 — GET /tablos/{unknownID} with session cookie returns 404
|
|
- Test: TestGetTabloDetailPage_Returns400 — GET /tablos/not-a-uuid returns 400
|
|
- Test: TestGetTabloDetailPage_Unauthenticated — GET /tablos/{validID} without session cookie returns 302 to /login
|
|
- Test: TestTabloDetailKanbanColumns — response body contains the 4 kanban column status values (todo, in_progress, in_review, done) per Pitfall 3
|
|
- Test: TestGetTabloDetailPage_ContainsSortableScript — response body contains substring "initTabloDetailSortable"
|
|
</behavior>
|
|
<action>
|
|
Create go-backend/internal/web/views/tablo_detail_view.go in package views. Define:
|
|
1. TabloDetailTaskView struct with ID, Title, DeleteHref, EditHref string fields.
|
|
2. TabloDetailColumnView struct with ID, Label, Tasks []TabloDetailTaskView, CreateHref string fields.
|
|
3. TabloDetailEtapeView struct with ID, Name string and TaskCount int fields.
|
|
4. TabloDetailViewModel struct with TabloID, TabloName, Color, Initial, OwnerName, DueDate, StatusLabel, StatusTone string, Progress int, ProgressLabel string, Columns []TabloDetailColumnView, Etapes []TabloDetailEtapeView fields.
|
|
5. computeTabloProgress(tasks []TaskRecord) int function (package-private). Excludes tasks with IsEtape==true from computation. Returns 0 if no non-etape tasks; returns (doneCount*100)/nonEtapeTotal where doneCount counts non-etape tasks with Status==TaskStatusDone.
|
|
6. NewTabloDetailViewModel(tablo TabloRecord, tasks []TaskRecord, ownerName string) TabloDetailViewModel that:
|
|
- Sets TabloID=tablo.ID.String(), TabloName=tablo.Name, Color=tablo.Color, Initial=projectInitial(tablo.Name)
|
|
- Sets OwnerName=ownerName
|
|
- Calls tabloStatusPresentation(tablo.Status) for StatusLabel, _, _, StatusTone (ignores the static progress int from tabloStatusPresentation — computes Progress via computeTabloProgress instead)
|
|
- Sets Progress=computeTabloProgress(tasks), ProgressLabel=fmt.Sprintf("%d%%", progress)
|
|
- Builds Columns slice of 4 TabloDetailColumnView in order: {ID:"todo", Label:"À faire"}, {ID:"in_progress", Label:"En cours"}, {ID:"in_review", Label:"En révision"}, {ID:"done", Label:"Terminé"}
|
|
- Populates each column's Tasks by filtering non-etape tasks (IsEtape==false) by Status; for each task: DeleteHref="/tasks/"+task.ID.String(), EditHref="/tasks/"+task.ID.String()+"/edit"
|
|
- Sets CreateHref="/tablos/"+tablo.ID.String()+"/tasks/create?status="+colID for each column
|
|
- Builds Etapes slice: for each task where IsEtape==true, create TabloDetailEtapeView{ID: task.ID.String(), Name: task.Title, TaskCount: count of tasks where IsEtape==false && ParentTaskID==&task.ID}
|
|
- DueDate: tablo.UpdatedAt is not a due date — set DueDate="" (no due_date field on tablo model yet; template shows "—" fallback)
|
|
|
|
Create go-backend/internal/web/views/tablo_detail_view_test.go in package views for view model unit tests. Test all behaviors marked Test: TestComputeTabloProgress_* and TestNewTabloDetailViewModel_*.
|
|
|
|
Create go-backend/internal/web/handlers/tablo_detail_test.go in package handlers for handler integration tests. Use the router/test helper pattern from existing handlers tests. Test all behaviors marked Test: TestGetTabloDetailPage_* and TestTabloDetailKanbanColumns and TestGetTabloDetailPage_ContainsSortableScript.
|
|
|
|
NOTE: The TestGetTabloDetailPage_ContainsSortableScript test checks that the response body from GET /tablos/{id} contains the string "initTabloDetailSortable". This is the verifiable proxy for Sortable.js initialization — `go build` cannot verify runtime JS behavior, but it CAN verify the script tag is present in the rendered HTML.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" -count=1 -v 2>&1 | tail -20</automated>
|
|
</verify>
|
|
<done>go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" passes; all view-layer tests are green; tablo_detail_view.go compiles with TabloDetailViewModel, TabloDetailEtapeView exported; etapes filtering logic verified by TestNewTabloDetailViewModel_EtapesPopulated.</done>
|
|
</task>
|
|
|
|
<task type="auto" tdd="true">
|
|
<name>Task 2: GetTabloDetailPage handler + router registration</name>
|
|
<files>go-backend/internal/web/handlers/tablo_detail.go, go-backend/router.go</files>
|
|
<read_first>
|
|
- go-backend/internal/web/handlers/tablos.go (findTabloByID, renderTablosResponse pattern, tabloStatusPresentation, authenticatedUser)
|
|
- go-backend/internal/web/handlers/tasks.go (type assertion pattern h.repo.(taskPageRepository), ListTasksByTabloInput)
|
|
- go-backend/internal/web/handlers/auth.go (AuthHandler struct, AuthRepository interface)
|
|
- go-backend/router.go (current route list — locate where to insert GET /tablos/{tabloID} before edit/post/delete)
|
|
- go-backend/internal/web/views/tablo_detail_view.go (just created — NewTabloDetailViewModel signature)
|
|
</read_first>
|
|
<behavior>
|
|
- tabloDetailRepository interface exposes ListTasksByTablo(ctx, ListTasksByTabloInput) ([]TaskRecord, error)
|
|
- GetTabloDetailPage returns http.HandlerFunc that: (a) checks auth, (b) parses tabloID as UUID, (c) ListTablos+findTabloByID, (d) type-asserts h.repo to tabloDetailRepository, (e) ListTasksByTablo, (f) fetches owner via GetPublicUserByID (falls back to empty string on error), (g) builds TabloDetailViewModel, (h) renders DashboardPageWithMainClass or DashboardContentSwapWithMainClass with activePath="/tablos" and a placeholder views.TabloDetailPage(vm) stub
|
|
- On auth failure: 302 /login
|
|
- On invalid UUID: 400 "invalid tablo id"
|
|
- On tablo not found: 404 "tablo not found"
|
|
- On type assertion failure (should never happen in production): 500 "tasks repository not configured"
|
|
- activePath passed to Dashboard helpers is "/tablos" (not "/tablos/{id}") so sidebar "Tablos" nav item stays highlighted (Pitfall 3 from RESEARCH)
|
|
- router.go adds: mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage()) — positioned before the existing mux.Get("/tablos/{tabloID}/edit", ...) line
|
|
</behavior>
|
|
<action>
|
|
Create go-backend/internal/web/handlers/tablo_detail.go in package handlers. Import context, net/http, github.com/google/uuid, xtablo-backend/internal/web/views.
|
|
|
|
Define tabloDetailRepository interface:
|
|
type tabloDetailRepository interface {
|
|
ListTasksByTablo(ctx context.Context, input ListTasksByTabloInput) ([]TaskRecord, error)
|
|
}
|
|
|
|
Define GetTabloDetailPage() http.HandlerFunc on *AuthHandler:
|
|
- Call h.authenticatedUser; on failure redirect /login
|
|
- uuid.Parse(r.PathValue("tabloID")); on error http.Error 400 "invalid tablo id"
|
|
- h.repo.ListTablos(ctx, ListTablosInput{OwnerID: user.ID}); on error 500
|
|
- findTabloByID(tablos, tabloID); on not-found http.Error 404 "tablo not found"
|
|
- taskRepo, ok := h.repo.(tabloDetailRepository); if !ok http.Error 500 "tasks repository not configured"
|
|
- taskRepo.ListTasksByTablo(ctx, ListTasksByTabloInput{OwnerID: user.ID, TabloID: tabloID}); on error 500
|
|
- ownerName: call h.repo.GetPublicUserByID(ctx, user.ID); use owner.DisplayName; on error ownerName=""
|
|
- vm := views.NewTabloDetailViewModel(tablo, tasks, ownerName)
|
|
- content := views.TabloDetailPage(vm) [this templ function will be a stub until Plan 02]
|
|
- Render using DashboardPageWithMainClass (non-HX) or DashboardContentSwapWithMainClass (HX) with activePath="/tablos", mainClass="flex-1 overflow-auto"
|
|
|
|
IMPORTANT: views.TabloDetailPage does not exist yet. Add a temporary stub in go-backend/internal/web/views/tablo_detail_view.go:
|
|
func TabloDetailPage(vm TabloDetailViewModel) templ.Component {
|
|
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
|
|
_, err := fmt.Fprintf(w, "<div>%s<script>function initTabloDetailSortable(){}</script></div>", vm.TabloName)
|
|
return err
|
|
})
|
|
}
|
|
The stub emits the tablo name AND the initTabloDetailSortable function name so that TestGetTabloDetailPage_ContainsSortableScript passes. This stub will be replaced in Plan 02 with the real templ component. Import "context", "fmt", "io", "github.com/a-h/templ" for the stub.
|
|
|
|
Update go-backend/router.go: add mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage()) immediately before the existing mux.Get("/tablos/{tabloID}/edit", ...) line.
|
|
|
|
Verify handler tests from tablo_detail_test.go pass.
|
|
</action>
|
|
<verify>
|
|
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/go-backend && go test ./... -run "TestGetTabloDetailPage" -count=1 -v 2>&1 | tail -30</automated>
|
|
</verify>
|
|
<done>All TestGetTabloDetailPage_* tests pass including TestGetTabloDetailPage_ContainsSortableScript; go build ./... succeeds; GET /tablos/{validID} integration test returns 200 with tablo name in body.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| client→GET /tablos/{tabloID} | Unauthenticated or cross-user path value |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-20-01 | Elevation of Privilege | GetTabloDetailPage — IDOR via guessed UUID | mitigate | findTabloByID filters by OwnerID from authenticated session; returns 404 for any tablo not owned by authenticated user |
|
|
| T-20-02 | Spoofing | Cookie-based session auth | mitigate | h.authenticatedUser validates session cookie via GetSessionByToken + expiry check — existing pattern |
|
|
| T-20-03 | Tampering | ListTasksByTablo input | mitigate | OwnerID comes from authenticated session, not from request params; tabloID validated via uuid.Parse before use |
|
|
| T-20-SC | Tampering | npm/pip/cargo installs | accept | No package installs in this plan — pure Go code using existing dependencies |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
After Plan 01:
|
|
- `cd go-backend && go build ./...` exits 0
|
|
- `go test ./internal/web/views/... -run "TestCompute|TestNewTabloDetail" -count=1` all pass
|
|
- `go test ./... -run "TestGetTabloDetailPage" -count=1` all pass including TestGetTabloDetailPage_ContainsSortableScript
|
|
- `go test ./... -count=1` full suite still green (no regressions)
|
|
- router.go contains `mux.Get("/tablos/{tabloID}", authHandler.GetTabloDetailPage())`
|
|
- tablo_detail.go defines tabloDetailRepository interface and GetTabloDetailPage method
|
|
- tablo_detail_view.go exports TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView, NewTabloDetailViewModel
|
|
- TabloDetailViewModel.Etapes is populated from tasks where IsEtape==true
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- GET /tablos/{validUUID} (authenticated, owned) returns 200 containing tablo name
|
|
- GET /tablos/{validUUID} (authenticated, not owned) returns 404
|
|
- GET /tablos/not-a-uuid returns 400
|
|
- GET /tablos/{validUUID} (unauthenticated) returns 302 to /login
|
|
- Progress computed from non-etape task statuses, not tablo.Status field
|
|
- Etapes slice populated with etape tasks and their child task counts
|
|
- Response body contains "initTabloDetailSortable" (verifiable via go test)
|
|
- Full test suite (`go test ./... -count=1`) remains green
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/20-tablo-detail-kanban-restyle/20-01-SUMMARY.md` when done.
|
|
</output>
|