From ef7ccd8c6f3412c44503f1304f58a58e7f3f6b83 Mon Sep 17 00:00:00 2001 From: Arthur Belleville Date: Sun, 10 May 2026 18:25:18 +0200 Subject: [PATCH] Add go-backend tasks and etapes design spec --- ...26-05-10-go-backend-tasks-etapes-design.md | 338 ++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 docs/superpowers/specs/2026-05-10-go-backend-tasks-etapes-design.md 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 new file mode 100644 index 0000000..534b7e1 --- /dev/null +++ b/docs/superpowers/specs/2026-05-10-go-backend-tasks-etapes-design.md @@ -0,0 +1,338 @@ +# Go Backend Tasks And Etapes Design + +**Date:** 2026-05-10 + +**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. + +**Scope** + +- Work exclusively in `go-backend`. +- Add persistent SQL schema for tasks and etapes. +- Represent etapes inside the same table as tasks. +- Support owner-only CRUD for: + - regular tasks + - etapes +- Support updating a task or etape with: + - title + - description + - status + - due date + - parent etape +- Render a real `/tasks` page instead of placeholder content. +- Keep the page server-rendered with HTMX-driven form/modal flows. +- Build the runtime-only "Sans etape" grouping in the view layer rather than storing it in the database. + +**Out of Scope** + +- 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 +- Single-task detail pages outside the `/tasks` page workflow + +**Architecture** + +The feature should live entirely inside the existing Go rewrite stack: + +- schema updates in `go-backend/internal/db/schema.sql` +- sqlc statements in `go-backend/internal/db/queries.sql` +- repository methods in `go-backend/internal/db/repository.go` +- task domain types in a new `go-backend/internal/tasks/` package +- HTTP handlers in `go-backend/internal/web/handlers/` +- `templ` views in `go-backend/internal/web/views/` +- route registration in `go-backend/router.go` + +The current repository abstraction is broader than auth now, so it should be expanded or renamed to cover task persistence cleanly instead of treating task CRUD as an auth concern. + +The `/tasks` page remains server-rendered. HTMX should be used to swap page content and modal fragments, not to introduce a separate SPA data layer. + +**Recommended Data Model** + +Use a single self-referential `public.tasks` table. + +Columns: + +- `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` +- `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` + +Suggested indexes: + +- active tablo task reads: + - `(owner_id, tablo_id, deleted_at)` +- etape grouping: + - `(tablo_id, is_etape, deleted_at)` +- child lookup: + - `(parent_task_id)` +- due date filtering/sorting: + - `(tablo_id, due_date)` with active rows preference if needed later + +This table models the product rules directly: + +- an etape is a row where `is_etape = true` +- a regular task is a row where `is_etape = false` +- a task without parent etape has `parent_task_id = null` +- "Sans etape" is built at runtime from those parentless regular tasks + +**Data Invariants** + +Allowed status values: + +- `todo` +- `in_progress` +- `in_review` +- `done` + +Enforce these invariants: + +- `status` must be one of the four allowed values +- an etape cannot itself belong to another etape +- `parent_task_id`, when set, must point to an active row with `is_etape = true` +- parent and child must share the same `tablo_id` +- parent and child must share the same `owner_id` +- normal list queries must exclude rows where `deleted_at is not null` + +Implementation notes: + +- keep `status` as plain text in Postgres for consistency with the current `go-backend` schema style +- enforce allowed status values with a `CHECK` +- enforce cross-row parent validation with a trigger, not a `CHECK`, because Postgres `CHECK` constraints should not query sibling rows + +The trigger should reject: + +- an etape with a non-null `parent_task_id` +- a task pointing to a parent in another tablo +- a task pointing to a parent owned by someone else +- a task pointing to a non-etape row +- a task pointing to a soft-deleted parent + +**Route Design** + +- `GET /tasks` + - Returns the full dashboard page for normal requests. + - Returns the swapped `/tasks` content fragment for HTMX requests. +- `POST /tasks` + - Creates either a regular task or an etape from form data. + - Owner is taken from the authenticated session, never from user input. + - 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. + - Must be owner-scoped. + - On success, returns refreshed `/tasks` content or updated fragment. +- `DELETE /tasks/{taskID}` + - Soft-deletes a regular task or an etape. + - Must be owner-scoped. +- `GET /tasks/{taskID}/edit` + - Returns an edit form/modal fragment for HTMX. +- Optional helper fragments if the view composition benefits from them: + - `GET /tasks/new` + - `GET /tasks/new-etape` + +**Mutation Semantics** + +Create: + +- regular task: + - `is_etape = false` + - `parent_task_id` may be null +- etape: + - `is_etape = true` + - `parent_task_id = null` + +Update: + +- allowed editable fields: + - `title` + - `description` + - `status` + - `due_date` + - `parent_task_id` +- for etapes: + - `parent_task_id` must remain null +- for regular tasks: + - `parent_task_id` may be null or point to an etape + +Delete: + +- regular task: + - soft-delete the row +- etape: + - soft-delete the etape + - clear `parent_task_id` on its active child tasks so they fall back into runtime "Sans etape" + +That child-rehoming should happen inside the same repository operation or database transaction so the page never renders a broken intermediate state. + +**Repository Design** + +Add explicit repository methods for task work, parallel to the current tablo methods. + +Minimum methods: + +- create task +- update task +- soft delete task +- list tasks for owner across tablos +- list tasks for one owner-scoped tablo +- fetch one owner-scoped task +- clear children for a deleted etape + +List semantics: + +- always scope by `owner_id` +- always exclude `deleted_at is not null` +- return both etapes and regular tasks for the page +- preserve deterministic ordering + +Suggested ordering for per-tablo reads: + +- etapes first by `created_at` or a future explicit position +- then regular tasks by: + - grouped under their parent etape in Go + - or sorted by `parent_task_id nulls first, created_at desc` and regrouped in Go + +The repository should own transaction boundaries for etape deletion plus child-parent clearing. + +**Handler Design** + +Handlers should follow the current `tablos` pattern: + +- resolve the authenticated user first +- reject unauthenticated requests with redirect to `/login` +- scope every repo call by that owner +- re-render server HTML on success or validation failure + +Validation rules: + +- title required +- status required and must be one of the four allowed values +- malformed UUIDs return `400` +- etapes cannot have a parent +- parent, when present, must be a valid owner-scoped etape in the same tablo + +Error mapping: + +- `401` when no session +- `400` for malformed ids or invalid HTTP payload shape +- `422` for user-correctable validation failures +- `404` when the requested task does not exist for that owner +- `500` for storage or rendering failures + +**View And Page Behavior** + +Replace the placeholder `TasksMainContent()` with a real tasks page view model. + +The page should group rows by tablo. + +Inside each tablo: + +- render etape sections for rows where `is_etape = true` +- render child tasks beneath their parent etape +- render a synthetic "Sans etape" section only when parentless regular tasks exist + +Each displayed task row should support: + +- title +- optional description preview +- status indicator or control +- due date when present +- parent etape display when relevant +- edit and delete actions + +Each displayed etape row or section should support: + +- title +- optional description +- status +- optional due date +- edit and delete actions +- nested child tasks + +The page should remain intentionally simple for this slice. No drag-and-drop, no reordering, and no client-side state model beyond HTMX interactions. + +**HTMX Response Strategy** + +Normal requests return the full dashboard page. + +HTMX requests return only the relevant fragment: + +- `/tasks` main content swap for list refresh +- edit modal fragment +- create modal fragment +- validation-state fragment on `422` + +Fragment boundaries should stay explicit so later work can evolve the `/tasks` page without undoing the current server-rendered structure. + +**Testing Strategy** + +Use TDD across repository and handler layers. + +Minimum repository coverage: + +- create regular task +- create etape +- list excludes soft-deleted rows +- owner scoping is enforced +- parent etape must belong to same owner +- parent etape must belong to same tablo +- etape cannot point to a parent +- deleting a regular task soft-deletes it +- deleting an etape clears `parent_task_id` for active children + +Minimum handler coverage: + +- authenticated `GET /tasks` renders the real tasks page +- HTMX `GET /tasks` renders the content fragment +- `POST /tasks` creates a regular task +- `POST /tasks` creates an etape +- `POST /tasks` rejects invalid parent selection +- `PATCH /tasks/{taskID}` updates title +- `PATCH /tasks/{taskID}` updates description +- `PATCH /tasks/{taskID}` updates status +- `PATCH /tasks/{taskID}` updates due date +- `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" + +Minimum rendering assertions: + +- etape sections appear +- tasks under etapes appear in the correct group +- parentless regular tasks appear under "Sans etape" +- owner can only see their own tablos and related tasks + +**Risks And Mitigations** + +- Self-referential task models can silently drift into invalid parent chains. + - Mitigation: enforce parent validity in the database with a dedicated trigger. +- Etape deletion can orphan children in surprising ways. + - Mitigation: make the repository clear `parent_task_id` inside the same transaction. +- The current repository interface is auth-oriented. + - Mitigation: expand or rename it now rather than leaking task persistence into auth-only abstractions. +- The `/tasks` page can become view-heavy quickly. + - Mitigation: keep grouping logic in small helpers and separate page view models from raw DB rows. + +**Acceptance Criteria** + +- `go-backend/internal/db/schema.sql` includes a persistent tasks table and required integrity rules. +- 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. +- 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.