diff --git a/docs/superpowers/plans/2026-05-10-go-backend-tasks-multi-view-dashboard.md b/docs/superpowers/plans/2026-05-10-go-backend-tasks-multi-view-dashboard.md new file mode 100644 index 0000000..59a2807 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-go-backend-tasks-multi-view-dashboard.md @@ -0,0 +1,832 @@ +# Go Backend Tasks Multi-View Dashboard Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the simple `go-backend` `/tasks` CRUD page with a server-rendered cross-tablo dashboard that supports `kanban`, `list`, and `roadmap` views, URL-driven filtering, and tablo-aware create/edit flows. + +**Architecture:** Keep `/tasks` as a single owner-scoped route whose state is driven by query params. Parse page state once in the handler, load one shared task dataset across all owned tablos, then shape that dataset into dedicated `kanban`, `list`, and `roadmap` view models. Reuse the existing task persistence layer, and extend the create/edit flow so `tablo_id` and tablo-scoped étape selection are enforced on the server. + +**Tech Stack:** Go, chi, templ components rendered from Go view models, HTMX, PostgreSQL, pgx, sqlc, Go standard `net/http` testing + +--- + +## File Structure + +**Existing files to modify** + +- `go-backend/internal/tasks/model.go` + - Add owner-level dashboard page-state types, filter types, and shared dashboard record shapes. +- `go-backend/internal/web/handlers/tasks.go` + - Parse query params, normalize page state, load shared option data, preserve query state through mutations, and validate `tablo_id` plus tablo-scoped `parent_task_id`. +- `go-backend/internal/web/handlers/tasks_test.go` + - Add handler tests for view switching, filter normalization, roadmap mode behavior, and cross-tablo form validation. +- `go-backend/internal/web/handlers/in_memory_auth_repository.go` + - Add any list/filter support needed by handler tests, while keeping the repository owner-scoped. +- `go-backend/internal/web/views/tasks_view.go` + - Replace the simple grouped CRUD view model with a shared page shell and dedicated `kanban`, `list`, and `roadmap` view builders/renderers. +- `go-backend/router.go` + - Keep `/tasks` on one route, but ensure the new page behavior continues to work through the existing handlers. +- `go-backend/router_test.go` + - Add end-to-end route coverage for query-param-driven rendering and current-state-preserving mutation responses. + +**Files that may be modified if needed** + +- `go-backend/internal/db/repository.go` + - Only if the dashboard needs a small repository helper for owner-scoped task loading or deterministic ordering not already exposed. +- `go-backend/internal/db/queries.sql` + - Only if owner-scoped loading needs a dedicated query shape not already present. +- `go-backend/internal/db/sqlc/queries.sql.go` + - Regenerated if SQL changes are needed. + +**No new persistence design files are required** + +- This plan assumes the task and etape storage added earlier remains the source of truth. + +**Verification commands** + +- `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` +- `cd go-backend && go test ./...` +- `cd go-backend && just build` + +## Task 1: Add Query-State Parsing And Handler Coverage + +**Files:** +- Modify: `go-backend/internal/web/handlers/tasks.go` +- Modify: `go-backend/internal/web/handlers/tasks_test.go` +- Modify: `go-backend/internal/tasks/model.go` + +- [ ] **Step 1: Write the failing handler tests for view and filter state** + +Add focused tests to `go-backend/internal/web/handlers/tasks_test.go`: + +```go +func TestGetTasksPageDefaultsToKanbanView(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/tasks", nil) + req.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetTasksPage().ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + body := rec.Body.String() + if !strings.Contains(body, `href="/tasks?view=kanban"`) { + t.Fatalf("expected kanban tab in body, got %q", body) + } + if !strings.Contains(body, `data-current-view="kanban"`) { + t.Fatalf("expected default view to be kanban, got %q", body) + } +} + +func TestGetTasksPageIgnoresInvalidQueryState(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/tasks?view=nope&roadmap_mode=nope&status=bad", nil) + req.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetTasksPage().ServeHTTP(rec, req) + + if rec.Code != http.StatusOK { + t.Fatalf("expected status 200, got %d", rec.Code) + } + body := rec.Body.String() + if !strings.Contains(body, `data-current-view="kanban"`) { + t.Fatalf("expected normalized kanban view, got %q", body) + } + if strings.Contains(body, `value="bad" selected`) { + t.Fatalf("expected invalid status filter to be dropped, got %q", body) + } +} +``` + +- [ ] **Step 2: Run the focused tests to verify they fail** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: FAIL because the current `/tasks` page has no query-state model, no `data-current-view`, and no normalized view handling. + +- [ ] **Step 3: Add the dashboard page-state types** + +Extend `go-backend/internal/tasks/model.go` with a dedicated page-state model: + +```go +type DashboardView string + +const ( + DashboardViewKanban DashboardView = "kanban" + DashboardViewList DashboardView = "list" + DashboardViewRoadmap DashboardView = "roadmap" +) + +type RoadmapMode string + +const ( + RoadmapModeWeek RoadmapMode = "week" + RoadmapModeMonth RoadmapMode = "month" +) + +type DashboardPageState struct { + View DashboardView + RoadmapMode RoadmapMode + TabloIDs []uuid.UUID + AssigneeIDs []uuid.UUID + Statuses []Status +} +``` + +Add parsing helpers with explicit normalization: + +```go +func NormalizeDashboardView(raw string) DashboardView { + switch DashboardView(raw) { + case DashboardViewList: + return DashboardViewList + case DashboardViewRoadmap: + return DashboardViewRoadmap + default: + return DashboardViewKanban + } +} + +func NormalizeRoadmapMode(raw string) RoadmapMode { + if RoadmapMode(raw) == RoadmapModeMonth { + return RoadmapModeMonth + } + return RoadmapModeWeek +} +``` + +- [ ] **Step 4: Parse query state in the handler** + +In `go-backend/internal/web/handlers/tasks.go`, add a helper used by `renderTasksPage`: + +```go +func parseDashboardPageState(r *http.Request) taskmodel.DashboardPageState { + query := r.URL.Query() + state := taskmodel.DashboardPageState{ + View: taskmodel.NormalizeDashboardView(query.Get("view")), + RoadmapMode: taskmodel.NormalizeRoadmapMode(query.Get("roadmap_mode")), + TabloIDs: parseUUIDList(query["tablo"]), + AssigneeIDs: parseUUIDList(query["assignee"]), + Statuses: parseStatusList(query["status"]), + } + if state.View != taskmodel.DashboardViewRoadmap { + state.RoadmapMode = taskmodel.RoadmapModeWeek + } + return state +} +``` + +Add the parsing helpers in the same file: + +```go +func parseUUIDList(values []string) []uuid.UUID { + parsed := make([]uuid.UUID, 0, len(values)) + for _, value := range values { + id, err := uuid.Parse(strings.TrimSpace(value)) + if err == nil { + parsed = append(parsed, id) + } + } + return parsed +} + +func parseStatusList(values []string) []taskmodel.Status { + parsed := make([]taskmodel.Status, 0, len(values)) + for _, value := range values { + status, err := taskmodel.ParseStatus(strings.TrimSpace(value)) + if err == nil { + parsed = append(parsed, status) + } + } + return parsed +} +``` + +- [ ] **Step 5: Re-run the focused tests to verify the parsing and normalization contract** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: still FAIL, but now only on missing page rendering markers and missing multi-view content. Query parsing should compile cleanly. + +- [ ] **Step 6: Commit** + +```bash +git add go-backend/internal/tasks/model.go go-backend/internal/web/handlers/tasks.go go-backend/internal/web/handlers/tasks_test.go +git commit -m "feat: add tasks dashboard query state parsing" +``` + +## Task 2: Build Shared Dashboard Dataset And Grouping Logic + +**Files:** +- Modify: `go-backend/internal/tasks/model.go` +- Modify: `go-backend/internal/web/views/tasks_view.go` +- Modify: `go-backend/internal/web/handlers/tasks_test.go` + +- [ ] **Step 1: Write the failing view-model tests for cross-tablo grouping** + +Add tests to `go-backend/internal/web/handlers/tasks_test.go` or a new focused view-model test section in the same file: + +```go +func TestTasksDashboardListGroupsByStatusAcrossTablos(t *testing.T) { + now := time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC) + tabloA := tablomodel.Record{ID: uuid.New(), Name: "Alpha", Color: "#111111"} + tabloB := tablomodel.Record{ID: uuid.New(), Name: "Beta", Color: "#222222"} + + records := []taskmodel.Record{ + {ID: uuid.New(), TabloID: tabloA.ID, Title: "Write copy", Status: taskmodel.StatusTodo, CreatedAt: now}, + {ID: uuid.New(), TabloID: tabloB.ID, Title: "Ship docs", Status: taskmodel.StatusTodo, CreatedAt: now.Add(time.Minute)}, + {ID: uuid.New(), TabloID: tabloB.ID, Title: "Review launch", Status: taskmodel.StatusInReview, CreatedAt: now.Add(2 * time.Minute)}, + } + + vm := views.NewTasksDashboardPageViewModel( + []tablomodel.Record{tabloA, tabloB}, + records, + nil, + taskmodel.DashboardPageState{View: taskmodel.DashboardViewList, RoadmapMode: taskmodel.RoadmapModeWeek}, + now, + ) + + if len(vm.List.Groups) != 4 { + t.Fatalf("expected four status groups, got %d", len(vm.List.Groups)) + } + if got := len(vm.List.Groups[0].Tasks); got != 2 { + t.Fatalf("expected two todo tasks across tablos, got %d", got) + } +} +``` + +Add a roadmap test: + +```go +func TestTasksDashboardRoadmapCreatesPerTabloSansEtapeLanes(t *testing.T) { + now := time.Date(2026, 5, 10, 12, 0, 0, 0, time.UTC) + tabloA := tablomodel.Record{ID: uuid.New(), Name: "Alpha"} + tabloB := tablomodel.Record{ID: uuid.New(), Name: "Beta"} + + due := now.AddDate(0, 0, 3) + records := []taskmodel.Record{ + {ID: uuid.New(), TabloID: tabloA.ID, Title: "A task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now}, + {ID: uuid.New(), TabloID: tabloB.ID, Title: "B task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now}, + } + + vm := views.NewTasksDashboardPageViewModel( + []tablomodel.Record{tabloA, tabloB}, + records, + nil, + taskmodel.DashboardPageState{View: taskmodel.DashboardViewRoadmap, RoadmapMode: taskmodel.RoadmapModeWeek}, + now, + ) + + if len(vm.Roadmap.Lanes) != 2 { + t.Fatalf("expected one Sans étape lane per tablo, got %d", len(vm.Roadmap.Lanes)) + } +} +``` + +- [ ] **Step 2: Run the focused tests to verify they fail** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: FAIL because the current view model has no `List` or `Roadmap` structures and still groups by tablo sections. + +- [ ] **Step 3: Replace the old simple page model with dashboard view models** + +Reshape `go-backend/internal/web/views/tasks_view.go` around a new top-level model: + +```go +type TasksDashboardPageViewModel struct { + State taskmodel.DashboardPageState + Filters TasksDashboardFiltersView + Kanban TasksKanbanView + List TasksListView + Roadmap TasksRoadmapView + Form TasksFormOptionsView + HasTasks bool +} +``` + +Add focused sub-models: + +```go +type TasksKanbanView struct { + Columns []TasksKanbanColumnView +} + +type TasksListView struct { + Groups []TasksStatusGroupView +} + +type TasksRoadmapView struct { + Mode string + Buckets []TasksRoadmapBucketView + Lanes []TasksRoadmapLaneView +} +``` + +- [ ] **Step 4: Add pure grouping builders for kanban, list, and roadmap** + +Implement these builders in `go-backend/internal/web/views/tasks_view.go`: + +```go +func buildKanbanView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, assigneeLabels map[uuid.UUID]string) TasksKanbanView +func buildListView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, assigneeLabels map[uuid.UUID]string) TasksListView +func buildRoadmapView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, assigneeLabels map[uuid.UUID]string, mode taskmodel.RoadmapMode, now time.Time) TasksRoadmapView +``` + +Use deterministic ordering: + +```go +slices.SortFunc(records, func(a, b taskmodel.Record) int { + if diff := strings.Compare(string(a.Status), string(b.Status)); diff != 0 { + return diff + } + return a.CreatedAt.Compare(b.CreatedAt) +}) +``` + +For roadmap lanes, use stable lane IDs: + +```go +func roadmapLaneID(tabloID uuid.UUID, parentTaskID *uuid.UUID) string { + if parentTaskID == nil { + return tabloID.String() + ":sans-etape" + } + return tabloID.String() + ":" + parentTaskID.String() +} +``` + +- [ ] **Step 5: Re-run the focused tests to verify grouping passes** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: PASS for the new grouping tests, while handler rendering tests may still fail until the page shell is updated. + +- [ ] **Step 6: Commit** + +```bash +git add go-backend/internal/tasks/model.go go-backend/internal/web/views/tasks_view.go go-backend/internal/web/handlers/tasks_test.go +git commit -m "feat: add tasks dashboard grouping models" +``` + +## Task 3: Extend Create/Edit Flow With Tablo-Aware Options And Validation + +**Files:** +- Modify: `go-backend/internal/web/handlers/tasks.go` +- Modify: `go-backend/internal/web/handlers/in_memory_auth_repository.go` +- Modify: `go-backend/internal/web/handlers/tasks_test.go` + +- [ ] **Step 1: Write the failing mutation tests for cross-tablo validation** + +Add tests to `go-backend/internal/web/handlers/tasks_test.go`: + +```go +func TestPatchTaskRejectsParentEtapeFromAnotherTablo(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(sessionCookie) + userID, ok := handler.currentUserID(req.Context(), req) + if !ok { + t.Fatal("expected authenticated user") + } + + tabloA := mustCreateOwnedTablo(t, repo, userID) + tabloB := mustCreateOwnedTablo(t, repo, userID) + task := mustCreateTask(t, repo, userID, tabloA.ID, nil, "Alpha task") + foreignEtape := mustCreateEtape(t, repo, userID, tabloB.ID, "Beta stage") + + form := url.Values{} + form.Set("tablo_id", tabloA.ID.String()) + form.Set("title", task.Title) + form.Set("status", string(taskmodel.StatusTodo)) + form.Set("parent_task_id", foreignEtape.ID.String()) + + patchReq := httptest.NewRequest(http.MethodPatch, "/tasks/"+task.ID.String(), strings.NewReader(form.Encode())) + patchReq.Header.Set("Content-Type", "application/x-www-form-urlencoded") + patchReq.SetPathValue("taskID", task.ID.String()) + patchReq.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.PatchTask().ServeHTTP(rec, patchReq) + + if rec.Code != http.StatusUnprocessableEntity { + t.Fatalf("expected 422 for cross-tablo parent, got %d", rec.Code) + } +} +``` + +Add a create-form scoping test: + +```go +func TestGetEditTaskModalShowsOnlyEtapesFromSelectedTablo(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(sessionCookie) + userID, ok := handler.currentUserID(req.Context(), req) + if !ok { + t.Fatal("expected authenticated user") + } + + tabloA := mustCreateOwnedTablo(t, repo, userID) + tabloB := mustCreateOwnedTablo(t, repo, userID) + task := mustCreateTask(t, repo, userID, tabloA.ID, nil, "Editable") + etapeA := mustCreateEtape(t, repo, userID, tabloA.ID, "Stage A") + _ = mustCreateEtape(t, repo, userID, tabloB.ID, "Stage B") + + editReq := httptest.NewRequest(http.MethodGet, "/tasks/"+task.ID.String()+"/edit", nil) + editReq.SetPathValue("taskID", task.ID.String()) + editReq.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetEditTaskModal().ServeHTTP(rec, editReq) + + body := rec.Body.String() + if !strings.Contains(body, etapeA.ID.String()) { + t.Fatalf("expected same-tablo etape in edit form, got %q", body) + } + if strings.Contains(body, "Stage B") { + t.Fatalf("did not expect foreign tablo etape in edit form, got %q", body) + } +} +``` + +- [ ] **Step 2: Run the focused tests to verify they fail** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: FAIL because the current create/edit flow neither requires `tablo_id` on update nor renders tablo-scoped étape options. + +- [ ] **Step 3: Require `tablo_id` and load owner-scoped option data for forms** + +Update `go-backend/internal/web/handlers/tasks.go` so create and edit forms both build a consistent options model: + +```go +type taskFormOptions struct { + Tablos []tablomodel.Record + EtapesByTablo map[uuid.UUID][]taskmodel.Record + Assignees map[uuid.UUID]string +} + +func (h *AuthHandler) loadTaskFormOptions(ctx context.Context, ownerID uuid.UUID) (taskFormOptions, error) { + tablos, err := h.repo.ListTablos(ctx, ListTablosInput{OwnerID: ownerID}) + if err != nil { + return taskFormOptions{}, err + } + records, err := h.repo.(taskPageRepository).ListTasksByOwner(ctx, ownerID) + if err != nil { + return taskFormOptions{}, err + } + return buildTaskFormOptions(tablos, records, h.repo), nil +} +``` + +Make `parseUpdateTaskInput` require `tablo_id`: + +```go +tabloID, err := uuid.Parse(strings.TrimSpace(r.FormValue("tablo_id"))) +if err != nil { + return UpdateTaskInput{}, errors.New("tablo_id is required") +} +``` + +- [ ] **Step 4: Enforce selected tablo and selected étape consistency** + +Add a dedicated validator in `go-backend/internal/web/handlers/tasks.go`: + +```go +func validateTaskTabloAndParent(records []TaskRecord, tabloID uuid.UUID, parentTaskID *uuid.UUID) error { + if parentTaskID == nil { + return nil + } + for _, record := range records { + if record.ID != *parentTaskID { + continue + } + if !record.IsEtape { + return errors.New("parent_task_id must reference an étape") + } + if record.TabloID != tabloID { + return errors.New("parent_task_id must belong to the selected tablo") + } + return nil + } + return errors.New("parent_task_id must reference an active étape") +} +``` + +Call this from both create and patch handlers after parsing input and before repository mutation. + +- [ ] **Step 5: Re-run the focused tests to verify the mutation flow passes** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: PASS for the new cross-tablo validation tests and edit-form scoping test. + +- [ ] **Step 6: Commit** + +```bash +git add go-backend/internal/web/handlers/tasks.go go-backend/internal/web/handlers/in_memory_auth_repository.go go-backend/internal/web/handlers/tasks_test.go +git commit -m "feat: add tablo-aware task form validation" +``` + +## Task 4: Replace The `/tasks` Page Rendering With Multi-View Dashboard UI + +**Files:** +- Modify: `go-backend/internal/web/views/tasks_view.go` +- Modify: `go-backend/internal/web/handlers/tasks.go` +- Modify: `go-backend/internal/web/handlers/tasks_test.go` + +- [ ] **Step 1: Write the failing rendering tests for view-specific content** + +Add tests to `go-backend/internal/web/handlers/tasks_test.go`: + +```go +func TestGetTasksPageRendersListViewAcrossTablos(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(sessionCookie) + userID, ok := handler.currentUserID(req.Context(), req) + if !ok { + t.Fatal("expected authenticated user") + } + + tabloA := mustCreateOwnedTablo(t, repo, userID) + tabloB := mustCreateOwnedTablo(t, repo, userID) + _ = mustCreateTask(t, repo, userID, tabloA.ID, nil, "Alpha task") + _ = mustCreateTask(t, repo, userID, tabloB.ID, nil, "Beta task") + + pageReq := httptest.NewRequest(http.MethodGet, "/tasks?view=list", nil) + pageReq.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetTasksPage().ServeHTTP(rec, pageReq) + + body := rec.Body.String() + for _, want := range []string{"Alpha task", "Beta task", "Liste", "À faire"} { + if !strings.Contains(body, want) { + t.Fatalf("expected body to contain %q, got %q", want, body) + } + } +} + +func TestGetTasksPageRendersRoadmapWeekMode(t *testing.T) { + repo := NewInMemoryAuthRepository() + handler := NewAuthHandler(repo) + sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo") + + req := httptest.NewRequest(http.MethodGet, "/", nil) + req.AddCookie(sessionCookie) + userID, ok := handler.currentUserID(req.Context(), req) + if !ok { + t.Fatal("expected authenticated user") + } + + tablo := mustCreateOwnedTablo(t, repo, userID) + etape := mustCreateEtape(t, repo, userID, tablo.ID, "Préparation") + task := mustCreateTask(t, repo, userID, tablo.ID, &etape.ID, "Planifier") + due := time.Date(2026, 5, 12, 0, 0, 0, 0, time.UTC) + _, _ = repo.UpdateTask(context.Background(), UpdateTaskInput{ + ID: task.ID, OwnerID: userID, TabloID: tablo.ID, Title: task.Title, Description: task.Description, + Status: task.Status, DueDate: &due, ParentTaskID: &etape.ID, + }) + + pageReq := httptest.NewRequest(http.MethodGet, "/tasks?view=roadmap&roadmap_mode=week", nil) + pageReq.AddCookie(sessionCookie) + rec := httptest.NewRecorder() + + handler.GetTasksPage().ServeHTTP(rec, pageReq) + + body := rec.Body.String() + for _, want := range []string{"Roadmap", "Préparation", "Planifier", "Semaine"} { + if !strings.Contains(body, want) { + t.Fatalf("expected body to contain %q, got %q", want, body) + } + } +} +``` + +- [ ] **Step 2: Run the focused tests to verify they fail** + +Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|Dashboard'` + +Expected: FAIL because the current page still renders simple tablo sections and has no list/roadmap UI. + +- [ ] **Step 3: Build a shared dashboard page shell** + +Replace `TasksPageContent` in `go-backend/internal/web/views/tasks_view.go` with a richer shell: + +```go +func TasksPageContent(vm TasksDashboardPageViewModel) templ.Component { + return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error { + _, _ = io.WriteString(w, `