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