go-htmx-gsd #1

Merged
arthur merged 558 commits from go-htmx-gsd into main 2026-05-23 15:16:44 +00:00
Showing only changes of commit faf3199b71 - Show all commits

View file

@ -2,7 +2,7 @@
> **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.
**Goal:** Replace the simple `go-backend` `/tasks` CRUD page with a server-rendered cross-tablo tasks page 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.
@ -15,7 +15,7 @@
**Existing files to modify**
- `go-backend/internal/tasks/model.go`
- Add owner-level dashboard page-state types, filter types, and shared dashboard record shapes.
- Add owner-level task page-state types, filter types, and shared task page 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`
@ -118,24 +118,24 @@ Expected: FAIL because the current `/tasks` page has no query-state model, no `d
Extend `go-backend/internal/tasks/model.go` with a dedicated page-state model:
```go
type DashboardView string
type TaskView string
const (
DashboardViewKanban DashboardView = "kanban"
DashboardViewList DashboardView = "list"
DashboardViewRoadmap DashboardView = "roadmap"
TaskViewKanban TaskView = "kanban"
TaskViewList TaskView = "list"
TaskViewRoadmap TaskView = "roadmap"
)
type RoadmapMode string
type TaskRoadmapMode string
const (
RoadmapModeWeek RoadmapMode = "week"
RoadmapModeMonth RoadmapMode = "month"
TaskRoadmapModeWeek TaskRoadmapMode = "week"
TaskRoadmapModeMonth TaskRoadmapMode = "month"
)
type DashboardPageState struct {
View DashboardView
RoadmapMode RoadmapMode
type TaskPageState struct {
View TaskView
RoadmapMode TaskRoadmapMode
TabloIDs []uuid.UUID
AssigneeIDs []uuid.UUID
Statuses []Status
@ -145,22 +145,22 @@ type DashboardPageState struct {
Add parsing helpers with explicit normalization:
```go
func NormalizeDashboardView(raw string) DashboardView {
switch DashboardView(raw) {
case DashboardViewList:
return DashboardViewList
case DashboardViewRoadmap:
return DashboardViewRoadmap
func NormalizeTaskView(raw string) TaskView {
switch TaskView(raw) {
case TaskViewList:
return TaskViewList
case TaskViewRoadmap:
return TaskViewRoadmap
default:
return DashboardViewKanban
return TaskViewKanban
}
}
func NormalizeRoadmapMode(raw string) RoadmapMode {
if RoadmapMode(raw) == RoadmapModeMonth {
return RoadmapModeMonth
func NormalizeTaskRoadmapMode(raw string) TaskRoadmapMode {
if TaskRoadmapMode(raw) == TaskRoadmapModeMonth {
return TaskRoadmapModeMonth
}
return RoadmapModeWeek
return TaskRoadmapModeWeek
}
```
@ -169,17 +169,17 @@ func NormalizeRoadmapMode(raw string) RoadmapMode {
In `go-backend/internal/web/handlers/tasks.go`, add a helper used by `renderTasksPage`:
```go
func parseDashboardPageState(r *http.Request) taskmodel.DashboardPageState {
func parseTaskPageState(r *http.Request) taskmodel.TaskPageState {
query := r.URL.Query()
state := taskmodel.DashboardPageState{
View: taskmodel.NormalizeDashboardView(query.Get("view")),
RoadmapMode: taskmodel.NormalizeRoadmapMode(query.Get("roadmap_mode")),
state := taskmodel.TaskPageState{
View: taskmodel.NormalizeTaskView(query.Get("view")),
RoadmapMode: taskmodel.NormalizeTaskRoadmapMode(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
if state.View != taskmodel.TaskViewRoadmap {
state.RoadmapMode = taskmodel.TaskRoadmapModeWeek
}
return state
}
@ -224,7 +224,7 @@ git add go-backend/internal/tasks/model.go go-backend/internal/web/handlers/task
git commit -m "feat: add tasks dashboard query state parsing"
```
## Task 2: Build Shared Dashboard Dataset And Grouping Logic
## Task 2: Build Shared Task Dataset And Grouping Logic
**Files:**
- Modify: `go-backend/internal/tasks/model.go`
@ -236,7 +236,7 @@ git commit -m "feat: add tasks dashboard query state parsing"
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) {
func TestTasksListGroupsByStatusAcrossTablos(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"}
@ -247,11 +247,11 @@ func TestTasksDashboardListGroupsByStatusAcrossTablos(t *testing.T) {
{ID: uuid.New(), TabloID: tabloB.ID, Title: "Review launch", Status: taskmodel.StatusInReview, CreatedAt: now.Add(2 * time.Minute)},
}
vm := views.NewTasksDashboardPageViewModel(
vm := views.NewTasksPageViewModel(
[]tablomodel.Record{tabloA, tabloB},
records,
nil,
taskmodel.DashboardPageState{View: taskmodel.DashboardViewList, RoadmapMode: taskmodel.RoadmapModeWeek},
taskmodel.TaskPageState{View: taskmodel.TaskViewList, RoadmapMode: taskmodel.TaskRoadmapModeWeek},
now,
)
@ -267,7 +267,7 @@ func TestTasksDashboardListGroupsByStatusAcrossTablos(t *testing.T) {
Add a roadmap test:
```go
func TestTasksDashboardRoadmapCreatesPerTabloSansEtapeLanes(t *testing.T) {
func TestTasksRoadmapCreatesPerTabloSansEtapeLanes(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"}
@ -278,11 +278,11 @@ func TestTasksDashboardRoadmapCreatesPerTabloSansEtapeLanes(t *testing.T) {
{ID: uuid.New(), TabloID: tabloB.ID, Title: "B task", Status: taskmodel.StatusTodo, DueDate: &due, CreatedAt: now},
}
vm := views.NewTasksDashboardPageViewModel(
vm := views.NewTasksPageViewModel(
[]tablomodel.Record{tabloA, tabloB},
records,
nil,
taskmodel.DashboardPageState{View: taskmodel.DashboardViewRoadmap, RoadmapMode: taskmodel.RoadmapModeWeek},
taskmodel.TaskPageState{View: taskmodel.TaskViewRoadmap, RoadmapMode: taskmodel.TaskRoadmapModeWeek},
now,
)
@ -303,9 +303,9 @@ Expected: FAIL because the current view model has no `List` or `Roadmap` structu
Reshape `go-backend/internal/web/views/tasks_view.go` around a new top-level model:
```go
type TasksDashboardPageViewModel struct {
State taskmodel.DashboardPageState
Filters TasksDashboardFiltersView
type TasksPageViewModel struct {
State taskmodel.TaskPageState
Filters TasksFiltersView
Kanban TasksKanbanView
List TasksListView
Roadmap TasksRoadmapView
@ -339,7 +339,7 @@ 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
func buildRoadmapView(records []taskmodel.Record, tabloByID map[uuid.UUID]tablomodel.Record, assigneeLabels map[uuid.UUID]string, mode taskmodel.TaskRoadmapMode, now time.Time) TasksRoadmapView
```
Use deterministic ordering:
@ -543,7 +543,7 @@ git add go-backend/internal/web/handlers/tasks.go go-backend/internal/web/handle
git commit -m "feat: add tablo-aware task form validation"
```
## Task 4: Replace The `/tasks` Page Rendering With Multi-View Dashboard UI
## Task 4: Replace The `/tasks` Page Rendering With Multi-View Task UI
**Files:**
- Modify: `go-backend/internal/web/views/tasks_view.go`
@ -633,7 +633,7 @@ Expected: FAIL because the current page still renders simple tablo sections and
Replace `TasksPageContent` in `go-backend/internal/web/views/tasks_view.go` with a richer shell:
```go
func TasksPageContent(vm TasksDashboardPageViewModel) templ.Component {
func TasksPageContent(vm TasksPageViewModel) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, _ = io.WriteString(w, `<div class="app-section-page tasks-page" data-current-view="`+html.EscapeString(string(vm.State.View))+`">`)
_, _ = io.WriteString(w, `<div class="app-section-surface"><div class="tasks-page-header">`)
@ -651,9 +651,9 @@ func TasksPageContent(vm TasksDashboardPageViewModel) templ.Component {
Add explicit renderers:
```go
func renderCurrentTasksView(w io.Writer, vm TasksDashboardPageViewModel) error
func renderTasksViewTabs(state taskmodel.DashboardPageState) string
func renderTasksFilters(filters TasksDashboardFiltersView, state taskmodel.DashboardPageState) string
func renderCurrentTasksView(w io.Writer, vm TasksPageViewModel) error
func renderTasksViewTabs(state taskmodel.TaskPageState) string
func renderTasksFilters(filters TasksFiltersView, state taskmodel.TaskPageState) string
```
- [ ] **Step 4: Implement the view-specific renderers**
@ -734,7 +734,7 @@ If your existing router tests already log in first, use that pattern instead and
Add a mutation-state preservation test in handler tests:
```go
func TestPatchTaskRendersCurrentDashboardQueryState(t *testing.T) {
func TestPatchTaskRendersCurrentTaskQueryState(t *testing.T) {
repo := NewInMemoryAuthRepository()
handler := NewAuthHandler(repo)
sessionCookie := loginTestUser(t, handler, "demo@xtablo.com", "xtablo-demo")
@ -781,9 +781,9 @@ Make `renderTasksPage` fully query-driven so post-mutation handlers reuse the re
```go
func (h *AuthHandler) renderTasksPage(w http.ResponseWriter, r *http.Request) {
state := parseDashboardPageState(r)
state := parseTaskPageState(r)
// load tablos, tasks, assignees
vm := views.NewTasksDashboardPageViewModel(tablos, filteredTasks, assigneeLabels, state, time.Now().UTC())
vm := views.NewTasksPageViewModel(tablos, filteredTasks, assigneeLabels, state, time.Now().UTC())
// existing DashboardPage / DashboardContentSwap render path
}
```
@ -827,6 +827,6 @@ git commit -m "test: verify tasks dashboard routing and state preservation"
**Type consistency**
- View-state types use `DashboardView`, `RoadmapMode`, and `DashboardPageState` consistently.
- View-state types use `TaskView`, `TaskRoadmapMode`, and `TaskPageState` consistently.
- Renderer naming is consistent across `buildKanbanView`, `buildListView`, `buildRoadmapView`, and `renderKanbanView`, `renderListView`, `renderRoadmapView`.
- Mutation validation consistently uses `tablo_id` and `parent_task_id`.