---
phase: 03-tablos-crud
plan: 03
subsystem: web-handlers + templates
tags: [go, htmx, templ, chi, csrf, ownership, inline-edit, delete-confirm]

# Dependency graph
requires:
  - phase: 03-tablos-crud
    plan: 01
    provides: tablos table migration, sqlc queries, TablosDeps stub, RED test scaffold
  - phase: 03-tablos-crud
    plan: 02
    provides: dashboard/create slice, TabloCard with .tablo-delete-zone, router with /tablos/new + POST /tablos
provides:
  - TabloDetailPage template (full detail page)
  - TabloTitleDisplay/EditFragment templates (inline title edit)
  - TabloDescDisplay/EditFragment templates (inline description edit)
  - TabloDeleteButtonFragment template (canonical delete zone — TabloCard now delegates here)
  - TabloDeleteConfirmFragment template (inline confirmation dialog)
  - TabloNotFoundPage template (404 with ownership copy)
  - TabloUpdateErrors struct in tablos_forms.go
  - loadOwnedTablo helper: uuid.Parse + GetTabloByID + ownership check
  - 9 handler functions: Detail, EditTitle, ShowTitle, EditDesc, ShowDesc, Update, DeleteConfirm, DeleteCancel, Delete
  - 9 new routes in RequireAuth group (static-before-parametric order preserved)
  - All 10 TABLO tests green (TABLO-01..06 fully covered)
affects: []

# Tech tracking
tech-stack:
  added: []
  patterns:
    - loadOwnedTablo shared preamble helper (uuid.Parse + GetTabloByID + ownership 404)
    - _zone hidden field for unified POST /tablos/{id} dispatch (title vs desc fragment)
    - HX-Redirect:/ for HTMX delete navigation (vs 303 for non-HTMX)
    - outerHTML swap with same zone class on outermost element (Display + Edit share .tablo-X-zone)
    - D-04: 404 not 403 for non-owner access (no existence leakage)

key-files:
  created: []
  modified:
    - backend/templates/tablos.templ
    - backend/templates/tablos_forms.go
    - backend/templates/tablos_templ.go
    - backend/internal/web/handlers_tablos.go
    - backend/internal/web/handlers_tablos_test.go
    - backend/internal/web/router.go

key-decisions:
  - "_zone hidden field (title|desc) used in both edit fragments so POST /tablos/{id} handler can render the correct display zone on success"
  - "TabloCard delegates delete-zone rendering to TabloDeleteButtonFragment (single source of truth for zone HTML)"
  - "TabloDetailPage wraps TabloTitleDisplay in .tablo-title-zone div — outer wrapper retained for correct hx-target closest resolution from detail page context"
  - "Test title apostrophe fix: TestTabloDetail_Owner used 'Owner's Tablo' which templ HTML-encodes to Owner&#39;s Tablo; changed to 'Owners Detail Tablo' to avoid assertion mismatch"

# Metrics
duration: ~5min
completed: 2026-05-15
---

# Phase 03 Plan 03: Detail + Edit + Delete Slice Summary

**tablos detail page, inline title/description edit fragments, inline-confirmation delete, 9 handler functions wired; all 10 TABLO tests green**

## Performance

- **Duration:** ~5 min (code) + human verify
- **Started:** 2026-05-15T05:57:42Z
- **Completed:** 2026-05-15 (human verify approved)
- **Tasks:** 3 of 3 complete
- **Files modified:** 6

## Accomplishments

- 8 new templ components in `tablos.templ`:
  - `TabloDetailPage`: full detail layout with back link, title zone, desc zone, delete zone
  - `TabloTitleDisplay`: clickable `<h1 class="tablo-title-zone">` with hx-get=/edit-title
  - `TabloTitleEditFragment`: form with `class="tablo-title-zone"`, _zone=title hidden, Save/Discard buttons
  - `TabloDescDisplay`: clickable `<div class="tablo-desc-zone">` with empty-state hint
  - `TabloDescEditFragment`: textarea form with `class="tablo-desc-zone"`, _zone=desc hidden
  - `TabloDeleteButtonFragment`: canonical `<div class="tablo-delete-zone">` with Delete button (TabloCard now delegates here)
  - `TabloDeleteConfirmFragment`: `<div class="tablo-delete-zone">` with "Delete tablo?", "Yes, delete", "Keep tablo"
  - `TabloNotFoundPage`: 404 page with UI-SPEC copy
- `TabloUpdateErrors` struct added to `tablos_forms.go`
- `loadOwnedTablo` helper in `handlers_tablos.go` — shared uuid.Parse + GetTabloByID + ownership 404 preamble
- 9 new handler functions in `handlers_tablos.go` (all use loadOwnedTablo)
- 9 new routes in `router.go` inside RequireAuth group, static-before-parametric order verified
- All 10 TABLO tests pass; all Phase 1/2 tests still pass (no regressions)

## Task Commits

1. **Task 1: tablos.templ detail/edit/delete fragments + TabloUpdateErrors** - `6f167e2` (feat)
2. **Task 2: handlers + router wiring + test fix** - `ab6937c` (feat)
3. **Task 3: human-verify checkpoint — approved** - all 13 sub-checks passed (ownership 404, CSRF forms, inline edit/discard, inline delete confirm/cancel, HX-Redirect, non-JS fallback)

