Add go-backend tasks and etapes design spec
This commit is contained in:
parent
354785edff
commit
ef7ccd8c6f
1 changed files with 338 additions and 0 deletions
|
|
@ -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.
|
||||||
Loading…
Reference in a new issue