go-htmx-gsd #1
Loading…
Reference in a new issue
No description provided.
Delete branch "go-htmx-gsd"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
- internal/{db,session,tablos,tasks,files}/doc.go (D-02 placeholder packages) - internal/db/{queries,sqlc}, templates/, migrations/, bin/, static/ via .gitkeep - 'go build ./internal/...' compiles cleanly - cmd/web, cmd/worker, internal/web are deliberately deferred to Plans 01-02 / 01-03- bootstrap: installs goose/templ/sqlc/air at pinned versions; downloads Tailwind v4 standalone binary via explicit OS/arch case mapping (darwin->macos, x86_64->x64, arm64/aarch64->arm64) resolving to one of {tailwindcss-macos-x64,tailwindcss-macos-arm64,tailwindcss-linux-x64,tailwindcss-linux-arm64} (Codex concern #2); bootstrap-downloads htmx.min.js from unpkg into static/ - migrate: GOOSE_DRIVER + GOOSE_DBSTRING + GOOSE_MIGRATION_DIR wired - generate / styles-watch / dev / test / lint / build / clean recipes complete - 'just --list' enumerates 11 recipes; no pnpm/npm/node references (D-12) - clean recipe removes bin/, tmp/, generated CSS, bootstrap-downloaded htmx, *_templ.go (Codex #10) - Tailwind watch is run separately (RESEARCH Open Q2 two-terminal workflow) - The only CDN URLs in the entire backend are inside this justfile's bootstrap recipe; served HTML/CSS/JS reference only /static/* (CONTEXT D-10 clarified)- TestButton_DefaultSolidMD: asserts root class and label - TestButton_PassesThroughAttrs: asserts hx-* attribute spread - TestButton_ExplicitTypeSubmit: type override - TestCard_RendersChildren: templ.WithChildren child injection - TestBadge_{Info,Success}Variant + zero-value normalization - TestButtonClass_String / TestBadgeClass_String: class contract - TestNormalizers_ZeroValueDefaults: every Normalized* returns safe default- Add handlers_auth.go: SignupPageHandler + SignupPostHandler (validate -> hash -> insert -> session -> redirect) - Add AuthDeps struct; wire argon2id hash, InsertUser, Store.Create, SetSessionCookie - Update router.go: NewRouter accepts AuthDeps; mount ResolveSession (D-24); wire /signup routes behind RedirectIfAuthed - Update cmd/web/main.go: build AuthDeps (sqlc.Queries + auth.Store + secure flag) and pass to NewRouter - Add nil-Store guard to auth.ResolveSession for Phase 1 unit-test compatibility - Update handlers_test.go: pass AuthDeps{} zero value to NewRouter (Phase 1 routes unaffected) - Add testdb_test.go: isolated-schema test helper for web package integration tests - Add handlers_auth_test.go: 8 TestSignup_* integration tests (all pass against real Postgres)- 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- Implement TablosListHandler, TablosNewHandler, TablosCreateHandler in handlers_tablos.go replacing the Plan 01 stub - TablosCreateHandler: reads via r.PostFormValue, validates title (required, <=255), inserts with pgtype.Text nullable params, sends HX-Retarget + HX-Reswap on HTMX success, 303 redirect on non-HTMX success - router.go: replace r.Get("/", IndexHandler()) with TablosListHandler; add GET /tablos/new and POST /tablos (static before parametric — Pitfall 1) - handlers.go: remove IndexHandler + unused auth/csrf imports - index.templ: reduced to bare package declaration (dashboard moved to tablos.templ) - index_templ.go: deleted (empty templ file generates broken import) - TestTabloList, TestTabloList_Empty, TestTabloCreate, TestTabloCreate_Validation: PASS - TestSignup, TestLogin, TestLogout, TestCSRF: still PASS (no regression)- 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- 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>- TaskEditHandler: GET /tablos/{id}/tasks/{task_id}/edit returns TaskEditFragment pre-filled with existing title+description - TaskUpdateHandler: POST validates title (required, max 255), updates title+description preserving status+position (T-04-12) - TaskEditFragment: outer .task-card-zone wrapper with outerHTML round-trip, discard restores via /show - Sortable.js htmx.onLoad init script added to KanbanBoard (Pitfall 2 protection) - TaskEditFragment added to tasks.templ; remove t.Skip from TestTaskUpdate- 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- 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)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>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>- TestFileUpload: POST /tablos/{id}/files → 303 redirect + DB row + S3 key check - TestFileUploadTooLarge: oversized file → 422 + 'too large' message - TestFilesList: GET /tablos/{id}/files lists pre-inserted file with filename + size - TestFilesTab: HTMX fragment vs full-page rendering - stubbedFileStorer records uploadedKey for assertion - TestFileDownload/Delete/Ownership remain t.Skip (Plan 03)- FilesDeps struct with Queries, Files FileStorer, MaxUploadMB - loadOwnedTabloForFile helper (mirrors loadOwnedTabloForTask) - TabloFilesTabHandler: nil guard first, loadOwnedTablo, list files, HTMX/full-page dispatch - TabloTasksTabHandler: loadOwnedTablo, list tasks, HTMX/full-page dispatch - FileUploadHandler: nil guard, MaxBytesReader before ParseMultipartForm, S3 key files/{uuid}, InsertTabloFile, list + redirect - FileDownloadHandler/FileDeleteConfirmHandler/FileDeleteHandler: 501 stubs for Plan 03 - Security: D-04 S3 key isolation, T-05-02-02 size guard, T-05-02-04 ownership- tablos.templ: TabloDetailPage gains files+activeTab params, 3-tab nav with hx-push-url - tablos.templ: TabloOverviewTabFragment + TasksTabFragment (wraps KanbanBoard) added - files.templ: FilesTabFragment, FileUploadForm (hx-encoding=multipart/form-data), FileListRow, FileListEmpty, FileRowGone, UploadErrorFragment - files_helpers.go: formatBytes() converts int64 bytes to human-readable string - router.go: fileDeps FilesDeps param added; TabloTasksTabHandler + file routes wired - handlers_tablos.go: both TabloDetailPage call sites updated (nil, 'overview') - main.go: S3_ENDPOINT/S3_BUCKET/S3_REGION env vars read; files.NewStore constructed; fileDeps wired; nil filesStore allowed when S3 env unset (503 from handlers) - All test routers updated to pass FilesDeps{} in new param position- go get github.com/riverqueue/river@v0.37.0 + riverpgxv5@v0.37.0 - append ListOrphanFiles :many query to files.sql (orphan tablo_files rows) - regenerate sqlc: ListOrphanFilesRow{ID, TabloID, S3Key} exported - go build ./... exits 0- 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- 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- 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)- 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- 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- 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)- 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- 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>- 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>- TestComputeTabloProgress_{Empty,AllDone,Half,EtapesIgnored} - TestNewTabloDetailViewModel_{GroupsTasksByStatus,EtapesExcludedFromColumns,EtapesPopulated} - TestGetTabloDetailPage_{Returns200,Returns404,Returns400,Unauthenticated} - TestTabloDetailKanbanColumns - TestGetTabloDetailPage_ContainsSortableScript- tabloDetailRepository interface with ListTasksByTablo - GetTabloDetailPage: auth check, UUID parse, ownership-scoped tablo lookup, task fetch, vm build, render - router.go: mux.Get('/tablos/{tabloID}') registered before /edit route - activePath='/tablos' so sidebar Tablos item stays highlighted - Threat T-20-01 mitigated: findTabloByID filters by OwnerID from session- TabloDetailViewModel, TabloDetailColumnView, TabloDetailEtapeView exported - computeTabloProgress excludes etape tasks - GetTabloDetailPage handler with IDOR mitigation - GET /tablos/{tabloID} route registered - Full test suite green (13 packages)- Create tablo_detail.templ with 8 templ components: TabloDetailPage, TabloDetailHeader, TabloDetailTabBar, TabloDetailKanbanBoard, TabloDetailKanbanColumn, TabloDetailTaskCard, TabloDetailEtapesSection, TabloDetailSortableScript - Tab links use hx-get + hx-target="#tab-content" + hx-push-url="true" per UI-SPEC interaction contract - Each kanban column has hidden reorder form id="reorder-form-{status}" for Sortable.js onEnd - Create tablo_detail_tab.go with GetTabloDetailTab handler for HTMX tab content swaps - Tasks tab returns kanban board fragment; other tabs return "coming soon" placeholder- 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_*