diff --git a/docs/superpowers/plans/2026-05-10-go-backend-tasks-etapes.md b/docs/superpowers/plans/2026-05-10-go-backend-tasks-etapes.md new file mode 100644 index 0000000..8878198 --- /dev/null +++ b/docs/superpowers/plans/2026-05-10-go-backend-tasks-etapes.md @@ -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? diff --git a/docs/superpowers/specs/2026-05-10-go-backend-tasks-etapes-design.md b/docs/superpowers/specs/2026-05-10-go-backend-tasks-etapes-design.md index 534b7e1..bbdf705 100644 --- a/docs/superpowers/specs/2026-05-10-go-backend-tasks-etapes-design.md +++ b/docs/superpowers/specs/2026-05-10-go-backend-tasks-etapes-design.md @@ -4,7 +4,7 @@ **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** @@ -19,6 +19,7 @@ Build the first real tasks vertical slice in `go-backend`: owner-scoped storage, - description - status - due date + - assignee - parent etape - Render a real `/tasks` page instead of placeholder content. - 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 - Drag and drop - Reordering tasks or etapes -- Assignees - Comments, attachments, or activity history - Separate API-only JSON endpoints - Persisting a synthetic "Sans etape" record @@ -63,6 +63,7 @@ Columns: - `title text not null` - `description text not null default ''` - `status text not null` +- `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` @@ -74,6 +75,8 @@ Suggested indexes: - active tablo task reads: - `(owner_id, tablo_id, deleted_at)` +- assignee lookups: + - `(assignee_id, deleted_at)` - etape grouping: - `(tablo_id, is_etape, deleted_at)` - child lookup: @@ -131,7 +134,7 @@ The trigger should reject: - On success, returns refreshed `/tasks` content. - On validation failure, returns modal or inline form content with status `422`. - `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. - On success, returns refreshed `/tasks` content or updated fragment. - `DELETE /tasks/{taskID}` @@ -161,6 +164,7 @@ Update: - `description` - `status` - `due_date` + - `assignee_id` - `parent_task_id` - for etapes: - `parent_task_id` must remain null @@ -221,6 +225,7 @@ Validation rules: - title required - status required and must be one of the four allowed values - malformed UUIDs return `400` +- assignee, when present, must reference an existing user row - etapes cannot have a parent - 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 - status indicator or control - due date when present +- assignee when present - parent etape display when relevant - edit and delete actions @@ -259,6 +265,7 @@ Each displayed etape row or section should support: - optional description - status - optional due date +- optional assignee - edit and delete actions - nested child tasks @@ -287,6 +294,7 @@ Minimum repository coverage: - create etape - list excludes soft-deleted rows - 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 tablo - etape cannot point to a parent @@ -304,6 +312,7 @@ Minimum handler coverage: - `PATCH /tasks/{taskID}` updates description - `PATCH /tasks/{taskID}` updates status - `PATCH /tasks/{taskID}` updates due date +- `PATCH /tasks/{taskID}` updates assignee - `PATCH /tasks/{taskID}` updates parent etape - `DELETE /tasks/{taskID}` soft-deletes a regular task - `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. - `/tasks` renders real owner data instead of placeholder content. - 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". - No collaborator or RBAC behavior is introduced. - Targeted repository and handler tests cover the core flows and invariants.