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:
parent
28e05b5fc1
commit
c3b470a1a7
4 changed files with 1052 additions and 1 deletions
|
|
@ -70,12 +70,18 @@ Plans:
|
||||||
### Phase 19: Tablo List Revamp
|
### Phase 19: Tablo List Revamp
|
||||||
**Goal:** Restyle the tablos page with revamped cards, real progress data, list/card toggle, and status field.
|
**Goal:** Restyle the tablos page with revamped cards, real progress data, list/card toggle, and status field.
|
||||||
**Requirements:** LIST-01, LIST-02, LIST-03
|
**Requirements:** LIST-01, LIST-02, LIST-03
|
||||||
|
**Plans:** 3 plans
|
||||||
**Success criteria:**
|
**Success criteria:**
|
||||||
1. Tablo cards display with updated Figma layout including a progress bar showing real task completion %
|
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
|
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
|
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
|
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
|
### Phase 20: Tablo Detail & Kanban Restyle
|
||||||
**Goal:** Restyle the tablo detail page and kanban board to match Figma.
|
**Goal:** Restyle the tablo detail page and kanban board to match Figma.
|
||||||
**Requirements:** DETAIL-01, TASK-01
|
**Requirements:** DETAIL-01, TASK-01
|
||||||
|
|
@ -125,7 +131,7 @@ Plans:
|
||||||
| 16. Tablo Detail | v3.0 | 4/4 | Complete | 2026-05-17 |
|
| 16. Tablo Detail | v3.0 | 4/4 | Complete | 2026-05-17 |
|
||||||
| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 |
|
| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 |
|
||||||
| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — |
|
| 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 | — |
|
| 20. Tablo Detail & Kanban | v4.0 | — | Pending | — |
|
||||||
| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |
|
| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |
|
||||||
| 22. Calendar Rework | v4.0 | — | Pending | — |
|
| 22. Calendar Rework | v4.0 | — | Pending | — |
|
||||||
|
|
|
||||||
338
.planning/phases/19-tablo-list-revamp/19-01-PLAN.md
Normal file
338
.planning/phases/19-tablo-list-revamp/19-01-PLAN.md
Normal 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 (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>
|
||||||
370
.planning/phases/19-tablo-list-revamp/19-02-PLAN.md
Normal file
370
.planning/phases/19-tablo-list-revamp/19-02-PLAN.md
Normal 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, 0–100)"
|
||||||
|
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 // 0–100
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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 0–100 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 (0–100); 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>
|
||||||
337
.planning/phases/19-tablo-list-revamp/19-03-PLAN.md
Normal file
337
.planning/phases/19-tablo-list-revamp/19-03-PLAN.md
Normal 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: <div id="tablos-list" class="project-grid">
|
||||||
|
To: <div id="tablos-list" class="project-grid" data-view="grid">
|
||||||
|
|
||||||
|
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>
|
||||||
Loading…
Reference in a new issue