docs(phase-18): complete phase execution — sidebar + header restyle approved
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9ba650b345
commit
3542f3b105
6 changed files with 1365 additions and 13 deletions
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
## v4.0 Active Phases
|
||||
|
||||
- [ ] Phase 18: App Shell & Navigation — sidebar redesign + top header bar (NAV-01, NAV-02)
|
||||
- [x] Phase 18: App Shell & Navigation — sidebar redesign + top header bar (NAV-01, NAV-02) — completed 2026-05-17
|
||||
- [ ] Phase 19: Tablo List Revamp — card redesign, progress bar, status field, list/card toggle (LIST-01, LIST-02, LIST-03)
|
||||
- [ ] Phase 20: Tablo Detail & Kanban — detail page + kanban restyled to Figma (DETAIL-01, TASK-01)
|
||||
- [ ] Phase 21: Task Grid & Roadmap Views — new grid view + new roadmap view with date fields (TASK-02, TASK-03)
|
||||
|
|
@ -56,11 +56,17 @@
|
|||
### Phase 18: App Shell & Navigation
|
||||
**Goal:** Redesign the sidebar and top header bar to match the Figma design.
|
||||
**Requirements:** NAV-01, NAV-02
|
||||
**Plans:** 3 plans
|
||||
**Success criteria:**
|
||||
1. Sidebar renders brand section, icon nav items, tablo list section, and user footer matching Figma
|
||||
2. Every authenticated page shows a top header bar with page title and contextual actions
|
||||
3. Existing navigation functionality (logout, tablo selection) is preserved
|
||||
|
||||
Plans:
|
||||
- [ ] 18-01-PLAN.md — AppLayout signature extension + BreadcrumbItem struct + all call sites updated
|
||||
- [ ] 18-02-PLAN.md — Sidebar full HTML/CSS rebuild to Figma spec with collapse toggle
|
||||
- [ ] 18-03-PLAN.md — PageHeader component, avatar dropdown, /settings stub, tests updated
|
||||
|
||||
### 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
|
||||
|
|
@ -118,7 +124,7 @@
|
|||
| 15. Dashboard & Tablos | v3.0 | 3/3 | Complete | 2026-05-16 |
|
||||
| 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 | — | Pending | — |
|
||||
| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — |
|
||||
| 19. Tablo List Revamp | v4.0 | — | Pending | — |
|
||||
| 20. Tablo Detail & Kanban | v4.0 | — | Pending | — |
|
||||
| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — |
|
||||
|
|
|
|||
|
|
@ -2,15 +2,14 @@
|
|||
gsd_state_version: 1.0
|
||||
milestone: v4.0
|
||||
milestone_name: Figma Design Parity
|
||||
status: planning
|
||||
last_updated: "2026-05-17T12:22:07.603Z"
|
||||
last_activity: 2026-05-17
|
||||
status: Executing
|
||||
last_updated: "2026-05-17T16:05:00.000Z"
|
||||
last_activity: 2026-05-17 — Phase 18 complete (sidebar + header restyle approved)
|
||||
progress:
|
||||
total_phases: 0
|
||||
completed_phases: 0
|
||||
total_plans: 0
|
||||
completed_plans: 0
|
||||
percent: 0
|
||||
total_phases: 5
|
||||
completed_phases: 1
|
||||
total_plans: 3
|
||||
completed_plans: 3
|
||||
---
|
||||
|
||||
# STATE
|
||||
|
|
@ -28,10 +27,10 @@ See: `.planning/PROJECT.md` (updated 2026-05-17)
|
|||
|
||||
## Current Position
|
||||
|
||||
Phase: Not started (defining requirements)
|
||||
Phase: 19 — Tablo List Revamp (next)
|
||||
Plan: —
|
||||
Status: Defining requirements
|
||||
Last activity: 2026-05-17 — Milestone v4.0 started
|
||||
Status: Executing
|
||||
Last activity: 2026-05-17 — Phase 18 complete
|
||||
|
||||
## Previous Milestone Status
|
||||
|
||||
|
|
|
|||
318
.planning/phases/18-app-shell-navigation/18-01-PLAN.md
Normal file
318
.planning/phases/18-app-shell-navigation/18-01-PLAN.md
Normal file
|
|
@ -0,0 +1,318 @@
|
|||
---
|
||||
phase: 18-app-shell-navigation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- backend/templates/app_layout_helpers.go
|
||||
- backend/templates/app_layout.templ
|
||||
- backend/templates/tablos.templ
|
||||
- backend/templates/planning.templ
|
||||
- backend/templates/account_providers.templ
|
||||
- backend/internal/web/handlers_tablos.go
|
||||
- backend/internal/web/handlers_planning.go
|
||||
- backend/internal/web/handlers_account.go
|
||||
- backend/internal/web/handlers_discussion.go
|
||||
- backend/internal/web/handlers_files.go
|
||||
- backend/internal/web/handlers_events.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- NAV-01
|
||||
- NAV-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "The Go codebase compiles with zero errors after AppLayout signature is extended"
|
||||
- "Every authenticated page still renders (no 500 errors) — dashboard, tablo detail, planning, account providers"
|
||||
- "go test ./... passes with no failures"
|
||||
artifacts:
|
||||
- path: "backend/templates/app_layout_helpers.go"
|
||||
provides: "BreadcrumbItem struct export"
|
||||
contains: "type BreadcrumbItem struct"
|
||||
- path: "backend/templates/app_layout.templ"
|
||||
provides: "Extended AppLayout signature with pageTitle, breadcrumb, headerActions"
|
||||
contains: "pageTitle string"
|
||||
- path: "backend/templates/tablos.templ"
|
||||
provides: "TablosDashboard and TabloDetailPage updated with breadcrumb params"
|
||||
key_links:
|
||||
- from: "backend/internal/web/handlers_tablos.go"
|
||||
to: "backend/templates/tablos.templ"
|
||||
via: "TablosDashboard / TabloDetailPage call with breadcrumb args"
|
||||
pattern: "BreadcrumbItem"
|
||||
- from: "backend/templates/tablos.templ"
|
||||
to: "backend/templates/app_layout.templ"
|
||||
via: "AppLayout call with pageTitle and breadcrumb"
|
||||
pattern: "AppLayout.*pageTitle"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Extend AppLayout with three new parameters (pageTitle, breadcrumb, headerActions) and update every call site mechanically so the codebase compiles. No visual changes yet — the new params are threaded through but rendered minimally (pageTitle in the HTML title tag, breadcrumb as plain inline text).
|
||||
|
||||
Purpose: Establishes the Go/templ contracts that Plans 02 and 03 build on. Plans 02 and 03 cannot safely start until this compiles clean.
|
||||
Output: A green compile + passing test suite. All handlers supply correct per-page breadcrumbs.
|
||||
</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>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/REQUIREMENTS.md
|
||||
@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
|
||||
@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Current AppLayout signature (to be replaced) -->
|
||||
<!-- From backend/templates/app_layout.templ line 153 -->
|
||||
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)
|
||||
|
||||
<!-- Current templ call sites that wrap AppLayout -->
|
||||
<!-- tablos.templ line 12 -->
|
||||
templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView)
|
||||
|
||||
<!-- tablos.templ line 241 -->
|
||||
templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo, tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, files []sqlc.TabloFile, events EventsCalendar, discussion DiscussionTabData, activeTab string)
|
||||
|
||||
<!-- planning.templ line 8 -->
|
||||
templ PlanningPage(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, agenda PlanningAgenda)
|
||||
|
||||
<!-- account_providers.templ line 8 -->
|
||||
templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string, activePath string, tablos []sqlc.Tablo)
|
||||
|
||||
<!-- Handler call sites (handlers_tablos.go) -->
|
||||
templates.TablosDashboard(user, csrf.Token(r), "/", sidebarTablos, cardViews)
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", sidebarTablos, tablo, tasks, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "overview")
|
||||
|
||||
<!-- Handler call sites (other handlers) -->
|
||||
templates.PlanningPage(user, csrf.Token(r), "/planning", sidebarTablos, agenda)
|
||||
templates.AccountProvidersPage(user, statuses, csrf.Token(r), "/", sidebarTablos)
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", discussionSidebarTablos, tablo, nil, nil, templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{}, data, "discussion")
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Define BreadcrumbItem and extend AppLayout signature</name>
|
||||
<files>backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ</files>
|
||||
<action>
|
||||
In app_layout_helpers.go, add the BreadcrumbItem struct immediately after the sidebarNavItem type declaration (around line 6):
|
||||
|
||||
// BreadcrumbItem represents one crumb in the top header breadcrumb trail.
|
||||
// If Href is empty, the item renders as plain text (current page — no link).
|
||||
type BreadcrumbItem struct {
|
||||
Label string
|
||||
Href string
|
||||
}
|
||||
|
||||
In app_layout.templ, update the AppLayout signature from:
|
||||
|
||||
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)
|
||||
|
||||
to:
|
||||
|
||||
templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component)
|
||||
|
||||
The templ import for "github.com/a-h/templ" is already pulled in transitively via the ui package usage. If the compiler complains, add it explicitly to the import block at the top of app_layout.templ.
|
||||
|
||||
Inside the AppLayout body, make a minimal placeholder so breadcrumb params are consumed (prevents unused-variable compile errors). Immediately before the DashboardSidebar call or inside the dashboard-main div, add a comment-only breadcrumb stub:
|
||||
|
||||
if len(breadcrumb) > 0 {
|
||||
<!-- breadcrumb: Plan 03 will render this properly -->
|
||||
}
|
||||
if headerActions != nil {
|
||||
@headerActions
|
||||
}
|
||||
|
||||
The nil-guard on headerActions is required (per research Pitfall 6 — rendering nil templ.Component panics).
|
||||
|
||||
Do NOT change any visual output yet. The goal is compiler compatibility only.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&1 | head -20</automated>
|
||||
</verify>
|
||||
<done>app_layout_helpers.go exports BreadcrumbItem struct. app_layout.templ AppLayout signature has 8 parameters. templ generate produces no errors.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Update all templ wrapper signatures and their internal AppLayout calls</name>
|
||||
<files>backend/templates/tablos.templ, backend/templates/planning.templ, backend/templates/account_providers.templ</files>
|
||||
<action>
|
||||
Each templ function that calls AppLayout must (a) accept pageTitle and breadcrumb in its own signature, and (b) pass them through to AppLayout. Update each file as follows.
|
||||
|
||||
--- tablos.templ ---
|
||||
|
||||
TablosDashboard (line 12): Add two params at the end of its signature:
|
||||
templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView, pageTitle string, breadcrumb []BreadcrumbItem)
|
||||
Update its AppLayout call (line 13) to:
|
||||
@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos, pageTitle, breadcrumb, nil)
|
||||
|
||||
TabloDetailPage (line 241): Add two params at the end:
|
||||
templ TabloDetailPage(user *auth.User, csrfToken string, activePath string, sidebarTablos []sqlc.Tablo, tablo sqlc.Tablo, tasks []sqlc.Task, etapes []sqlc.Etape, counts EtapeTaskCounts, filter EtapeFilter, files []sqlc.TabloFile, events EventsCalendar, discussion DiscussionTabData, activeTab string, pageTitle string, breadcrumb []BreadcrumbItem)
|
||||
Update its two AppLayout calls (lines ~242 and ~648) to pass pageTitle, breadcrumb, nil as the last three args.
|
||||
|
||||
The TabloNotFoundPage function near line 648 also wraps AppLayout — update it too. It can pass hardcoded values:
|
||||
@AppLayout("Not found", user, csrfToken, activePath, sidebarTablos, "Not found", nil, nil)
|
||||
|
||||
--- planning.templ ---
|
||||
|
||||
PlanningPage (line 8): Add two params at end:
|
||||
templ PlanningPage(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, agenda PlanningAgenda, pageTitle string, breadcrumb []BreadcrumbItem)
|
||||
Update AppLayout call (line 9) to pass pageTitle, breadcrumb, nil.
|
||||
|
||||
--- account_providers.templ ---
|
||||
|
||||
AccountProvidersPage (line 8): Add two params at end:
|
||||
templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string, activePath string, tablos []sqlc.Tablo, pageTitle string, breadcrumb []BreadcrumbItem)
|
||||
Update AppLayout call (line 9) to pass pageTitle, breadcrumb, nil.
|
||||
|
||||
Keep all other content in each file exactly as-is.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>All three templ files compile without errors. TablosDashboard, TabloDetailPage, PlanningPage, and AccountProvidersPage all accept and pass through pageTitle and breadcrumb.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Update all handler call sites with per-page breadcrumbs</name>
|
||||
<files>backend/internal/web/handlers_tablos.go, backend/internal/web/handlers_planning.go, backend/internal/web/handlers_account.go, backend/internal/web/handlers_discussion.go, backend/internal/web/handlers_files.go, backend/internal/web/handlers_events.go</files>
|
||||
<action>
|
||||
Each handler that calls a templ function must now pass two additional arguments. Use the breadcrumb values below. The BreadcrumbItem type is referenced as templates.BreadcrumbItem in handler files.
|
||||
|
||||
--- handlers_tablos.go ---
|
||||
|
||||
TablosListHandler (line ~60, calls TablosDashboard):
|
||||
templates.TablosDashboard(user, csrf.Token(r), "/", sidebarTablos, cardViews,
|
||||
"Dashboard",
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: ""}},
|
||||
)
|
||||
|
||||
Error fallback call at line ~465 (same handler pattern):
|
||||
templates.TablosDashboard(user, csrf.Token(r), "/", errorSidebarTablos, errorCardViews,
|
||||
"Dashboard",
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: ""}},
|
||||
)
|
||||
|
||||
TabloDetailHandler (line ~225, calls TabloDetailPage):
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", sidebarTablos, tablo, tasks, nil,
|
||||
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{},
|
||||
templates.DiscussionTabData{}, "overview",
|
||||
tablo.Title,
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
|
||||
)
|
||||
|
||||
TabloUpdateHandler (line ~338, calls TabloDetailPage):
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", updateSidebarTablos, tablo, tasks, nil,
|
||||
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{},
|
||||
templates.DiscussionTabData{}, "overview",
|
||||
tablo.Title,
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
|
||||
)
|
||||
|
||||
--- handlers_planning.go ---
|
||||
|
||||
PlanningPageHandler (line ~68, calls PlanningPage):
|
||||
templates.PlanningPage(user, csrf.Token(r), "/planning", sidebarTablos, agenda,
|
||||
"Planning",
|
||||
[]templates.BreadcrumbItem{{Label: "Planning", Href: ""}},
|
||||
)
|
||||
|
||||
--- handlers_account.go ---
|
||||
|
||||
AccountProvidersHandler (line ~49, calls AccountProvidersPage):
|
||||
templates.AccountProvidersPage(user, statuses, csrf.Token(r), "/", sidebarTablos,
|
||||
"Linked Providers",
|
||||
[]templates.BreadcrumbItem{{Label: "Linked Providers", Href: ""}},
|
||||
)
|
||||
|
||||
--- handlers_discussion.go ---
|
||||
|
||||
Discussion handler (line ~78, calls TabloDetailPage):
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", discussionSidebarTablos, tablo, nil, nil,
|
||||
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, templates.EventsCalendar{},
|
||||
data, "discussion",
|
||||
tablo.Title,
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
|
||||
)
|
||||
|
||||
--- handlers_files.go ---
|
||||
|
||||
Files tab handler (line ~106, calls TabloDetailPage):
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", filesSidebarTablos, tablo, nil, nil,
|
||||
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, fileList, templates.EventsCalendar{},
|
||||
templates.DiscussionTabData{}, "files",
|
||||
tablo.Title,
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
|
||||
)
|
||||
|
||||
Tasks-tab-from-files handler (line ~138, calls TabloDetailPage):
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", tasksSidebarTablos, tablo, tasks, etapes,
|
||||
counts, filter, nil, templates.EventsCalendar{}, templates.DiscussionTabData{}, "tasks",
|
||||
tablo.Title,
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
|
||||
)
|
||||
|
||||
--- handlers_events.go ---
|
||||
|
||||
Events tab handler (line ~177, calls TabloDetailPage):
|
||||
templates.TabloDetailPage(user, csrf.Token(r), "", eventsSidebarTablos, tablo, nil, nil,
|
||||
templates.EtapeTaskCounts{}, templates.EtapeFilter{}, nil, calendar,
|
||||
templates.DiscussionTabData{}, "events",
|
||||
tablo.Title,
|
||||
[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}},
|
||||
)
|
||||
|
||||
After all edits, run the full build to confirm zero compile errors:
|
||||
cd backend && go build ./...
|
||||
|
||||
The build MUST succeed before considering this task complete. If any call site is missed, the compiler reports "too many arguments" or "not enough arguments" — fix each reported site.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... && go test ./... 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>go build ./... succeeds with no errors. go test ./... passes. Every authenticated page (dashboard, tablo detail, planning, account providers) renders without 500 errors when the server is started.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Handler → Template | Handlers supply breadcrumb strings — these originate from DB data (tablo.Title) which is user-controlled content rendered via templ's auto-escaping |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-18-01-01 | Tampering | tablo.Title in breadcrumb | accept | templ auto-escapes all string interpolations; no raw HTML insertion |
|
||||
| T-18-01-02 | Denial of Service | templ generate fails mid-compile leaving partial _templ.go files | mitigate | Always run templ generate before go build; check templ output first |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
1. `cd backend && templ generate ./...` — zero errors
|
||||
2. `cd backend && go build ./...` — zero compile errors
|
||||
3. `cd backend && go test ./...` — all tests pass
|
||||
4. Manual smoke: `cd backend && go run . serve` then visit `http://localhost:8080` — dashboard renders without 500
|
||||
5. Verify grep: `grep -r "BreadcrumbItem" backend/templates/app_layout_helpers.go` — shows struct definition
|
||||
6. Verify grep: `grep -c "pageTitle" backend/templates/app_layout.templ` — returns 1 or more (param present)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- BreadcrumbItem struct is exported from backend/templates package
|
||||
- AppLayout has 8 parameters (was 5)
|
||||
- TablosDashboard, TabloDetailPage, PlanningPage, AccountProvidersPage all accept and forward pageTitle + breadcrumb
|
||||
- All 10 handler call sites pass the two new arguments
|
||||
- go build ./... succeeds
|
||||
- go test ./... passes
|
||||
- No authenticated page returns a 500
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md`
|
||||
</output>
|
||||
326
.planning/phases/18-app-shell-navigation/18-02-PLAN.md
Normal file
326
.planning/phases/18-app-shell-navigation/18-02-PLAN.md
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
---
|
||||
phase: 18-app-shell-navigation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 18-01
|
||||
files_modified:
|
||||
- backend/templates/app_layout.templ
|
||||
- backend/templates/app_layout_helpers.go
|
||||
- backend/internal/web/ui/app.css
|
||||
autonomous: true
|
||||
requirements:
|
||||
- NAV-01
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Sidebar shows a GENERAL section label above Home, My Tasks, Projects, Events, Team Members nav items"
|
||||
- "Sidebar shows a PROJECTS section label above the tablo list"
|
||||
- "Chat and Files nav items are removed from the sidebar"
|
||||
- "Clicking the collapse button toggles the sidebar between full-width and icon-only states"
|
||||
- "Sidebar collapse does not trigger a server round-trip — state resets on page reload"
|
||||
- "go build ./... succeeds after sidebar rebuild"
|
||||
artifacts:
|
||||
- path: "backend/templates/app_layout.templ"
|
||||
provides: "Rebuilt DashboardSidebar with GENERAL + PROJECTS section labels and collapse button"
|
||||
contains: "GENERAL"
|
||||
- path: "backend/templates/app_layout_helpers.go"
|
||||
provides: "Updated sidebarPrimaryNavItems with Figma-spec items"
|
||||
contains: "My Tasks"
|
||||
- path: "backend/internal/web/ui/app.css"
|
||||
provides: "is-collapsed CSS rules, sidebar section label styles"
|
||||
contains: "is-collapsed"
|
||||
key_links:
|
||||
- from: "backend/templates/app_layout.templ"
|
||||
to: "backend/internal/web/ui/app.css"
|
||||
via: "is-collapsed class toggled by inline JS, picked up by CSS rule"
|
||||
pattern: "is-collapsed"
|
||||
- from: "backend/templates/app_layout_helpers.go"
|
||||
to: "backend/templates/app_layout.templ"
|
||||
via: "sidebarPrimaryNavItems called in DashboardSidebar"
|
||||
pattern: "sidebarPrimaryNavItems"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Full HTML/CSS rebuild of the sidebar to match the Figma design (Homepage.png, Board.png). Replaces the Phase 15 DashboardSidebar internals with the new two-section structure (GENERAL + PROJECTS), wires the collapse toggle via inline JS, and updates app.css with collapsed-state rules.
|
||||
|
||||
Purpose: Delivers NAV-01. After this plan the sidebar visually matches Figma.
|
||||
Output: Rebuilt DashboardSidebar templ component + updated sidebarPrimaryNavItems + new CSS rules in app.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>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
|
||||
@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
|
||||
@.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Read screenshots/Homepage.png and screenshots/Board.png before implementing visual details -->
|
||||
<!-- They are at /Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/ -->
|
||||
|
||||
<!-- Current DashboardSidebar signature (unchanged in Plan 01) -->
|
||||
templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string)
|
||||
|
||||
<!-- Existing CSS classes to preserve (used in existing tests) -->
|
||||
.dashboard-sidebar — outer aside element
|
||||
.sidebar-nav-shell — inner nav element
|
||||
.sidebar-brand — brand/logo section
|
||||
.sidebar-primary — nav list container
|
||||
.sidebar-nav-item — individual nav item
|
||||
.sidebar-nav-item.is-active
|
||||
.sidebar-projects — projects section container
|
||||
.sidebar-section-label — section label text (GENERAL, PROJECTS)
|
||||
|
||||
<!-- Existing CSS classes to update/add in app.css -->
|
||||
.dashboard-shell — grid container (currently: minmax(16rem, 18rem) 1fr)
|
||||
.dashboard-shell.sidebar-is-collapsed — new rule: grid-template-columns: 4rem 1fr
|
||||
.sidebar-collapse-button — exists but unwired; now gets onclick JS
|
||||
.is-collapsed on .dashboard-sidebar — triggers label/text hiding
|
||||
|
||||
<!-- sidebarNavItem struct (unchanged) -->
|
||||
type sidebarNavItem struct {
|
||||
Href string
|
||||
Label string
|
||||
Icon string
|
||||
Active bool
|
||||
DividerAfter bool
|
||||
}
|
||||
|
||||
<!-- Existing icon kinds in SidebarNavIcon: panels, tasks, layers, planning, chat, files -->
|
||||
<!-- "layers" is the icon for Projects per Figma (stacked layers = projects/tablos) -->
|
||||
<!-- "tasks" exists and maps to My Tasks -->
|
||||
<!-- No "team" icon exists — add a new "team" case to SidebarNavIcon using a users SVG -->
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Update sidebarPrimaryNavItems and add team icon to SidebarNavIcon</name>
|
||||
<files>backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ</files>
|
||||
<action>
|
||||
In app_layout_helpers.go, replace the sidebarPrimaryNavItems function body per D-08. The new nav items are:
|
||||
|
||||
func sidebarPrimaryNavItems(activePath string) []sidebarNavItem {
|
||||
return []sidebarNavItem{
|
||||
{Href: "/", Label: "Home", Icon: "panels", Active: isActivePath(activePath, "/")},
|
||||
{Href: "#", Label: "My Tasks", Icon: "tasks", Active: false},
|
||||
{Href: "/", Label: "Projects", Icon: "layers", Active: false},
|
||||
{Href: "/planning", Label: "Events", Icon: "planning", Active: isActivePath(activePath, "/planning")},
|
||||
{Href: "#", Label: "Team Members", Icon: "team", Active: false},
|
||||
}
|
||||
}
|
||||
|
||||
Notes:
|
||||
- "Projects" links to "/" (the tablos dashboard — no /tablos GET route exists per router.go).
|
||||
- "My Tasks" and "Team Members" use href="#" (no route yet in v4.0 per D-08).
|
||||
- DividerAfter is no longer needed (section labels replace dividers); remove it from all items.
|
||||
- Chat and Files are removed entirely.
|
||||
|
||||
In app_layout.templ, add a "team" case to the SidebarNavIcon switch, after the "files" case:
|
||||
|
||||
case "team":
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M22 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
|
||||
This is a standard Lucide "users" SVG — consistent with the existing icon set's stroke style.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>sidebarPrimaryNavItems returns 5 items (Home, My Tasks, Projects, Events, Team Members). SidebarNavIcon handles "team" without falling to default. Build passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Rebuild DashboardSidebar HTML with GENERAL/PROJECTS sections and collapse button</name>
|
||||
<files>backend/templates/app_layout.templ</files>
|
||||
<action>
|
||||
Replace the DashboardSidebar templ component body with the new Figma-spec structure. The signature stays the same:
|
||||
|
||||
templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string)
|
||||
|
||||
New body structure:
|
||||
|
||||
<aside class="dashboard-sidebar">
|
||||
<nav aria-label="Main navigation" class="sidebar-nav-shell">
|
||||
<!-- Brand / logo -->
|
||||
<div class="sidebar-brand">
|
||||
<a class="sidebar-brand-link" href="/" aria-label="Home">
|
||||
<img class="sidebar-brand-logo" src="/static/logo_dark.png" alt="Logo XTablo"/>
|
||||
<h1 class="sidebar-brand-title">XTablo</h1>
|
||||
</a>
|
||||
<button
|
||||
class="sidebar-collapse-button"
|
||||
aria-label="Toggle sidebar"
|
||||
onclick="(function(btn){var shell=document.querySelector('.dashboard-shell');if(shell){shell.classList.toggle('sidebar-is-collapsed');}})(this)"
|
||||
type="button"
|
||||
>
|
||||
<!-- chevron-left icon (collapses) — use existing icon or inline SVG -->
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16">
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- GENERAL section -->
|
||||
<div class="sidebar-primary">
|
||||
<div class="sidebar-section-label">General</div>
|
||||
<ul class="sidebar-list" role="list">
|
||||
for _, item := range sidebarPrimaryNavItems(activePath) {
|
||||
<li>
|
||||
@SidebarNavItemRow(item)
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
<!-- PROJECTS section (tablo list) -->
|
||||
<div class="sidebar-projects">
|
||||
<div class="sidebar-section-label">Projects</div>
|
||||
<ul class="sidebar-project-list">
|
||||
for _, tablo := range tablos {
|
||||
<li>
|
||||
<a href={ templ.SafeURL("/tablos/" + tablo.ID.String()) } class="sidebar-project-link">
|
||||
if tablo.Color.Valid && tablo.Color.String != "" {
|
||||
<span class="sidebar-project-icon" style={ "background-color: " + tablo.Color.String }></span>
|
||||
} else {
|
||||
<span class="sidebar-project-icon"></span>
|
||||
}
|
||||
<span class="sidebar-project-label">{ tablo.Title }</span>
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</aside>
|
||||
|
||||
Key changes vs Phase 15:
|
||||
- SidebarProjectsSection and SidebarOrganizationFooter components are no longer called from DashboardSidebar. SidebarOrganizationFooter (logout form) moves to the avatar dropdown in Plan 03. For now, remove it — Plan 03 adds it to the header dropdown.
|
||||
- The PROJECTS section is inlined directly inside DashboardSidebar (the separate SidebarProjectsSection component still exists in the file for now; it simply won't be called).
|
||||
- The HR separator before the projects section is removed; section labels replace dividers.
|
||||
- Collapse button uses inline onclick JS that toggles 'sidebar-is-collapsed' on the .dashboard-shell element. No Alpine.js. No server round-trip. State resets on reload per D-09.
|
||||
- The DividerAfter rendering branch in DashboardSidebar is removed (the sidebarNavItem struct field can stay in the struct for now; it just won't be used).
|
||||
|
||||
Do NOT remove the SidebarOrganizationFooter, SidebarProjectsSection, or SidebarNavItemRow templ functions from the file — Plan 03 will clean up or repurpose them.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>DashboardSidebar no longer calls SidebarOrganizationFooter. New structure has sidebar-section-label divs for both General and Projects sections. Collapse button is wired. Build passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Add sidebar collapsed-state CSS to app.css</name>
|
||||
<files>backend/internal/web/ui/app.css</files>
|
||||
<action>
|
||||
Locate the end of the existing sidebar CSS block in backend/internal/web/ui/app.css (currently ends around the .dashboard-main rule). Add the following new rules in a clearly labelled section after the existing sidebar block. Do NOT edit backend/static/tailwind.css directly — it is a compiled artifact.
|
||||
|
||||
Add these rules:
|
||||
|
||||
/* ── Sidebar collapse state ────────────────────────────────────────── */
|
||||
|
||||
/* When sidebar-is-collapsed is toggled on the shell, shrink the grid column */
|
||||
.dashboard-shell.sidebar-is-collapsed {
|
||||
grid-template-columns: 4rem 1fr;
|
||||
}
|
||||
|
||||
/* Hide text labels and section headings in collapsed state */
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-brand-title,
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-section-label,
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-nav-label,
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-project-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Center icons in collapsed state */
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-nav-link-inner {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Hide the full project list in collapsed state (icons not present for projects) */
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-project-list {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Flip the collapse button chevron when collapsed */
|
||||
.dashboard-shell.sidebar-is-collapsed .sidebar-collapse-button svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
/* Ensure sidebar-section-label uses correct token styling if not already set */
|
||||
/* (Only add if not already defined above in the file) */
|
||||
.sidebar-section-label {
|
||||
font-size: 0.6875rem; /* 11px */
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
padding: 0.75rem 0.75rem 0.25rem;
|
||||
}
|
||||
|
||||
After adding these rules, regenerate tailwind.css by running the CSS build command. The build pipeline is:
|
||||
cd backend && npx tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
|
||||
|
||||
If the just/justfile command is available: `just build:css` or equivalent. Check the justfile for the exact command. The goal is to get the new CSS into the compiled tailwind.css that the browser loads.
|
||||
|
||||
Verify the compiled file now contains the new classes:
|
||||
grep -c "sidebar-is-collapsed" backend/static/tailwind.css
|
||||
This must return a number greater than 0.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -c "sidebar-is-collapsed" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/static/tailwind.css</automated>
|
||||
</verify>
|
||||
<done>app.css contains the sidebar-is-collapsed block. tailwind.css is regenerated and contains "sidebar-is-collapsed" class rules. go build ./... still passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Inline JS onclick → DOM | Collapse toggle runs inline script; no user input is processed |
|
||||
| Tablo color field → CSS style attribute | tablo.Color.String is interpolated into a style attribute for the project icon color |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-18-02-01 | Tampering | tablo.Color in style attribute | accept | Color is stored in DB by the authenticated user themselves; templ does not sanitize style values but the content is user's own data |
|
||||
| T-18-02-02 | Tampering | tailwind.css overwritten directly | mitigate | Always edit app.css; run build pipeline to regenerate tailwind.css — never hand-edit the artifact |
|
||||
| T-18-02-03 | DoS | Sidebar collapse breaks layout (grid column not shrinking) | mitigate | CSS rule uses .dashboard-shell.sidebar-is-collapsed targeting the shell wrapper, not just the sidebar child — per Research Pitfall 3 |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
1. `cd backend && templ generate ./... && go build ./...` — zero errors
|
||||
2. `grep -c "sidebar-is-collapsed" backend/static/tailwind.css` — returns > 0
|
||||
3. `grep "My Tasks\|Team Members\|GENERAL\|General" backend/templates/app_layout_helpers.go` — shows updated nav items
|
||||
4. `cd backend && go test ./internal/web/ -run TestTablosDashboard_Sidebar -v` — existing test passes (class names .dashboard-sidebar and .sidebar-nav-shell are preserved)
|
||||
5. Visual check: start server, visit dashboard — sidebar shows General section label, 5 nav items, Projects section label with tablo list; clicking collapse button shrinks sidebar to icon-only width
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- sidebarPrimaryNavItems returns exactly 5 items: Home, My Tasks, Projects, Events, Team Members
|
||||
- Chat and Files items are gone from the nav
|
||||
- DashboardSidebar renders GENERAL section label above nav items and PROJECTS section label above tablo list
|
||||
- SidebarOrganizationFooter is no longer called from DashboardSidebar
|
||||
- Collapse button is present and toggles sidebar-is-collapsed on .dashboard-shell via inline JS
|
||||
- Collapsed state hides labels, shrinks grid column to 4rem
|
||||
- go build ./... and go test ./... both pass
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-app-shell-navigation/18-02-SUMMARY.md`
|
||||
</output>
|
||||
555
.planning/phases/18-app-shell-navigation/18-03-PLAN.md
Normal file
555
.planning/phases/18-app-shell-navigation/18-03-PLAN.md
Normal file
|
|
@ -0,0 +1,555 @@
|
|||
---
|
||||
phase: 18-app-shell-navigation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- 18-01
|
||||
- 18-02
|
||||
files_modified:
|
||||
- backend/templates/app_layout.templ
|
||||
- backend/internal/web/ui/app.css
|
||||
- backend/internal/web/handlers_tablos_test.go
|
||||
- backend/internal/web/router.go
|
||||
autonomous: true
|
||||
requirements:
|
||||
- NAV-01
|
||||
- NAV-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Every authenticated page shows a top header bar with breadcrumb, search placeholder, and right-side icons"
|
||||
- "Breadcrumb renders the correct path for each page (e.g., 'Dashboard' on the home page, 'Dashboard > Project Details' on tablo detail)"
|
||||
- "Clicking the avatar circle opens a dropdown with workspace info, settings link, and logout button"
|
||||
- "Logout from the avatar dropdown still works — POST /logout succeeds"
|
||||
- "Settings link in dropdown navigates to /settings which renders a stub page"
|
||||
- "go test ./... passes including updated sidebar test and new header test"
|
||||
artifacts:
|
||||
- path: "backend/templates/app_layout.templ"
|
||||
provides: "PageHeader templ component with three zones and avatar dropdown"
|
||||
contains: "PageHeader"
|
||||
- path: "backend/internal/web/ui/app.css"
|
||||
provides: "CSS for .page-header, .breadcrumb, .header-avatar-menu, .header-avatar-dropdown"
|
||||
contains: "page-header"
|
||||
- path: "backend/internal/web/handlers_tablos_test.go"
|
||||
provides: "Updated TestTablosDashboard_Sidebar + new TestTablosDashboard_Header test"
|
||||
contains: "page-header"
|
||||
- path: "backend/internal/web/router.go"
|
||||
provides: "/settings stub route"
|
||||
contains: "/settings"
|
||||
key_links:
|
||||
- from: "backend/templates/app_layout.templ"
|
||||
to: "backend/internal/web/ui/app.css"
|
||||
via: ".page-header, .header-avatar-menu, .breadcrumb classes rendered in PageHeader"
|
||||
pattern: "page-header"
|
||||
- from: "backend/templates/app_layout.templ"
|
||||
to: "/logout route"
|
||||
via: "form method=POST action=/logout inside avatar dropdown, CSRF token via ui.CSRFField"
|
||||
pattern: "action.*logout"
|
||||
- from: "PageHeader"
|
||||
to: "AppLayout"
|
||||
via: "called inside dashboard-main div, above children"
|
||||
pattern: "@PageHeader"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement the PageHeader templ component (three-zone top bar: breadcrumb left, search placeholder center, bell/inbox/avatar right), wire it into AppLayout, add all supporting CSS to app.css, implement the avatar dropdown using native details/summary HTML, add a /settings stub route, and update tests to assert the new shell structure.
|
||||
|
||||
Purpose: Delivers NAV-02. After this plan every authenticated page has the top header bar matching Figma, the avatar dropdown works, and logout continues to function.
|
||||
Output: PageHeader component, avatar dropdown, /settings stub, updated tests, new CSS in app.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>
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
|
||||
@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
|
||||
@.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md
|
||||
@.planning/phases/18-app-shell-navigation/18-02-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- Read screenshots/Homepage.png and screenshots/Board.png before implementing -->
|
||||
<!-- Located at /Users/arthur.belleville/Documents/perso/projects/xtablo-source/screenshots/ -->
|
||||
|
||||
<!-- AppLayout signature after Plan 01 -->
|
||||
templ AppLayout(
|
||||
title string,
|
||||
user *auth.User,
|
||||
csrfToken string,
|
||||
activePath string,
|
||||
tablos []sqlc.Tablo,
|
||||
pageTitle string,
|
||||
breadcrumb []BreadcrumbItem,
|
||||
headerActions templ.Component,
|
||||
)
|
||||
|
||||
<!-- BreadcrumbItem struct (defined in Plan 01) -->
|
||||
type BreadcrumbItem struct {
|
||||
Label string
|
||||
Href string
|
||||
}
|
||||
|
||||
<!-- CSRFField usage (existing pattern) -->
|
||||
@ui.CSRFField(csrfToken) // renders a hidden input with CSRF token
|
||||
|
||||
<!-- auth.User struct -->
|
||||
type User struct {
|
||||
ID uuid.UUID
|
||||
Email string
|
||||
PasswordHash pgtype.Text
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
// Only Email is available. No Name or AvatarURL.
|
||||
|
||||
<!-- Existing CSS design tokens available -->
|
||||
var(--color-surface-elevated)
|
||||
var(--color-border-panel)
|
||||
var(--color-text-muted)
|
||||
var(--color-text-brand)
|
||||
var(--shadow-floating-control)
|
||||
var(--color-surface-elevated-strong)
|
||||
var(--color-border-panel-muted)
|
||||
|
||||
<!-- Existing test file location -->
|
||||
backend/internal/web/handlers_tablos_test.go
|
||||
// Existing test: TestTablosDashboard_Sidebar (line ~602) asserts "dashboard-sidebar" and "sidebar-nav-shell"
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Implement PageHeader component and wire into AppLayout</name>
|
||||
<files>backend/templates/app_layout.templ</files>
|
||||
<action>
|
||||
Add the PageHeader templ component to app_layout.templ. It renders the full-width top bar with three zones. Place the function after DashboardSidebar and before AppLayout in the file.
|
||||
|
||||
PageHeader signature:
|
||||
templ PageHeader(pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component, user *auth.User, csrfToken string)
|
||||
|
||||
Structure:
|
||||
|
||||
<header class="page-header">
|
||||
<!-- LEFT: breadcrumb zone -->
|
||||
<div class="page-header-left">
|
||||
<nav class="breadcrumb" aria-label="Breadcrumb">
|
||||
for i, crumb := range breadcrumb {
|
||||
if i > 0 {
|
||||
<span class="breadcrumb-separator" aria-hidden="true">/</span>
|
||||
}
|
||||
if crumb.Href != "" {
|
||||
<a href={ templ.SafeURL(crumb.Href) } class="breadcrumb-item breadcrumb-item--link">{ crumb.Label }</a>
|
||||
} else {
|
||||
<span class="breadcrumb-item breadcrumb-item--current" aria-current="page">{ crumb.Label }</span>
|
||||
}
|
||||
}
|
||||
if len(breadcrumb) == 0 {
|
||||
<span class="breadcrumb-item breadcrumb-item--current">{ pageTitle }</span>
|
||||
}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- CENTER: search placeholder -->
|
||||
<div class="page-header-center">
|
||||
<div class="header-search-placeholder" aria-label="Search (coming soon)" role="search">
|
||||
<svg class="header-search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="16" height="16">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<span class="header-search-text">Search...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RIGHT: bell, inbox, avatar -->
|
||||
<div class="page-header-right">
|
||||
<!-- Bell placeholder -->
|
||||
<button class="header-icon-button" aria-label="Notifications (coming soon)" type="button" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
|
||||
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9"/>
|
||||
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Inbox placeholder -->
|
||||
<button class="header-icon-button" aria-label="Inbox (coming soon)" type="button" disabled>
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" width="18" height="18">
|
||||
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
|
||||
<path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<!-- Avatar dropdown using native details/summary -->
|
||||
<details class="header-avatar-menu">
|
||||
<summary class="header-avatar-button" aria-label="User menu">
|
||||
<span class="header-avatar-initial">{ string([]rune(user.Email)[:1]) }</span>
|
||||
</summary>
|
||||
<div class="header-avatar-dropdown">
|
||||
<!-- Workspace / user info -->
|
||||
<div class="avatar-dropdown-workspace">
|
||||
<span class="avatar-dropdown-workspace-name">{ user.Email }</span>
|
||||
<span class="avatar-dropdown-workspace-meta">1 member</span>
|
||||
</div>
|
||||
<hr class="avatar-dropdown-divider"/>
|
||||
<!-- Settings -->
|
||||
<a href="/settings" class="avatar-dropdown-item">Settings</a>
|
||||
<hr class="avatar-dropdown-divider"/>
|
||||
<!-- Logout -->
|
||||
<form method="POST" action="/logout" class="avatar-dropdown-logout-form">
|
||||
@ui.CSRFField(csrfToken)
|
||||
<button type="submit" class="avatar-dropdown-item avatar-dropdown-item--danger">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<!-- Per-page header actions slot (Phase 19-22 will populate) -->
|
||||
if headerActions != nil {
|
||||
<div class="page-header-actions">
|
||||
@headerActions
|
||||
</div>
|
||||
}
|
||||
</header>
|
||||
|
||||
Wire PageHeader into AppLayout. In the AppLayout body, inside the dashboard-main div, call PageHeader BEFORE { children... }:
|
||||
|
||||
<main id="app-main-content" class="dashboard-main">
|
||||
@PageHeader(pageTitle, breadcrumb, headerActions, user, csrfToken)
|
||||
{ children... }
|
||||
</main>
|
||||
|
||||
Remove the earlier minimal breadcrumb stub that was added in Plan 01 (the `if len(breadcrumb) > 0 { <!-- comment --> }` and `if headerActions != nil { @headerActions }` blocks added as placeholders). The PageHeader call now handles both breadcrumb and headerActions rendering.
|
||||
|
||||
Add a small inline script at the bottom of the body (after existing script tags) for the avatar dropdown outside-click close behavior:
|
||||
|
||||
<script>
|
||||
document.addEventListener('click', function(e) {
|
||||
document.querySelectorAll('details.header-avatar-menu').forEach(function(d) {
|
||||
if (!d.contains(e.target)) { d.removeAttribute('open'); }
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>PageHeader component exists in app_layout.templ. AppLayout calls @PageHeader inside dashboard-main before children. Build passes. templ generate produces no errors.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add page-header CSS to app.css, add /settings stub route, update tests</name>
|
||||
<files>backend/internal/web/ui/app.css, backend/internal/web/router.go, backend/internal/web/handlers_tablos_test.go</files>
|
||||
<action>
|
||||
--- app.css: Add header bar CSS ---
|
||||
|
||||
Append a new section after the sidebar collapse rules added in Plan 02:
|
||||
|
||||
/* ── Page header bar ───────────────────────────────────────────────── */
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: var(--color-surface-elevated);
|
||||
border-bottom: 1px solid var(--color-border-panel);
|
||||
margin: -2rem -2rem 1.5rem -2rem; /* cancel .dashboard-main padding on three sides, add bottom gap */
|
||||
}
|
||||
|
||||
.page-header-left {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.page-header-center {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-header-right {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header-actions {
|
||||
flex: 0 0 auto;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Breadcrumb */
|
||||
.breadcrumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.breadcrumb-separator {
|
||||
color: var(--color-text-muted);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item {
|
||||
color: var(--color-text-muted);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.breadcrumb-item--link {
|
||||
color: var(--color-text-brand);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb-item--link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.breadcrumb-item--current {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Search placeholder */
|
||||
.header-search-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border: 1px solid var(--color-border-panel);
|
||||
border-radius: 0.375rem;
|
||||
background: var(--color-surface-muted-inverse);
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.875rem;
|
||||
min-width: 16rem;
|
||||
max-width: 28rem;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Icon buttons (bell, inbox) */
|
||||
.header-icon-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.375rem;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--color-text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-icon-button:hover {
|
||||
background: var(--overlay-dark-soft);
|
||||
}
|
||||
|
||||
.header-icon-button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Avatar dropdown */
|
||||
.header-avatar-menu {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.header-avatar-menu > summary {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header-avatar-menu > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-avatar-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-brand);
|
||||
color: #fff;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header-avatar-initial {
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.header-avatar-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 0.5rem);
|
||||
min-width: 14rem;
|
||||
background: var(--color-surface-elevated);
|
||||
border: 1px solid var(--color-border-panel);
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: var(--shadow-floating-control);
|
||||
z-index: 50;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-dropdown-workspace {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
}
|
||||
|
||||
.avatar-dropdown-workspace-name {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.avatar-dropdown-workspace-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.avatar-dropdown-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border-panel-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.avatar-dropdown-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0.625rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
background: transparent;
|
||||
border: none;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-dropdown-item:hover {
|
||||
background: var(--overlay-dark-soft);
|
||||
}
|
||||
|
||||
.avatar-dropdown-item--danger {
|
||||
color: #dc2626; /* red — logout action; no token for this; matches Figma red logout */
|
||||
}
|
||||
|
||||
.avatar-dropdown-item--danger:hover {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.avatar-dropdown-logout-form {
|
||||
display: contents; /* form element itself doesn't affect layout */
|
||||
}
|
||||
|
||||
After adding these rules, regenerate tailwind.css:
|
||||
cd backend && npx tailwindcss -i tailwind.input.css -o static/tailwind.css --minify
|
||||
|
||||
(Use the same CSS build command identified in Plan 02.)
|
||||
|
||||
--- router.go: Add /settings stub ---
|
||||
|
||||
In the authenticated routes block (inside the auth middleware group), add a GET handler for /settings that renders a simple stub page. Since no full settings template exists, use an inline http.HandlerFunc that writes a minimal HTML response:
|
||||
|
||||
r.Get("/settings", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
_, _ = w.Write([]byte(`<!DOCTYPE html><html><head><title>Settings - Xtablo</title><link rel="stylesheet" href="/static/tailwind.css"/></head><body style="padding:2rem;font-family:sans-serif"><h1>Settings</h1><p>Coming soon.</p><a href="/">Back to dashboard</a></body></html>`))
|
||||
})
|
||||
|
||||
Place this route near the other single-page GET routes (e.g., next to /account/providers). This stub satisfies the D-06 requirement that the settings link navigates somewhere. A full settings page is deferred.
|
||||
|
||||
--- handlers_tablos_test.go: Update existing test and add header test ---
|
||||
|
||||
Locate TestTablosDashboard_Sidebar (around line 602). This test currently asserts "dashboard-sidebar" and "sidebar-nav-shell". These class names are preserved by the rebuild, so the test already passes. Add two new assertions to this test:
|
||||
|
||||
1. Check that "page-header" appears in the response body (the new header bar is present).
|
||||
2. Check that "breadcrumb" appears in the response body.
|
||||
|
||||
Example using the existing assertion pattern:
|
||||
if !strings.Contains(body, "page-header") {
|
||||
t.Errorf("expected page-header class in response body")
|
||||
}
|
||||
if !strings.Contains(body, "breadcrumb") {
|
||||
t.Errorf("expected breadcrumb class in response body")
|
||||
}
|
||||
|
||||
Also add a new standalone test named TestTablosDashboard_Header that:
|
||||
- Calls the TablosListHandler (same setup as TestTablosDashboard_Sidebar)
|
||||
- Asserts the response body contains "page-header"
|
||||
- Asserts the response body contains "header-avatar-menu" (the avatar dropdown element)
|
||||
- Asserts the response body contains "Dashboard" (the breadcrumb label for the home page)
|
||||
- Asserts the response body contains "header-search-placeholder"
|
||||
|
||||
Pattern: copy the setup from TestTablosDashboard_Sidebar — same mock setup, same request/response recorder pattern.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && grep -c "page-header" static/tailwind.css && go test ./internal/web/ -run "TestTablosDashboard" -v 2>&1 | tail -20</automated>
|
||||
</verify>
|
||||
<done>tailwind.css contains "page-header" CSS. TestTablosDashboard_Sidebar passes with new assertions. TestTablosDashboard_Header exists and passes. /settings route is registered and returns 200. Logout from avatar dropdown POSTs to /logout and succeeds.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Client → POST /logout | Logout is a state-changing POST; CSRF token guards against cross-site forgery |
|
||||
| Avatar dropdown → DOM | Dropdown contains a logout form; clicking outside closes it via JS listener |
|
||||
| user.Email → HTML | Email rendered inside dropdown; templ auto-escapes it |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-18-03-01 | Cross-site request forgery | POST /logout from avatar dropdown | mitigate | ui.CSRFField(csrfToken) in the logout form — same pattern as the original SidebarOrganizationFooter logout form |
|
||||
| T-18-03-02 | Tampering | user.Email rendered in dropdown | accept | templ auto-escapes all string interpolations; no raw HTML insertion possible |
|
||||
| T-18-03-03 | Information disclosure | /settings stub exposes that settings page exists | accept | Page is behind authentication middleware; no sensitive data exposed |
|
||||
| T-18-03-04 | Elevation of privilege | nil headerActions panic | mitigate | Plan 01 stub guard replaced by PageHeader's own nil-guard before @headerActions |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
1. `cd backend && templ generate ./... && go build ./...` — zero errors
|
||||
2. `grep -c "page-header" backend/static/tailwind.css` — returns > 0
|
||||
3. `grep -c "header-avatar-menu" backend/static/tailwind.css` — returns > 0
|
||||
4. `cd backend && go test ./... 2>&1 | tail -10` — all tests pass including TestTablosDashboard_Header
|
||||
5. `cd backend && go test ./internal/web/ -run TestTablosDashboard_Header -v` — explicitly verify new test exists and passes
|
||||
6. Manual: Start server, visit dashboard — top header bar visible with breadcrumb "Dashboard", search placeholder, bell and inbox placeholders, avatar circle
|
||||
7. Manual: Click avatar circle — dropdown opens showing email, Settings link, Log out button (red text)
|
||||
8. Manual: Click Log out — redirects to login page (POST /logout succeeds)
|
||||
9. Manual: Visit a tablo detail page — breadcrumb shows "Dashboard > [tablo name]"
|
||||
10. Manual: Click Settings in dropdown — navigates to /settings, shows "Coming soon" stub
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- PageHeader component renders in every authenticated page (verified by page-header class in test)
|
||||
- Breadcrumb shows correct labels per page (Dashboard on home, Dashboard > title on tablo detail, Planning on /planning)
|
||||
- Avatar dropdown uses details/summary HTML — no Alpine.js
|
||||
- Avatar dropdown contains: email as workspace name, Settings link to /settings, Logout form with CSRF
|
||||
- Logout POST succeeds from the avatar dropdown
|
||||
- /settings route returns 200 with stub content
|
||||
- TestTablosDashboard_Sidebar still passes (existing class names preserved)
|
||||
- TestTablosDashboard_Header passes (new assertions for page-header, breadcrumb, header-avatar-menu)
|
||||
- go build ./... and go test ./... both pass
|
||||
- tailwind.css contains page-header and header-avatar-dropdown CSS
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/18-app-shell-navigation/18-03-SUMMARY.md`
|
||||
</output>
|
||||
148
.planning/phases/18-app-shell-navigation/18-VERIFICATION.md
Normal file
148
.planning/phases/18-app-shell-navigation/18-VERIFICATION.md
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
---
|
||||
phase: 18-app-shell-navigation
|
||||
verified: 2026-05-17T00:00:00Z
|
||||
status: human_needed
|
||||
score: 8/9 must-haves verified
|
||||
overrides_applied: 0
|
||||
human_verification:
|
||||
- test: "Visit the dashboard at http://localhost:8080 and inspect the sidebar"
|
||||
expected: "Sidebar shows 'General' section label above 5 nav items (Home, My Tasks, Projects, Events, Team Members), 'Projects' section label above tablo list, and collapse button that shrinks sidebar to icon-only on click"
|
||||
why_human: "Visual Figma match requires rendering the browser; CSS collapse behavior is JavaScript-driven and cannot be verified without a live browser"
|
||||
- test: "Click the avatar circle in the top header bar"
|
||||
expected: "Dropdown opens showing user email, a Settings link, and a red Log out button; clicking outside closes it"
|
||||
why_human: "Native details/summary open/close behavior and outside-click JS listener require live browser interaction"
|
||||
- test: "Click Log out in the avatar dropdown"
|
||||
expected: "POST /logout is submitted with CSRF token, session is destroyed, user is redirected to login"
|
||||
why_human: "End-to-end logout flow requires a running server with an active session"
|
||||
- test: "Navigate to a tablo detail page"
|
||||
expected: "Breadcrumb reads 'Dashboard > [Tablo Title]', not just 'Dashboard'"
|
||||
why_human: "Requires live server and a real tablo in the database to verify dynamic breadcrumb rendering"
|
||||
- test: "Navigate to /settings"
|
||||
expected: "Returns 200 with 'Coming soon' stub page and no 500 error"
|
||||
why_human: "Requires a running server to verify the inline handler response"
|
||||
---
|
||||
|
||||
# Phase 18: App Shell & Navigation Verification Report
|
||||
|
||||
**Phase Goal:** Redesign the sidebar and top header bar to match the Figma design.
|
||||
**Verified:** 2026-05-17
|
||||
**Status:** human_needed
|
||||
**Re-verification:** No — initial verification
|
||||
|
||||
## Goal Achievement
|
||||
|
||||
### Observable Truths
|
||||
|
||||
| # | Truth | Status | Evidence |
|
||||
|---|-------|--------|----------|
|
||||
| 1 | The Go codebase compiles with zero errors after AppLayout signature is extended | VERIFIED | `go build ./...` exits 0 with no output |
|
||||
| 2 | Every authenticated page still renders (no 500 errors) | UNCERTAIN (human) | Build passes; runtime smoke test requires live server |
|
||||
| 3 | go test ./... passes with no failures | VERIFIED | All packages pass: internal/auth, internal/db, internal/files, internal/jobs, internal/web, internal/web/ui, templates |
|
||||
| 4 | Sidebar shows a GENERAL section label above Home, My Tasks, Projects, Events, Team Members nav items | VERIFIED | `DashboardSidebar` in `app_layout.templ` lines 151-157: `sidebar-section-label` div with text "General", `sidebarPrimaryNavItems` returns exactly 5 items |
|
||||
| 5 | Sidebar shows a PROJECTS section label above the tablo list | VERIFIED | `app_layout.templ` lines 161-177: inner `sidebar-section-label` div with text "Projects" above tablo loop |
|
||||
| 6 | Chat and Files nav items are removed from the sidebar | VERIFIED | `sidebarPrimaryNavItems` in `app_layout_helpers.go` returns only: Home, My Tasks, Projects, Events, Team Members — no Chat or Files |
|
||||
| 7 | Clicking the collapse button toggles the sidebar between full-width and icon-only states | UNCERTAIN (human) | Collapse button has correct inline `onclick` JS toggling `sidebar-is-collapsed` on `.dashboard-shell`; `tailwind.css` contains 5 occurrences of `sidebar-is-collapsed` CSS rules — requires browser to verify visual behavior |
|
||||
| 8 | Every authenticated page shows a top header bar with breadcrumb, search placeholder, and right-side icons | VERIFIED | `PageHeader` component exists in `app_layout.templ` lines 186-267; called from `AppLayout` line 290 inside `.dashboard-main` before `{ children... }` |
|
||||
| 9 | Breadcrumb renders the correct path for each page | VERIFIED | All 10 handler call sites in handlers_tablos.go, handlers_planning.go, handlers_account.go, handlers_discussion.go, handlers_files.go, handlers_events.go pass `[]templates.BreadcrumbItem` with correct labels and hrefs |
|
||||
| 10 | Clicking the avatar circle opens a dropdown with workspace info, settings link, and logout button | UNCERTAIN (human) | `details.header-avatar-menu` with `summary` and dropdown div confirmed in `app_layout.templ` lines 237-257; outside-click JS listener at lines 298-302; requires live browser |
|
||||
| 11 | Logout from the avatar dropdown still works — POST /logout succeeds | UNCERTAIN (human) | Logout `<form method="POST" action="/logout">` with `@ui.CSRFField(csrfToken)` is present in `PageHeader` (line 252-255); requires live server to verify end-to-end |
|
||||
| 12 | Settings link in dropdown navigates to /settings which renders a stub page | VERIFIED | `/settings` route found at `router.go:90`; inline stub handler returning HTML with "Coming soon" content |
|
||||
| 13 | go test ./... passes including updated sidebar test and new header test | VERIFIED | All packages pass; `TestTablosDashboard_Header` function confirmed at `handlers_tablos_test.go:647`; assertions for `page-header`, `header-avatar-menu`, `Dashboard`, `header-search-placeholder` all present |
|
||||
|
||||
**Score:** 8/9 automated truths verified (4 require human browser/server testing)
|
||||
|
||||
### Required Artifacts
|
||||
|
||||
| Artifact | Expected | Status | Details |
|
||||
|----------|----------|--------|---------|
|
||||
| `backend/templates/app_layout_helpers.go` | BreadcrumbItem struct export | VERIFIED | Lines 6-10: `type BreadcrumbItem struct { Label string; Href string }` |
|
||||
| `backend/templates/app_layout_helpers.go` | sidebarPrimaryNavItems with Figma-spec items | VERIFIED | Lines 55-63: 5 items, "My Tasks" and "Team Members" present, Chat/Files absent |
|
||||
| `backend/templates/app_layout.templ` | Extended AppLayout signature (8 params) | VERIFIED | Line 277: `templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, pageTitle string, breadcrumb []BreadcrumbItem, headerActions templ.Component)` |
|
||||
| `backend/templates/app_layout.templ` | DashboardSidebar with GENERAL/PROJECTS sections and collapse button | VERIFIED | Lines 128-181: sections present, inline onclick JS on collapse button at lines 139-147 |
|
||||
| `backend/templates/app_layout.templ` | PageHeader component with three zones and avatar dropdown | VERIFIED | Lines 186-267: breadcrumb left, search center, bell/inbox/avatar right, details/summary dropdown |
|
||||
| `backend/internal/web/ui/app.css` | is-collapsed CSS rules | VERIFIED | Lines 333-358: `.dashboard-shell.sidebar-is-collapsed` rules covering grid-template-columns, label hiding, icon centering, project-list hiding, chevron flip |
|
||||
| `backend/internal/web/ui/app.css` | page-header CSS block | VERIFIED | Lines 362-567: complete `.page-header`, `.breadcrumb`, `.header-avatar-menu`, `.header-avatar-dropdown` CSS |
|
||||
| `backend/internal/web/handlers_tablos_test.go` | TestTablosDashboard_Header test | VERIFIED | Lines 647-683: function exists with assertions for page-header, header-avatar-menu, Dashboard, header-search-placeholder |
|
||||
| `backend/internal/web/router.go` | /settings stub route | VERIFIED | Line 90: `r.Get("/settings", ...)` inline handler |
|
||||
|
||||
### Key Link Verification
|
||||
|
||||
| From | To | Via | Status | Details |
|
||||
|------|----|-----|--------|---------|
|
||||
| `handlers_tablos.go` | `templates/tablos.templ` | `TablosDashboard / TabloDetailPage` with `BreadcrumbItem` args | VERIFIED | Grep confirms `[]templates.BreadcrumbItem{...}` at lines 62, 232, 350, 480 |
|
||||
| `app_layout_helpers.go` | `app_layout.templ` | `sidebarPrimaryNavItems` called in `DashboardSidebar` | VERIFIED | `app_layout.templ` line 153: `for _, item := range sidebarPrimaryNavItems(activePath)` |
|
||||
| `app_layout.templ` | `app.css` | `sidebar-is-collapsed` toggled by inline JS, CSS rule picks it up | VERIFIED | Both sides confirmed; CSS in app.css and tailwind.css (5 occurrences) |
|
||||
| `app_layout.templ` | `app.css` | `.page-header` class rendered in `PageHeader`, CSS defines it | VERIFIED | `tailwind.css` has 5 occurrences of `page-header`; 3 occurrences of `header-avatar-menu` |
|
||||
| `PageHeader` | `AppLayout` | `@PageHeader(...)` called inside `dashboard-main` | VERIFIED | `app_layout.templ` line 290: `@PageHeader(pageTitle, breadcrumb, headerActions, user, csrfToken)` |
|
||||
| `app_layout.templ` | `/logout` route | `form method=POST action=/logout` with `@ui.CSRFField` | VERIFIED | `app_layout.templ` lines 252-255: form present with CSRF field |
|
||||
| `handlers_planning.go` | `templates/planning.templ` | `PlanningPage` with breadcrumb args | VERIFIED | `handlers_planning.go:70` contains `[]templates.BreadcrumbItem{{Label: "Planning", Href: ""}}` |
|
||||
| `handlers_account.go` | `templates/account_providers.templ` | `AccountProvidersPage` with breadcrumb args | VERIFIED | `handlers_account.go:51` contains `[]templates.BreadcrumbItem{{Label: "Linked Providers", Href: ""}}` |
|
||||
| `handlers_discussion.go` | `templates/tablos.templ` | `TabloDetailPage` with breadcrumb args | VERIFIED | `handlers_discussion.go:82` contains `[]templates.BreadcrumbItem{{Label: "Dashboard", Href: "/"}, {Label: tablo.Title, Href: ""}}` |
|
||||
| `handlers_files.go` | `templates/tablos.templ` | `TabloDetailPage` with breadcrumb args (2 call sites) | VERIFIED | `handlers_files.go:110,146` both contain breadcrumb args |
|
||||
| `handlers_events.go` | `templates/tablos.templ` | `TabloDetailPage` with breadcrumb args | VERIFIED | `handlers_events.go:181` contains breadcrumb args |
|
||||
|
||||
### Requirements Coverage
|
||||
|
||||
| Requirement | Source Plan | Description | Status | Evidence |
|
||||
|-------------|-------------|-------------|--------|----------|
|
||||
| NAV-01 | 18-01, 18-02, 18-03 | User sees a redesigned sidebar/nav bar matching Figma (brand section, icon nav items, tablo list section, user footer) | VERIFIED (code) / NEEDS HUMAN (visual match) | `DashboardSidebar` rebuilt with two-section structure; brand/logo at lines 133-147; nav items at 153-157; tablo list at 164-177. SidebarOrganizationFooter moved to avatar dropdown in header — user info accessible via header not sidebar footer. Visual Figma match requires human. |
|
||||
| NAV-02 | 18-01, 18-03 | User sees a per-page top header bar with page title and contextual action buttons matching Figma | VERIFIED (code) / NEEDS HUMAN (visual match) | `PageHeader` component at `app_layout.templ:186-267`; breadcrumb wired from all 10 handler call sites; `headerActions` slot present with nil-guard |
|
||||
|
||||
### Anti-Patterns Found
|
||||
|
||||
| File | Line | Pattern | Severity | Impact |
|
||||
|------|------|---------|----------|--------|
|
||||
| `backend/templates/app_layout.templ` | 109-122 | `SidebarOrganizationFooter` component defined but never called | Info | Dead code — intentional per Plan 02/03 decisions; safe to remove in a cleanup pass. Not a blocker. |
|
||||
| `backend/templates/app_layout.templ` | 86-105 | `SidebarProjectsSection` component defined but never called | Info | Dead code — inlined directly into `DashboardSidebar` per Plan 02; safe to remove in cleanup. Not a blocker. |
|
||||
|
||||
No TBD, FIXME, or XXX markers found in phase-modified files.
|
||||
|
||||
### Human Verification Required
|
||||
|
||||
#### 1. Sidebar Visual Figma Match
|
||||
|
||||
**Test:** Start the server (`cd backend && go run . serve`), navigate to `http://localhost:8080`, inspect the sidebar.
|
||||
**Expected:** Sidebar shows brand/logo section at top, "General" section label above 5 nav items (Home, My Tasks, Projects, Events, Team Members), "Projects" section label above the tablo list. Clicking the collapse button shrinks the sidebar to icon-only (~4rem) width and hides all text labels.
|
||||
**Why human:** CSS collapse behavior is JavaScript-driven; the visual Figma match cannot be verified programmatically.
|
||||
|
||||
#### 2. Avatar Dropdown Interaction
|
||||
|
||||
**Test:** Click the user avatar circle in the top right of the header bar.
|
||||
**Expected:** A dropdown opens showing the user email at the top, a "Settings" link, and a red "Log out" button. Clicking outside the dropdown closes it.
|
||||
**Why human:** Native `<details>/<summary>` open/close behavior and the outside-click JS listener require live browser interaction.
|
||||
|
||||
#### 3. Logout Flow from Avatar Dropdown
|
||||
|
||||
**Test:** Click "Log out" in the avatar dropdown while authenticated.
|
||||
**Expected:** A POST request is sent to `/logout` with a valid CSRF token, the session is destroyed, and the user is redirected to the login page.
|
||||
**Why human:** Requires a running server with an active authenticated session.
|
||||
|
||||
#### 4. Tablo Detail Breadcrumb
|
||||
|
||||
**Test:** Navigate to a tablo detail page (`/tablos/{id}`).
|
||||
**Expected:** The breadcrumb in the top header shows "Dashboard > [Tablo Title]" where "Dashboard" is a clickable link back to `/` and "[Tablo Title]" is plain text.
|
||||
**Why human:** Requires a live server and a real tablo in the database to verify dynamic breadcrumb rendering.
|
||||
|
||||
#### 5. /settings Stub
|
||||
|
||||
**Test:** Navigate to `/settings` (authenticated).
|
||||
**Expected:** Returns a 200 response with "Coming soon" text and a back link to the dashboard. No 500 error.
|
||||
**Why human:** Requires a running server; the handler is an inline `http.HandlerFunc`, not a full template-rendered page.
|
||||
|
||||
### Gaps Summary
|
||||
|
||||
No gaps blocking goal achievement. All automated checks passed:
|
||||
- `go build ./...` succeeds with zero errors
|
||||
- `go test ./...` passes across all packages
|
||||
- All 10 handler call sites supply `BreadcrumbItem` args
|
||||
- `PageHeader` component exists and is wired into `AppLayout`
|
||||
- `/settings` route is registered
|
||||
- `TestTablosDashboard_Header` test exists with all required assertions
|
||||
- Collapsed sidebar CSS rules are in both `app.css` and compiled `tailwind.css`
|
||||
|
||||
The 5 human verification items are runtime/visual checks that cannot be validated by static analysis alone. They are expected outcomes of the implementation — the code wiring is complete. Deferred dead code (`SidebarOrganizationFooter`, `SidebarProjectsSection` unused components) is informational only and does not block the phase goal.
|
||||
|
||||
---
|
||||
|
||||
_Verified: 2026-05-17_
|
||||
_Verifier: Claude (gsd-verifier)_
|
||||
Loading…
Reference in a new issue