diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index ccfcad4..abea957 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -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 | — |
diff --git a/.planning/STATE.md b/.planning/STATE.md
index 89cb333..7eed0c4 100644
--- a/.planning/STATE.md
+++ b/.planning/STATE.md
@@ -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
diff --git a/.planning/phases/18-app-shell-navigation/18-01-PLAN.md b/.planning/phases/18-app-shell-navigation/18-01-PLAN.md
new file mode 100644
index 0000000..d79e23b
--- /dev/null
+++ b/.planning/phases/18-app-shell-navigation/18-01-PLAN.md
@@ -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"
+---
+
+
+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.
+
+
+
+@/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
+
+
+
+@.planning/ROADMAP.md
+@.planning/REQUIREMENTS.md
+@.planning/phases/18-app-shell-navigation/18-CONTEXT.md
+@.planning/phases/18-app-shell-navigation/18-RESEARCH.md
+
+
+
+
+
+templ AppLayout(title string, user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo)
+
+
+
+templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView)
+
+
+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)
+
+
+templ PlanningPage(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, agenda PlanningAgenda)
+
+
+templ AccountProvidersPage(user *auth.User, providers []LinkedProviderStatus, csrfToken string, activePath string, tablos []sqlc.Tablo)
+
+
+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")
+
+
+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")
+
+
+
+
+
+ Task 1: Define BreadcrumbItem and extend AppLayout signature
+ backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ
+
+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 {
+
+ }
+ 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.
+
+
+ cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&1 | head -20
+
+ app_layout_helpers.go exports BreadcrumbItem struct. app_layout.templ AppLayout signature has 8 parameters. templ generate produces no errors.
+
+
+
+ Task 2: Update all templ wrapper signatures and their internal AppLayout calls
+ backend/templates/tablos.templ, backend/templates/planning.templ, backend/templates/account_providers.templ
+
+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.
+
+
+ cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... 2>&1 | tail -5
+
+ All three templ files compile without errors. TablosDashboard, TabloDetailPage, PlanningPage, and AccountProvidersPage all accept and pass through pageTitle and breadcrumb.
+
+
+
+ Task 3: Update all handler call sites with per-page breadcrumbs
+ 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
+
+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.
+
+
+ cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && go build ./... && go test ./... 2>&1 | tail -20
+
+ 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.
+
+
+
+
+
+## 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 |
+
+
+
+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)
+
+
+
+- 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
+
+
+
+After completion, create `.planning/phases/18-app-shell-navigation/18-01-SUMMARY.md`
+
diff --git a/.planning/phases/18-app-shell-navigation/18-02-PLAN.md b/.planning/phases/18-app-shell-navigation/18-02-PLAN.md
new file mode 100644
index 0000000..25bd096
--- /dev/null
+++ b/.planning/phases/18-app-shell-navigation/18-02-PLAN.md
@@ -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"
+---
+
+
+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.
+
+
+
+@/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
+
+
+
+@.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
+
+
+
+
+
+
+
+templ DashboardSidebar(activePath string, tablos []sqlc.Tablo, user *auth.User, csrfToken string)
+
+
+.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)
+
+
+.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
+
+
+type sidebarNavItem struct {
+ Href string
+ Label string
+ Icon string
+ Active bool
+ DividerAfter bool
+}
+
+
+
+
+
+
+
+
+
+
+ Task 1: Update sidebarPrimaryNavItems and add team icon to SidebarNavIcon
+ backend/templates/app_layout_helpers.go, backend/templates/app_layout.templ
+
+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":
+
+
+
+
+
+
+
+This is a standard Lucide "users" SVG — consistent with the existing icon set's stroke style.
+
+
+ cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5
+
+ sidebarPrimaryNavItems returns 5 items (Home, My Tasks, Projects, Events, Team Members). SidebarNavIcon handles "team" without falling to default. Build passes.
+
+
+
+ Task 2: Rebuild DashboardSidebar HTML with GENERAL/PROJECTS sections and collapse button
+ backend/templates/app_layout.templ
+
+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:
+
+
+
+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.
+
+
+ cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5
+
+ DashboardSidebar no longer calls SidebarOrganizationFooter. New structure has sidebar-section-label divs for both General and Projects sections. Collapse button is wired. Build passes.
+
+
+
+ Task 3: Add sidebar collapsed-state CSS to app.css
+ backend/internal/web/ui/app.css
+
+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.
+
+
+ grep -c "sidebar-is-collapsed" /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend/static/tailwind.css
+
+ app.css contains the sidebar-is-collapsed block. tailwind.css is regenerated and contains "sidebar-is-collapsed" class rules. go build ./... still passes.
+
+
+
+
+
+## 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 |
+
+
+
+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
+
+
+
+- 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
+
+
+
+After completion, create `.planning/phases/18-app-shell-navigation/18-02-SUMMARY.md`
+
diff --git a/.planning/phases/18-app-shell-navigation/18-03-PLAN.md b/.planning/phases/18-app-shell-navigation/18-03-PLAN.md
new file mode 100644
index 0000000..ab9d660
--- /dev/null
+++ b/.planning/phases/18-app-shell-navigation/18-03-PLAN.md
@@ -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"
+---
+
+
+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.
+
+
+
+@/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
+
+
+
+@.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
+
+
+
+
+
+
+
+templ AppLayout(
+ title string,
+ user *auth.User,
+ csrfToken string,
+ activePath string,
+ tablos []sqlc.Tablo,
+ pageTitle string,
+ breadcrumb []BreadcrumbItem,
+ headerActions templ.Component,
+)
+
+
+type BreadcrumbItem struct {
+ Label string
+ Href string
+}
+
+
+@ui.CSRFField(csrfToken) // renders a hidden input with CSRF token
+
+
+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.
+
+
+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)
+
+
+backend/internal/web/handlers_tablos_test.go
+// Existing test: TestTablosDashboard_Sidebar (line ~602) asserts "dashboard-sidebar" and "sidebar-nav-shell"
+
+
+
+
+
+ Task 1: Implement PageHeader component and wire into AppLayout
+ backend/templates/app_layout.templ
+
+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:
+
+
+
+Wire PageHeader into AppLayout. In the AppLayout body, inside the dashboard-main div, call PageHeader BEFORE { children... }:
+
+
+ @PageHeader(pageTitle, breadcrumb, headerActions, user, csrfToken)
+ { children... }
+
+
+Remove the earlier minimal breadcrumb stub that was added in Plan 01 (the `if len(breadcrumb) > 0 { }` 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:
+
+
+
+
+ cd /Users/arthur.belleville/Documents/perso/projects/xtablo-source/backend && templ generate ./... && go build ./... 2>&1 | tail -5
+
+ PageHeader component exists in app_layout.templ. AppLayout calls @PageHeader inside dashboard-main before children. Build passes. templ generate produces no errors.
+
+
+
+ Task 2: Add page-header CSS to app.css, add /settings stub route, update tests
+ backend/internal/web/ui/app.css, backend/internal/web/router.go, backend/internal/web/handlers_tablos_test.go
+
+--- 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(`Settings - Xtablo Settings Coming soon.
Back to dashboard `))
+ })
+
+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.
+
+
+ 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
+
+ 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.
+
+
+
+
+
+## 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 |
+
+
+
+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
+
+
+
+- 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
+
+
+
+After completion, create `.planning/phases/18-app-shell-navigation/18-03-SUMMARY.md`
+
diff --git a/.planning/phases/18-app-shell-navigation/18-VERIFICATION.md b/.planning/phases/18-app-shell-navigation/18-VERIFICATION.md
new file mode 100644
index 0000000..41a629e
--- /dev/null
+++ b/.planning/phases/18-app-shell-navigation/18-VERIFICATION.md
@@ -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 `