## Handler Contracts Established

```
loadOwnedTablo(w, r, deps) (sqlc.Tablo, *auth.User, bool)
  → uuid.Parse(chi.URLParam(r, "id"))  — 404 on parse failure
  → deps.Queries.GetTabloByID           — 404 on pgx.ErrNoRows, 500 otherwise
  → tablo.UserID != user.ID             — 404 (D-04: not 403)

TabloDetailHandler(deps)      → GET /tablos/{id}
TabloEditTitleHandler(deps)   → GET /tablos/{id}/edit-title
TabloShowTitleHandler(deps)   → GET /tablos/{id}/show-title
TabloEditDescHandler(deps)    → GET /tablos/{id}/edit-desc
TabloShowDescHandler(deps)    → GET /tablos/{id}/show-desc
TabloUpdateHandler(deps)      → POST /tablos/{id}
TabloDeleteConfirmHandler(deps) → GET /tablos/{id}/delete-confirm
TabloDeleteCancelHandler(deps)  → GET /tablos/{id}/delete-cancel
TabloDeleteHandler(deps)        → POST /tablos/{id}/delete
```

## Router Wiring (New Lines)

```go
r.Get("/tablos/{id}", TabloDetailHandler(tabloDeps))
r.Post("/tablos/{id}", TabloUpdateHandler(tabloDeps))
r.Get("/tablos/{id}/edit-title", TabloEditTitleHandler(tabloDeps))
r.Get("/tablos/{id}/show-title", TabloShowTitleHandler(tabloDeps))
r.Get("/tablos/{id}/edit-desc", TabloEditDescHandler(tabloDeps))
r.Get("/tablos/{id}/show-desc", TabloShowDescHandler(tabloDeps))
r.Get("/tablos/{id}/delete-confirm", TabloDeleteConfirmHandler(tabloDeps))
r.Get("/tablos/{id}/delete-cancel", TabloDeleteCancelHandler(tabloDeps))
r.Post("/tablos/{id}/delete", TabloDeleteHandler(tabloDeps))
```

Static-before-parametric order: `/tablos/new` declared before `/tablos/{id}` (Pitfall 1) — verified by awk check.

## _zone Hidden Field Approach

`TabloUpdateHandler` receives both title and description on every POST (whichever edit form was submitted includes the other field as a hidden input). The `_zone` hidden field (`"title"` or `"desc"`) tells the handler which display fragment to render as the HTMX response body. DB update always writes both fields regardless of zone. Non-HTMX path: 303 to `/tablos/{id}`.

## TABLO Test Status: 10/10 Green

| Test | Status |
|------|--------|
| TestTabloList | GREEN |
| TestTabloList_Empty | GREEN |
| TestTabloCreate/HTMX_create | GREEN |
| TestTabloCreate/non-HTMX_create | GREEN |
| TestTabloCreate_Validation | GREEN |
| TestTabloDetail_Owner | GREEN |
| TestTabloDetail_NonOwner | GREEN |
| TestTabloDetail_InvalidID | GREEN |
| TestTabloUpdate | GREEN |
| TestTabloDeleteConfirm | GREEN |
| TestTabloDelete/HTMX_delete | GREEN |
| TestTabloDelete/non-HTMX_delete | GREEN |

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Test title apostrophe causes HTML entity mismatch**
- **Found during:** Task 2 (running TestTabloDetail_Owner)
- **Issue:** TestTabloDetail_Owner inserted tablo with title "Owner's Tablo" and checked `strings.Contains(body, "Owner's Tablo")`. templ correctly HTML-escapes the apostrophe to `&#39;`, so the body contains `Owner&#39;s Tablo` — the string check fails.
- **Fix:** Changed test title to "Owners Detail Tablo" (no apostrophe) and updated the assertion string to match.
- **Files modified:** backend/internal/web/handlers_tablos_test.go
- **Committed in:** ab6937c (Task 2)

---

**Total deviations:** 1 auto-fixed (Rule 1 - test assertion apostrophe)

## Known Stubs

None — all plan functionality implemented and browser-verified.

## Threat Surface Scan

No new network endpoints, auth paths, or schema changes beyond what the plan's threat model covers. All 9 new routes are inside the RequireAuth group (T-03-03-09). loadOwnedTablo enforces ownership on every request (T-03-03-01). All user content rendered via templ (no templ.Raw) (T-03-03-06).

## Self-Check: PASSED

- `backend/templates/tablos.templ` — present, 8 new components confirmed
- `backend/internal/web/handlers_tablos.go` — 9 handler functions confirmed
- `backend/internal/web/router.go` — 9 new routes confirmed
- Commit `6f167e2` — exists (Task 1)
- Commit `ab6937c` — exists (Task 2)
- All 10 TABLO tests green (verified with TEST_DATABASE_URL set)
- go build ./... — exits 0

---
*Phase: 03-tablos-crud*
*Completed: 2026-05-15*
