---
phase: 19-tablo-list-revamp
plan: "01"
type: execute
wave: 1
depends_on: []
files_modified:
  - backend/migrations/0010_tablo_status.sql
  - backend/internal/db/queries/tablos.sql
  - backend/internal/db/sqlc/models.go          # regenerated by sqlc generate
  - backend/internal/db/sqlc/query.sql.go       # regenerated by sqlc generate
  - backend/templates/discussion_forms.go
  - backend/internal/web/handlers_tablos.go
autonomous: true
requirements: [LIST-01, LIST-03]

must_haves:
  truths:
    - "GET / renders without 500 after migration runs"
    - "TabloCardView carries a Progress int field (0–100) populated by the handler"
    - "sqlc.Tablo struct has a Status string field after sqlc regeneration"
    - "TabloCardsFromUnreadRows maps Status from the query row into the Tablo struct"
    - "Handler calls ListTabloProgressByIDs once per page load (batch, not N+1)"
  artifacts:
    - path: "backend/migrations/0010_tablo_status.sql"
      provides: "Reversible goose migration adding status column to tablos"
      contains: "+goose Up"
    - path: "backend/internal/db/queries/tablos.sql"
      provides: "Updated tablo queries with status column + new ListTabloProgressByIDs query"
      contains: "ListTabloProgressByIDs"
    - path: "backend/templates/discussion_forms.go"
      provides: "TabloCardView with Progress int field"
      contains: "Progress int"
    - path: "backend/internal/web/handlers_tablos.go"
      provides: "Handler wiring progress batch query"
      contains: "ListTabloProgressByIDs"
  key_links:
    - from: "backend/internal/web/handlers_tablos.go (TablosListHandler)"
      to: "backend/internal/db/sqlc/query.sql.go (ListTabloProgressByIDs)"
      via: "deps.Queries.ListTabloProgressByIDs(ctx, tabloIDs)"
      pattern: "ListTabloProgressByIDs"
    - from: "backend/templates/discussion_forms.go (TabloCardsFromUnreadRows)"
      to: "sqlc.Tablo.Status"
      via: "row.Status mapped into Tablo struct literal"
      pattern: "Status.*row.Status"
---

<objective>
Add the `status` column to the tablos table, regenerate sqlc types, add a batch progress query, and wire progress data into the handler before any template changes.

Purpose: Establishes the data layer that Plans 02 and 03 render. The app must build and all existing tests must pass at the end of this plan.
Output: Migration file, updated SQL queries, regenerated sqlc files, enriched TabloCardView, and handler progress wiring.
</objective>

<execution_context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/ROADMAP.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-CONTEXT.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-RESEARCH.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/18-app-shell-navigation/18-03-SUMMARY.md
</context>

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From backend/templates/discussion_forms.go (current — will be modified):
```go
type TabloCardView struct {
    Tablo                 sqlc.Tablo
    DiscussionUnreadCount int64
}

// TabloCardsFromUnreadRows — builds TabloCardView slice from query rows
// Must be updated to include Status from the row and Progress field (zero-initialized here)
func TabloCardsFromUnreadRows(rows []sqlc.ListTablosByUserWithDiscussionUnreadRow) []TabloCardView

// TabloCardFromTablo — also needs Progress default of 0 (already implicit via zero-value)
func TabloCardFromTablo(tablo sqlc.Tablo) TabloCardView
```

From backend/internal/db/sqlc/models.go (current — will gain Status after regen):
```go
type Tablo struct {
    ID          uuid.UUID
    UserID      uuid.UUID
    Title       string
    Description pgtype.Text
    Color       pgtype.Text
    CreatedAt   pgtype.Timestamptz
    UpdatedAt   pgtype.Timestamptz
    // Status string  ← added by migration + sqlc regen
}
```

From backend/internal/web/handlers_tablos.go (TablosListHandler — will be extended):
```go
type TablosDeps struct {
    Queries *sqlc.Queries
}

func TablosListHandler(deps TablosDeps) http.HandlerFunc {
    // Currently calls:
    //   tabloRows, err := deps.Queries.ListTablosByUserWithDiscussionUnread(ctx, user.ID)
    //   cardViews := templates.TabloCardsFromUnreadRows(tabloRows)
    // Must also call ListTabloProgressByIDs and populate cardViews[i].Progress
}
```

