xtablo-source/docs/superpowers/plans/2026-05-08-go-backend-tablos-index-crud.md
2026-05-09 20:18:24 +02:00

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.tablos table and any required index.
  • go-backend/internal/db/queries.sql
    • Add list/create/delete SQL queries for tablos.
  • go-backend/internal/db/repository.go
    • Extend the repository with tablos methods backed by sqlc. Take the time to split this file and create a new repository package that contains the tablos.go and users.go
  • go-backend/router.go
    • Register POST /tablos and DELETE /tablos/{id}.
  • go-backend/internal/web/handlers/auth.go
    • Update GetTablosPage() to use real data instead of placeholder content.
  • 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 /tablos page to real tablos content if needed by the final component split.
    • Wire the / page to real tablos content
  • 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 /tablos page load and HTMX fragment behavior.

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 /tablos filtering, create, and delete behavior.
  • go-backend/internal/web/views/tablos.templ
    • Main /tablos page content, grid/list variants, filter bar, and modal fragments.
  • 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 generate after schema/query updates.
  • go-backend/internal/web/views/*_templ.go
    • Regenerated by just generate after templ changes.

Test and verification commands

  • cd go-backend && go test ./internal/web/handlers -run Tablos
  • cd go-backend && go test ./...
  • cd go-backend && just generate
  • cd 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 /tablos loads current user, parses query state, lists tablos, renders full page or HTMX fragment

  • POST /tablos trims name, rejects empty names with 422, creates with status=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 Projets
  • Nouveau projet
  • Vue en grille
  • Vue en liste
  • Rechercher...
  • 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 -> À faire
    • in_progress -> En cours
    • done -> Terminé
  • date formatting for French-style labels

  • progress mapping:

    • todo -> 0
    • in_progress -> 50
    • done -> 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-get with 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:

  • plus
  • grid3x3
  • list
  • search
  • filter

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 name returns 422
  • returned body still contains the modal and inline error
  • create success preserves current view, q, and status
  • delete success preserves current view, q, and status

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:

  • /tablos uses 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.go rather than continuing to grow auth.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 status constrained 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?