docs(19): research tablo list revamp phase

This commit is contained in:
Arthur Belleville 2026-05-17 16:16:27 +02:00
parent 3542f3b105
commit 28e05b5fc1
No known key found for this signature in database

View file

@ -0,0 +1,577 @@
# Phase 19: Tablo List Revamp — Research
**Researched:** 2026-05-17
**Domain:** Go + templ UI, CSS layout, PostgreSQL migration, sqlc
**Confidence:** HIGH
---
<user_constraints>
## User Constraints (from CONTEXT.md)
### Locked Decisions
- **D-01:** Screenshots in `screenshots/` are the primary visual reference. `Homepage.png` shows the Figma design; `ssidebar-header.png` shows the production app with: status badge top-left, delete button top-right, colored initial circle, title, date, progress bar "Progression: X%".
- **D-02:** The Figma design is the authoritative design source. Executors MUST read screenshots before making visual decisions.
- **D-03:** View toggle has **no persistence** — resets to card grid on every page load. A small inline JS click handler toggles a class on the container. No server round-trip, no localStorage, no cookie.
- **D-04:** Card grid is the default view. List view shows tablos as rows.
- **D-05:** Progress = `completed_tasks / total_tasks * 100`. Completed tasks = tasks with `status = 'done'`. If no tasks exist, progress = 0%.
- **D-06:** The query for progress must be efficient — single JOIN/aggregation over N+1 queries.
- **D-07:** Add `status text NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'archived'))` to tablos table.
- **D-08:** Phase 19 only implements the 'active' state in the UI. No archiving button. All existing tablos default to 'active'.
- **D-09:** A status indicator (small badge) is visible on cards and list rows showing "Active".
- **D-10:** Card shows: status badge (top-left), delete button (top-right), colored initial circle, tablo title, creation date, progress bar with percentage.
- **D-11:** Progress bar uses existing `var(--color-accent)` or purple accent token. Bar background is a lighter tint.
- **D-12:** List view shows tablos as table rows: status badge, title, creation date, progress %, delete button.
### Claude's Discretion
- Exact CSS class naming for new card elements — follow `.tablo-card-*` naming convention.
- Whether progress query is a new sqlc query or computed in handler.
- Table structure for list view — `<table>` or flex rows.
### Deferred Ideas (OUT OF SCOPE)
- Archiving UI (archive button, confirmation, filter toggle)
- "Show archived" toggle
- Status values beyond active/archived
- Tablo filtering/sorting
</user_constraints>
<phase_requirements>
## Phase Requirements
| ID | Description | Research Support |
|----|-------------|------------------|
| LIST-01 | Tablo cards revamped to match Figma — updated layout, typography, progress bar with real task completion % | TabloCardView needs Progress int; batch SQL query identified; CSS classes inventoried |
| LIST-02 | User can toggle between card grid and list view (no persistence, resets on page load) | JS data-attribute toggle pattern confirmed; list view CSS approach documented |
| LIST-03 | Tablos have active/archived status field in DB, visible as UI indicator on cards and list rows | Migration 0010 confirmed; Tablo struct needs Status string; badge component available |
</phase_requirements>
---
## Summary
Phase 19 revamps the tablos dashboard to match the production design. The work touches four distinct layers: DB migration, sqlc codegen, Go handler/view-model, and templ template + CSS.
The current `TabloProjectCard` template renders a colored avatar circle, title, creation date, and edit/delete icon buttons. It has no progress bar and no status badge. The screenshot confirms the target design: status badge top-left (e.g., "En cours"), delete icon top-right, large colored initial circle, title, date, progress bar labeled "Progression: X%". The production screenshot matches this pattern exactly.
The current `TabloCardView` struct holds only `Tablo sqlc.Tablo` and `DiscussionUnreadCount int64`. It needs `Progress int` (0100) and `Status string` added. Progress must be fetched in batch — a single SQL query aggregating done/total tasks across all tablo IDs avoids N+1. The new `status` column is added via migration `0010_tablo_status.sql`; sqlc must be re-run after migration. The view toggle is pure CSS+JS: a `data-view` attribute on `#tablos-list` toggled by a button click, with CSS rules for `[data-view="list"]` to switch layout.
**Primary recommendation:** Three plans — (1) DB migration + sqlc regen + model changes, (2) revamped card template + progress query + handler wiring, (3) list view CSS + toggle JS.
---
## Architectural Responsibility Map
| Capability | Primary Tier | Secondary Tier | Rationale |
|------------|-------------|----------------|-----------|
| DB status column | Database/Storage | — | Goose migration + CHECK constraint |
| Progress aggregation SQL | Database/Storage | API/Backend | sqlc query, batch join |
| TabloCardView enrichment | API/Backend | — | Handler builds view model before render |
| Card template revamp | Frontend Server (templ) | — | templ components own HTML structure |
| Progress bar rendering | Frontend Server (templ) | — | Inline style `width: X%` from view model |
| Status badge | Frontend Server (templ) | — | Conditional badge in card template |
| View toggle mechanism | Browser/Client | — | JS toggles data attribute; CSS does layout |
| List view CSS | Frontend Server (CSS) | — | Static CSS file, no JS framework |
---
## Screenshot Analysis
### `screenshots/Homepage.png` (Figma design)
The Figma design shows a "Mes Projets" (My Projects) section with a 3-column card grid. Each card contains:
- **Top row:** A colored status pill badge in the top-left (e.g., "En cours", "À faire"), a small trash/delete icon in the top-right.
- **Avatar row:** A large rounded-square colored initial circle (48px, colored by tablo color), followed by the tablo title.
- **Date row:** Calendar icon + formatted date (e.g., "Apr 15, 2026").
- **Progress row:** Label "Progression:" with a percentage (e.g., "50%"), and below it a filled progress bar (purple/accent, rounded, ~8px tall). Bar fills to match the percentage.
Colors: status badge "En cours" uses purple/blue tint (brand color); "À faire" uses a yellow/gray tint. Progress bar fill is purple accent.
### `screenshots/ssidebar-header.png` (Production app)
Confirms the same layout is already partially implemented but now with production data. Cards show the exact same structure. The status badge values visible are "En cours" and "À faire" (translated). Progress bar is present and labeled "Progression: 50%" or "0%". The avatar circle shows the first letter of the tablo title. Delete icon is in the top-right corner of each card.
**Key observation:** The production screenshot matches the Figma faithfully. The current `TabloProjectCard` template in `tablos.templ` already has the structural skeleton (`.project-card`, `.project-card-top`, `.project-card-title-row`, `.project-date-row`) but is missing: (a) progress bar + percentage, (b) status badge, and (c) the tablo initial in the avatar circle.
---
## TabloCardView Audit
### Current struct (in `backend/templates/discussion_forms.go`)
```go
type TabloCardView struct {
Tablo sqlc.Tablo // ID, UserID, Title, Description, Color, CreatedAt, UpdatedAt
DiscussionUnreadCount int64
}
```
### Current `sqlc.Tablo` struct (in `backend/internal/db/sqlc/models.go`)
```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` is NOT present — it gets added by migration 0010, then sqlc regen adds it to the struct.
### Required additions to `TabloCardView`
| Field | Type | Purpose |
|-------|------|---------|
| `Progress` | `int` | 0100, percentage of done tasks |
| `Status` | `string` | "active" or "archived" — display as badge |
After sqlc regen, `sqlc.Tablo` will gain `Status string`. The `TabloCardView` can either read `card.Tablo.Status` directly or store it as a separate `Status string` field for convenience. Since it mirrors the DB value, reading `card.Tablo.Status` directly in the template is cleaner — no duplication.
`Progress int` cannot come from the tablo row — it must be computed from the tasks table and stored in `TabloCardView`.
### Construction path
`TabloCardsFromUnreadRows` in `discussion_forms.go` builds `TabloCardView` slices from `ListTablosByUserWithDiscussionUnreadRow`. After migration + regen, this function must also map the new `Status` column from the row. `Progress` is filled separately by the handler after a batch query.
---
## Progress Query Recommendation
### Recommended SQL (batch aggregation)
[VERIFIED: tasks.sql — `status` column with `TaskStatusDone = "done"` confirmed]
```sql
-- name: ListTabloProgressByIDs :many
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;
```
This is a single query for all tablos on the dashboard. The handler:
1. Calls `ListTablosByUserWithDiscussionUnread` to get tablo rows.
2. Extracts tablo IDs into a `[]uuid.UUID` slice.
3. Calls `ListTabloProgressByIDs` with that slice.
4. Builds a `map[uuid.UUID]int` of tablo ID → progress %.
5. Assigns `Progress` when constructing each `TabloCardView`.
**Why not per-tablo query:** N+1 — one query per tablo on a page of 20 tablos would issue 20 DB round-trips. The batch approach is one round-trip regardless of count. [VERIFIED: pattern confirmed from codebase — `ListTablosByUserWithDiscussionUnread` already uses this JOIN approach for unread counts]
**Progress formula (per D-05):**
```go
func computeProgress(done, total int) int {
if total == 0 {
return 0
}
return (done * 100) / total
}
```
**sqlc type for `tablo_ids`:** sqlc with pgx/v5 handles `ANY(@tablo_ids::uuid[])` with `pgtype.Array[pgtype.UUID]` or `[]uuid.UUID` depending on sqlc config. The existing codebase uses `pgtype.UUID` / `uuid.UUID` — check `sqlc.yaml` for the override. [ASSUMED: exact pgx array type binding — verify against sqlc.yaml and existing query patterns]
---
## Migration Number Confirmation
[VERIFIED: `ls backend/migrations/`]
Existing migrations:
```
0001_init.sql
0002_auth.sql
0003_tablos.sql
0004_tasks.sql
0005_files.sql
0006_social_identities.sql
0007_etapes.sql
0008_events.sql
0009_discussion.sql
```
**Next migration number: `0010`**
File: `backend/migrations/0010_tablo_status.sql`
```sql
-- migrations/0010_tablo_status.sql
-- Phase 19: Add status column to tablos
-- +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;
```
After running this migration, sqlc must be regenerated:
```bash
cd backend && sqlc generate
```
The `sqlc.Tablo` struct in `models.go` will gain `Status string`. All existing queries (`ListTablosByUser`, `ListTablosByUserWithDiscussionUnread`, `GetTabloByID`, `InsertTablo`, `UpdateTablo`) will automatically include the column in their RETURNING or SELECT * results — but since the queries enumerate columns explicitly, they must be updated to include `status`.
---
## Existing CSS Inventory
[VERIFIED: `backend/internal/web/ui/app.css`]
### Relevant existing classes for card work
| Class | Section | Purpose |
|-------|---------|---------|
| `.project-grid` | §14 | 3-column grid, `gap: 1.25rem` |
| `.project-card` | §15 | Card shell — border, radius 1rem, padding 1rem, hover shadow |
| `.project-card-top` | §15 | Flex row, space-between, `margin-bottom: 1rem` |
| `.project-card-title-row` | §17 | Flex row, gap 0.75rem, `margin-bottom: 1rem` |
| `.project-avatar` | §17 | 3rem × 3rem colored circle, first letter display |
| `.project-date-row` | §18 | Flex row, muted color, `font-size: 0.875rem`, `margin-bottom: 1rem` |
| `.project-progress-track` | §20 | Muted background, `border-radius: 999px`, `height: 0.5rem` |
| `.project-progress-bar` | §20 | Uses `var(--project-color, var(--color-project-fallback))`, fills to width% |
### What is already wired
- `.project-progress-track` and `.project-progress-bar` exist in app.css (§20) but are NOT used in `TabloProjectCard` — they are used in `TabloDetailPage`'s metadata row.
- `.project-avatar` already renders the initial (see `TabloDetailPage` — it shows `string([]rune(tablo.Title)[0:1])`), but `TabloProjectCard` uses `<span class="project-avatar">` without the initial letter. This needs to be added.
### What needs to be added (new CSS)
- `.project-card-progress-row` — flex row wrapping progress label + bar (mirrors `.project-date-row` pattern)
- `.project-card-progress-label` — "Progression: X%" text styling
- Status badge — can reuse `ui.Badge` component or a bespoke `.tablo-status-badge` class
- List view styles — `[data-view="list"] #tablos-list` override to switch from grid to flex-column rows
- List row styles — `.tablo-list-row` for the row layout in list view
### Table CSS (`table.css`)
Minimal: only `.ui-table-shell` (overflow-x auto) and `.ui-table` (border-collapse, min-width). Not directly usable for list view rows. D-12 says "use existing table CSS patterns" — the list view should use `<table>` with `.ui-table` class for consistency, or flex rows following the `.task-row` pattern (§19 in app.css — already used for task rows, similar visual style needed).
**Recommendation (Claude's discretion):** Use a `<table class="ui-table">` for list view — this matches D-12 literally and is accessible. The `.ui-table-shell` wrapper handles overflow.
---
## View Toggle Implementation
### Recommended approach
[VERIFIED: D-03 confirmed — inline JS, no Alpine.js, no persistence]
**HTML structure:**
```html
<div class="overview-section-heading">
<h3>Your Tablos</h3>
<div class="flex items-center gap-2">
<button
class="view-toggle-btn"
onclick="document.getElementById('tablos-list').dataset.view =
document.getElementById('tablos-list').dataset.view === 'list' ? 'grid' : 'list';
this.setAttribute('aria-pressed', this.getAttribute('aria-pressed') === 'true' ? 'false' : 'true')"
aria-pressed="false"
aria-label="Toggle list view"
>
<!-- list icon SVG -->
</button>
<!-- New tablo button -->
</div>
</div>
<div id="tablos-list" data-view="grid" class="project-grid">
...
</div>
```
**CSS toggle rules (append to app.css):**
```css
/* List view override — toggled via data-view attribute */
#tablos-list[data-view="list"] {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#tablos-list[data-view="list"] .project-card {
/* Hide card layout, show as row */
display: none;
}
#tablos-list[data-view="list"] .tablo-list-row {
display: flex; /* shown only in list view */
}
.tablo-list-row {
display: none; /* hidden in grid view */
}
```
**Implementation pattern:** Each `TabloProjectCard` renders BOTH a `.project-card` element AND a `.tablo-list-row` sibling element, with CSS toggling which is visible. This avoids HTMX round-trips to re-render in a different mode and keeps the toggle instant.
Alternatively: render only the card, and use CSS to transform the card layout at `[data-view="list"]`. This is simpler (one element, not two) but harder to get right for the table-row list format specified in D-12.
**Final recommendation (Claude's discretion):** Use the dual-element approach — render `.project-card` and `.tablo-list-row` per tablo, CSS shows one or the other based on `data-view`. This cleanly handles D-12's "table rows" requirement without complex CSS overrides.
---
## Recommended Plan Decomposition
### Plan 1: DB + sqlc + Model Layer
**Scope:** Everything below the template layer.
1. Create `backend/migrations/0010_tablo_status.sql`
2. Run migration locally (`goose up` or `just migrate`)
3. Update all tablo SQL queries in `backend/internal/db/queries/tablos.sql` to include `status` in SELECT/RETURNING lists
4. Add new sqlc query `ListTabloProgressByIDs` in `tablos.sql`
5. Run `sqlc generate` to regenerate `models.go` and query files
6. Add `Progress int` to `TabloCardView` struct in `discussion_forms.go`
7. Update `TabloCardsFromUnreadRows` to map `Status` from the regenerated row type
8. Update `TabloListHandler` (currently `TablosListHandler`) to call `ListTabloProgressByIDs` and populate `Progress` on each card view
**Tests:** Extend `handlers_tablos_test.go` — verify GET / response includes "Progression" string when tasks exist.
### Plan 2: Revamped Card Template + CSS
**Scope:** `TabloProjectCard` template + CSS additions for card + list row.
1. Rewrite `TabloProjectCard` in `tablos.templ`:
- Top row: status badge (left) + delete button (right)
- Title row: colored avatar circle with initial + title
- Date row: calendar icon + formatted date
- Progress row: "Progression: X%" label + progress bar
2. Add `tablo-list-row` sibling element inside `TabloProjectCard` for list view
3. Add CSS to `app.css`:
- `.project-card-progress-row`, `.project-card-progress-label`
- `.tablo-list-row` and its children
- `[data-view="list"]` toggle rules
4. Add the status badge — use `ui.Badge` component with a new `BadgeVariantSubtle` or an inline span with `.tablo-status-badge` class
**Tests:** Template render test — confirm `.project-progress-bar` has correct `width` style when `Progress = 50`.
### Plan 3: Toggle Button + View Toggle Integration
**Scope:** Header UI addition only — toggle button in `TablosDashboard`.
1. Add toggle button to `TablosDashboard` heading row (grid/list icon SVGs)
2. Wire `onclick` JS as described in the View Toggle section
3. Verify CSS `data-view` switching works in both directions
4. Accessibility: `aria-pressed` state on button
**Tests:** Manual smoke test (listed in HUMAN-UAT). Automated: assert toggle button exists in GET / response.
---
## Common Pitfalls
### Pitfall 1: Forgetting to update explicit SELECT column lists after migration
**What goes wrong:** Migration adds `status` to the table, but all existing queries enumerate columns explicitly (`id, user_id, title, description, color, created_at, updated_at`). sqlc will NOT include `status` in the generated struct fields until the queries are updated.
**Why it happens:** sqlc generates types from the SQL, not from the table schema. `SELECT *` would pick it up automatically, but the existing queries use explicit lists.
**How to avoid:** In Plan 1, update EVERY query in `tablos.sql` that has an explicit column list to add `status`. Also update RETURNING clauses in INSERT/UPDATE.
**Warning signs:** After `sqlc generate`, `sqlc.Tablo` still lacks `Status` field.
### Pitfall 2: sqlc array type for `ListTabloProgressByIDs`
**What goes wrong:** `WHERE tablo_id = ANY(@tablo_ids::uuid[])` may require a specific pgx type in the generated Go code. If the array binding fails at runtime, progress will always be 0.
**Why it happens:** sqlc + pgx/v5 array handling requires correct type annotation or sqlc override config.
**How to avoid:** Check `backend/sqlc.yaml` for existing array overrides. Test the query in isolation with a small slice. [ASSUMED: array binding behavior — verify against existing patterns]
**Warning signs:** `ListTabloProgressByIDs` returns 0 rows even when tasks exist.
### Pitfall 3: Progress bar CSS variable vs inline style conflict
**What goes wrong:** `.project-progress-bar` uses `background: var(--project-color, var(--color-project-fallback))`, which makes the bar the same color as the tablo's avatar color. In `TabloDetailPage` this is set via `style="--project-color: X"` on a parent. If the dashboard cards don't set this CSS variable, the bar falls back to `--color-project-fallback`.
**Why it happens:** CSS custom property scoping — the bar color is inherited from an ancestor `style` attribute.
**How to avoid:** Either (a) set `style="--project-color: {{ card.Tablo.Color.String }}"` on `.project-card` when color is valid, or (b) define a separate accent-colored bar class for the dashboard that ignores tablo color (per D-11: "use var(--color-accent)").
**Warning signs:** Progress bar renders in fallback color on cards even when tablo has a color set.
### Pitfall 4: Dual-element list rows break HTMX OOB card insertion
**What goes wrong:** `TabloCardWithOOBFormClear` uses `HX-Retarget: #tablos-list` + `afterbegin` to prepend a new card after creation. If each card now renders two sibling elements (`.project-card` + `.tablo-list-row`), `hx-swap="afterbegin"` on a single fragment still works, but the fragment must be a single root element.
**Why it happens:** templ components return a single root element; rendering two siblings from one component call requires a fragment wrapper `<div>` or `<template>` — but a `<div>` wrapper would break the grid CSS.
**How to avoid:** Wrap the pair in `<article id="tablo-{id}">` as the single root, and put `.project-card` and `.tablo-list-row` inside it. The `id` attribute already exists on `TabloProjectCard`'s root `<article>`. Use `<article id="tablo-{id}" class="tablo-card-wrapper">` as the outer anchor, with CSS making it transparent in the grid.
**Warning signs:** New cards created via HTMX don't toggle correctly, or the grid layout breaks for new cards.
### Pitfall 5: Forgetting `etape_id` column in tasks when computing progress
**What goes wrong:** Tasks with `etape_id IS NULL` (unassigned tasks) should still count toward progress. The batch progress query uses `WHERE tablo_id = ANY(...)` with no etape filter, which is correct — but if a future planner accidentally adds an etape filter, unassigned tasks would be excluded.
**Why it happens:** The etape concept (phases/stages) is a distinct dimension from status. Tasks exist independently of etapes.
**How to avoid:** The recommended SQL is intentionally etape-agnostic. Comment it clearly in the query.
---
## Code Examples
### TabloCardView with new fields
```go
// Source: discussion_forms.go (current) — add Progress field
type TabloCardView struct {
Tablo sqlc.Tablo
DiscussionUnreadCount int64
Progress int // 0100; 0 when no tasks
}
```
### Batch progress query (sqlc)
```sql
-- name: ListTabloProgressByIDs :many
-- Source: D-06 (batch aggregation) + tasks.sql pattern
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;
```
### Handler progress wiring
```go
// After fetching tabloRows and building cardViews:
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", "err", err)
// non-fatal: proceed with 0% progress
}
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]
}
```
### Progress bar in templ
```go
// Source: D-05, D-11, existing .project-progress-track/.project-progress-bar CSS
<div class="project-card-progress-row">
<span class="project-card-progress-label">
Progression: { strconv.Itoa(card.Progress) }%
</span>
<div class="project-progress-track">
<div class="project-progress-bar" style={ "width: " + strconv.Itoa(card.Progress) + "%;" }></div>
</div>
</div>
```
### Status badge in templ
```go
// Uses existing ui.Badge component — BadgeVariantPrimary for "Active" (subtle tint)
@ui.Badge(ui.BadgeProps{Label: card.Tablo.Status, Variant: ui.BadgeVariantPrimary})
```
### View toggle JS (inline onclick)
```html
<button
class="view-toggle-btn"
onclick="var el=document.getElementById('tablos-list');
el.dataset.view = el.dataset.view==='list' ? 'grid' : 'list'"
aria-label="Toggle list view"
type="button"
>
<!-- list SVG icon -->
</button>
```
### Migration file
```sql
-- backend/migrations/0010_tablo_status.sql
-- +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;
```
---
## Assumptions Log
| # | Claim | Section | Risk if Wrong |
|---|-------|---------|---------------|
| A1 | sqlc `ANY(@tablo_ids::uuid[])` works with `[]uuid.UUID` in this pgx/v5 config without additional type override in sqlc.yaml | Progress Query | Build error or runtime panic on progress query |
| A2 | `ui.Badge` accepts arbitrary `Label` string values (e.g., "active") without hardcoded whitelist | Code Examples | Badge renders blank or panics |
| A3 | `BadgeVariantPrimary` is the correct variant for a subtle active indicator (not danger/warning) | Code Examples | Wrong color for status badge |
---
## Open Questions
1. **sqlc.yaml array override for `uuid[]`**
- What we know: pgx/v5 with sqlc requires type overrides for PostgreSQL arrays in some configurations.
- What's unclear: whether the existing `sqlc.yaml` has a `uuid[]` override that makes `ANY(@tablo_ids::uuid[])` compile correctly.
- Recommendation: Plan 1 executor must check `backend/sqlc.yaml` before writing the new query.
2. **`ui.Badge` label casing**
- What we know: the badge label in the screenshot is capitalized ("En cours", "Active").
- What's unclear: should the DB value ("active") be title-cased in the template or stored as-is?
- Recommendation: Title-case in the template with a simple helper — `strings.Title(card.Tablo.Status)` — rather than storing "Active" in the DB.
---
## Environment Availability
Step 2.6: SKIPPED — phase is CSS/Go/SQL changes within the existing backend; no new external tools required. `goose` and `sqlc` are assumed to be available based on existing migration files and generated sqlc output in the repo.
---
## Validation Architecture
### Test Framework
| Property | Value |
|----------|-------|
| Framework | Go testing + httptest (existing) |
| Config file | none — `go test ./...` from `backend/` |
| Quick run command | `cd backend && go test ./internal/web/... -run TestTablos -v` |
| Full suite command | `cd backend && go test ./...` |
### Phase Requirements → Test Map
| Req ID | Behavior | Test Type | Automated Command | File Exists? |
|--------|----------|-----------|-------------------|-------------|
| LIST-01 | GET / response includes progress bar HTML with correct % | integration | `go test ./internal/web/... -run TestTablosList` | ✅ `handlers_tablos_test.go` |
| LIST-02 | GET / response includes toggle button; `data-view="grid"` on container | integration | `go test ./internal/web/... -run TestTablosList` | ✅ `handlers_tablos_test.go` |
| LIST-03 | GET / response includes status badge element | integration | `go test ./internal/web/... -run TestTablosList` | ✅ `handlers_tablos_test.go` |
### Wave 0 Gaps
- [ ] Add test cases to `handlers_tablos_test.go` covering LIST-01 (progress bar), LIST-02 (toggle button exists), LIST-03 (status badge) — existing file needs new test functions, not a new file.
---
## Security Domain
### Applicable ASVS Categories
| ASVS Category | Applies | Standard Control |
|---------------|---------|-----------------|
| V2 Authentication | no | — |
| V3 Session Management | no | — |
| V4 Access Control | no | existing `loadOwnedTablo` enforces ownership |
| V5 Input Validation | no | no new user input — status is DB-only default |
| V6 Cryptography | no | — |
No new user-facing input surfaces. The `status` column has a DB-level CHECK constraint (`IN ('active', 'archived')`) that prevents invalid values. The progress query uses parameterized `ANY(@tablo_ids)` — no SQL injection surface. [VERIFIED: existing codebase uses sqlc parameterized queries throughout]
---
## Sources
### Primary (HIGH confidence)
- `backend/templates/tablos.templ` — current template structure read directly
- `backend/templates/discussion_forms.go``TabloCardView` struct definition, confirmed fields
- `backend/internal/db/sqlc/models.go``sqlc.Tablo` struct, confirmed no `Status` field
- `backend/internal/web/handlers_tablos.go``TablosDashboardHandler`, query call chain
- `backend/internal/db/queries/tablos.sql` — all tablo queries, confirmed no progress query
- `backend/internal/db/queries/tasks.sql` — task status values confirmed (`done`)
- `backend/internal/web/ui/app.css` — full CSS inventory, all card classes documented
- `backend/internal/web/ui/table.css` — table CSS confirmed minimal
- `backend/migrations/` listing — confirmed highest existing number is `0009`
- `screenshots/Homepage.png` — Figma card design read as image
- `screenshots/ssidebar-header.png` — production app screenshot read as image
- `.planning/phases/19-tablo-list-revamp/19-CONTEXT.md` — all locked decisions confirmed
### Secondary (MEDIUM confidence)
- sqlc documentation pattern for `ANY(@param::uuid[])` — [ASSUMED: A1 above]
---
## Metadata
**Confidence breakdown:**
- Standard stack: HIGH — all libraries already in the codebase, no new dependencies
- Architecture: HIGH — all file paths verified, struct fields confirmed by direct read
- Pitfalls: HIGH — based on direct code inspection of existing patterns
- Progress SQL: MEDIUM — query logic is sound; array binding (A1) needs confirmation in sqlc.yaml
**Research date:** 2026-05-17
**Valid until:** Stable — no external dependencies; valid until codebase changes