Update go-backend tasks docs for assignee support
This commit is contained in:
parent
ef7ccd8c6f
commit
dd6e5b7d64
2 changed files with 715 additions and 4 deletions
701
docs/superpowers/plans/2026-05-10-go-backend-tasks-etapes.md
Normal file
701
docs/superpowers/plans/2026-05-10-go-backend-tasks-etapes.md
Normal file
|
|
@ -0,0 +1,701 @@
|
||||||
|
# Go Backend Tasks And Etapes Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Build the first real `/tasks` vertical slice in `go-backend` with SQL-backed owner-only CRUD for tasks and etapes, optional `assignee_id`, runtime "Sans etape" grouping, and HTMX-driven server-rendered page flows.
|
||||||
|
|
||||||
|
**Architecture:** Extend the existing Go app by adding a small `tasks` domain, a self-referential `public.tasks` table with optional `assignee_id`, sqlc-backed repository methods, owner-scoped handlers, and `templ` views for grouped task rendering. Keep all interactions server-rendered, with HTMX only refreshing the `/tasks` content or modal fragments, and keep "Sans etape" as a pure view concern derived from regular tasks with `parent_task_id = null`.
|
||||||
|
|
||||||
|
**Tech Stack:** Go, chi, templ, HTMX, PostgreSQL, pgx, sqlc, Go standard `net/http` testing
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Existing files to modify**
|
||||||
|
|
||||||
|
- `go-backend/internal/db/schema.sql`
|
||||||
|
- Add the `public.tasks` table, optional `assignee_id`, indexes, `CHECK` constraints, and trigger/function for parent validation.
|
||||||
|
- `go-backend/internal/db/queries.sql`
|
||||||
|
- Add sqlc queries for owner-scoped create, list, fetch, update, assignee persistence, child-clearing, and soft delete.
|
||||||
|
- `go-backend/internal/db/repository.go`
|
||||||
|
- Add Postgres-backed task methods and the transaction for deleting an etape and clearing child `parent_task_id`.
|
||||||
|
- `go-backend/internal/web/handlers/auth.go`
|
||||||
|
- Extend the repository interface with task methods and delegate `GetTasksPage()` to real task rendering.
|
||||||
|
- `go-backend/internal/web/handlers/in_memory_auth_repository.go`
|
||||||
|
- Add in-memory task storage and behavior for tests.
|
||||||
|
- `go-backend/internal/web/views/home.go`
|
||||||
|
- Remove or stop depending on hard-coded task placeholder data if `/tasks` and overview rendering share helpers.
|
||||||
|
- `go-backend/router.go`
|
||||||
|
- Register task mutation and fragment routes.
|
||||||
|
- `go-backend/router_test.go`
|
||||||
|
- Add full-router coverage for `/tasks` page and mutation flows where end-to-end routing matters.
|
||||||
|
|
||||||
|
**New files to create**
|
||||||
|
|
||||||
|
- `go-backend/internal/tasks/model.go`
|
||||||
|
- Task and etape domain types, status constants, validation helpers, and list/update inputs, including optional assignee support.
|
||||||
|
- `go-backend/internal/web/handlers/tasks.go`
|
||||||
|
- Query-state parsing, form parsing, owner checks, list rendering, and mutation handlers.
|
||||||
|
- `go-backend/internal/web/handlers/tasks_test.go`
|
||||||
|
- Focused handler and in-memory repository tests for tasks and etapes.
|
||||||
|
- `go-backend/internal/web/views/tasks.templ`
|
||||||
|
- `/tasks` page content, grouped sections, task rows, etape sections, assignee display, and modal/form fragments.
|
||||||
|
- `go-backend/internal/web/views/tasks_view.go`
|
||||||
|
- View models and grouping helpers, including runtime "Sans etape" construction.
|
||||||
|
|
||||||
|
**Generated files expected to change**
|
||||||
|
|
||||||
|
- `go-backend/internal/db/sqlc/*.go`
|
||||||
|
- Regenerated by `just generate` after schema/query updates.
|
||||||
|
- `go-backend/internal/web/views/*_templ.go`
|
||||||
|
- Regenerated by `just generate` after `templ` changes.
|
||||||
|
|
||||||
|
**Test and verification commands**
|
||||||
|
|
||||||
|
- `cd go-backend && go test ./internal/web/handlers -run 'Tasks|InMemoryTasks'`
|
||||||
|
- `cd go-backend && go test ./...`
|
||||||
|
- `cd go-backend && just generate`
|
||||||
|
- `cd go-backend && just build`
|
||||||
|
|
||||||
|
## Chunk 1: Task Domain And In-Memory Contract
|
||||||
|
|
||||||
|
### Task 1: Add failing in-memory repository tests for task and etape behavior
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/handlers/tasks_test.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/in_memory_auth_repository.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests for create/list/delete task behavior**
|
||||||
|
|
||||||
|
Add focused tests that define the repository contract before production code exists:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestInMemoryTasksListExcludesSoftDeletedRows(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
user, _ := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
|
||||||
|
etape, err := repo.CreateTask(context.Background(), CreateTaskInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
TabloID: mustCreateOwnedTablo(t, repo, user.ID).ID,
|
||||||
|
Title: "Etape 1",
|
||||||
|
IsEtape: true,
|
||||||
|
Status: tasks.StatusTodo,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
task, err := repo.CreateTask(context.Background(), CreateTaskInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
TabloID: etape.TabloID,
|
||||||
|
Title: "Task 1",
|
||||||
|
Status: tasks.StatusTodo,
|
||||||
|
ParentTaskID: &etape.ID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := repo.SoftDeleteTask(context.Background(), task.ID, user.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := repo.ListTasksByTablo(context.Background(), ListTasksByTabloInput{
|
||||||
|
OwnerID: user.ID,
|
||||||
|
TabloID: etape.TabloID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(records) != 1 || records[0].ID != etape.ID {
|
||||||
|
t.Fatalf("expected only etape to remain visible, got %#v", records)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write failing tests for etape-specific invariants**
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
|
||||||
|
- etape cannot have a parent
|
||||||
|
- parent must be an etape
|
||||||
|
- assignee persists when set and clears to null when unset
|
||||||
|
- deleting an etape clears active child `parent_task_id`
|
||||||
|
- owner scoping rejects another user
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestInMemoryDeleteEtapeClearsChildParentID(t *testing.T) {
|
||||||
|
repo := NewInMemoryAuthRepository()
|
||||||
|
user, _ := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
|
||||||
|
tablo := mustCreateOwnedTablo(t, repo, user.ID)
|
||||||
|
etape := mustCreateEtape(t, repo, user.ID, tablo.ID, "Launch")
|
||||||
|
child := mustCreateTask(t, repo, user.ID, tablo.ID, etape.ID, "Ship copy")
|
||||||
|
|
||||||
|
if err := repo.SoftDeleteTask(context.Background(), etape.ID, user.ID); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
updated, err := repo.GetTaskByID(context.Background(), child.ID, user.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if updated.ParentTaskID != nil {
|
||||||
|
t.Fatalf("expected child task to move to Sans etape, got parent %v", *updated.ParentTaskID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|InMemoryTasks'`
|
||||||
|
|
||||||
|
Expected: FAIL with missing task types or missing repository methods.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the minimal task domain types and in-memory storage**
|
||||||
|
|
||||||
|
Create `internal/tasks/model.go` with:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package tasks
|
||||||
|
|
||||||
|
type Status string
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusTodo Status = "todo"
|
||||||
|
StatusInProgress Status = "in_progress"
|
||||||
|
StatusInReview Status = "in_review"
|
||||||
|
StatusDone Status = "done"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Add repository-facing types used by handlers:
|
||||||
|
|
||||||
|
- `TaskRecord`
|
||||||
|
- `CreateTaskInput`
|
||||||
|
- `UpdateTaskInput`
|
||||||
|
- `ListTasksByTabloInput`
|
||||||
|
- validation helpers such as `ParseStatus`
|
||||||
|
- optional `AssigneeID *uuid.UUID` on the task record and mutation inputs
|
||||||
|
|
||||||
|
Extend `InMemoryAuthRepository` with:
|
||||||
|
|
||||||
|
- `tasks map[uuid.UUID]TaskRecord`
|
||||||
|
- `CreateTask`
|
||||||
|
- `ListTasksByTablo`
|
||||||
|
- `GetTaskByID`
|
||||||
|
- `UpdateTask`
|
||||||
|
- `SoftDeleteTask`
|
||||||
|
|
||||||
|
- [ ] **Step 5: Re-run the focused tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|InMemoryTasks'`
|
||||||
|
|
||||||
|
Expected: PASS for the new in-memory task and etape behavior tests.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/tasks/model.go go-backend/internal/web/handlers/in_memory_auth_repository.go go-backend/internal/web/handlers/tasks_test.go
|
||||||
|
git commit -m "feat: add in-memory tasks and etapes support"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 2: SQL Schema, sqlc, And Postgres Repository
|
||||||
|
|
||||||
|
### Task 2: Add the SQL schema for tasks and etapes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/db/schema.sql`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing repository-shaped test comment block or focused TODO test to lock the SQL contract**
|
||||||
|
|
||||||
|
If there is still no dedicated DB integration harness, add a small repository contract test stub in `tasks_test.go` that documents the needed shape:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestTaskRepositoryContractDocumentsOwnerScopedCRUD(t *testing.T) {
|
||||||
|
t.Skip("Enable once Postgres-backed repository tests exist for go-backend tasks")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The point is to define the target before editing SQL.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to keep the failure visible**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|InMemoryTasks'`
|
||||||
|
|
||||||
|
Expected: existing tests still pass, and the repository contract work remains unimplemented in Postgres.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the `public.tasks` table, indexes, and constraints**
|
||||||
|
|
||||||
|
Update `schema.sql` with the minimal approved schema:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS public.tasks (
|
||||||
|
id uuid PRIMARY KEY,
|
||||||
|
tablo_id uuid NOT NULL REFERENCES public.tablos(id) ON DELETE CASCADE,
|
||||||
|
owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
|
||||||
|
title text NOT NULL,
|
||||||
|
description text NOT NULL DEFAULT '',
|
||||||
|
status text NOT NULL CHECK (status IN ('todo', 'in_progress', 'in_review', 'done')),
|
||||||
|
assignee_id uuid NULL REFERENCES public.users(id) ON DELETE SET NULL,
|
||||||
|
is_etape boolean NOT NULL DEFAULT false,
|
||||||
|
parent_task_id uuid NULL REFERENCES public.tasks(id) ON DELETE SET NULL,
|
||||||
|
due_date date NULL,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
deleted_at timestamptz NULL,
|
||||||
|
CHECK (NOT is_etape OR parent_task_id IS NULL)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
Add indexes for:
|
||||||
|
|
||||||
|
- `(owner_id, tablo_id, deleted_at)`
|
||||||
|
- `(assignee_id, deleted_at)`
|
||||||
|
- `(tablo_id, is_etape, deleted_at)`
|
||||||
|
- `(parent_task_id)`
|
||||||
|
- `(tablo_id, due_date)`
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the parent validation function and trigger**
|
||||||
|
|
||||||
|
Implement a trigger function in `schema.sql` that rejects:
|
||||||
|
|
||||||
|
- etape rows with a parent
|
||||||
|
- parent rows outside the same owner
|
||||||
|
- parent rows outside the same tablo
|
||||||
|
- parent rows that are not etapes
|
||||||
|
- parent rows that are soft-deleted
|
||||||
|
|
||||||
|
- [ ] **Step 5: Regenerate sqlc once the schema compiles**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate`
|
||||||
|
|
||||||
|
Expected: PASS with updated generated database types.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/db/schema.sql go-backend/internal/db/sqlc
|
||||||
|
git commit -m "feat: add tasks schema and integrity rules"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 3: Add sqlc queries and Postgres repository methods
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/db/queries.sql`
|
||||||
|
- Modify: `go-backend/internal/db/repository.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/auth.go`
|
||||||
|
- Generated: `go-backend/internal/db/sqlc/*`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the failing task repository method signatures to the shared interface**
|
||||||
|
|
||||||
|
Extend the repository interface in `auth.go` with:
|
||||||
|
|
||||||
|
```go
|
||||||
|
CreateTask(ctx context.Context, input CreateTaskInput) (TaskRecord, error)
|
||||||
|
GetTaskByID(ctx context.Context, taskID uuid.UUID, ownerID uuid.UUID) (TaskRecord, error)
|
||||||
|
ListTasksByTablo(ctx context.Context, input ListTasksByTabloInput) ([]TaskRecord, error)
|
||||||
|
ListTasksByOwner(ctx context.Context, ownerID uuid.UUID) ([]TaskRecord, error)
|
||||||
|
UpdateTask(ctx context.Context, input UpdateTaskInput) (TaskRecord, error)
|
||||||
|
SoftDeleteTask(ctx context.Context, taskID uuid.UUID, ownerID uuid.UUID) error
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify the Postgres implementation is still missing**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|InMemoryTasks'`
|
||||||
|
|
||||||
|
Expected: FAIL or compile errors in the Postgres repository until real methods are added.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add sqlc queries for owner-scoped CRUD**
|
||||||
|
|
||||||
|
Add at minimum:
|
||||||
|
|
||||||
|
- `CreateTask`
|
||||||
|
- `GetTaskByID`
|
||||||
|
- `ListTasksByOwner`
|
||||||
|
- `ListTasksByTablo`
|
||||||
|
- `UpdateTask`
|
||||||
|
- `ClearTaskChildrenParent`
|
||||||
|
- `SoftDeleteTask`
|
||||||
|
|
||||||
|
Example update query shape:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- name: UpdateTask :one
|
||||||
|
UPDATE public.tasks
|
||||||
|
SET
|
||||||
|
title = $3,
|
||||||
|
description = $4,
|
||||||
|
status = $5,
|
||||||
|
due_date = $6,
|
||||||
|
assignee_id = $7,
|
||||||
|
parent_task_id = $8,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
AND owner_id = $2
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
RETURNING *;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Regenerate sqlc**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate`
|
||||||
|
|
||||||
|
Expected: PASS with generated query methods for the new statements.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement the Postgres repository methods**
|
||||||
|
|
||||||
|
Add Go mapping code in `repository.go`:
|
||||||
|
|
||||||
|
- trim titles
|
||||||
|
- default empty description to `""`
|
||||||
|
- map nullable `due_date`, `deleted_at`, `assignee_id`, and `parent_task_id`
|
||||||
|
- wrap etape delete in a transaction:
|
||||||
|
- fetch current row
|
||||||
|
- if etape, clear active child parents
|
||||||
|
- soft-delete the target row
|
||||||
|
|
||||||
|
- [ ] **Step 6: Re-run the focused tests to verify compilation and behavior**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|InMemoryTasks'`
|
||||||
|
|
||||||
|
Expected: PASS with repository interface and implementations in sync.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/db/queries.sql go-backend/internal/db/repository.go go-backend/internal/db/sqlc go-backend/internal/web/handlers/auth.go
|
||||||
|
git commit -m "feat: add sqlc-backed task repository methods"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 3: Task Page Rendering And Grouping
|
||||||
|
|
||||||
|
### Task 4: Replace the placeholder `/tasks` page with real grouped task content
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/views/tasks_view.go`
|
||||||
|
- Create: `go-backend/internal/web/views/tasks.templ`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/auth.go`
|
||||||
|
- Modify: `go-backend/internal/web/views/home.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write a failing handler test for real `/tasks` page rendering**
|
||||||
|
|
||||||
|
Add a test that proves the page is no longer placeholder-only:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestGetTasksPageRendersEtapesAndSansEtapeSections(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, _ := handler.currentUserID(req.Context(), req)
|
||||||
|
tablo := mustCreateOwnedTablo(t, repo, userID)
|
||||||
|
etape := mustCreateEtape(t, repo, userID, tablo.ID, "Production")
|
||||||
|
_ = mustCreateTask(t, repo, userID, tablo.ID, etape.ID, "Cut footage")
|
||||||
|
_ = mustCreateParentlessTask(t, repo, userID, tablo.ID, "Inbox task")
|
||||||
|
|
||||||
|
pageReq := httptest.NewRequest(http.MethodGet, "/tasks", nil)
|
||||||
|
pageReq.AddCookie(sessionCookie)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
handler.GetTasksPage().ServeHTTP(rec, pageReq)
|
||||||
|
|
||||||
|
body := rec.Body.String()
|
||||||
|
for _, want := range []string{"Taches", "Production", "Sans etape", "Inbox task"} {
|
||||||
|
if !strings.Contains(body, want) {
|
||||||
|
t.Fatalf("expected body to contain %q, got %q", want, body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused test to verify it fails**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|GetTasksPage'`
|
||||||
|
|
||||||
|
Expected: FAIL because `/tasks` still renders placeholder content.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build the task page view model and grouping helpers**
|
||||||
|
|
||||||
|
Implement `tasks_view.go` with helpers to:
|
||||||
|
|
||||||
|
- group rows by tablo
|
||||||
|
- split etapes from regular tasks
|
||||||
|
- attach child tasks under their etape
|
||||||
|
- synthesize a "Sans etape" section from parentless regular tasks
|
||||||
|
- surface assignee labels in row view models when present
|
||||||
|
- format due dates and labels for the view
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add the real `templ` page content**
|
||||||
|
|
||||||
|
Implement `tasks.templ` components for:
|
||||||
|
|
||||||
|
- page shell
|
||||||
|
- tablo section
|
||||||
|
- etape section
|
||||||
|
- "Sans etape" section
|
||||||
|
- task row
|
||||||
|
- empty state
|
||||||
|
|
||||||
|
- [ ] **Step 5: Update `GetTasksPage()` to load owner data and render the new view**
|
||||||
|
|
||||||
|
Load:
|
||||||
|
|
||||||
|
- current owner
|
||||||
|
- owner tablos for the sidebar
|
||||||
|
- owner tasks for grouped rendering
|
||||||
|
|
||||||
|
Render the real task content for both full-page and HTMX requests.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Re-run the focused tests to verify the page rendering passes**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'Tasks|GetTasksPage'`
|
||||||
|
|
||||||
|
Expected: PASS with grouped task content visible.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/views/tasks_view.go go-backend/internal/web/views/tasks.templ go-backend/internal/web/handlers/auth.go go-backend/internal/web/views/home.go go-backend/internal/web/views/*_templ.go go-backend/internal/web/handlers/tasks_test.go
|
||||||
|
git commit -m "feat: render grouped tasks page"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 4: Create And Edit Flows
|
||||||
|
|
||||||
|
### Task 5: Add create and edit handlers for tasks and etapes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `go-backend/internal/web/handlers/tasks.go`
|
||||||
|
- Modify: `go-backend/router.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tasks_test.go`
|
||||||
|
- Modify: `go-backend/internal/web/views/tasks.templ`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing handler tests for `POST /tasks` create flows**
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
|
||||||
|
- creating a regular task
|
||||||
|
- creating an etape
|
||||||
|
- rejecting empty title
|
||||||
|
- rejecting a non-etape parent
|
||||||
|
- persisting `assignee_id` when provided
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestPostTasksCreatesEtape(t *testing.T) {
|
||||||
|
// Arrange authenticated owner, tablo, form post with is_etape=true
|
||||||
|
// Assert 200 and page contains the new etape title.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write a failing handler test for `GET /tasks/{taskID}/edit`**
|
||||||
|
|
||||||
|
Assert the returned fragment contains the current title, description, status, parent selector state, and assignee selector state.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'PostTasks|EditTask|Tasks'`
|
||||||
|
|
||||||
|
Expected: FAIL due to missing routes and handlers.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement form parsing and owner-scoped create logic**
|
||||||
|
|
||||||
|
In `tasks.go`, parse:
|
||||||
|
|
||||||
|
- `tablo_id`
|
||||||
|
- `title`
|
||||||
|
- `description`
|
||||||
|
- `status`
|
||||||
|
- `due_date`
|
||||||
|
- `assignee_id`
|
||||||
|
- `parent_task_id`
|
||||||
|
- `is_etape`
|
||||||
|
|
||||||
|
On success, create the row through the repository and re-render `/tasks`.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Implement the edit fragment handler**
|
||||||
|
|
||||||
|
Load the owner-scoped task and render a fragment with:
|
||||||
|
|
||||||
|
- title field
|
||||||
|
- description field
|
||||||
|
- status control
|
||||||
|
- due date input
|
||||||
|
- parent etape select for regular tasks only
|
||||||
|
|
||||||
|
- [ ] **Step 6: Re-run the focused tests to verify create and edit pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'PostTasks|EditTask|Tasks'`
|
||||||
|
|
||||||
|
Expected: PASS for create and edit-fragment coverage.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/handlers/tasks.go go-backend/router.go go-backend/internal/web/handlers/tasks_test.go go-backend/internal/web/views/tasks.templ go-backend/internal/web/views/*_templ.go
|
||||||
|
git commit -m "feat: add task and etape create flows"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 6: Add `PATCH /tasks/{taskID}` for updates
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tasks.go`
|
||||||
|
- Modify: `go-backend/router.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tasks_test.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing handler tests for each editable field**
|
||||||
|
|
||||||
|
Add coverage for:
|
||||||
|
|
||||||
|
- updating title
|
||||||
|
- updating description
|
||||||
|
- updating status
|
||||||
|
- updating due date
|
||||||
|
- updating assignee
|
||||||
|
- updating parent etape
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestPatchTaskUpdatesParentEtape(t *testing.T) {
|
||||||
|
// Arrange a task with no parent and a valid etape in the same tablo.
|
||||||
|
// Submit PATCH /tasks/{id}.
|
||||||
|
// Assert the refreshed page or fragment shows the updated grouping.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'PatchTask|Tasks'`
|
||||||
|
|
||||||
|
Expected: FAIL because the PATCH route does not exist yet.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement `PATCH /tasks/{taskID}`**
|
||||||
|
|
||||||
|
Add handler logic to:
|
||||||
|
|
||||||
|
- parse owner session
|
||||||
|
- parse and validate target id
|
||||||
|
- read form payload
|
||||||
|
- allow `assignee_id` to be set or cleared
|
||||||
|
- keep etapes parentless
|
||||||
|
- update the task through the repository
|
||||||
|
- return refreshed page content or task fragment
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused tests to verify the PATCH flow passes**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'PatchTask|Tasks'`
|
||||||
|
|
||||||
|
Expected: PASS for all editable-field update cases.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/handlers/tasks.go go-backend/router.go go-backend/internal/web/handlers/tasks_test.go
|
||||||
|
git commit -m "feat: add task patch update flow"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Chunk 5: Delete Flow, Full-Router Coverage, And Verification
|
||||||
|
|
||||||
|
### Task 7: Add owner-scoped delete behavior for tasks and etapes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tasks.go`
|
||||||
|
- Modify: `go-backend/internal/web/handlers/tasks_test.go`
|
||||||
|
- Modify: `go-backend/router.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing delete tests**
|
||||||
|
|
||||||
|
Add tests for:
|
||||||
|
|
||||||
|
- deleting a regular task
|
||||||
|
- deleting an etape
|
||||||
|
- child tasks moving into "Sans etape" after etape deletion
|
||||||
|
- rejecting delete from another owner
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused tests to verify they fail**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'DeleteTask|Tasks'`
|
||||||
|
|
||||||
|
Expected: FAIL because delete behavior is still missing or incomplete.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement `DELETE /tasks/{taskID}`**
|
||||||
|
|
||||||
|
Call the repository `SoftDeleteTask` method and re-render the `/tasks` page. Ensure etape child-parent clearing remains transaction-backed in the repository rather than reproduced in handlers.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Re-run the focused tests to verify they pass**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./internal/web/handlers -run 'DeleteTask|Tasks'`
|
||||||
|
|
||||||
|
Expected: PASS for delete flows.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/internal/web/handlers/tasks.go go-backend/internal/web/handlers/tasks_test.go go-backend/router.go
|
||||||
|
git commit -m "feat: add task and etape delete flow"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task 8: Add full-router coverage and run full verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `go-backend/router_test.go`
|
||||||
|
- Generated: `go-backend/internal/db/sqlc/*.go`
|
||||||
|
- Generated: `go-backend/internal/web/views/*_templ.go`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add end-to-end router tests for `/tasks`**
|
||||||
|
|
||||||
|
Cover:
|
||||||
|
|
||||||
|
- authenticated `GET /tasks`
|
||||||
|
- HTMX `GET /tasks`
|
||||||
|
- at least one create flow through the router
|
||||||
|
- at least one patch flow through the router
|
||||||
|
- at least one delete flow through the router
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the focused router tests**
|
||||||
|
|
||||||
|
Run: `cd go-backend && go test ./... -run 'TasksPage|TasksRouter|PatchTask|DeleteTask'`
|
||||||
|
|
||||||
|
Expected: PASS for router-level task coverage.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Regenerate assets and generated code**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just generate`
|
||||||
|
|
||||||
|
Expected: PASS with fresh `templ` and `sqlc` outputs.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Format and run the full test suite**
|
||||||
|
|
||||||
|
Run: `cd go-backend && gofmt -w . && go test ./...`
|
||||||
|
|
||||||
|
Expected: PASS for the full Go suite.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run build verification**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just build`
|
||||||
|
|
||||||
|
Expected: PASS with a successful Go build and CSS generation.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Manual smoke check**
|
||||||
|
|
||||||
|
Run: `cd go-backend && just run`
|
||||||
|
|
||||||
|
Expected: the app starts, `/tasks` renders real grouped data, create/edit/delete interactions work for the demo owner, and assignee flows behave correctly.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add go-backend/router_test.go go-backend/internal/db/sqlc go-backend/internal/web/views/*_templ.go
|
||||||
|
git commit -m "test: verify go-backend tasks and etapes flows"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Plan complete and saved to `docs/superpowers/plans/2026-05-10-go-backend-tasks-etapes.md`. Ready to execute?
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
**Goal**
|
**Goal**
|
||||||
|
|
||||||
Build the first real tasks vertical slice in `go-backend`: owner-scoped storage, SQL-backed CRUD for both tasks and etapes, and a server-rendered `/tasks` page that can create, list, update, and delete them.
|
Build the first real tasks vertical slice in `go-backend`: owner-scoped storage, SQL-backed CRUD for both tasks and etapes, optional assignees via `assignee_id`, and a server-rendered `/tasks` page that can create, list, update, and delete them.
|
||||||
|
|
||||||
**Scope**
|
**Scope**
|
||||||
|
|
||||||
|
|
@ -19,6 +19,7 @@ Build the first real tasks vertical slice in `go-backend`: owner-scoped storage,
|
||||||
- description
|
- description
|
||||||
- status
|
- status
|
||||||
- due date
|
- due date
|
||||||
|
- assignee
|
||||||
- parent etape
|
- parent etape
|
||||||
- Render a real `/tasks` page instead of placeholder content.
|
- Render a real `/tasks` page instead of placeholder content.
|
||||||
- Keep the page server-rendered with HTMX-driven form/modal flows.
|
- Keep the page server-rendered with HTMX-driven form/modal flows.
|
||||||
|
|
@ -29,7 +30,6 @@ Build the first real tasks vertical slice in `go-backend`: owner-scoped storage,
|
||||||
- RBAC, collaborators, tablo sharing, or organization-level permissions
|
- RBAC, collaborators, tablo sharing, or organization-level permissions
|
||||||
- Drag and drop
|
- Drag and drop
|
||||||
- Reordering tasks or etapes
|
- Reordering tasks or etapes
|
||||||
- Assignees
|
|
||||||
- Comments, attachments, or activity history
|
- Comments, attachments, or activity history
|
||||||
- Separate API-only JSON endpoints
|
- Separate API-only JSON endpoints
|
||||||
- Persisting a synthetic "Sans etape" record
|
- Persisting a synthetic "Sans etape" record
|
||||||
|
|
@ -63,6 +63,7 @@ Columns:
|
||||||
- `title text not null`
|
- `title text not null`
|
||||||
- `description text not null default ''`
|
- `description text not null default ''`
|
||||||
- `status text not null`
|
- `status text not null`
|
||||||
|
- `assignee_id uuid null references public.users(id) on delete set null`
|
||||||
- `is_etape boolean not null default false`
|
- `is_etape boolean not null default false`
|
||||||
- `parent_task_id uuid null references public.tasks(id) on delete set null`
|
- `parent_task_id uuid null references public.tasks(id) on delete set null`
|
||||||
- `due_date date null`
|
- `due_date date null`
|
||||||
|
|
@ -74,6 +75,8 @@ Suggested indexes:
|
||||||
|
|
||||||
- active tablo task reads:
|
- active tablo task reads:
|
||||||
- `(owner_id, tablo_id, deleted_at)`
|
- `(owner_id, tablo_id, deleted_at)`
|
||||||
|
- assignee lookups:
|
||||||
|
- `(assignee_id, deleted_at)`
|
||||||
- etape grouping:
|
- etape grouping:
|
||||||
- `(tablo_id, is_etape, deleted_at)`
|
- `(tablo_id, is_etape, deleted_at)`
|
||||||
- child lookup:
|
- child lookup:
|
||||||
|
|
@ -131,7 +134,7 @@ The trigger should reject:
|
||||||
- On success, returns refreshed `/tasks` content.
|
- On success, returns refreshed `/tasks` content.
|
||||||
- On validation failure, returns modal or inline form content with status `422`.
|
- On validation failure, returns modal or inline form content with status `422`.
|
||||||
- `PATCH /tasks/{taskID}`
|
- `PATCH /tasks/{taskID}`
|
||||||
- Updates title, description, status, due date, and parent etape.
|
- Updates title, description, status, due date, assignee, and parent etape.
|
||||||
- Must be owner-scoped.
|
- Must be owner-scoped.
|
||||||
- On success, returns refreshed `/tasks` content or updated fragment.
|
- On success, returns refreshed `/tasks` content or updated fragment.
|
||||||
- `DELETE /tasks/{taskID}`
|
- `DELETE /tasks/{taskID}`
|
||||||
|
|
@ -161,6 +164,7 @@ Update:
|
||||||
- `description`
|
- `description`
|
||||||
- `status`
|
- `status`
|
||||||
- `due_date`
|
- `due_date`
|
||||||
|
- `assignee_id`
|
||||||
- `parent_task_id`
|
- `parent_task_id`
|
||||||
- for etapes:
|
- for etapes:
|
||||||
- `parent_task_id` must remain null
|
- `parent_task_id` must remain null
|
||||||
|
|
@ -221,6 +225,7 @@ Validation rules:
|
||||||
- title required
|
- title required
|
||||||
- status required and must be one of the four allowed values
|
- status required and must be one of the four allowed values
|
||||||
- malformed UUIDs return `400`
|
- malformed UUIDs return `400`
|
||||||
|
- assignee, when present, must reference an existing user row
|
||||||
- etapes cannot have a parent
|
- etapes cannot have a parent
|
||||||
- parent, when present, must be a valid owner-scoped etape in the same tablo
|
- parent, when present, must be a valid owner-scoped etape in the same tablo
|
||||||
|
|
||||||
|
|
@ -250,6 +255,7 @@ Each displayed task row should support:
|
||||||
- optional description preview
|
- optional description preview
|
||||||
- status indicator or control
|
- status indicator or control
|
||||||
- due date when present
|
- due date when present
|
||||||
|
- assignee when present
|
||||||
- parent etape display when relevant
|
- parent etape display when relevant
|
||||||
- edit and delete actions
|
- edit and delete actions
|
||||||
|
|
||||||
|
|
@ -259,6 +265,7 @@ Each displayed etape row or section should support:
|
||||||
- optional description
|
- optional description
|
||||||
- status
|
- status
|
||||||
- optional due date
|
- optional due date
|
||||||
|
- optional assignee
|
||||||
- edit and delete actions
|
- edit and delete actions
|
||||||
- nested child tasks
|
- nested child tasks
|
||||||
|
|
||||||
|
|
@ -287,6 +294,7 @@ Minimum repository coverage:
|
||||||
- create etape
|
- create etape
|
||||||
- list excludes soft-deleted rows
|
- list excludes soft-deleted rows
|
||||||
- owner scoping is enforced
|
- owner scoping is enforced
|
||||||
|
- assignee persists when set and clears to null when unset
|
||||||
- parent etape must belong to same owner
|
- parent etape must belong to same owner
|
||||||
- parent etape must belong to same tablo
|
- parent etape must belong to same tablo
|
||||||
- etape cannot point to a parent
|
- etape cannot point to a parent
|
||||||
|
|
@ -304,6 +312,7 @@ Minimum handler coverage:
|
||||||
- `PATCH /tasks/{taskID}` updates description
|
- `PATCH /tasks/{taskID}` updates description
|
||||||
- `PATCH /tasks/{taskID}` updates status
|
- `PATCH /tasks/{taskID}` updates status
|
||||||
- `PATCH /tasks/{taskID}` updates due date
|
- `PATCH /tasks/{taskID}` updates due date
|
||||||
|
- `PATCH /tasks/{taskID}` updates assignee
|
||||||
- `PATCH /tasks/{taskID}` updates parent etape
|
- `PATCH /tasks/{taskID}` updates parent etape
|
||||||
- `DELETE /tasks/{taskID}` soft-deletes a regular task
|
- `DELETE /tasks/{taskID}` soft-deletes a regular task
|
||||||
- `DELETE /tasks/{taskID}` soft-deletes an etape and moves children into runtime "Sans etape"
|
- `DELETE /tasks/{taskID}` soft-deletes an etape and moves children into runtime "Sans etape"
|
||||||
|
|
@ -332,7 +341,8 @@ Minimum rendering assertions:
|
||||||
- sqlc queries and repository methods exist for owner-scoped task and etape CRUD.
|
- sqlc queries and repository methods exist for owner-scoped task and etape CRUD.
|
||||||
- `/tasks` renders real owner data instead of placeholder content.
|
- `/tasks` renders real owner data instead of placeholder content.
|
||||||
- Owners can create, list, update, and delete both tasks and etapes.
|
- Owners can create, list, update, and delete both tasks and etapes.
|
||||||
- `PATCH /tasks/{taskID}` updates title, description, status, due date, and parent etape.
|
- Tasks and etapes may have an optional `assignee_id`.
|
||||||
|
- `PATCH /tasks/{taskID}` updates title, description, status, due date, assignee, and parent etape.
|
||||||
- Deleting an etape causes its active child tasks to appear in runtime "Sans etape".
|
- Deleting an etape causes its active child tasks to appear in runtime "Sans etape".
|
||||||
- No collaborator or RBAC behavior is introduced.
|
- No collaborator or RBAC behavior is introduced.
|
||||||
- Targeted repository and handler tests cover the core flows and invariants.
|
- Targeted repository and handler tests cover the core flows and invariants.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue