# 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?
