docs(19): create phase 19 tablo list revamp plans (3 plans, 3 waves)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Arthur Belleville 2026-05-17 16:22:30 +02:00
parent 28e05b5fc1
commit c3b470a1a7
No known key found for this signature in database
4 changed files with 1052 additions and 1 deletions

View file

@ -70,12 +70,18 @@ Plans:
### Phase 19: Tablo List Revamp
**Goal:** Restyle the tablos page with revamped cards, real progress data, list/card toggle, and status field.
**Requirements:** LIST-01, LIST-02, LIST-03
**Plans:** 3 plans
**Success criteria:**
1. Tablo cards display with updated Figma layout including a progress bar showing real task completion %
2. User can switch between card grid and list view; selection persists for the session
3. Tablos have an active/archived status field in DB; a status indicator is visible on cards and list rows
4. DB migration for status field is reversible
Plans:
- [ ] 19-01-PLAN.md — DB migration 0010, sqlc regen with status column, batch progress query, handler wiring
- [ ] 19-02-PLAN.md — Revamped TabloProjectCard template (badge, initial, progress bar) + list row CSS
- [ ] 19-03-PLAN.md — View toggle button + inline JS + tests for LIST-01/02/03
### Phase 20: Tablo Detail & Kanban Restyle
**Goal:** Restyle the tablo detail page and kanban board to match Figma.
**Requirements:** DETAIL-01, TASK-01
@ -125,7 +131,7 @@ Plans:
| 16. Tablo Detail | v3.0 | 4/4 | Complete | 2026-05-17 |
| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 |
| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — |
| 19. Tablo List Revamp | v4.0 | | Pending | — |
| 19. Tablo List Revamp | v4.0 | 0/3 | Pending | — |
| 20. Tablo Detail & Kanban | v4.0 | — | Pending | — |
| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |
| 22. Calendar Rework | v4.0 | — | Pending | — |

View file

@ -0,0 +1,338 @@
---
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 (0100) 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 // 0100; 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>

View file

