# 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, optional assignees via `assignee_id`, 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
  - assignee
  - 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
- 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`
- `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`

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:
  - `(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, assignee, 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`
  - `assignee_id`
  - `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`
- 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

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
- assignee 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
- optional assignee
- 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
- 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
- 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 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"

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.
- 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.
