Phase 20 work was executed against go-backend/ (prototype) instead of
backend/ (production). This commit ports the Figma-aligned restyle to
the correct codebase.
Changes:
- tablos.templ: replace project-card-top header with tablo-detail-header,
tablo-detail-avatar, tablo-detail-title; update tab bar to tablo-tab-bar;
localise tab labels to French (Vue d'ensemble, Tâches, Fichiers,
Discussion, Événements)
- tasks.templ: update KanbanBoard to tablo-kanban-board; KanbanColumn to
tablo-kanban-column with new header/count/empty classes; TaskCard to
card-style layout (task-card-top-row, task-card-title, task-card-delete)
- app.css: add sections 25–27 — tablo detail page, kanban board/columns,
and task card (card appearance, hover states)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Remove stub templ.ComponentFunc from tablo_detail_view.go; real TabloDetailPage now comes from tablo_detail_templ.go
- Remove context/io/templ imports that were only used by the stub
- Register GET /tablos/{tabloID}/{tab} route in router.go after /tablos/{tabloID}
- All handler tests pass: TestGetTabloDetailPage_*, TestTabloDetailKanbanColumns, TestComputeTabloProgress, TestNewTabloDetailViewModel_*
- TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView exported
- computeTabloProgress excludes etape tasks
- GetTabloDetailPage handler with IDOR mitigation
- GET /tablos/{tabloID} route registered
- Full test suite green (13 packages)
- Two separate containers: #tablos-grid (card grid) and #tablos-table (table), toggled via .hidden
- TabloListRow: new templ for <tr> with icon+title, Actif/Archivé badge, date, progress bar, trash
- JS: setTablosView toggles hidden on both containers; filterTablos targets both article and tr elements
- Remove old .tablo-list-row / #tablos-list[data-view] CSS (no longer needed)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Dashboard: French header "Mes Projets", underline-tab view toggle (grid/list), filter tabs (Tous/Pas commencé/En cours/Terminé) with JS client-side filtering
- Cards: display status derived from progress (À faire/En cours/Terminé), rounded-xl p-6, w-8 h-8 avatar, green-500 progress bar, dashed "Créé le" footer
- Click on card/row navigates to /tablos/{uuid} via event delegation (delete zone stops propagation)
- List view: single-column grid, rows show status + title + date + task count
- CSS: .view-tab and .filter-tab with .is-active state
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- TestTablosDashboard_ProgressBar: asserts "Progression:" label and project-card-progress-row class
- TestTablosDashboard_ViewToggle: asserts view-toggle-btn class and data-view="grid" attribute
- TestTablosDashboard_StatusBadge: asserts "Active" badge and tablo-list-row dual-element structure
- All tests use unique email addresses; go build ./... exits 0
- Rewrote TabloProjectCard with outer article.tablo-card-wrapper (display:contents)
- Added .project-card child: status badge top-left, delete button top-right
- Avatar circle now shows initial letter from card.Tablo.Title
- Added calendar icon + formatted date in .project-date-row
- Added .project-card-progress-row with "Progression: X%" label and .project-card-progress-bar
- Added .tablo-list-row sibling (hidden by default) for list view toggle
- Added strconv and strings imports for Itoa and title-casing Status
- Add Progress int field to TabloCardView (D-05)
- Map row.Status into Tablo struct in TabloCardsFromUnreadRows
- Wire ListTabloProgressByIDs batch query in TablosListHandler (no N+1)
- Non-fatal error handling: progress defaults to 0 on query failure
- Add migration 0010_tablo_status.sql with reversible goose Up/Down
- Update all tablo SQL queries to include status column in SELECT/RETURNING/GROUP BY
- Add ListTabloProgressByIDs batch aggregation query (D-06)
- sqlc regenerated locally (files gitignored — regen via sqlc generate)
- Sidebar: Tailwind utilities, French labels (Aperçu/Tâches/Projets/Planning/Discussions/Fichiers), hr separators, collapse button on group hover, tablo list with circle icons, org footer with avatar + "1 membre"
- Header: 75px height, border-b #EAECF0, search input left, bell + avatar-dropdown right (no breadcrumb visible — retained sr-only for a11y + tests)
- Layout: flex instead of grid, dashboard-main no padding, dashboard-main-content wrapper at 2rem
- Tests updated for new search-input assertion
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add PageHeader templ component with three-zone layout (breadcrumb left, search placeholder center, bell/inbox/avatar right)
- Avatar dropdown uses native details/summary HTML (D-06, no Alpine.js)
- Logout form inside dropdown with CSRF token (T-18-03-01 mitigated)
- Remove Plan 01 breadcrumb/headerActions stubs from AppLayout body
- Wire @PageHeader call inside dashboard-main before children
- Add inline JS for avatar dropdown outside-click close behavior
- Add .dashboard-shell.sidebar-is-collapsed grid-template-columns: 4rem 1fr
- Hide sidebar-brand-title, section labels, nav labels, project labels when collapsed
- Center icons in collapsed state via justify-content: center on sidebar-nav-link-inner
- Hide sidebar-project-list in collapsed state (no icons for project entries)
- Flip collapse button chevron with rotate(180deg) when collapsed
- Regenerate static/tailwind.css via bin/tailwindcss --minify
- Add GENERAL section label above primary nav items list
- Add PROJECTS section label and tablo list inlined directly in DashboardSidebar
- Wire collapse button with inline JS toggling sidebar-is-collapsed on .dashboard-shell
- Remove SidebarOrganizationFooter call (moves to avatar dropdown in Plan 03)
- Remove DividerAfter rendering branch (section labels replace dividers)
- Replace Dashboard/Tasks/Planning/Chat/Files with Home/My Tasks/Projects/Events/Team Members
- Remove DividerAfter from all items (section labels replace dividers per D-08)
- Add "team" case to SidebarNavIcon switch using Lucide users SVG paths
Phases 13-17 (design-system-foundation, auth-pages, dashboard-tablos,
tablo-detail, chat-planning) moved from .planning/phases/ to
.planning/milestones/v3.0-phases/ as execution history archive.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Fresh REQUIREMENTS.md will be created for v4.0 via /gsd-new-milestone.
History preserved — see .planning/milestones/v3.0-REQUIREMENTS.md.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- PlanningShowDaySeparator in planning_forms.go — returns true when date
changes between consecutive events
- PlanningDaySeparator templ: slate-50 header row with date label and
data-day-separator attribute
- PlanningEventListItem: remove redundant DateLabel column (now in separator)
- Loop uses index to call PlanningShowDaySeparator before each event row
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- #discussion-messages: max-height 32rem, overflow-y auto, smooth scroll
- Auto-scroll to bottom on initial load, on own message sent (htmx:afterRequest),
and on SSE message received from other users
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Remove static/ from exclude_dir, add js to include_ext.
Exclude static/tailwind.css via regex to prevent rebuild loop from
the Tailwind output file triggering its own regeneration.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The server-side flush didn't eliminate the race because browser HTMX and SSE
event processing are inherently async.
Real fix: embed data-current-user-id on #discussion-tab (from DiscussionTabData.
CurrentUserID). The SSE handler now skips any event whose authorUserId matches
the current user — those messages are always delivered via HTMX (IsOwn=true,
right-aligned). SSE only appends messages from other users.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The server was publishing to the SSE broker before writing the HTMX response,
causing a race: if the SSE event (IsOwn=false, left-aligned) arrived at the
browser before HTMX appended the response (IsOwn=true, right-aligned), the
SSE path won and messageExists() then blocked the correct HTMX append.
Fix: write and flush the HTMX response first, then publish to SSE. This ensures
the sender's own message lands in the DOM right-aligned before the SSE event fires.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- CR-01: add id="discussion-message-list" to .divide-y; change hx-target
to #discussion-message-list so HTMX appends inside the list, not after it.
Always render the list div so the target exists even when messages is empty.
- WR-01: SSE broadcast now renders IsOwn=false for all recipients so other
users don't receive the sender's right-aligned bubble
- WR-02: add HX-Retarget/HX-Reswap headers on 422 and 500 error responses
so validation errors reach the composer form in the DOM
- WR-03: replace hardcoded rgba(128,78,236,0.10) with color-mix() using
the --color-brand-primary token
- WR-04: remove hardcoded "1 participant" subtitle (no participant count in data)
- IN-02: DiscussionMessageFromRow now accepts currentUserID uuid.UUID (matching
DiscussionMessagesFromRows) instead of a pre-computed bool
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
TabloDetailHandler was always rendering the full page regardless of
HX-Request, causing HTMX to swap the entire layout (sidebar, header)
into #tab-content instead of just the overview fragment.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add IsOwn bool to DiscussionMessageView; set via user.ID comparison in
DiscussionMessagesFromRows and DiscussionMessageFromRow
- Thread currentUserID through loadDiscussionTabData and all call sites
- discussion.templ: branch message-own vs message-other on message.IsOwn
- Restore .divide-y wrapper inside #discussion-messages (discussion-sse.js
depends on it to locate the message list before appending SSE events)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Replace ChatMainContent() stub with ChatMainContent(data DiscussionTabData)
- Renders .ui-card container with .message-own/.message-other rows and .message-bubble
- Update GetChatPage handler to pass views.NewDiscussionTabData()
- Run templ generate; TestChatMainContentRendersBubbleClasses passes
- Full go test ./... -count=1 exits 0
Phase 16 executor removed the .task-drag-handle div from TaskCard
during restyling. Sortable.js handle: '.task-drag-handle' had no
matching element → dragging completely non-functional.
Restores the grip element with CSS-token styling (no Tailwind).
- Replace <ul>/<li> layout with @ui.Table using fileTableHead/fileTableBody helpers
- Add .overview-section-heading header with "Files" h3 and Upload file button
- Convert FileListRow outer element from <li> to <tr class="file-row-zone">
- Convert FileDeleteConfirmFragment outer element from <div> to <tr class="file-row-zone"> with <td colspan="4">
- Add Download and Delete @ui.IconButton in FileListRow actions column
- Replace FileListEmpty with @ui.EmptyState in FilesTabFragment and UploadErrorFragment
- Convert FileRowGone from <div> to <tr> for DOM consistency
- All 9 file handler tests pass; go build ./... exits 0
- tablos.templ TasksTabFragment: add etapes as 5th argument to @KanbanBoard
- handlers_tasks.go reorder (single): capture etapes from loadTasksTabData; pass to KanbanBoard
- handlers_tasks.go reorder (batch): capture etapes from loadTasksTabData; pass to KanbanBoard
- go build ./backend/... exits 0; go test ./backend/internal/web/... -count=1 passes
- Replace header with project-card-top layout: color avatar with first char, tablo-title-zone
- Replace Discussion link/Invite button/Delete button with @ui.IconButton and @ui.Button using design token variants
- Add inline tablo-delete-zone with trash @ui.IconButton (does not use TabloDeleteButtonFragment)
- Replace metadata row hardcoded flex/hex classes with tablo-metadata-row, @ui.Badge(BadgeVariantPrimary), project-progress-track/bar
- Replace 5 tab nav <a> elements from long inline Tailwind hex classes to tab-nav-item / tab-nav-item is-active
- Wrap tab nav in class="tab-nav" replacing raw flex container
- Move @TabloDescDisplay call from persistent header into TabloOverviewTabFragment
- Remove @EtapeStrip call from TasksTabFragment (D-E01; KanbanBoard call site update deferred to Plan 03)
- Remove last #804EEC hex value from TabloTitleDisplay hover class
- Regenerated tablos_templ.go via templ generate
- add download + chat icon SVG cases to UIIcon switch
- append CSS Sections 19-25 to app.css (tasks-section through task-list-empty)
- all tests pass; zero hardcoded hex values in new CSS
- insert case "download" with arrow-down SVG (path + polyline + line) before default case
- insert case "chat" with speech-bubble SVG (path) before default case
- icon_button_templ.go regenerated via templ generate (gitignored, regenerated at build time)
- RESEARCH.md: mark Open Questions as RESOLVED; resolve A2 (ButtonVariantGhost does not exist as ButtonTone; use ButtonToneSoft+ButtonVariantDefault for Invite button); confirm TaskCardGone/OOB keep params; confirm FileDeleteConfirmFragment <tr> is safe
- VALIDATION.md: fix automated command paths to match actual plan verify commands (go test ./backend/internal/web/... not cd go-backend); set nyquist_compliant: true and wave_0_complete: true
- 16-02 Task 2: remove KanbanBoard call site update (was causing accepted compile error); Task 2 now only deletes EtapeStrip call and requires clean go build ./backend/... exit 0
- 16-02 Task 1: clarify Discussion link as <a> wrapping @ui.IconButton (not ambiguous OR); fix Invite button to ButtonToneSoft+ButtonVariantDefault
- 16-03 Task 2: add tablos.templ as modified file; update action to include all three KanbanBoard call sites atomically
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Phase 16 delivers DETAIL-01/02/03/04: header restyling, kanban
tasks-section layout with server-side etape grouping, and files
table component. Ends with a browser verify checkpoint.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Merge conflict resolution had taken the old template signatures (pre-AppLayout).
Restored HEAD~1 signatures then applied the header/tab-nav restyle:
- Title row: md:text-3xl font-bold, Discussion + Invite action buttons
- Metadata row: created date, status badge, 0% progress bar
- Sticky purple-accent tab bar (Overview, Tasks, Files, Discussion, Events)
- All HTMX wiring and AppLayout wrapping preserved
- Replace plain back-link + title with structured header: title row + metadata row
- Title uses larger md:text-3xl font-bold style with inline-edit preserved
- Add Discussion and Invite action buttons in header (purple brand colors)
- Add metadata row: created date, hardcoded En cours status badge, 0% progress bar
- Replace plain slate tab nav with purple-accent tab bar (icon + label per tab)
- Tab active state: text-[#804EEC] border-[#804EEC]; inactive: text-[#667085]
- Tab bar is sticky (top-0 z-40) with horizontal scroll and hidden scrollbar
- Keep all HTMX attributes, hx-push-url, hx-target="#tab-content" logic unchanged
- tablo-title-zone, tablo-desc-zone, tablo-delete-zone elements retained
- Updated TablosDashboard signature to accept activePath and tablos for AppLayout
- Replaced old @Layout call with @AppLayout (sidebar-based shell)
- Added TabloProjectCard component with project-card grid, colored avatar, tablo-title-zone, edit/delete icon buttons
- Replaced TablosEmptyState raw HTML with @ui.EmptyState component (ui-empty-state class)
- Updated TabloDetailPage signature with activePath and sidebarTablos params
- Updated TabloNotFoundPage signature with activePath and sidebarTablos params
- Both detail pages switch from @Layout to @AppLayout
- Create backend/internal/web/ui/app.css with dashboard shell, sidebar, and project-card CSS ported verbatim from go-backend
- All color values use var(--...) design tokens — no hex colors
- Add @import "./internal/web/ui/app.css" to backend/tailwind.input.css
- TestTablosDashboard_Sidebar: asserts dashboard-sidebar + sidebar-nav-shell in GET / body
- TestTablosDashboard_ProjectCards: asserts project-card in GET / body with a pre-inserted tablo
- TestTablosDashboard_EmptyState: asserts ui-empty-state in GET / body with zero tablos
- All three skip without TEST_DATABASE_URL; compile cleanly; existing TestTablos* tests unaffected
- RESEARCH.md: rename '## Open Questions' to '## Open Questions (RESOLVED)';
add [RESOLVED] markers to all three questions with verified answers.
Q2 resolved: edit-title route confirmed via codebase, no single /edit route.
- 15-03-PLAN.md Task 1: add handlers_tablos.go to read_first; make edit
icon button definitively use hx-get=/tablos/{id}/edit-title with
hx-target="closest .tablo-title-zone"; remove open-ended href fallback
discretion; add edit-title grep to acceptance criteria.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Add css to air's include_ext so edits to internal/web/ui/*.css trigger a
rebuild. Add Tailwind build step to the air cmd so static/tailwind.css is
always up to date on reload. static/ remains excluded so the generated
output file doesn't cause a loop.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
width: auto on a block element still fills the parent. Use width: max-content
so the button wraps its icon + label + padding only. Remove flex-grow: 1 on
the label span so it doesn't stretch within the now-natural-width button.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Google's gsi-material-button uses width: auto, not 100%. Replace width: 100%
/ max-width: 400px with width: auto + margin: 0 auto so the button sizes to
its content (icon + label + padding) and stays centered in the card.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Per Google's branding guidelines, the gsi-material-button requires Roboto
Medium. Add Google Fonts preconnect + stylesheet link to AuthLayout head.
Also add display:block + 100% dimensions to the SVG inside the icon
container to prevent inline baseline gaps.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Replace Layout+Card pattern with AuthLayout("Create your account", csrfToken)
- Wire GoogleButton and AuthDivider into SignupPage body
- Replace raw <input> elements with @ui.FormField/@ui.Input design system components
- Password placeholder "12 characters minimum", autocomplete="new-password"
- Add signup-copy nav link ("Already have an account? Sign in") pointing to /login
- Preserve HTMX swap: hx-post="/signup" hx-target="#signup-form" hx-swap="outerHTML"
- Remove signupCardBody helper
- Replace Layout+Card pattern with AuthLayout("Sign in to Xtablo", csrfToken)
- Wire GoogleButton and AuthDivider into LoginPage body
- Replace raw <input> elements with @ui.FormField/@ui.Input design system components
- Add signup-copy nav link ("Don't have an account? Sign up")
- Preserve HTMX swap: hx-post="/login" hx-target="#login-form" hx-swap="outerHTML"
- Remove loginCardBody, AuthProviderButtonsBlock, AuthProviderButtonControl helpers
- Update test assertions for new GoogleButton labels ("Sign in with Google" / disabled attr)
- auth_components.templ: AnimatedBackground (35 elements, /static/logo_dark.png, no light/dark pairs), GoogleButton (a/button variant, English label 'Sign in with Google'), AuthDivider ('or' divider)
- auth_layout.templ: standalone HTML shell with .login-screen, @AnimatedBackground(), .card-wrap/.card-glow/.auth-card-shell, htmx.min.js only (no sortable/sse scripts)
- No auth.User param on AuthLayout (auth pages always unauthenticated)
- just generate exits 0, all Go tests pass
- Copy logo_dark.png and logo_white.png from go-backend/static into backend/static
- Replace minimal 62-line auth.css with 828-line extraction from go-backend/static/styles.css
- Sections: login-screen shell, animated background (bg-01..35, size/opacity utilities), card wrapper, card internals, gsi-material-button, legacy auth-provider controls, 66 keyframe definitions, animate-* utility classes
- Added display:block and text-decoration:none to .gsi-material-button for the anchor variant
- Does NOT include .app-shell or .dashboard-shell rules
Two-wave plan: Plan 01 creates foundation (logo assets, full auth.css
replacement with animations, auth_components.templ, auth_layout.templ);
Plan 02 migrates auth_login.templ and auth_signup.templ to AuthLayout
with @ui.FormField inputs and cross-page nav links, closing AUTH-UI-01..03.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Add backend/internal/web/ui/catalog/catalog.templ: single-page layout
with 240px sidebar nav (11 anchor links) and 11 component sections with
section headings matching DS-XX requirement IDs
- Add backend/internal/web/ui/catalog/examples.go: Example struct + typed
example functions for all 11 component types (badge/button/card/empty-state/
form-field/icon-button/input/modal/select/table/textarea); modal renders
panel-only (no backdrop wrapper, Pitfall 7)
- Add backend/internal/web/catalog_route_catalog.go (//go:build catalog):
RegisterCatalogRoute mounts GET /ui-catalog via catalogPageHandler()
- Add backend/internal/web/catalog_route_stub.go (//go:build !catalog):
no-op RegisterCatalogRoute for production builds
- Wire RegisterCatalogRoute(r) unconditionally in NewRouter after protected routes
- Add justfile catalog target: just generate + go run -tags catalog ./cmd/web
- go build ./... and go build -tags catalog ./... both pass; go test ./... green
- Modal, EmptyState, Table, IconButton, UIIcon, Space components ported
- UIIcon wired into button.templ; tailwind.input.css updated to 14 imports
- All 13 new tests pass; full go test ./... green
- Input, Textarea, Select, FormField components ported from go-backend
- TDD gates: 4 RED/GREEN commits, all 13 new tests passing
- Full go test ./... is green (all packages)
- select_helpers.go: 9 helper functions verbatim from go-backend
- select.templ: SelectProps/SelectOption structs, inline JS with __uiSelectInitAll
and htmx:afterSwap re-init listener (Pitfall 6)
- select.css: .ui-select-control (min-height 44px), .ui-select-menu (max-height 16rem)
- form_field.templ: FormFieldProps with Label/For/Field/Error/Hint; conditional regions
- form-field.css: .ui-form-field/.ui-form-label/.ui-form-hint/.ui-form-error
- tailwind.input.css: add @import for select.css and form-field.css
- All 6 TestSelect/TestFormField tests passing; full go test ./... is green
- TestSelect_RendersControl: expects ui-select-control in output
- TestSelect_HasInlineScript: expects __uiSelectInitAll in script block
- TestSelect_HasHtmxListener: expects htmx:afterSwap re-init listener
- TestFormField_RendersLabel: expects ui-form-field and ui-form-label
- TestFormField_RendersError: expects ui-form-error when Error is set
- TestFormField_NoErrorWhenEmpty: expects no ui-form-error when Error is empty
- input.css: .ui-input with min-height 44px, border-radius 0.75rem, focus ring
- input.templ: InputProps with ID/Name/Type/Placeholder/Value/Disabled/Required/Attrs
- textarea.css: .ui-textarea with min-height 7rem, resize vertical, focus ring
- textarea.templ: TextareaProps with ID/Name/Value/Placeholder/Rows/Disabled/Required/Attrs
- tailwind.input.css: add @import for input.css and textarea.css
- All 7 TestInput/TestTextarea tests passing
- TestInput_DefaultType: expects type="text" for empty Type
- TestInput_EmailType: expects type="email" for explicit Type
- TestInput_IDFallback: expects id from Name when no ID set
- TestInput_ExplicitID: expects explicit ID to take precedence
- TestTextarea_RendersClass: expects class="ui-textarea"
- TestTextarea_DefaultRows: expects rows="4" for zero Rows
- TestTextarea_ExplicitRows: expects rows="6" for explicit Rows
- button.css: replaced with go-backend multi-class selector version + ghost variant rules
- badge.css: replaced with go-backend pill-shape version + primary variant
- card.css: replaced with go-backend token-based header/body/footer version
- card.templ: migrated from children passthrough to typed CardProps{Header/Body/Footer}
- ui_test.go: rewrote TestCard_RendersChildren -> TestCard_RendersTypedRegions; added TestBadge_PrimaryVariant; added textComponent helper + io import
- auth_login.templ, auth_signup.templ: migrated Card usage to typed CardProps API
- tablos.templ: migrated TabloCard to typed CardProps API with extracted tabloCardBody
- planning.templ, tasks.templ, events.templ, etapes.templ: all compound button class strings updated to multi-class pattern
- go test ./... passes (all packages green)
- just generate succeeds
- Add ButtonVariantGhost to ButtonVariant enum and NormalizedButtonVariant switch
- Add BadgeVariantPrimary to BadgeVariant enum and NormalizedBadgeVariant switch
- Add IconButtonVariant type (Neutral/Warning/Success/Danger) with normalizer
- Add IconButtonTone type (Solid/Ghost) with normalizer
- Add SpacingStep type (XS/SM/MD/LG/XL) with normalizer
- Add IconButtonClass(), SpaceXClass(), SpaceYClass() exported class functions
- Add buttonType(), inputType(), inputID(), textareaRows() helper functions to helpers.go
- Fix TestButtonClass_GhostVariant assertion to match compound class format preserved for Plan 02
- Collapse 5-size type scale to 4 by removing 12px from page scale;
annotate text-xs as badge-system token scoped to .ui-badge only
- Metadata (file size, date) stays at 14px, differentiated by slate-500
- Update CTAs to verb+noun: Upload File, Delete File, Download File
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Production compose stack with postgres, web, worker, caddy services (D-01..D-04, D-08)
- postgres service has no host ports binding (internal network only, T-07-09 mitigated)
- web and worker use same image with different command: values (/app/web, /app/worker)
- Both web and worker depend_on postgres with service_healthy condition (T-07-12 mitigated)
- Caddy handles TLS via Let's Encrypt with persistent caddy_data and caddy_config volumes (D-04)
- Caddyfile uses {$DOMAIN} env var interpolation for the site block (RESEARCH Pattern 6)
- Caddyfile includes Let's Encrypt staging note to avoid rate limits (RESEARCH Pitfall 4)
- Add S3_ENDPOINT, S3_BUCKET, S3_REGION, S3_ACCESS_KEY, S3_SECRET_KEY with MinIO dev defaults
- Add S3_USE_PATH_STYLE (true for MinIO, false for R2 virtual-hosted)
- Add MAX_UPLOAD_SIZE_MB=25 with default note
- Add commented DOMAIN=app.yourdomain.com for Caddy TLS in docker-compose.prod.yaml (D-04)
- Clarify TEST_DATABASE_URL is dev/test only and must not appear in .env.prod
- All original vars (DATABASE_URL, SESSION_SECRET, PORT, ENV) preserved
- backend/internal/web/router.go: staticDir string -> staticFS fs.FS; /healthz uses HealthzHandler(); /readyz registered with ReadyzHandler(pinger); embedded FS served via fs.Sub()
- backend/cmd/web/main.go: import assets "backend"; db.RunMigrations(ctx, pool, assets.Migrations) before router; web.NewRouter now receives assets.Static
- All *_test.go NewRouter call sites updated from "./static" to os.DirFS("./static"); "os" import added where missing
- TestHealthz_OK now calls HealthzHandler() with no args (liveness, no db field)
- TestHealthz_Down deleted (new HealthzHandler has no failure mode)
- TestReadyz_OK added: ReadyzHandler(stubPinger{err: nil}) -> 200 + db:ok
- TestReadyz_Down added: ReadyzHandler(stubPinger{err: ...}) -> 503 + degraded
ROADMAP SC-3 (list-failed-jobs CLI) and SC-4 (web binary enqueue) struck
through as deferred per CONTEXT.md decisions D-03 and D-06. VERIFICATION.md
status updated to complete (3/3 active success criteria).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Closes SC-3 (WORK-04) with a list-failed-jobs subcommand in cmd/worker and
SC-4 (WORK-01) by wiring a river insert-only client into cmd/web with a
/debug/enqueue-test handler that proves the web→worker enqueue path.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
All WORK-01 through WORK-04 requirements verified live against Postgres + MinIO.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- rivermigrate at startup (idempotent, before client construction)
- S3 store init from env vars (S3_ENDPOINT/S3_BUCKET/S3_ACCESS_KEY/S3_SECRET_KEY/S3_REGION/S3_USE_PATH_STYLE)
- signal.NotifyContext created AFTER all startup I/O (PATTERNS.md critical ordering)
- HeartbeatWorker + OrphanCleanupWorker registered via river.AddWorker
- river.Client with slog.Default() Logger, SlogErrorHandler, MaxWorkers:10
- HeartbeatArgs periodic every 1 min (RunOnStart:true), OrphanCleanupArgs every 1 hr
- StopAndCancel(10s timeout) on shutdown; pool.Close after StopAndCancel
- HeartbeatArgs + HeartbeatWorker (logs slog.Info on each tick)
- OrphanCleanupArgs + OrphanCleanupWorker (S3 delete then DB delete loop)
- NewOrphanCleanupWorker constructor with pool + FileStorer injection
- SlogErrorHandler implementing river.ErrorHandler (HandleError + HandlePanic)
- fileQuerier interface for test injection without real DB
- Unit tests: 7 tests pass (pure mock-based, no DB required)
- go build ./... exits 0
Three plans: Wave 1 adds river dependency + internal/jobs package with unit
tests; Wave 2 wires cmd/worker/main.go with full river runtime + justfile
target + README; Wave 3 is human-verify checkpoint.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Set nyquist_compliant: true and wave_0_complete: true in frontmatter
- Update Per-Task Verification Map: FILE-01..06 integration tests marked ⚠️
(clean skip without TEST_DATABASE_URL); new unit tests marked ✅ green
- Check all Wave 0 requirements as complete
- Fill in Validation Sign-Off checklist
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Gap fill: three no-infrastructure unit tests that run without TEST_DATABASE_URL or S3_ENDPOINT:
- backend/templates/files_helpers_test.go — formatBytes boundary cases (B/KB/MB/GB)
- backend/internal/files/store_unit_test.go — byteCountReader accumulation, io.ErrUnexpectedEOF
guard for small files, and MultiReader body reconstruction after 512-byte sniff
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
15s was too short for 25MB uploads on slow connections (~256KB/s takes
~100s). Both timeouts are raised to 120s to accommodate MAX_UPLOAD_SIZE_MB
at worst-case bandwidth with headroom.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- Expand stubbedFileStorer with deletedKey tracking and deleteErr injection field
- Implement TestFileDownload (FILE-04): 302 redirect to presigned URL
- Implement TestFileDownload_NonOwner: non-owner gets 404
- Implement TestFileDelete (FILE-05): HTMX delete, S3+DB both deleted
- Implement TestFileDelete_S3Failure: S3 error does not abort DB delete, 200 returned
- Implement TestFileOwnership (FILE-06): non-owner gets 404 on all three routes
All 6 in-scope findings (CR-01, CR-02, WR-01, WR-02, WR-03, WR-04)
have been applied and verified. Updated status from issues_found to fixed.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
maxPos + 100 could silently overflow to a negative value when maxPos
approached MaxInt32. Added a maxAllowedPosition guard that returns a
validation error before the InsertTask call if the column position space
is exhausted.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
A description of spaces-only was being stored as a valid non-null DB value
because the empty-string check ran before trimming. Now consistent with how
other nullable text fields are handled.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Without these headers, HTMX used the form's own hx-target="#column-{status}"
+ hx-swap="beforeend", appending the error form into the task column and
destroying all visible task cards. The error form now lands back in the
add-task slot where it belongs.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Both the single-task branch and the main loop were using _, _ = to
discard UpdateTask errors. Now both log the error and return 500 so
the client is never shown a false success when DB writes fail.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
The raw fmt.Fprintf bypassed templ's auto-escaping pipeline and was
inconsistent with every other handler. Added TaskCardGone(taskID uuid.UUID)
to tasks.templ and updated TaskDeleteHandler to use it. Ran just generate.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Both handlers were missing the mandatory ParseForm call before reading
PostFormValue. This caused gorilla/csrf (which reads the body for CSRF
token validation) to consume the body, leaving PostFormValue to return
empty strings. TaskReorderHandler was used as the correct reference.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
All 7 TASK requirements verified in browser. Two bugs found and fixed
during verification (badge count, drag-and-drop draggable selector).
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Sortable.js draggable must match direct children of .sortable-column.
Using .task-card (grandchild) caused Sortable to detach it from its
.task-card-zone wrapper, breaking HTMX OOB swap targets and making
drag appear to do nothing. Changed to .task-card-zone so the full
wrapper moves, keeping id= attributes intact for HTMX round-trips.
Also removed redundant form.dispatchEvent() before htmx.trigger()
which could cause a double submit on reorder.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Replace htmx.onLoad (requires htmx at parse time) with native
document.addEventListener('DOMContentLoaded') + 'htmx:afterSettle'
so Sortable.js is guaranteed loaded before init runs.
Add task-count-badge-{status} wrapper IDs and updateBadges() that
recounts .task-card elements on every HTMX settle so badge counts
stay in sync after create, delete, and reorder operations.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- 04-03-SUMMARY.md created with full execution details
- STATE.md updated: decisions, metrics, notes for plan 03
- ROADMAP.md updated with plan progress (3/4 summaries for phase 4)
- Remove t.Skip("handlers_tasks not yet implemented") from TestTaskOrderPersists
- Full test suite green: go test ./... exits 0, no FAIL lines
- All 9 TestTask* tests active (skip on missing TEST_DATABASE_URL per existing pattern)
- TaskReorderHandler: POST /tablos/{id}/tasks/reorder updates status+position
- Fetches existing task via GetTaskByID before UpdateTask (T-04-08 mass assignment guard)
- Supports both array form (task_id[]/task_col[]) and single-value form (task_id/status/position)
- Invalid UUIDs silently skipped (D-05); tasks from other tablos skipped (T-04-10)
- Returns updated KanbanBoard outerHTML for HTMX swap
- Remove t.Skip from TestTaskReorderCrossColumn and TestTaskReorderSameColumn
- Add handlers_tasks.go: TasksDeps, TaskNewFormHandler, TaskCancelNewHandler, TaskCreateHandler, TaskShowHandler, TaskDeleteConfirmHandler, TaskDeleteHandler, plus stub Edit/Update/Reorder handlers
- Add task routes to router.go (static before parametric per Pitfall 1)
- Add TasksDeps param to NewRouter; update main.go and all test callers
- Move TaskColumns/TaskColumnLabels to templates package to avoid import cycle
- handlers_tasks_test.go: 9 TestTask* functions (TASK-01..07 + IDOR) all skip
- TasksDeps stub struct declared in test file for Plan 02 wiring
- tasks_forms.go: TaskCreateForm, TaskCreateErrors, TaskUpdateForm, TaskUpdateErrors structs
- go build ./... passes; go test -run TestTask exits 0 with all 9 SKIP
- TabloCreateErrors: add Color field for server-side hex validation error
- TabloCreateFormFragment: render FieldError for color field and update
placeholder to hex-only hint (#6366f1) matching the validation constraint
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- TabloUpdateHandler: capture user from loadOwnedTablo (was discarded with _)
- Pass captured user to TabloDetailPage on non-HTMX validation error path
instead of nil, preventing broken layout (no logout button/email shown)
- TabloUpdateHandler: pass tablo.Color to UpdateTablo to preserve color on update (CR-01)
- loadOwnedTablo: pass GetTabloByIDParams{ID, UserID} to DB query (WR-01 call site)
- TabloDeleteHandler: pass DeleteTabloParams{ID, UserID} to DB query (WR-02 call site)
- TabloDeleteHandler: on DB error with HX-Request, render TabloDeleteConfirmFragment
instead of plain http.Error to avoid broken HTMX DOM state (CR-03)
- renderTabloCreateError: log secondary ListTablosByUser fetch failure (WR-03)
- TablosCreateHandler: validate color with isValidCSSColor (hex only) and surface
TabloCreateErrors.Color field error to prevent CSS injection (WR-04)
- Add isValidCSSColor helper using ^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$ regex
- Update test call sites for GetTabloByID and DeleteTablo new param types
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- UpdateTablo SQL: add color = \$4 so color is preserved across title/description edits
- GetTabloByID SQL: add AND user_id = \$2 to push ownership enforcement into the DB layer
- DeleteTablo SQL: add AND user_id = \$2 to push authorization into the DB layer
- sqlc bindings regenerated (UpdateTabloParams+Color, GetTabloByIDParams, DeleteTabloParams)
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- loadOwnedTablo helper: uuid.Parse, GetTabloByID, ownership check (D-04: 404 not 403)
- TabloDetailHandler: GET /tablos/{id} renders detail page
- TabloEditTitleHandler/ShowTitleHandler: GET /tablos/{id}/edit-title|show-title fragments
- TabloEditDescHandler/ShowDescHandler: GET /tablos/{id}/edit-desc|show-desc fragments
- TabloUpdateHandler: POST /tablos/{id} — validates, updates DB, renders matching zone fragment
- TabloDeleteConfirmHandler/CancelHandler: GET /tablos/{id}/delete-confirm|delete-cancel
- TabloDeleteHandler: POST /tablos/{id}/delete — deletes row, HX-Redirect:/ or 303
- router.go: 9 new routes in RequireAuth group, static-before-parametric order preserved
- Fix [Rule 1 - Bug]: test title "Owner's Tablo" caused HTML entity mismatch — changed to "Owners Detail Tablo"
- go test ./internal/web/... -run TestTablo: 10/10 PASS; full suite: all PASS
- TabloDetailPage: full detail layout with title/desc/delete zones
- TabloTitleDisplay/EditFragment: outerHTML-swappable title zone with _zone=title hidden field
- TabloDescDisplay/EditFragment: outerHTML-swappable desc zone with _zone=desc hidden field
- TabloDeleteButtonFragment: canonical single-source delete zone (TabloCard now delegates here)
- TabloDeleteConfirmFragment: inline confirm with "Delete tablo?", "Yes, delete", "Keep tablo"
- TabloNotFoundPage: 404 page with UI-SPEC copy
- TabloUpdateErrors struct added to tablos_forms.go
- just generate + go build ./... both exit 0
Plans cover TABLO-01..06 via MVP vertical slices: foundation (migration
+ sqlc + test scaffold + button CSS), list+create (dashboard, inline
form, OOB swap), and detail+edit+delete (ownership 404, inline edit
fragments, inline confirm delete). Includes Nyquist VALIDATION.md and
PATTERNS.md with real analog excerpts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace generic "Save"/"Cancel" with specific labels: "Save changes",
"Discard changes", "Keep tablo" throughout spec, component inventory,
interaction contracts, and HTMX reference table
- Remove 500 (medium) weight; form labels now use font-semibold (600),
consistent with Button convention — 2 declared weights total
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
- TestLoadCSRFKey_* in internal/auth for env key loading
- TestCSRF_*MissingToken / TestCSRF_*ValidToken for all three POST routes
- TestForms_ContainCSRFField for hidden _csrf input in rendered HTML
- TestRouter_CSRFMountedAfterResolveSession for middleware order (D-24)
- TestCSRF_HeaderFallback for X-CSRF-Token header support
- Add gorilla/csrf v1.7.3 dependency
- Add LogoutHandler: deletes session row (D-06), clears cookie, redirects to /login
- Protect GET / inside RequireAuth group; remove old top-level registration
- Add POST /logout inside same RequireAuth group (D-22: POST-only logout)
- Update Layout signature to accept *auth.User; render logout form + email when authed
- Update Index template to accept *auth.User and show "Signed in as {email}"
- Update SignupPage/LoginPage to pass nil to Layout (auth pages are unauthed)
- Update IndexHandler to pull user from auth.Authed(ctx) and pass to template
- Update TestIndex_RendersHxGet -> TestIndex_UnauthRedirects (GET / now protected)
- AUTH-04 (logout) and AUTH-05 (protected /) are now closed
- TestLogout_Success: POST /logout with valid cookie -> 303, cookie cleared, session deleted
- TestLogout_UnauthRedirectsToLogin: POST /logout without cookie -> 303 from RequireAuth
- TestLogout_HXRedirect: HTMX logout -> 200 + HX-Redirect: /login
- TestLogout_AfterLogoutSubsequentRequestUnauth: stale cookie blocked after logout
- TestProtected_HomeUnauthRedirects: GET / without session -> 303 /login
- TestProtected_HomeUnauthHXRedirect: HTMX GET / without session -> 200 + HX-Redirect
- TestProtected_HomeAuthRendersUserEmail: authed GET / -> 200 with user email
- TestLayout_LogoutFormVisibleWhenAuthed: Layout with user shows logout form
- TestLayout_LogoutFormHiddenWhenUnauthed: Layout with nil user hides logout form
- auth_login.templ: LoginPage + LoginFormFragment (mirrors signup shape)
- LoginForm + LoginErrors types added to templates/auth_forms.go
- LoginPageHandler + LoginPostHandler in handlers_auth.go
- Rate-limit check before user lookup (D-16, T-2-14)
- Single errInvalidCreds constant for D-20 enumeration defense
- Session rotation via Store.Rotate on success (D-10, T-2-04)
- HTMX-aware redirect and fragment responses (D-19, D-21)
- AuthDeps extended with Limiter *auth.LimiterStore field
- router.go: GET /login in RedirectIfAuthed group (D-23)
- main.go: LimiterStore created with janitor goroutine (D-16)
- Export NewLimiterStoreWithClock + SetLimiterClock for cross-package tests
- 12 TestLogin_* integration tests all pass with real DB
- Five-minute clone-to-page onboarding doc covering prereqs, bootstrap,
two-terminal dev workflow, common commands, and troubleshooting.
- Documents docker compose fallback for contributors not on podman (D-11).
- Documents two-terminal workflow: just dev + just styles-watch (D-14).
- Every `just <recipe>` referenced is a real recipe in backend/justfile.
- Uses 'bootstrap-downloaded' wording (not 'vendored') for tailwindcss
and htmx.min.js per Codex review concern #4.
- Clarifies HTMX runtime no-CDN policy; the unpkg URL in justfile is the
single authoritative version pin (D-10 / Codex concern #5).
- Top 3 pitfalls from RESEARCH surfaced in Troubleshooting.
- 195 lines, no emoji, no marketing voice.
- Opens pgxpool from DATABASE_URL, logs 'worker ready', blocks on
SIGINT/SIGTERM, closes pool, exits 0
- Reuses web.NewSlogHandler — pure helper, no HTTP coupling
- No job queue libraries (river/asynq/pg_notify) — Phase 6 replaces this
file in full
- 48 lines (under 80-line budget signals 'we did not implement Phase 6
by accident')
- Reads DATABASE_URL (required), PORT (default 8080), ENV (default
development) from os.Getenv only — no third-party env loader (D-15:
.env exported by just dev, prod injects real env)
- slog.SetDefault wired before fatal-on-missing-DSN so even startup errors
emit structured output; sanitization per T-01-12 (never log DSN)
- pgxpool opened via db.NewPool; chi router mounted with ./static as the
asset root
- http.Server: ReadTimeout=15s, WriteTimeout=15s, IdleTimeout=60s
(T-01-10 slow-client mitigation)
- signal.NotifyContext (Go 1.21+) traps SIGINT/SIGTERM, propagates through
ctx; on signal: srv.Shutdown with 10s timeout, then explicit pool.Close
(Pitfall 4 — never via defer)
- No /readyz route (Phase 7 scope)
- templates/layout.templ: base HTML shell per UI-SPEC §Base Layout Contract
(max-w-5xl container, slate-50 header, slate-200 borders, footer copy,
/static/tailwind.css in <head>, /static/htmx.min.js deferred at body end —
D-10: HTMX never loaded from a CDN)
- templates/index.templ: root page consuming @ui.Card and @ui.Button for the
canonical HTMX demo (UI-SPEC §Component Library Contract canonical block)
- templates/fragments.templ: TimeFragment renders <span> with RFC3339 UTC
timestamp; templ auto-escapes interpolation (T-01-13)
- internal/web/handlers.go: HealthzHandler (200 ok / 503 degraded per D-20,
2s Ping timeout), IndexHandler, DemoTimeHandler with injected clock
- internal/web/router.go: Pinger interface; NewRouter wires
RequestIDMiddleware → RealIP → SlogLoggerMiddleware → Recoverer (D-08
+ Pitfall 6 — chi middleware.Logger deliberately NOT registered) and
routes /, /healthz, /demo/time, /static/* via http.FileServer (T-01-08
path traversal blocked by http.Dir)
All six handler tests + ui package tests are GREEN under default go test.
- Remove //go:build red_gate tag from internal/web/handlers_test.go and
internal/db/pool_test.go now that consumer symbols are about to exist
- go mod tidy after real importers land (deferred from Plan 01-01 per
Codex concern #1) — chi/v5, templ, pgx/v5, google/uuid now in require list
- internal/db/pool.go: NewPool(ctx, dsn) builds a pgxpool.Pool with
MaxConns=10, MinConns=1; no eager Ping (RESEARCH Pitfall 2)
- internal/web/slog.go: NewSlogHandler returns JSON when env='production',
text otherwise; pure helper, no slog.SetDefault side effect
- internal/web/middleware.go: RequestIDMiddleware (UUIDv4 → ctx +
X-Request-ID header), LoggerFromContext helper, SlogLoggerMiddleware
factory using chi WrapResponseWriter; field allowlist per V7/T-01-09
- handlers_test.go (//go:build red_gate): TestHealthz_OK, TestHealthz_Down,
TestIndex_RendersHxGet, TestDemoTime_Fragment, TestRequestID_HeaderSet,
TestSlog_HandlerSwitch — reference web.HealthzHandler / NewRouter /
NewSlogHandler / Pinger to be implemented in Plan 01-03
- pool_test.go (//go:build red_gate): TestPool_Connects with t.Skip
fallback when DATABASE_URL is unset
- Build tag isolates the RED state from default 'go test ./...' (Codex #3)
- sqlc.yaml: postgresql engine, schema points at migrations/ (Pitfall 7), pgx/v5 sql_package
- tailwind.input.css: @source globs for templ + internal/web Go files (Pitfall 3), @imports four ui/*.css files per UI-SPEC
- .air.toml: watches .go + .templ, excludes generated *_templ.go (Pitfall 5), runs 'templ generate' pre-build
- Tailwind watch is NOT wired into air pre_cmd (two-terminal workflow per RESEARCH Open Q2)
Adds a Component Library Contract section locking in a small in-house
templ design-system under backend/internal/web/ui/, modeled on the
reference at go-backend/internal/web/ui/. Phase 1 ships only Button,
Card, and Badge; later phases extend the same package.
Lock base layout, spacing/typography/color tokens, and the canonical
HTMX interaction pattern that later phases inherit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace inline button markup with `ui.Button` component calls for
consistency and maintainability. Add filter menu component with dropdown
functionality. Convert roadmap mode toggle from link-based to select
dropdown. Include filter counter badge and clear filter actions.
Create a new tasks module with full CRUD operations, supporting both
regular tasks and etapes (phases). Implements task hierarchy with
parent-child relationships, assignees, and due dates. Includes database
schema with validation triggers, SQLC query generation, in-memory
repository implementation, HTTP handlers, view templates, and
comprehensive test coverage.
Update DashboardPage and DashboardContentSwap to receive the list of
tablos so the sidebar can display real project data. Extract tablo icon
palette logic into a new tabloicons package that maps hex colors to
presentation attributes (icon, background, foreground colors).
Update handlers to load tablos from the repository before rendering
dashboard views. Refactor TabloCardView to use icon presentation instead
of initials when available.
match
Instead of selecting icons based on tablo name length, compute the
closest matching icon from a predefined palette by comparing hex color
values. This ensures consistent icon-color pairing and better visual
harmony.
This commit moves project search filtering from the server to the
client.
Changes include:
- Remove `Query` field from `ListTablosInput` and related handlers
- Add French date formatting for project cards
- Convert search form to client-side filter with data attributes
- Add empty state message for no search results
- Update button border-radius from 0 to 0.7rem
- Increase air.toml build command to include Tailwind CSS generation
backend
Create the foundational structure for managing design-system CSS with
co-located sources and semantic tokens:
- Add `cmd/buildstyles` to concatenate ordered CSS sources into a single
shipped stylesheet
- Define semantic color and effect tokens in `internal/web/ui/base.css`
- Move primitive and catalog CSS sources from `static/css/` to
co-located locations under `internal/web/ui/`
- Update test contract to verify token presence and proper stylesheet
generation
- Regenerate `static/styles.css` with new semantic token layer and
source annotations
profiles.id has ON DELETE CASCADE from auth.users, so calling
auth.admin.deleteUser already removes the profile row. Only the
org soft-delete needs to happen explicitly.
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>