@ -0,0 +1,370 @@
---
phase: 19-tablo-list-revamp
plan: "02"
type: execute
wave: 2
depends_on: ["19-01"]
files_modified:
- backend/templates/tablos.templ
- backend/internal/web/ui/app.css
- backend/static/tailwind.css # regenerated by tailwindcss CLI
- backend/templates/tablos_templ.go # regenerated by templ generate
autonomous: true
requirements: [LIST-01, LIST-03]
must_haves:
truths:
- "TabloProjectCard shows a status badge (top-left) and delete button (top-right)"
- "TabloProjectCard shows the tablo initial letter inside the colored avatar circle"
- "TabloProjectCard shows a progress bar filled to card.Progress % with label 'Progression: X%'"
- "Each tablo card renders both a .project-card element and a .tablo-list-row sibling"
- "app.css contains .tablo-list-row, .project-card-progress-row, and [data-view='list'] CSS rules"
- "go build and templ generate both exit 0 after changes"
artifacts:
- path: "backend/templates/tablos.templ"
provides: "Revamped TabloProjectCard matching production screenshot"
contains: "project-card-progress-row"
- path: "backend/internal/web/ui/app.css"
provides: "Progress row CSS + list row CSS + data-view toggle rules"
contains: "tablo-list-row"
- path: "backend/static/tailwind.css"
provides: "Regenerated Tailwind output"
contains: "tablo-list-row"
key_links:
- from: "TabloProjectCard template"
to: "card.Progress (int, 0100)"
via: "inline style width: X% on .project-progress-bar"
pattern: "project-progress-bar"
- from: "TabloProjectCard template"
to: "card.Tablo.Status (string)"
via: "ui.Badge label"
pattern: "ui.Badge"
- from: ".project-card-progress-row CSS"
to: ".project-progress-track / .project-progress-bar"
via: "nested child selectors in app.css §20"
pattern: "project-progress-track"
---
<objective>
Rebuild `TabloProjectCard` to match the production screenshot design — status badge, colored avatar initial, title, date, progress bar — and add both the new card CSS and the dual-element list row foundation.
Purpose: Delivers LIST-01 and the visual part of LIST-03. Plan 03 adds the toggle button that switches between the two elements rendered here.
Output: Revamped `TabloProjectCard` template + new CSS classes in app.css + regenerated tailwind.css.
</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
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-01-SUMMARY.md
Read before making visual decisions:
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/Homepage.png
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/ssidebar-header.png
</context>
<interfaces>
<!-- Key contracts the executor needs. Extracted from codebase. -->
From backend/templates/discussion_forms.go (after Plan 01):
```go
type TabloCardView struct {
Tablo sqlc.Tablo // .ID, .Title, .Color (pgtype.Text), .CreatedAt (pgtype.Timestamptz), .Status (string)
DiscussionUnreadCount int64
Progress int // 0100
}
```
From backend/internal/web/ui/variants.go:
```go
type BadgeVariant string
const (
BadgeVariantInfo BadgeVariant = "info"
BadgeVariantWarning BadgeVariant = "warning"
BadgeVariantSuccess BadgeVariant = "success"
BadgeVariantDanger BadgeVariant = "danger"
BadgeVariantPrimary BadgeVariant = "primary" // ← use for "Active" status badge
)
```
From backend/internal/web/ui/badge.templ:
```go
type BadgeProps struct {
Label string
Variant BadgeVariant
}
templ Badge(props BadgeProps) { ... }
// Usage: @ui.Badge(ui.BadgeProps{Label: "Active", Variant: ui.BadgeVariantPrimary})
```
From backend/templates/tablos.templ (current TabloProjectCard — root element is <article>):
```go
// Current root: <article id={"tablo-" + card.Tablo.ID.String()} class="project-card">
// HTMX OOB: TabloCardWithOOBFormClear calls TabloCard (NOT TabloProjectCard) for new tablo insertion.
// TabloProjectCard is rendered in the dashboard grid only.
// Safe to change its root element to a wrapper article with class="tablo-card-wrapper".
```
From backend/internal/web/ui/app.css (existing CSS — do NOT remove or rename):
```css
/* §14 */ .project-grid — 3-column grid, gap 1.25rem
/* §15 */ .project-card — card shell (border, radius 1rem, padding 1rem)
/* §16 */ .project-card-top — flex space-between, margin-bottom 1rem
/* §17 */ .project-card-title-row — flex, gap 0.75rem, margin-bottom 1rem
/* §17 */ .project-avatar — 3rem×3rem colored circle, background: var(--project-color, fallback)
/* §18 */ .project-date-row — flex, muted color, font-size 0.875rem, margin-bottom 1rem
/* §20 */ .project-progress-track — muted bg, border-radius 999px, height 0.5rem
/* §20 */ .project-progress-bar — background: var(--project-color, fallback), border-radius 999px, height 100%
```
Progress bar color note (Pitfall 3 from RESEARCH.md):
- .project-progress-bar uses var(--project-color, var(--color-project-fallback)).
- Per D-11: use var(--color-accent) for the progress bar on cards. Add a new class
.project-card-progress-bar that overrides background to var(--color-accent) instead
of var(--project-color). Use this class (not .project-progress-bar) on dashboard cards.
- Alternatively, set style="--project-color: var(--color-accent)" on the .project-card
wrapper. Either approach is acceptable.
Initial letter pattern (from TabloDetailPage):
```go
// Confirmed pattern for avatar initial:
if card.Tablo.Color.Valid && card.Tablo.Color.String != "" {
<span class="project-avatar" style={ "background-color: " + card.Tablo.Color.String }>
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
} else {
<span class="project-avatar">
if len(card.Tablo.Title) > 0 {
{ string([]rune(card.Tablo.Title)[0:1]) }
}
</span>
}
```
String import for Itoa (already in discussion_forms.go — NOT in tablos.templ yet):
```go
// tablos.templ needs: import "strconv" for strconv.Itoa(card.Progress)
// Add "strconv" to the import block in tablos.templ.
// strings.Title is deprecated — use strings.ToUpper(card.Tablo.Status[:1]) +
// card.Tablo.Status[1:] for title-casing, OR just pass "Active" as the badge label
// directly since Phase 19 only shows 'active' status. Recommended: title-case helper.
```
Tailwind rebuild command:
```bash
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Rebuild TabloProjectCard template with dual card+row output</name>
<files>
backend/templates/tablos.templ
</files>
<action>
Read the production screenshot at screenshots/ssidebar-header.png before editing.
The target layout (per D-10): status badge top-left, delete icon top-right, colored
avatar circle with initial letter, title, creation date with calendar icon,
"Progression: X%" label + progress bar.
Rewrite the TabloProjectCard component in backend/templates/tablos.templ.
1. Add "strconv" and "strings" to the import block at the top of tablos.templ.
2. Replace the current TabloProjectCard body. The new structure:
a. Outer wrapper article (same id, new class "tablo-card-wrapper") containing
both the .project-card div and the .tablo-list-row div as siblings.
This satisfies the dual-element approach (see RESEARCH.md pitfall 4) so that
the HTMX OOB fragment remains a single root element.
b. Inside .project-card:
- .project-card-top: left side = status badge, right side = delete icon button
- .project-card-title-row: avatar circle (with initial letter), title h4
- .project-date-row: calendar SVG icon + formatted date
- .project-card-progress-row: "Progression: X%" span + .project-progress-track
containing .project-card-progress-bar (see Pitfall 3 note in interfaces)
c. Inside .tablo-list-row (hidden by default via CSS, shown in list view):
- Status badge
- Title span
- Date span
- Progress label "X%"
- Delete icon button
(Full table-row layout is styled in Plan 03's CSS additions. This task
renders the HTML structure; Plan 03 finalizes the toggle mechanism.)
For the status badge label: title-case the Status string. Since strings.Title is
deprecated, use a local helper in the template:
{ strings.ToUpper(card.Tablo.Status[:1]) + card.Tablo.Status[1:] }
Guard against empty string: if len(card.Tablo.Status) > 0.
For the progress bar: use class "project-card-progress-bar" (a new CSS class
defined in Task 2 that uses var(--color-accent) instead of var(--project-color)).
Set inline style for width: { strconv.Itoa(card.Progress) + "%" }
For the delete icon button: keep the existing @ui.IconButton call with hx-get,
hx-target, hx-swap identical to the current implementation.
The edit-title pencil icon button from the current implementation can be removed
per the production screenshot — the design shows only the delete button top-right.
Do NOT rename or remove any other templ components in this file.
3. Run templ generate:
cd backend && templ generate ./...
4. Run go build ./... to confirm the file compiles.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1</automated>
</verify>
<done>
- TabloProjectCard renders an outer article.tablo-card-wrapper containing .project-card and .tablo-list-row siblings
- .project-card-top has status badge on left and delete button on right
- .project-card-title-row has avatar with initial letter
- .project-card-progress-row has "Progression: X%" label and .project-card-progress-bar
- `templ generate` exits 0
- `go build ./...` exits 0
</done>
</task>
<task type="auto">
<name>Task 2: Add progress-row and list-row CSS to app.css, rebuild Tailwind</name>
<files>
backend/internal/web/ui/app.css
backend/static/tailwind.css
</files>
<action>
Edit backend/internal/web/ui/app.css only (never edit static/tailwind.css directly).
Append a new CSS section after Section 20 (progress track/bar). Call it
"Section 20b — Dashboard card progress row and list-row":
1. .project-card-progress-row — wrapper for the progress label + bar on cards:
display: flex; flex-direction: column; gap: 0.35rem;
2. .project-card-progress-label — "Progression: X%" text:
color: var(--color-text-muted); font-size: 0.8rem;
3. .project-card-progress-bar — accent-colored bar for dashboard cards (overrides
the project-color var used in .project-progress-bar):
background: var(--color-accent);
border-radius: 999px;
height: 100%;
4. .tablo-card-wrapper — transparent grid item wrapper (so the grid layout still
works when each slot contains a wrapper instead of a bare .project-card):
display: contents;
/* display:contents makes the wrapper invisible to layout — children
(.project-card and .tablo-list-row) participate in the grid directly */
5. .tablo-list-row — row layout for list view (hidden by default):
display: none;
align-items: center;
gap: 1rem;
padding: 0.75rem 1rem;
border: 1px solid var(--color-border-subtle);
border-radius: 0.75rem;
background: var(--color-surface-default);
6. .tablo-list-row-title — flex 1, font-weight 500
7. .tablo-list-row-meta — muted color, font-size 0.875rem, flex items gap 0.5rem
8. [data-view="list"] toggle rules (append at the end of the new section):
#tablos-list[data-view="list"] {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
#tablos-list[data-view="list"] .project-card {
display: none;
}
#tablos-list[data-view="list"] .tablo-list-row {
display: flex;
}
Note on display:contents and list view: when data-view="list", the
#tablos-list switches from grid to flex-column. The .tablo-card-wrapper
wrappers (display:contents) are transparent to both layouts, so the switch
works without overriding the wrapper.
After editing app.css, rebuild Tailwind:
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && grep -c "tablo-list-row" internal/web/ui/app.css && grep -c "project-card-progress-row" internal/web/ui/app.css && grep -c "tablo-list-row" static/tailwind.css</automated>
</verify>
<done>
- app.css contains .tablo-card-wrapper (display: contents), .tablo-list-row, .project-card-progress-row, .project-card-progress-bar, and [data-view="list"] toggle rules
- static/tailwind.css is regenerated and contains "tablo-list-row"
- `go build ./...` still exits 0 after CSS changes
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Template → DB value (Status) | Status string from DB is rendered as badge label; templ auto-escapes |
| Template → Progress int | Integer 0100 from handler; rendered as inline style width and as text |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-19-02-01 | Information Disclosure | Status badge label | accept | Status value comes only from the authenticated user's own tablos; no cross-user leakage |
| T-19-02-02 | Tampering | Progress inline style | accept | Progress is server-computed int (0100); rendered as `width: X%` via strconv.Itoa — no user input in path |
| T-19-02-03 | Elevation of Privilege | Delete button HTMX target | accept | hx-get /tablos/{id}/delete-confirm requires server-side ownership check (loadOwnedTablo); client-side rendering does not bypass this |
</threat_model>
<verification>
After both tasks complete:
```bash
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
templ generate ./...
go build ./...
go test ./internal/web/... -run TestTablosDashboard_ProjectCards -v
grep -c "project-card-progress-row" internal/web/ui/app.css
grep -c "tablo-list-row" internal/web/ui/app.css
grep -c "tablo-card-wrapper" internal/web/ui/app.css
grep -c "tablo-list-row" static/tailwind.css
```
Expected: all greps return 1+; build exits 0; test passes or skips (no test DB).
</verification>
<success_criteria>
- `grep -c "project-card-progress-row" backend/internal/web/ui/app.css` returns 1
- `grep -c "tablo-list-row" backend/internal/web/ui/app.css` returns 1
- `grep -c "tablo-card-wrapper" backend/internal/web/ui/app.css` returns 1
- `grep -c "tablo-list-row" backend/static/tailwind.css` returns 1
- `grep -c "project-card-progress-row" backend/templates/tablos.templ` returns 1
- `grep -c "ui.Badge" backend/templates/tablos.templ` returns 1 (status badge)
- `cd backend && go build ./...` exits 0
- Visual: GET / shows cards with status badge, avatar initial, progress bar filled to correct %
</success_criteria>
<output>
After completion, create `/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-02-SUMMARY.md` following the summary template.
</output>

View file

@ -0,0 +1,337 @@
---
phase: 19-tablo-list-revamp
plan: "03"
type: execute
wave: 3
depends_on: ["19-01", "19-02"]
files_modified:
- backend/templates/tablos.templ
- backend/internal/web/ui/app.css
- backend/static/tailwind.css # regenerated by tailwindcss CLI
- backend/templates/tablos_templ.go # regenerated by templ generate
- backend/internal/web/handlers_tablos_test.go
autonomous: true
requirements: [LIST-01, LIST-02, LIST-03]
must_haves:
truths:
- "GET / renders a view-toggle button in the .overview-section-heading"
- "The toggle button click sets data-view='list' on #tablos-list and back to 'grid'"
- "In grid view, .project-card elements are visible and .tablo-list-row elements are hidden"
- "In list view, .tablo-list-row elements are visible and .project-card elements are hidden"
- "View resets to grid on page reload (no persistence per D-03)"
- "Test assertions for LIST-01/LIST-02/LIST-03 pass (or skip, not fail)"
artifacts:
- path: "backend/templates/tablos.templ"
provides: "View toggle button in TablosDashboard with inline onclick JS"
contains: "view-toggle-btn"
- path: "backend/internal/web/handlers_tablos_test.go"
provides: "Tests for progress bar, toggle button, and status badge presence"
contains: "TestTablosDashboard_ProgressBar"
- path: "backend/static/tailwind.css"
provides: "Rebuilt tailwind output containing view-toggle-btn"
contains: "view-toggle-btn"
key_links:
- from: "Toggle button onclick JS"
to: "#tablos-list element's data-view attribute"
via: "document.getElementById('tablos-list').dataset.view"
pattern: "dataset.view"
- from: "[data-view='list'] CSS rule"
to: ".tablo-list-row visibility"
via: "display: flex toggle in app.css"
pattern: "data-view.*list"
---
<objective>
Add the list/grid view toggle button to `TablosDashboard`, wire the inline JS, finalize list row CSS, and add automated test coverage for LIST-01/02/03.
Purpose: Completes the user-visible toggle feature (LIST-02) and adds test coverage for all three LIST requirements. The app ships Phase 19 after this plan.
Output: Toggle button in template, view-toggle-btn CSS, automated tests, rebuilt tailwind.css.
</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/19-tablo-list-revamp/19-01-SUMMARY.md
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-02-SUMMARY.md
Read before making visual decisions:
@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/ssidebar-header.png
</context>
<interfaces>
<!-- Key contracts the executor needs. Extracted from codebase. -->
From backend/templates/tablos.templ (after Plan 02):
```go
// TablosDashboard current .overview-section-heading block:
// <div class="overview-section-heading">
// <h3>Your Tablos</h3>
// @ui.Button(...) // "New tablo" button
// </div>
// <div id="tablos-list" class="project-grid">...</div>
//
// Must add: toggle button BEFORE "New tablo" button, inside a flex wrapper.
// #tablos-list must gain data-view="grid" attribute.
```
Toggle button HTML pattern (per D-03, D-04 from CONTEXT.md):
```html
<!-- Inline onclick JS: no Alpine.js, no localStorage, resets to grid on reload -->
<button
type="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"
>
<!-- list-view SVG icon (4 horizontal lines) -->
</button>
```
In templ syntax, multiline onclick via templ.Attributes:
```go
@ui.IconButton(ui.IconButtonProps{
Label: "Toggle list view",
Icon: "list", // check if "list" icon exists in ui package
Variant: ui.IconButtonVariantNeutral,
Tone: ui.IconButtonToneGhost,
Type: "button",
Attrs: templ.Attributes{
"onclick": "var el=document.getElementById('tablos-list');el.dataset.view=el.dataset.view==='list'?'grid':'list'",
},
})
// If "list" icon is not in the ui icon set, use a plain <button> with inline SVG instead.
```
From backend/internal/web/handlers_tablos_test.go (existing test patterns):
```go
// Existing test pattern — all tests follow this shape:
func TestTablosDashboard_X(t *testing.T) {
pool, cleanup := setupTestDB(t)
defer cleanup()
ctx := context.Background()
q := sqlc.New(pool)
store := auth.NewStore(q)
router := newTabloTestRouter(q, store)
user := preInsertUser(t, ctx, q, "email@example.com", "password")
// optional: insert tablos via q.InsertTablo(...)
cookieVal, _, err := store.Create(ctx, user.ID)
// ... set cookie, make GET / request, check body contains string ...
}
// Tests skip automatically when no test DB is available (setupTestDB fails fast).
// New tests MUST use unique email addresses not used by existing tests.
```
From backend/internal/web/ui — check if "list" icon is available:
```go
// Verify by checking backend/internal/web/ui/icons.go or the icon render function.
// If no "list" icon: use a plain <button type="button" class="view-toggle-btn"> with
// an inline SVG (4 horizontal lines, 16×16 viewBox). Do not add new icons to the ui
// package in this plan — use an inline SVG in tablos.templ instead.
```
Tailwind rebuild command:
```bash
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Add view toggle button to TablosDashboard + data-view attribute on container</name>
<files>
backend/templates/tablos.templ
backend/internal/web/ui/app.css
backend/static/tailwind.css
</files>
<action>
In backend/templates/tablos.templ, modify the TablosDashboard component:
1. Add data-view="grid" to the #tablos-list div:
Change: &lt;div id="tablos-list" class="project-grid"&gt;
To: &lt;div id="tablos-list" class="project-grid" data-view="grid"&gt;
2. Wrap the heading row content in a flex div so the toggle button and "New tablo"
button sit together on the right side:
Change the .overview-section-heading inner structure from:
<h3>Your Tablos</h3>
@ui.Button(...)
To:
<h3>Your Tablos</h3>
<div class="flex items-center gap-2">
<button type="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">
<!-- inline SVG: 4 horizontal lines (list icon), 16×16, stroke="currentColor" -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"
fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<line x1="8" x2="21" y1="6" y2="6"></line>
<line x1="8" x2="21" y1="12" y2="12"></line>
<line x1="8" x2="21" y1="18" y2="18"></line>
<line x1="3" x2="3.01" y1="6" y2="6"></line>
<line x1="3" x2="3.01" y1="12" y2="12"></line>
<line x1="3" x2="3.01" y1="18" y2="18"></line>
</svg>
</button>
@ui.Button(...) // existing "New tablo" button, unchanged
</div>
Note: Use a plain <button> with inline SVG rather than ui.IconButton to avoid
checking icon availability. The onclick attribute is single-line JavaScript; it
is safe for templ's attribute handling.
In backend/internal/web/ui/app.css, append a new section after Section 20b:
/* ============================================================
Section 20c — View toggle button
============================================================ */
.view-toggle-btn {
align-items: center;
background: transparent;
border: 1px solid var(--color-border-subtle);
border-radius: 0.5rem;
color: var(--color-text-secondary);
cursor: pointer;
display: inline-flex;
height: 2rem;
justify-content: center;
padding: 0;
transition: background-color 0.15s ease, color 0.15s ease;
width: 2rem;
}
.view-toggle-btn:hover {
background: var(--color-surface-neutral-hover);
color: var(--color-text-primary);
}
After editing, rebuild Tailwind:
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
./bin/tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
Run templ generate and go build to verify:
cd backend && templ generate ./... && go build ./...
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... && grep -c "view-toggle-btn" internal/web/ui/app.css && grep -c "view-toggle-btn" static/tailwind.css 2>&1</automated>
</verify>
<done>
- TablosDashboard has a toggle button with class view-toggle-btn and the inline onclick JS
- #tablos-list has data-view="grid" as a static HTML attribute
- app.css has .view-toggle-btn CSS block
- static/tailwind.css contains "view-toggle-btn"
- `templ generate ./...` and `go build ./...` both exit 0
</done>
</task>
<task type="auto">
<name>Task 2: Add automated tests for LIST-01, LIST-02, LIST-03</name>
<files>
backend/internal/web/handlers_tablos_test.go
</files>
<action>
Add three new test functions to the end of
backend/internal/web/handlers_tablos_test.go. Each follows the existing
pattern (setupTestDB, preInsertUser, router, GET /, check body).
Use these unique email addresses (not used by existing tests):
- "progressbar@example.com" — LIST-01 test
- "viewtoggle@example.com" — LIST-02 test
- "statusbadge@example.com" — LIST-03 test
Test 1 — TestTablosDashboard_ProgressBar (LIST-01):
Insert a tablo for the user. GET /. Assert body contains "Progression:".
The progress bar label "Progression:" must be present in the rendered HTML.
Also assert body contains "project-card-progress-row".
Test 2 — TestTablosDashboard_ViewToggle (LIST-02):
GET /. Assert body contains "view-toggle-btn".
Assert body contains `data-view="grid"` (the initial state attribute on #tablos-list).
Test 3 — TestTablosDashboard_StatusBadge (LIST-03):
Insert a tablo. GET /. Assert body contains "Active" (the capitalized badge label
rendered from the "active" DB default). Also assert body contains "tablo-list-row"
(confirming the dual-element structure is rendered).
Comment each test with the requirement ID it covers (e.g., // LIST-01: progress bar).
Do not modify any existing tests. Append only.
After adding tests, run:
cd backend && go test ./internal/web/... -run "TestTablosDashboard" -v
Tests will PASS when connected to a test DB, or SKIP/fail-fast when no DB is
available. The important thing is that the file compiles and go build ./... exits 0.
</action>
<verify>
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... && grep -c "TestTablosDashboard_ProgressBar\|TestTablosDashboard_ViewToggle\|TestTablosDashboard_StatusBadge" internal/web/handlers_tablos_test.go 2>&1</automated>
</verify>
<done>
- handlers_tablos_test.go contains TestTablosDashboard_ProgressBar, TestTablosDashboard_ViewToggle, TestTablosDashboard_StatusBadge
- `go build ./...` exits 0 (file compiles)
- The three new test functions assert "Progression:", "view-toggle-btn" / data-view, and "Active" / "tablo-list-row" respectively
</done>
</task>
</tasks>
<threat_model>
## Trust Boundaries
| Boundary | Description |
|----------|-------------|
| Browser JS → DOM | Toggle onclick manipulates data-view attribute; no server round-trip |
| Template attribute | data-view="grid" is a static server-rendered attribute; no user-controlled value |
## STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-19-03-01 | Tampering | Toggle onclick JS | accept | JS only toggles a CSS data-attribute on a client-side DOM node; no data is written to the server; cannot be used to bypass auth or modify data |
| T-19-03-02 | Information Disclosure | List view rows | accept | Same data as card view; all tablos already visible in grid view; list view exposes no additional data |
| T-19-03-03 | Spoofing | data-view attribute | accept | Client-side attribute only; server always renders the canonical data; no server state changes |
</threat_model>
<verification>
After both tasks complete:
```bash
cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend
templ generate ./...
go build ./...
go test ./internal/web/... -run "TestTablosDashboard" -v
grep -c "view-toggle-btn" internal/web/ui/app.css
grep -c "view-toggle-btn" static/tailwind.css
grep -c "view-toggle-btn" templates/tablos.templ
grep -c "TestTablosDashboard_ProgressBar" internal/web/handlers_tablos_test.go
grep -c "TestTablosDashboard_ViewToggle" internal/web/handlers_tablos_test.go
grep -c "TestTablosDashboard_StatusBadge" internal/web/handlers_tablos_test.go
```
Expected: all greps return 1+; go build exits 0; tests pass or skip (not compile-error).
</verification>
<success_criteria>
- `grep -c "view-toggle-btn" backend/templates/tablos.templ` returns 1
- `grep -c "data-view" backend/templates/tablos.templ` returns 1 (static attribute on #tablos-list)
- `grep -c "view-toggle-btn" backend/static/tailwind.css` returns 1
- `grep -c "TestTablosDashboard_ProgressBar" backend/internal/web/handlers_tablos_test.go` returns 1
- `grep -c "TestTablosDashboard_ViewToggle" backend/internal/web/handlers_tablos_test.go` returns 1
- `grep -c "TestTablosDashboard_StatusBadge" backend/internal/web/handlers_tablos_test.go` returns 1
- `cd backend && go build ./...` exits 0
- Browser: clicking the toggle button hides cards and shows list rows; clicking again reverses; page reload resets to grid
</success_criteria>
<output>
After completion, create `/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.planning/phases/19-tablo-list-revamp/19-03-SUMMARY.md` following the summary template.
</output>