From backend/internal/db/queries/tablos.sql (current — all explicit column lists):
```sql
-- Every SELECT and RETURNING in this file enumerates columns explicitly.
-- After migration: add `status` to EVERY column list and GROUP BY.
-- ListTablosByUserWithDiscussionUnread also has GROUP BY — add status there too.
```

From backend/sqlc.yaml (no uuid[] override defined):
```yaml
overrides:
  - db_type: "citext"
    go_type: "string"
  - db_type: "uuid"
    go_type:
      import: "github.com/google/uuid"
      type: "UUID"
# No array override for uuid[] exists.
# For ListTabloProgressByIDs with ANY(@tablo_ids::uuid[]):
# sqlc with pgx/v5 generates []uuid.UUID for the parameter (pgx handles it natively).
# Verify after `sqlc generate` that the generated param type compiles — if it does not,
# add an override for "uuid[]" → github.com/google/uuid UUID with is_array: true,
# OR rewrite the query to use unnest($1::uuid[]) to avoid the array param.
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Migration, SQL query updates, and sqlc regeneration</name>
  <files>
    backend/migrations/0010_tablo_status.sql
    backend/internal/db/queries/tablos.sql
    backend/internal/db/sqlc/models.go
    backend/internal/db/sqlc/query.sql.go
  </files>
  <action>
    1. Create backend/migrations/0010_tablo_status.sql with goose annotations (per D-07):

       -- +goose Up
       ALTER TABLE tablos
           ADD COLUMN status text NOT NULL DEFAULT 'active'
           CHECK (status IN ('active', 'archived'));

       -- +goose Down
       ALTER TABLE tablos DROP COLUMN status;

    2. Run the migration against the local dev database:
       cd backend && just migrate up

    3. Update backend/internal/db/queries/tablos.sql — add `status` to EVERY explicit
       column list and RETURNING clause. Affected queries:

       a. ListTablosByUser: add `status` after `updated_at` in SELECT list.

       b. ListTablosByUserWithDiscussionUnread: add `tablos.status` to the SELECT
          column list AND to the GROUP BY clause (it is already grouped by all
          non-aggregated tablo columns; add `tablos.status` after `tablos.updated_at`
          in both SELECT and GROUP BY).

       c. GetTabloByID: add `status` after `updated_at`.

       d. InsertTablo: add `status` after `updated_at` in the RETURNING clause.
          Do NOT add status to the INSERT column list — it has a DB DEFAULT.

       e. UpdateTablo: add `status` after `updated_at` in the RETURNING clause.
          Do NOT add status to the SET clause — Phase 19 does not update status.

    4. Add the new progress batch query to the end of tablos.sql:

       -- name: ListTabloProgressByIDs :many
       -- Batch aggregation: one query for all tablos on the dashboard (D-06).
       -- Counts all tasks regardless of etape assignment.
       SELECT
           tablo_id,
           COUNT(*) FILTER (WHERE status = 'done')::int AS done_tasks,
           COUNT(*)::int                                 AS total_tasks
       FROM tasks
       WHERE tablo_id = ANY(@tablo_ids::uuid[])
       GROUP BY tablo_id;

    5. Run sqlc generate:
       cd backend && sqlc generate

       After generation, verify that backend/internal/db/sqlc/models.go now has
       `Status string` on the Tablo struct, and that the generated Go code for
       ListTabloProgressByIDs compiles (check the parameter type — it should be
       []uuid.UUID with pgx/v5 native handling). If the file fails to compile due
       to the uuid[] array type, see the fallback in the interfaces section: add a
       sqlc.yaml override for uuid[] OR rewrite using unnest.

    6. Run go build ./... from backend/ to confirm everything compiles before
       proceeding to Task 2.
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... 2>&1</automated>
  </verify>
  <done>
    - backend/migrations/0010_tablo_status.sql exists with +goose Up and +goose Down
    - backend/internal/db/sqlc/models.go contains `Status string` on Tablo struct
    - backend/internal/db/sqlc/query.sql.go contains ListTabloProgressByIDs function
    - `go build ./...` exits 0
  </done>
</task>

<task type="auto">
  <name>Task 2: Enrich TabloCardView and wire progress in handler</name>
  <files>
    backend/templates/discussion_forms.go
    backend/internal/web/handlers_tablos.go
  </files>
  <action>
    In backend/templates/discussion_forms.go:

    1. Add `Progress int` field to TabloCardView:
       type TabloCardView struct {
           Tablo                 sqlc.Tablo
           DiscussionUnreadCount int64
           Progress              int // 0–100; 0 when no tasks (D-05)
       }

    2. Update TabloCardsFromUnreadRows to include Status in the Tablo struct
       literal. After sqlc regen, ListTablosByUserWithDiscussionUnreadRow will have
       a Status field. Map it:
       Tablo: sqlc.Tablo{
           ID:          row.ID,
           UserID:      row.UserID,
           Title:       row.Title,
           Description: row.Description,
           Color:       row.Color,
           CreatedAt:   row.CreatedAt,
           UpdatedAt:   row.UpdatedAt,
           Status:      row.Status,   // ← added
       },

    In backend/internal/web/handlers_tablos.go (TablosListHandler):

    3. After building cardViews, extract tablo IDs and call the progress batch query:

       tabloIDs := make([]uuid.UUID, len(cardViews))
       for i, cv := range cardViews {
           tabloIDs[i] = cv.Tablo.ID
       }

       progressRows, err := deps.Queries.ListTabloProgressByIDs(ctx, tabloIDs)
       if err != nil {
           slog.Default().Error("tablos list: progress query failed", "user_id", user.ID, "err", err)
           // non-fatal: proceed with Progress = 0 for all cards
           progressRows = nil
       }

       progressMap := make(map[uuid.UUID]int, len(progressRows))
       for _, p := range progressRows {
           if p.TotalTasks > 0 {
               progressMap[p.TabloID] = int(p.DoneTasks * 100 / p.TotalTasks)
           }
       }
       for i := range cardViews {
           cardViews[i].Progress = progressMap[cardViews[i].Tablo.ID]
       }

    The "uuid" import is already present in handlers_tablos.go. Confirm it is in
    scope (it is used for uuid.UUID in other handlers).

    4. Ensure the import block in discussion_forms.go still compiles — no new
       imports are required (sqlc and uuid are already imported).
  </action>
  <verify>
    <automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... && go test ./... 2>&1</automated>
  </verify>
  <done>
    - TabloCardView has Progress int field
    - TabloCardsFromUnreadRows maps row.Status into Tablo.Status
    - TablosListHandler calls ListTabloProgressByIDs and populates cardViews[i].Progress
    - `go build ./...` exits 0
    - `go test ./...` passes (no new failures — existing tests may skip due to no test DB)
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| DB migration → application | Schema change must be reversible; DOWN migration must not lose data |
| Handler → DB (progress query) | Parameterized batch query; no SQL injection surface |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-19-01-01 | Tampering | tablos.status column | mitigate | DB CHECK constraint `IN ('active', 'archived')` prevents invalid values at write time; application never sets status in Phase 19 (DEFAULT only) |
| T-19-01-02 | Information Disclosure | ListTabloProgressByIDs | accept | Query is scoped to tablo_ids from the authenticated user's own tablos; ownership enforced upstream in TablosListHandler via user_id filter in ListTablosByUserWithDiscussionUnread |
| T-19-01-03 | Denial of Service | Migration 0010 | accept | ALTER TABLE ADD COLUMN with DEFAULT on a small user table; no table lock risk for expected data volume |
</threat_model>

<verification>
After both tasks complete:

```bash
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
go build ./...
go test ./...
grep -c "ListTabloProgressByIDs" internal/db/sqlc/query.sql.go
grep -c "Progress int" templates/discussion_forms.go
grep -c "ListTabloProgressByIDs" internal/web/handlers_tablos.go
grep -c "status" internal/db/sqlc/models.go
```

Expected: all greps return 1 or more; go build exits 0.
</verification>

<success_criteria>
- `backend/migrations/0010_tablo_status.sql` exists with both +goose Up and +goose Down blocks
- `grep -c "Status string" backend/internal/db/sqlc/models.go` returns 1
- `grep -c "ListTabloProgressByIDs" backend/internal/db/sqlc/query.sql.go` returns 1
- `grep -c "Progress int" backend/templates/discussion_forms.go` returns 1
- `grep -c "ListTabloProgressByIDs" backend/internal/web/handlers_tablos.go` returns 1
- `cd backend && go build ./...` exits 0
- `cd backend && go test ./...` has no new failures
</success_criteria>

<output>
After completion, create `/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-01-SUMMARY.md` following the summary template.
</output>
