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
|
||||
**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 | — |
|
||||
|
|
|
|||
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