19 KiB
Go Backend Tablos Index CRUD Implementation Plan
For agentic workers: REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Build the first real /tablos vertical slice in the Go rewrite with server-rendered list, modal create, soft delete, and functional URL-backed grid/list, search, and status filters.
Architecture: Extend the current Go app from auth-only pages into a small app domain for tablos. Keep data access in Postgres via sqlc and the repository, keep request parsing and ownership checks in handlers, and keep HTMX fragment boundaries explicit in templ views so full-page and partial-page responses share the same rendering units.
Tech Stack: Go, chi, templ, HTMX, PostgreSQL, pgx, sqlc, Go standard net/http testing
File Structure
Existing files to modify
go-backend/internal/db/schema.sql- Add the
public.tablostable and any required index.
- Add the
go-backend/internal/db/queries.sql- Add list/create/delete SQL queries for tablos.
go-backend/internal/db/repository.go- Extend the repository with
tablosmethods backed by sqlc. Take the time to split this file and create a new repository package that contains the tablos.go and users.go
- Extend the repository with
go-backend/router.go- Register
POST /tablosandDELETE /tablos/{id}.
- Register
go-backend/internal/web/handlers/auth.go- Update
GetTablosPage()to use real data instead of placeholder content.
- Update
go-backend/internal/web/handlers/in_memory_auth_repository.go- Extend the in-memory test repository with tablo storage and filter/delete behavior.
go-backend/internal/web/views/pages.templ- Wire the
/tablospage to real tablos content if needed by the final component split. - Wire the
/page to real tablos content
- Wire the
go-backend/internal/web/views/home.go- Replace hard-coded overview/tablo placeholder helpers with real tablos view models where needed.
go-backend/router_test.go- Add full-router tests for
/tablospage load and HTMX fragment behavior.
- Add full-router tests for
New files to create
go-backend/internal/web/handlers/tablos.go- Parse query state, validate create input, validate ownership for delete, and render page/modal fragments.
go-backend/internal/web/handlers/tablos_test.go- Focused handler tests for
/tablosfiltering, create, and delete behavior.
- Focused handler tests for
go-backend/internal/web/views/tablos.templ- Main
/tablospage content, grid/list variants, filter bar, and modal fragments.
- Main
go-backend/internal/web/views/tablos_view.go- View models, formatting helpers, status labels, safe defaults, and query-state helpers for the tablos page.
Generated files expected to change
go-backend/internal/db/sqlc/*.go- Regenerated by
just generateafter schema/query updates.
- Regenerated by
go-backend/internal/web/views/*_templ.go- Regenerated by
just generateaftertemplchanges.
- Regenerated by
Test and verification commands
cd go-backend && go test ./internal/web/handlers -run Tabloscd go-backend && go test ./...cd go-backend && just generatecd go-backend && just build
Chunk 1: Data Model And Repository
Task 1: Add the failing repository tests for tablos storage behavior
Files:
-
Modify:
go-backend/internal/web/handlers/in_memory_auth_repository.go -
Create:
go-backend/internal/web/handlers/tablos_test.go -
Step 1: Write the failing tests for create/list/delete semantics
Add handler/repository-level tests that prove the repository contract before production code exists:
func TestInMemoryTablosListExcludesSoftDeletedRows(t *testing.T) {
repo := NewInMemoryAuthRepository()
user, _ := repo.GetAuthUserByEmail(context.Background(), "demo@xtablo.com")
first, err := repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Visible",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatal(err)
}
_, err = repo.CreateTablo(context.Background(), CreateTabloInput{
OwnerID: user.ID,
Name: "Hidden",
Status: TabloStatusTodo,
})
if err != nil {
t.Fatal(err)
}
if err := repo.SoftDeleteTablo(context.Background(), first.ID, user.ID); err != nil {
t.Fatal(err)
}
tablos, err := repo.ListTablos(context.Background(), ListTablosInput{OwnerID: user.ID})
if err != nil {
t.Fatal(err)
}
if len(tablos) != 1 {
t.Fatalf("expected 1 visible tablo, got %d", len(tablos))
}
}
- Step 2: Run the focused tests to verify they fail
Run: cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'
Expected: FAIL with missing tablo types or repository methods.
- Step 3: Add the minimal in-memory domain types and storage
Add the minimum runtime types used by both handlers and repository implementations:
type TabloStatus string
const (
TabloStatusTodo TabloStatus = "todo"
TabloStatusInProgress TabloStatus = "in_progress"
TabloStatusDone TabloStatus = "done"
)
type TabloRecord struct {
ID uuid.UUID
OwnerID uuid.UUID
Name string
Status TabloStatus
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt *time.Time
}
Extend the in-memory repo with:
-
tablos map[uuid.UUID]TabloRecord -
CreateTablo -
ListTablos -
SoftDeleteTablo -
Step 4: Re-run the focused tests to verify they pass
Run: cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'
Expected: PASS for the new in-memory storage behavior tests.
- Step 5: Commit
git add go-backend/internal/web/handlers/in_memory_auth_repository.go go-backend/internal/web/handlers/tablos_test.go
git commit -m "feat: add in-memory tablo repository support"
Task 2: Add the SQL schema and query contract through tests
Files:
-
Modify:
go-backend/internal/db/schema.sql -
Modify:
go-backend/internal/db/queries.sql -
Modify:
go-backend/internal/db/repository.go -
Generated:
go-backend/internal/db/sqlc/* -
Step 1: Write a failing repository integration-shaped test for query semantics
Add a focused repository test near the handler test seam if there is no separate db test package yet. The test should document the needed query contract:
func TestListTablosInputSupportsSearchAndStatus(t *testing.T) {
input := ListTablosInput{
OwnerID: ownerID,
Query: "hell",
Status: TabloStatusTodo,
}
_ = input
// This test initially fails because the production repository contract does not exist yet.
}
The purpose is to drive the API shape before editing SQL.
- Step 2: Run the focused tests to verify they fail
Run: cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'
Expected: FAIL due to missing repository input types or methods.
- Step 3: Add the SQL schema and queries
Update schema.sql with:
CREATE TABLE IF NOT EXISTS public.tablos (
id uuid PRIMARY KEY,
owner_id uuid NOT NULL REFERENCES public.users(id) ON DELETE CASCADE,
name text NOT NULL,
status text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL
);
CREATE INDEX IF NOT EXISTS tablos_owner_created_idx
ON public.tablos (owner_id, created_at DESC)
WHERE deleted_at IS NULL;
Add sqlc queries:
-- name: CreateTablo :one
INSERT INTO public.tablos (
id, owner_id, name, status, created_at, updated_at
) VALUES (
$1, $2, $3, $4, now(), now()
)
RETURNING id, owner_id, name, status, created_at, updated_at, deleted_at;
-- name: ListTablos :many
SELECT id, owner_id, name, status, created_at, updated_at, deleted_at
FROM public.tablos
WHERE owner_id = sqlc.arg(owner_id)
AND deleted_at IS NULL
AND (
sqlc.narg(status)::text IS NULL OR status = sqlc.narg(status)::text
)
AND (
sqlc.narg(query)::text IS NULL OR name ILIKE '%' || sqlc.narg(query)::text || '%'
)
ORDER BY created_at DESC;
-- name: SoftDeleteTablo :execrows
UPDATE public.tablos
SET deleted_at = now(), updated_at = now()
WHERE id = $1 AND owner_id = $2 AND deleted_at IS NULL;
- Step 4: Regenerate sqlc and templ artifacts
Run: cd go-backend && just generate
Expected: PASS and regenerated internal/db/sqlc plus any templ outputs with no generation errors.
- Step 5: Implement the Postgres repository methods
Extend repository.go with the real methods:
type ListTablosInput struct {
OwnerID uuid.UUID
Query string
Status *TabloStatus
}
type CreateTabloInput struct {
OwnerID uuid.UUID
Name string
Status TabloStatus
}
Implement:
CreateTablo(ctx, input)ListTablos(ctx, input)SoftDeleteTablo(ctx, tabloID, ownerID)
Normalize empty search strings before passing them to sqlc. Return a domain-level not-found/unauthorized error when SoftDeleteTablo affects zero rows.
- Step 6: Run the focused tests to verify they pass
Run: cd go-backend && go test ./internal/web/handlers -run 'Tablos|InMemoryTablos'
Expected: PASS with the new contract compiling and the in-memory implementation still satisfying it.
- Step 7: Commit
git add go-backend/internal/db/schema.sql go-backend/internal/db/queries.sql go-backend/internal/db/repository.go go-backend/internal/db/sqlc go-backend/internal/web/handlers/in_memory_auth_repository.go go-backend/internal/web/handlers/tablos_test.go
git commit -m "feat: add tablo persistence contract"
Chunk 2: Handler Routing And URL State
Task 3: Add failing handler tests for /tablos query state, create, and delete
Files:
-
Create:
go-backend/internal/web/handlers/tablos.go -
Modify:
go-backend/internal/web/handlers/tablos_test.go -
Modify:
go-backend/router.go -
Modify:
go-backend/router_test.go -
Step 1: Write failing handler tests for GET/POST/DELETE
Add tests that document the handler contract:
func TestGetTablosPageDefaultsToGridAndAllStatus(t *testing.T) {}
func TestGetTablosPageHonorsSearchAndStatus(t *testing.T) {}
func TestPostTablosCreatesTodoTablo(t *testing.T) {}
func TestPostTablosWithEmptyNameReturns422(t *testing.T) {}
func TestDeleteTabloSoftDeletesOwnedRow(t *testing.T) {}
func TestDeleteTabloRejectsDifferentOwner(t *testing.T) {}
Also extend router_test.go with an end-to-end page assertion:
func TestTablosPageRendersRealProjectsHeading(t *testing.T) {}
- Step 2: Run the focused tests to verify they fail
Run: cd go-backend && go test ./internal/web/handlers -run Tablos
Expected: FAIL with missing handler methods and route wiring.
- Step 3: Implement query parsing and safe defaults
Create tablos.go and add request parsing helpers:
type TablosQueryState struct {
View string
Query string
Status string
}
func parseTablosQueryState(r *http.Request) TablosQueryState {
q := strings.TrimSpace(r.URL.Query().Get("q"))
view := r.URL.Query().Get("view")
status := r.URL.Query().Get("status")
if view != "list" {
view = "grid"
}
switch status {
case "todo", "in_progress", "done":
default:
status = "all"
}
return TablosQueryState{
View: view,
Query: q,
Status: status,
}
}
- Step 4: Implement the minimal handler methods
Add handler methods:
GetTablosPage()PostTablos()DeleteTablo()
Behavior:
-
GET /tablosloads current user, parses query state, lists tablos, renders full page or HTMX fragment -
POST /tablostrimsname, rejects empty names with422, creates withstatus=todo -
DELETE /tablos/{id}verifies ownership through the repository delete contract and preserves query state -
Step 5: Wire the routes
Update router.go:
mux.Get("/tablos", authHandler.GetTablosPage())
mux.Post("/tablos", authHandler.PostTablos())
mux.Delete("/tablos/{tabloID}", authHandler.DeleteTablo())
- Step 6: Re-run focused tests to verify they pass
Run: cd go-backend && go test ./internal/web/handlers -run Tablos
Expected: PASS for handler-specific tests.
- Step 7: Commit
git add go-backend/internal/web/handlers/tablos.go go-backend/internal/web/handlers/tablos_test.go go-backend/router.go go-backend/router_test.go
git commit -m "feat: add tablo handlers and routes"
Chunk 3: Templ Views, Modal, And Functional Controls
Task 4: Add failing rendering tests for the /tablos page contract
Files:
-
Create:
go-backend/internal/web/views/tablos.templ -
Create:
go-backend/internal/web/views/tablos_view.go -
Modify:
go-backend/internal/web/views/pages.templ -
Modify:
go-backend/internal/web/views/home.go -
Modify:
go-backend/internal/web/views/icons.templ -
Modify:
go-backend/router_test.go -
Step 1: Write failing page-render tests for the agreed UI contract
Add assertions for:
Mes ProjetsNouveau projetVue en grilleVue en listeRechercher...Tous,Pas commencé,En cours,Terminé- grid markup in default mode
- list markup in
?view=list - modal markup or modal target container
Example:
if !strings.Contains(body, "Mes Projets") {
t.Fatalf("expected tablos page heading")
}
if !strings.Contains(body, "Nouveau projet") {
t.Fatalf("expected create trigger")
}
- Step 2: Run the router tests to verify they fail
Run: cd go-backend && go test ./... -run 'TablosPage|TablosHTMX'
Expected: FAIL because the placeholder page still renders.
- Step 3: Add view models and formatting helpers
Create tablos_view.go with focused helpers:
-
TabloCardView -
TablosPageViewModel -
French status label mapping:
todo -> À fairein_progress -> En coursdone -> Terminé
-
date formatting for French-style labels
-
progress mapping:
todo -> 0in_progress -> 50done -> 100
-
Step 4: Add the new templ components
Create tablos.templ with:
TablosMainContent(vm TablosPageViewModel)TablosToolbar(vm TablosPageViewModel)TablosGrid(vm TablosPageViewModel)TablosList(vm TablosPageViewModel)TabloCard(item TabloCardView, state TablosQueryState)CreateTabloModal(vm TablosPageViewModel)
The top-level content should follow the user-provided HTML structure closely:
<main class="flex-1 overflow-auto">
<div class="px-4 pt-8 pb-6">
...
</div>
</main>
Use HTMX attributes for:
-
view toggle
hx-get -
search
hx-getwith trigger debounce -
status pill filtering
hx-get -
modal form
hx-post="/tablos" -
delete buttons
hx-delete="/tablos/{id}" -
Step 5: Add any missing icons or class helpers
Extend icons.templ only if the provided /tablos structure needs icons not already present:
plusgrid3x3listsearchfilter
Keep icons centralized rather than inlining large SVGs repeatedly.
- Step 6: Regenerate templ artifacts
Run: cd go-backend && just generate
Expected: PASS with new *_templ.go output and no templ generation errors.
- Step 7: Re-run the page tests to verify they pass
Run: cd go-backend && go test ./... -run 'TablosPage|TablosHTMX'
Expected: PASS with the real /tablos page structure rendered.
- Step 8: Commit
git add go-backend/internal/web/views/tablos.templ go-backend/internal/web/views/tablos_view.go go-backend/internal/web/views/pages.templ go-backend/internal/web/views/home.go go-backend/internal/web/views/icons.templ go-backend/internal/web/views/*_templ.go go-backend/router_test.go
git commit -m "feat: render real tablos page"
Chunk 4: Modal Validation, Delete UX, And Full Verification
Task 5: Tighten validation and modal error behavior with failing tests first
Files:
-
Modify:
go-backend/internal/web/handlers/tablos.go -
Modify:
go-backend/internal/web/handlers/tablos_test.go -
Modify:
go-backend/internal/web/views/tablos.templ -
Step 1: Write the failing tests for modal error and state preservation
Add tests that verify:
- empty
namereturns422 - returned body still contains the modal and inline error
- create success preserves current
view,q, andstatus - delete success preserves current
view,q, andstatus
Example:
if rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("expected 422, got %d", rec.Code)
}
if !strings.Contains(rec.Body.String(), "Le nom du projet est requis") {
t.Fatalf("expected inline validation message")
}
- Step 2: Run the focused tests to verify they fail
Run: cd go-backend && go test ./internal/web/handlers -run 'PostTablos|DeleteTablo'
Expected: FAIL until modal error and state-preservation behavior is implemented.
- Step 3: Implement inline validation and state preservation
Update PostTablos() and DeleteTablo() to:
- read current query state from request values
- reuse the same state when rendering success fragments
- return modal fragment with inline error text on
422 - include confirm copy on delete buttons
Keep the logic minimal; do not add a client-side state store.
- Step 4: Re-run the focused tests to verify they pass
Run: cd go-backend && go test ./internal/web/handlers -run 'PostTablos|DeleteTablo'
Expected: PASS for create/delete edge cases.
- Step 5: Commit
git add go-backend/internal/web/handlers/tablos.go go-backend/internal/web/handlers/tablos_test.go go-backend/internal/web/views/tablos.templ
git commit -m "feat: finalize tablo modal and delete UX"
Task 6: Run full verification and record any gaps
Files:
-
Modify:
docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md -
Step 1: Run full project tests
Run: cd go-backend && go test ./...
Expected: PASS across handlers, router, and repository compile paths.
- Step 2: Run generation and build verification
Run: cd go-backend && just generate
Expected: PASS with no dirty generated output after generation.
Run: cd go-backend && just build
Expected: PASS with successful binary build.
- Step 3: Re-read the approved spec and verify acceptance criteria manually
Check:
-
/tablosuses the agreed layout direction -
create opens a modal
-
list, search, status filters, and view toggle are functional
-
delete is soft delete and owner-scoped
-
no custom app JavaScript was introduced
-
Step 4: Update the plan checklist and note any verification gaps
If any command fails, record the exact failure under this plan before making completion claims.
- Step 5: Commit
git add docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md
git commit -m "docs: update tablo implementation plan status"
Notes For Execution
- Prefer creating
go-backend/internal/web/handlers/tablos.gorather than continuing to growauth.go; the current codebase is still small enough to benefit from a clearer split. - Keep the first slice owner-scoped only. Do not pull in organization/shared-access behavior from the legacy app.
- Keep
statusconstrained in Go only. Do not add a DB enum or check constraint for this milestone. - Preserve HTMX fragment boundaries carefully so the modal can fail inline without forcing a full page reload.
- If CSS support for the exact class contract is incomplete, implement the semantic structure first, then add the necessary stylesheet updates in a follow-up execution pass inside the same task chunk.
Plan complete and saved to docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md. Ready to execute?