diff --git a/.github/workflows/go-backend-ci.yml b/.github/workflows/go-backend-ci.yml new file mode 100644 index 0000000..67ae9bf --- /dev/null +++ b/.github/workflows/go-backend-ci.yml @@ -0,0 +1,115 @@ +name: go-backend-ci + +on: + workflow_dispatch: + pull_request: + paths: + - "go-backend/**" + - ".github/workflows/go-backend-ci.yml" + push: + branches: + - main + - develop + paths: + - "go-backend/**" + - ".github/workflows/go-backend-ci.yml" + +concurrency: + group: go-backend-ci-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check-go-backend: + name: Check go-backend + runs-on: ubuntu-latest + timeout-minutes: 20 + + services: + postgres: + image: postgres:16 + env: + POSTGRES_DB: xtablo + POSTGRES_USER: xtablo + POSTGRES_PASSWORD: xtablo + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U xtablo -d xtablo" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + defaults: + run: + working-directory: go-backend + + env: + DATABASE_URL: postgres://xtablo:xtablo@127.0.0.1:5432/xtablo?sslmode=disable + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go-backend/go.mod + cache-dependency-path: go-backend/go.sum + + - name: Install PostgreSQL client + run: sudo apt-get update && sudo apt-get install -y postgresql-client + + - name: Install code generation tools + run: | + go install github.com/a-h/templ/cmd/templ@v0.3.1001 + go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 + + - name: Load database schema + run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f internal/db/schema.sql + + - name: Seed database + run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f internal/db/seed.sql + + - name: Generate templ and sqlc code + run: | + templ generate + sqlc generate + + - name: Verify generated files are committed + run: git diff --exit-code -- internal/db/sqlc internal/web/views + + - name: Run tests + run: go test ./... + + - name: Build + run: go build ./... + + - name: Smoke test app against Postgres + run: | + set -euo pipefail + + go run . >/tmp/go-backend.log 2>&1 & + app_pid=$! + trap 'kill $app_pid || true; wait $app_pid || true' EXIT + + for _ in $(seq 1 30); do + if curl -fsS http://127.0.0.1:3000/login >/tmp/go-backend-login.html; then + break + fi + sleep 1 + done + + grep -q "Se connecter à Xtablo" /tmp/go-backend-login.html + + curl -fsS \ + -D /tmp/go-backend-signup.headers \ + -o /tmp/go-backend-signup.body \ + -X POST \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data 'email=ci-user@example.com&password=xtablo-secret' \ + http://127.0.0.1:3000/signup + + grep -qi '^Hx-Redirect: /' /tmp/go-backend-signup.headers + test "$(psql "$DATABASE_URL" -tA -c "select count(*) from auth.users where email = 'ci-user@example.com'")" = "1" + test "$(psql "$DATABASE_URL" -tA -c "select count(*) from public.users where email = 'ci-user@example.com'")" = "1" + test "$(psql "$DATABASE_URL" -tA -c "select count(*) from auth.sessions where user_id = (select id from auth.users where email = 'ci-user@example.com')")" = "1" diff --git a/.gitignore b/.gitignore index 3cfb0b0..a1c325c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,14 @@ dist # Supabase supabase/.temp supabase/.branches + +# Podman +.podman-compose + +# AI +.claude +.codex + +# go built binaries +backend/web +backend/worker diff --git a/.planning/MILESTONES.md b/.planning/MILESTONES.md new file mode 100644 index 0000000..a1c1e65 --- /dev/null +++ b/.planning/MILESTONES.md @@ -0,0 +1,60 @@ +# Project Milestones: Xtablo + +## v3.0 Design System & Visual Polish (Shipped: 2026-05-17) + +**Delivered:** Full design system ported from go-backend and applied to every product surface — auth pages, app shell, tablo list, tablo detail, chat, and planning. + +**Phases completed:** 13-17 (5 phases, 16 plans) + +**Key accomplishments:** + +- Built a complete CSS token vocabulary and typed templ component library (11 types: button, badge, card, modal, empty-state, table, icon-button, input, textarea, select, form-field) with a build-tag-gated `/ui-catalog` visual verification page. +- Restyled auth pages (login/signup) with gradient animated background, centered auth-card, brand logo, and Google Material Design sign-in button with Roboto font. +- Implemented app shell: sidebar with brand section, icon nav items, tablo list section, and user footer; tablo list with project-card grid, color avatars, and action controls. +- Restyled tablo detail: project-card-top header, tasks-section kanban, etapes section, and files table using `@ui.Table` with `@ui.EmptyState` fallback. +- Added own-vs-others chat bubbles with brand-tint right-alignment and planning day-separator event list using overview-section layout. + +**Stats:** + +- 150 files changed, +23,505 / −993 lines across v3.0 git range +- 5 phases, 16 plans +- 2 days (2026-05-16 → 2026-05-17) +- Go backend: ~18,600 non-test LOC + ~3,400 templ LOC + +**Git range:** `3b9bd62` → `fa48d81` + +**Known deferred items at close:** 6 acknowledged open artifact items (all pre-existing from v1.0/v2.0). See `.planning/STATE.md` Deferred Items. + +--- + +Entries are listed newest first. + +## v2.0 Collaboration, Planning, and Social Sign-in (Shipped: 2026-05-16) + +**Delivered:** Google sign-in, one-level etapes, tablo-owned events, personal planning, and native realtime tablo discussions on the Go + HTMX stack. + +**Phases completed:** 8-12 (18 plans total) + +**Key accomplishments:** + +- Added Google social sign-in while preserving Xtablo-owned users and sessions. +- Added one-level etapes for task organization without breaking the kanban model. +- Added tablo-owned events and an authenticated personal planning agenda. +- Added native persisted realtime discussions using Postgres and Go-owned SSE. +- Kept collaboration and scheduling features inside the single Go + HTMX product architecture. + +**Stats:** + +- 148 files changed across the v2.0 git range +- 17,704 insertions and 527 deletions across the v2.0 git range +- 5 phases, 18 plans, approximately 39 implementation/verification tasks +- 2 days from milestone start to close (2026-05-15 -> 2026-05-16) +- Current backend source footprint: 29,101 lines across Go, templ, SQL, and CSS files + +**Git range:** `0d23d94` -> `58a9ce0` + +**Known deferred items at close:** 7 acknowledged open artifact items. See `.planning/STATE.md` Deferred Items and `.planning/milestones/v2.0-MILESTONE-AUDIT.md`. + +**What's next:** v3.0 Design System & Visual Polish is already planned in `.planning/ROADMAP.md`. + +--- diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 0000000..b26102a --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,147 @@ +# Xtablo — Go + HTMX Product + +## What This Is + +A Go + HTMX version of Xtablo centered on tablos as collaborative workspaces. v1 established the authenticated Tablos workflow (tasks, files, worker, deploy); v2 added collaboration, messaging, etapes, events, planning, and Google sign-in; v3 ported the full design system from the reference JS app — CSS token vocabulary, typed templ component library, and a visual reskin of every surface. + +Built for a developer who wants a simpler, durable product stack: one Go server, Postgres, server-rendered UI, and no managed chat provider. + +## Core Value + +**A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider.** + +If everything else fails, this must work end-to-end on a single Go binary backed by Postgres and an S3-compatible bucket. + +## Current Milestone: v4.0 Figma Design Parity + +**Goal:** Bring every product surface to match the Figma design — new task views (grid + roadmap), calendar rework, navigation refresh, and tablo list revamp. + +**Target features:** +- Navigation bar + top header redesigned to match Figma +- Tablo list: project card revamp + card/list view toggle +- Tasks: restyle kanban + new grid view + new roadmap view +- Calendar: restyle `/planning` as Google Calendar-style (month/week/day navigation, proper event blocks) +- Tablo detail view restyled to match Figma + +## Shipped: v3.0 Design System & Visual Polish (2026-05-17) + +**Delivered:** Full CSS token vocabulary and typed templ component library (11 types) applied to every surface — auth pages, app shell sidebar, tablo list, tablo detail, chat, and planning. Build-tag-gated `/ui-catalog` visual verification page included. + +## Requirements + +### Validated + + + +- ✓ Server-managed sessions with email/password auth — Phase 2 (v1.0) +- ✓ Tablos CRUD with ownership — Phase 3 (v1.0) +- ✓ Tasks kanban with drag-and-drop reorder — Phase 4 (v1.0) +- ✓ File attachments with orphan cleanup worker — Phases 5–6 (v1.0) +- ✓ Single-binary production deploy with Caddy TLS — Phase 7 (v1.0) +- ✓ Google sign-in (server-managed sessions) — Phase 8 (v2.0) +- ✓ Etapes as one-level task wrappers — Phase 9 (v2.0) +- ✓ Tablo events with CRUD — Phase 10 (v2.0) +- ✓ Individual planning view (`/planning`) — Phase 11 (v2.0) +- ✓ Native per-tablo chat with real-time SSE — Phase 12 (v2.0) +- ✓ CSS design token vocabulary + typed templ component library (11 types) in `backend/internal/web/ui` — Phase 13 (v3.0) +- ✓ Auth pages (login/signup) match JS app visual — Phase 14 (v3.0) +- ✓ App shell sidebar + project-card tablo grid — Phase 15 (v3.0) +- ✓ Tablo detail (tasks, etapes, files) restyled with design system components — Phase 16 (v3.0) +- ✓ Chat and planning views visually consistent with app shell — Phase 17 (v3.0) + +### Active + + + +- [ ] Navigation bar + top header redesigned to match Figma +- [ ] Tablo list project card revamped to match Figma +- [ ] Tablo list card/list view toggle +- [ ] Tasks kanban restyled to match Figma +- [ ] Tasks grid view (new — brand new data model + UI) +- [ ] Tasks roadmap view (new — brand new) +- [ ] Planning page restyled as Google Calendar-style (month/week/day, event blocks) +- [ ] Tablo detail view restyled to match Figma + +### Out of Scope + + + +- **Managed chat/messaging providers** — no Stream Chat, Ably, Pusher, Firebase Realtime Database, or equivalent for v2 chat +- **Stripe / billing** — defer monetization until product loop is validated +- **Public booking widget** — `apps/external` rewrite not in v1 (may return in a later milestone) +- **Client portal** — `apps/clients` magic-link experience deferred +- **Admin app** — `apps/admin` internal tooling deferred +- **Notes / rich documents** inside a Tablo — not part of this milestone +- **Managed auth platforms** — no Clerk/Auth0/Lucia; Google and Apple are identity providers only, with Xtablo still owning users and sessions +- **Mobile / Expo app** — out of scope for this rewrite +- **Supabase** as a runtime dependency — Postgres only; Supabase Auth / RLS replaced by Go-side authz +- **The existing `go-backend/` directory** — treated as scratch; new code lives in a fresh `backend/` Go package + +## Context + +- The JS monorepo (this repository) is the source of truth for product behavior and is fully mapped in `.planning/codebase/` (ARCHITECTURE, STACK, STRUCTURE, INTEGRATIONS, CONVENTIONS, CONCERNS, TESTING). Use these to derive expected behavior — but the rewrite is free to simplify both schema and visuals. +- The DB schema **will change** during the rewrite — the JS version's Supabase schema is a reference, not a constraint. The user wants to be in the loop on schema decisions for each domain (users/sessions, tablos, tasks, files). +- v3.0 shipped the full design system and visual reskin. `backend/internal/web/ui` now has: CSS token vocabulary, 11 typed templ components, an app shell, and surface-specific CSS (auth, dashboard, tablo detail, chat, planning). +- Current backend source footprint: ~18,600 non-test Go LOC + ~3,400 templ LOC. +- Developer is comfortable in Go and wants a low-dependency, server-rendered stack going forward. +- Known stubs shipped in v3.0: tablo status field hardcoded as "En cours", progress bar hardcoded at 0% — both require a DB schema extension to wire properly. + +## Constraints + +- **Tech stack**: Go (server + templates) + HTMX + Tailwind + Postgres + sqlc — no JS framework, no managed BaaS +- **Auth**: Server-managed sessions remain authoritative; Google/Apple sign-in may verify external ID tokens but must end by issuing Xtablo's own session cookie +- **Chat**: No third-party chat provider; messages and read state live in Postgres, with real-time delivery handled by the Go app +- **Storage**: Files in S3-compatible object storage (Cloudflare R2 to start) +- **Architecture**: One web server binary + one background worker (same repo, possibly same binary with subcommand) +- **Deploy target**: Single VPS / container — no Kubernetes +- **Planning**: Events belong to tablos, but each individual user can access their own planning view +- **Etapes**: One level only — a task can have at most one parent/wrapper, and a parent cannot itself have a parent +- **Scope discipline**: v2 adds collaboration and planning only; billing, booking, portal, admin, notes, and mobile remain deferred + +## Key Decisions + +| Decision | Rationale | Outcome | +|----------|-----------|---------| +| Rewrite in Go + HTMX (no SPA) | Simpler stack, developer preference, product pivot | ✓ Validated through v1-v2 | +| Built-in sessions, no 3rd-party auth | Avoid vendor coupling; sessions are well-trodden ground | ✓ Validated through email/password and Google sign-in | +| Drop Supabase (keep Postgres) | Owning the auth + RLS story in Go is simpler than maintaining the boundary | ✓ Validated through owned authz queries | +| Fresh `backend/` Go package, set `go-backend/` aside | Existing scaffold has decisions to revisit; cleaner to start over | ✓ Validated | +| v1 = Tablos workflow only (Tasks + Files), defer chat/billing/booking/portal/admin | Focus the rewrite around the load-bearing user loop first | ✓ Completed | +| Single binary + background worker, single VPS deploy | Matches the "simpler stack" thesis; avoid orchestration cost early | ✓ Validated for current deploy target | +| User-in-the-loop on Postgres schema for each domain | Schema is changing from JS version; explicit review before sqlc generation | — Pending | +| v2 chat is native, not vendor-backed | User explicitly does not want third-party chat; Postgres + Go should be enough for the first real-time version | ✓ Validated with SSE receive + HTMX POST send | +| Google/Apple are identity providers only | Preserve server-owned users/sessions while allowing social sign-in | ✓ Google validated; Apple disabled for now | +| Etapes are one-level wrappers around tasks | Matches the requested mental model and avoids recursive planning complexity | ✓ Validated | +| Planning is individual, events remain tablo-scoped | Lets users see their schedule while preserving tablos as the source of work context | ✓ Validated | +| Design system lives in `backend/internal/web/ui` (not go-backend) | Keep all production code in `backend/`; go-backend is scratch/reference only | ✓ Validated — all 11 components + token CSS in backend/ | +| Build-tag-gated `/ui-catalog` route for design system verification | Catalog must not ship in production; build tags are the idiomatic Go mechanism | ✓ Validated — production build passes, catalog isolated | +| Tablo status + progress bar shipped as visual stubs | DB schema extension needed; unblocking is lower value than shipping the visual design | ⚠️ Revisit — stub needs wiring in v4.0 | +| Air config extended to watch .css files and run Tailwind CLI on rebuild | CSS edits require Tailwind rebuild; air restarts on template changes but not CSS without this | ✓ Validated | + +## Evolution + +This document evolves at phase transitions and milestone boundaries. + +**After each phase transition** (via `/gsd-transition`): +1. Requirements invalidated? → Move to Out of Scope with reason +2. Requirements validated? → Move to Validated with phase reference +3. New requirements emerged? → Add to Active +4. Decisions to log? → Add to Key Decisions +5. "What This Is" still accurate? → Update if drifted + +**After each milestone** (via `/gsd-complete-milestone`): +1. Full review of all sections +2. Core Value check — still the right priority? +3. Audit Out of Scope — reasons still valid? +4. Update Context with current state + +## Phase History + +- **Phase 1: Foundation** — Completed 2026-05-14. Fresh `backend/` Go package boots a web server, renders an HTMX-driven base layout, connects to local Postgres with goose migrations. FOUND-01..FOUND-05 satisfied; user-approved manual walkthrough. Two inline justfile/tailwind fixes during UAT (commit `fix(01): guard sqlc on empty queries and correct tailwind paths`). +- **Phase 7: deploy-v1** — Completed 2026-05-15. Multi-stage Dockerfile (assets→builder→distroless nonroot), docker-compose.prod.yaml with 4 services (postgres/web/worker/caddy), Caddyfile with `{$DOMAIN}` TLS, README runbook covering first-deploy/rollback/incident. DEPLOY-01..DEPLOY-05 satisfied. 3 UAT items (Docker build, compose config, live smoke test) pending on a Docker-equipped machine. +- **Milestone v2.0 started: Collaboration, planning, and social sign-in** — Started 2026-05-15. Scope: native per-tablo chat, one-level etapes, Google/Apple sign-in, and individual planning with tablo events. +- **Milestone v2.0 shipped: Collaboration, planning, and social sign-in** — Shipped 2026-05-16. Delivered Google sign-in, etapes, events, individual planning, and native SSE-backed tablo discussions. Closed with 7 acknowledged deferred artifact items; see `.planning/milestones/v2.0-MILESTONE-AUDIT.md`. + +--- +--- +*Last updated: 2026-05-17 after v4.0 milestone start — Figma design parity milestone defined* diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 0000000..9270e91 --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,82 @@ +# Requirements: Xtablo — Go + HTMX Product + +**Defined:** 2026-05-17 +**Core Value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider. + +## v4.0 Requirements — Figma Design Parity + +Requirements for v4.0. Each maps to roadmap phases. + +### Navigation & Shell + +- [ ] **NAV-01**: User sees a redesigned sidebar/nav bar matching Figma (brand section, icon nav items, tablo list section, user footer) +- [ ] **NAV-02**: User sees a per-page top header bar with page title and contextual action buttons matching Figma + +### Tablo List + +- [ ] **LIST-01**: Tablo project cards are revamped to match Figma with updated layout, typography, and progress bar wired to real task completion percentage +- [ ] **LIST-02**: User can toggle between card grid view and list view on the tablos page +- [ ] **LIST-03**: Tablos have an active/archived status field stored in DB, visible as a UI indicator on cards and list rows + +### Tasks + +- [x] **TASK-01**: Kanban board columns, task cards, and drag-and-drop are restyled to match Figma +- [ ] **TASK-02**: User can switch to a grid/table view of tasks within a tablo (new view — data model + UI) +- [ ] **TASK-03**: User can switch to a roadmap/timeline view of tasks within a tablo (new view — requires due date fields added to tasks schema) + +### Calendar (Planning) + +- [ ] **CAL-01**: User can view their planning in a month/week/day calendar grid, replacing the current flat agenda list +- [ ] **CAL-02**: Events render as colored blocks inside calendar cells (not just text list items) +- [ ] **CAL-03**: User can filter calendar events by tablo + +### Tablo Detail + +- [x] **DETAIL-01**: Tablo detail page (header, tasks section, etapes section, files table) is restyled to match Figma + +## v5.0 Candidates (Deferred) + +### Polish & Accessibility + +- **RESP-01**: Responsive layout — sidebar collapses on small screens, kanban scrolls horizontally on mobile +- **DARK-01**: Dark mode — CSS token `prefers-color-scheme` counterparts or class-toggle variant + +## Out of Scope + +| Feature | Reason | +|---------|--------| +| Apple sign-in | Disabled in v2.0; remains out of scope | +| Stripe / billing | Deferred until product loop validated | +| Public booking widget | Deferred to a later milestone | +| Client portal | Deferred | +| Admin app | Deferred | +| Mobile / Expo app | Out of scope for this rewrite | +| Notes / rich documents | Not part of this milestone | + +## Traceability + +Which phases cover which requirements. Updated during roadmap creation. + +| Requirement | Phase | Status | +|-------------|-------|--------| +| NAV-01 | Phase 18 | Pending | +| NAV-02 | Phase 18 | Pending | +| LIST-01 | Phase 19 | Pending | +| LIST-02 | Phase 19 | Pending | +| LIST-03 | Phase 19 | Pending | +| TASK-01 | Phase 20 | Complete | +| DETAIL-01 | Phase 20 | Complete | +| TASK-02 | Phase 21 | Pending | +| TASK-03 | Phase 21 | Pending | +| CAL-01 | Phase 22 | Pending | +| CAL-02 | Phase 22 | Pending | +| CAL-03 | Phase 22 | Pending | + +**Coverage:** +- v4.0 requirements: 12 total +- Mapped to phases: 12 +- Unmapped: 0 ✓ + +--- +*Requirements defined: 2026-05-17* +*Last updated: 2026-05-17 after initial definition* diff --git a/.planning/RETROSPECTIVE.md b/.planning/RETROSPECTIVE.md new file mode 100644 index 0000000..ccdf375 --- /dev/null +++ b/.planning/RETROSPECTIVE.md @@ -0,0 +1,126 @@ +# Project Retrospective + +A living document updated after each milestone. Lessons feed forward into future planning. + +## Milestone: v2.0 — Collaboration, Planning, and Social Sign-in + +**Shipped:** 2026-05-16 +**Phases:** 5 | **Plans:** 18 | **Sessions:** multiple GSD sessions + +### What Was Built + +- Google sign-in with Xtablo-owned user/session issuance and Apple sign-in disabled for the shipped scope. +- One-level etapes for organizing tasks while preserving existing kanban behavior. +- Tablo-owned scheduled events plus an authenticated personal planning agenda. +- Native per-tablo discussion history, posting, unread state, and realtime SSE delivery. + +### What Worked + +- The Go + HTMX stack handled new auth, planning, and realtime surfaces without introducing a JS framework or managed chat provider. +- DB-backed handler tests gave strong coverage for ownership, validation, and cross-tablo aggregation. +- UAT feedback during Phase 12 caught real browser behavior around duplicate rows and composer reset. + +### What Was Inefficient + +- Milestone close happened after v3.0 was already initialized, so archival had to preserve current v3 planning files instead of using the standard current-milestone deletion path. +- Some verification artifacts lagged behind shipped behavior, which made milestone audit status noisier than implementation state. +- Earlier v1 UAT/verification records still carry human-needed or partial statuses and should be cleaned separately if strict historical reporting matters. + +### Patterns Established + +- Use owned Postgres queries plus handler tests as the primary guardrail for cross-user isolation. +- Keep social identity providers as login inputs only; Xtablo sessions remain authoritative. +- For v2 realtime, SSE receive plus HTMX POST send is sufficient and keeps infrastructure local. +- Validation files should be closed immediately after phase execution to avoid stale milestone audits. + +### Key Lessons + +1. Archive a milestone before starting the next one, or make the close workflow explicitly support archival from a historical commit. +2. Treat `*-VERIFICATION.md` as a required phase exit artifact, not a later audit cleanup. +3. Browser UAT remains necessary for HTMX swap/reset behavior even when handler tests are green. + +### Cost Observations + +- Model mix: not tracked. +- Sessions: multiple interactive GSD sessions. +- Notable: most rework came from artifact hygiene and UAT-discovered browser behavior, not from core backend design. + +--- + +## Milestone: v3.0 — Design System & Visual Polish + +**Shipped:** 2026-05-17 +**Phases:** 5 (13-17) | **Plans:** 16 + +### What Was Built + +- Full CSS token vocabulary in `base.css` (colors, spacing, typography, shadows, gradients) +- 11 typed templ components: button, badge, card, modal, empty-state, table, icon-button, input, textarea, select, form-field +- Build-tag-gated `/ui-catalog` visual verification page for the design system +- Auth pages (login/signup) with gradient animated background, auth-card layout, Google Material Design sign-in button +- App shell sidebar with brand section, nav icons, tablo list section, user footer +- Project-card tablo grid with color avatars, creation date, action controls +- Tablo detail: project-card-top header, tasks-section kanban, etapes section, files `@ui.Table` with `@ui.EmptyState` +- Chat view: own-vs-others message bubbles with brand-tint right-alignment +- Planning view: day-separator event list using overview-section layout + +### What Worked + +- Building the design system foundation (Phase 13) before any surface work paid off — phases 14-17 moved fast because components were ready +- Build-tag catalog (`just catalog`) made visual verification frictionless between phases +- Air CSS watching (added in Phase 14) eliminated manual restart friction for the rest of the milestone +- TDD for view model logic (planning view, chat IsOwn) caught integration bugs before browser verify + +### What Was Inefficient + +- REQUIREMENTS.md checkboxes for DASH-01/02/03 were not ticked even after Phase 15 completed — stale data discovered only at milestone close +- Phase 13 summary one-liners were raw "Task 1 —" prefixes from the executor rather than human-readable accomplishments — cleaned manually at close +- Some ROADMAP.md phase entries had stale "1/2 plans executed" counts that didn't reflect actual SUMMARY.md files + +### Patterns Established + +- Design system → auth surface → dashboard → detail → auxiliary (chat/planning) is the right reskin order +- `@ui.Table` + `@ui.EmptyState` as a pair for any list view +- `@AppLayout` wraps all authenticated pages; `@AuthLayout` wraps auth pages — switching is done by changing the outer template +- Build tags (`//go:build catalog` + `//go:build !catalog` stub) as the idiom for dev-only routes + +### Key Lessons + +- Validate REQUIREMENTS.md checkboxes during phase completion, not at milestone close +- Phase SUMMARY.md one-liners should be written by the executor in final form; raw task names create cleanup work at milestone close +- CSS dev-watch in air is non-negotiable for any CSS-heavy phase — add it at project start, not mid-milestone +- Catalog-first is worthwhile for design systems: visual inventory before any surface application prevents design drift + +### Cost Observations + +- 2-day milestone (2026-05-16 → 2026-05-17) +- 150 files, +23,505/−993 lines across 128 commits +- Most expensive phase: Phase 14 (auth pages) — auth_components + auth_layout from scratch took iteration +- Fastest phase: Phase 13 Plan 05 (catalog) — 3 minutes once components existed + +--- + +## Cross-Milestone Trends + +### Process Evolution + +| Milestone | Sessions | Phases | Key Change | +|-----------|----------|--------|------------| +| v2.0 | multiple | 5 | Collaboration, scheduling, and realtime work stayed inside Go + HTMX; artifact hygiene needs tighter close discipline. | +| v3.0 | multiple | 5 | Design-system-first approach validated; surface reskins moved fast. Stale requirement checkboxes and raw summary one-liners are recurring close-time friction. | + +### Cumulative Quality + +| Milestone | Tests | Coverage | Zero-Dep Additions | +|-----------|-------|----------|-------------------| +| v2.0 | Go handler/DB suites plus browser UAT | Strong behavior coverage; some archive artifacts deferred | Native SSE chat, server-owned sessions, Postgres-backed planning | +| v3.0 | TDD for view models (planning, chat IsOwn); browser verify checkpoints | Visual correctness tested via catalog + human UAT; unit coverage on behavior helpers | Design system (no new runtime deps); build-tag-gated catalog route | + +### Top Lessons + +1. Close phase validation and verification artifacts before running milestone audit. +2. Keep milestone archival before next-milestone initialization when possible. +3. Preserve browser UAT for HTMX interaction behavior. +4. Check REQUIREMENTS.md checkboxes during phase execution, not at milestone close — stale checkboxes create friction. +5. Design-system-first pays dividends: build the component library, verify in catalog, then apply to surfaces. +6. Add CSS dev-watch (Air) at project start — it's always needed for CSS-heavy work. diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..760b6d3 --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,143 @@ +# Roadmap: Xtablo Go + HTMX Rewrite + +## Milestones + +- ✅ **v1.0 MVP** — Phases 1-7 (shipped 2026-05-15). Archive: [`milestones/v1.0-ROADMAP.md`](milestones/v1.0-ROADMAP.md) +- ✅ **v2.0 Collaboration, Planning, and Social Sign-in** — Phases 8-12 (shipped 2026-05-16). Archive: [`milestones/v2.0-ROADMAP.md`](milestones/v2.0-ROADMAP.md) +- ✅ **v3.0 Design System & Visual Polish** — Phases 13-17 (shipped 2026-05-17). Archive: [`milestones/v3.0-ROADMAP.md`](milestones/v3.0-ROADMAP.md) + +## v4.0 Active Phases + +- [x] Phase 18: App Shell & Navigation — sidebar redesign + top header bar (NAV-01, NAV-02) — completed 2026-05-17 +- [ ] Phase 19: Tablo List Revamp — card redesign, progress bar, status field, list/card toggle (LIST-01, LIST-02, LIST-03) +- [ ] Phase 20: Tablo Detail & Kanban — detail page + kanban restyled to Figma (DETAIL-01, TASK-01) +- [ ] Phase 21: Task Grid & Roadmap Views — new grid view + new roadmap view with date fields (TASK-02, TASK-03) +- [ ] Phase 22: Calendar Rework — month/week/day grid, event blocks, tablo filter (CAL-01, CAL-02, CAL-03) + +## Phases + +
+✅ v1.0 MVP (Phases 1-7) — SHIPPED 2026-05-15 + +- [x] Phase 1: Foundation — completed 2026-05-14 +- [x] Phase 2: Authentication — completed 2026-05-15 +- [x] Phase 3: Tablos CRUD — completed 2026-05-15 +- [x] Phase 4: Tasks Kanban — completed 2026-05-15 +- [x] Phase 5: File Attachments — completed 2026-05-15 +- [x] Phase 6: Background Worker — completed 2026-05-15 +- [x] Phase 7: Deploy v1 — completed 2026-05-15 + +
+ +
+✅ v2.0 Collaboration, Planning, and Social Sign-in (Phases 8-12) — SHIPPED 2026-05-16 + +- [x] Phase 8: Social Sign-in (5/5 plans) — completed 2026-05-15 +- [x] Phase 9: Etapes (4/4 plans) — completed 2026-05-15 +- [x] Phase 10: Events (3/3 plans) — completed 2026-05-16 +- [x] Phase 11: Individual Planning (2/2 plans) — completed 2026-05-16 +- [x] Phase 12: Native Tablo Chat (3/3 plans) — completed 2026-05-16 + +
+ +
+✅ v3.0 Design System & Visual Polish (Phases 13-17) — SHIPPED 2026-05-17 + +- [x] Phase 13: Design System Foundation (5/5 plans) — completed 2026-05-16 +- [x] Phase 14: Auth Pages (2/2 plans) — completed 2026-05-16 +- [x] Phase 15: Dashboard & Tablos (3/3 plans) — completed 2026-05-16 +- [x] Phase 16: Tablo Detail (4/4 plans) — completed 2026-05-17 +- [x] Phase 17: Chat & Planning (2/2 plans) — completed 2026-05-17 + +
+ +## Phase Details — v4.0 + +### Phase 18: App Shell & Navigation +**Goal:** Redesign the sidebar and top header bar to match the Figma design. +**Requirements:** NAV-01, NAV-02 +**Plans:** 3 plans +**Success criteria:** +1. Sidebar renders brand section, icon nav items, tablo list section, and user footer matching Figma +2. Every authenticated page shows a top header bar with page title and contextual actions +3. Existing navigation functionality (logout, tablo selection) is preserved + +Plans: +- [ ] 18-01-PLAN.md — AppLayout signature extension + BreadcrumbItem struct + all call sites updated +- [ ] 18-02-PLAN.md — Sidebar full HTML/CSS rebuild to Figma spec with collapse toggle +- [ ] 18-03-PLAN.md — PageHeader component, avatar dropdown, /settings stub, tests updated + +### Phase 19: Tablo List Revamp +**Goal:** Restyle the tablos page with revamped cards, real progress data, list/card toggle, and status field. +**Requirements:** LIST-01, LIST-02, LIST-03 +**Plans:** 3 plans +**Success criteria:** +1. Tablo cards display with updated Figma layout including a progress bar showing real task completion % +2. User can switch between card grid and list view; selection persists for the session +3. Tablos have an active/archived status field in DB; a status indicator is visible on cards and list rows +4. DB migration for status field is reversible + +Plans: +- [ ] 19-01-PLAN.md — DB migration 0010, sqlc regen with status column, batch progress query, handler wiring +- [ ] 19-02-PLAN.md — Revamped TabloProjectCard template (badge, initial, progress bar) + list row CSS +- [ ] 19-03-PLAN.md — View toggle button + inline JS + tests for LIST-01/02/03 + +### Phase 20: Tablo Detail & Kanban Restyle +**Goal:** Restyle the tablo detail page and kanban board to match Figma. +**Requirements:** DETAIL-01, TASK-01 +**Plans:** 3/3 plans complete +**Success criteria:** +1. Tablo detail header shows tablo name, status, and progress matching Figma +2. Kanban columns and task cards are restyled to match Figma +3. Drag-and-drop reorder continues to work after restyle +4. Etapes section and files table match Figma layout + +Plans: +- [x] 20-01-PLAN.md — Handler + view model + route: GET /tablos/{tabloID}, TabloDetailViewModel, test scaffold +- [x] 20-02-PLAN.md — TabloDetailPage templ components: header, tab bar, kanban board, task cards +- [x] 20-03-PLAN.md — CSS restyle: tablo detail header, kanban board layout, task card, progress bar, files table + +### Phase 21: Task Grid & Roadmap Views +**Goal:** Add grid/table and roadmap/timeline views to the task section of a tablo. +**Requirements:** TASK-02, TASK-03 +**Success criteria:** +1. User can switch between kanban, grid, and roadmap views via a view switcher in the tablo detail +2. Grid view shows all tasks as a sortable/filterable table with key columns (title, status, assignee, due date) +3. Roadmap view shows tasks on a horizontal timeline grouped by status or date +4. Tasks have an optional due_date field in DB (migration + sqlc); roadmap uses it for positioning + +### Phase 22: Calendar Rework +**Goal:** Replace the flat agenda list on /planning with a full Google Calendar-style interface. +**Requirements:** CAL-01, CAL-02, CAL-03 +**Success criteria:** +1. /planning page shows a month/week/day calendar grid with navigation controls +2. Events render as colored blocks inside calendar cells rather than plain text rows +3. User can filter the calendar to show events from a specific tablo only +4. Existing event CRUD (create, edit, delete) continues to work within the new calendar UI + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|---------------|----------|------------| +| 1. Foundation | v1.0 | — | Complete | 2026-05-14 | +| 2. Authentication | v1.0 | 7/7 | Complete | 2026-05-15 | +| 3. Tablos CRUD | v1.0 | 3/3 | Complete | 2026-05-15 | +| 4. Tasks Kanban | v1.0 | 3/3 | Complete | 2026-05-15 | +| 5. File Attachments | v1.0 | — | Complete | 2026-05-15 | +| 6. Background Worker | v1.0 | 2/2 | Complete | 2026-05-15 | +| 7. Deploy v1 | v1.0 | — | Complete | 2026-05-15 | +| 8. Social Sign-in | v2.0 | 5/5 | Complete | 2026-05-15 | +| 9. Etapes | v2.0 | 4/4 | Complete | 2026-05-15 | +| 10. Events | v2.0 | 3/3 | Complete | 2026-05-16 | +| 11. Individual Planning | v2.0 | 2/2 | Complete | 2026-05-16 | +| 12. Native Tablo Chat | v2.0 | 3/3 | Complete | 2026-05-16 | +| 13. Design System Foundation | v3.0 | 5/5 | Complete | 2026-05-16 | +| 14. Auth Pages | v3.0 | 2/2 | Complete | 2026-05-16 | +| 15. Dashboard & Tablos | v3.0 | 3/3 | Complete | 2026-05-16 | +| 16. Tablo Detail | v3.0 | 4/4 | Complete | 2026-05-17 | +| 17. Chat & Planning | v3.0 | 2/2 | Complete | 2026-05-17 | +| 18. App Shell & Navigation | v4.0 | 0/3 | Pending | — | +| 19. Tablo List Revamp | v4.0 | 0/3 | Pending | — | +| 20. Tablo Detail & Kanban | v4.0 | 3/3 | Complete | 2026-05-18 | +| 21. Task Grid & Roadmap Views | v4.0 | — | Pending | — | +| 22. Calendar Rework | v4.0 | — | Pending | — | diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..3c6eb13 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,209 @@ +--- +gsd_state_version: 1.0 +milestone: v4.0 +milestone_name: Figma Design Parity +status: ready_to_plan +last_updated: 2026-05-18T14:04:17.721Z +last_activity: 2026-05-18 -- Phase 20 execution started +progress: + total_phases: 5 + completed_phases: 2 + total_plans: 9 + completed_plans: 9 + percent: 40 +stopped_at: Phase 20 complete (3/3) — ready to discuss Phase 21 +--- + +# STATE + +**Project:** Xtablo Go+HTMX Product +**Milestone:** v3.0 — Design System & Visual Polish +**Created:** 2026-05-14 + +## Project Reference + +See: `.planning/PROJECT.md` (updated 2026-05-17) + +**Core value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider. +**Current focus:** Phase 21 — task grid & roadmap views + +## Current Position + +Phase: 21 +Plan: Not started +Status: Ready to plan +Last activity: 2026-05-18 + +## Previous Milestone Status + +| # | Phase | Status | +|---|-------|--------| +| 8 | Social Sign-in | ✓ Complete | +| 9 | Etapes | ✓ Complete | +| 10 | Events | ✓ Complete | +| 11 | Individual Planning | ✓ Complete | +| 12 | Native Tablo Chat | ✓ Complete | + +## Verification Record + +- 2026-05-15: Phase 8 execution complete. Local verification passed with `go test ./... -count=1`; database-backed integration coverage skips unless `TEST_DATABASE_URL` is configured. +- 2026-05-15: Phase 9 UAT complete. Five browser checkpoints passed after fixing the selected-etape create gap; backend verification passed with `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`. +- 2026-05-16: Phase 10 execution complete. Events UAT approved; backend verification passed with `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`. +- 2026-05-16: Phase 12 execution complete. Discussion realtime UAT approved after duplicate-row and composer-reset fixes; backend verification passed with `just generate` and `TEST_DATABASE_URL='postgres://xtablo:xtablo@localhost:5432/xtablo?sslmode=disable' go test ./... -count=1`. +- 2026-05-16: Milestone v2.0 archived after acknowledging 7 legacy/open artifact items as deferred. + +## Deferred Items + +Items acknowledged and deferred at v3.0 milestone close on 2026-05-17 (all pre-existing from v1.0/v2.0): + +| Category | Item | Status | +|----------|------|--------| +| uat_gap | Phase 05: 05-HUMAN-UAT.md | partial; 6 pending scenarios | +| uat_gap | Phase 07: 07-HUMAN-UAT.md | partial; 3 pending scenarios | +| verification_gap | Phase 03: 03-VERIFICATION.md | human_needed | +| verification_gap | Phase 04: 04-VERIFICATION.md | human_needed | +| verification_gap | Phase 05: 05-VERIFICATION.md | human_needed | +| verification_gap | Phase 07: 07-VERIFICATION.md | human_needed | + +## Decisions + +- **Consolidated internal/auth package** (not split with internal/session) — RESEARCH Open Question 3 +- **compose Postgres + schema isolation for tests** (not testcontainers-go) — RESEARCH Open Question 1 +- **goose.SetTableName per test-schema** prevents public goose_db_version table collision +- **argon2id** over bcrypt for password hashing — D-08, confirmed by user +- **Hand-rolled PHC encode/decode** (Pattern 1 verbatim) — no alexedwards/argon2id wrapper dep; keeps code lean and self-tested +- **Store.now injectable field** (not method/interface) — simpler for single-test-clock use case in MaybeExtend threshold tests +- **sha256.Sum256 inlined at Create + Lookup** (not in helper) — satisfies >= 2 grep acceptance criterion; makes D-05 hashing visible at usage sites +- **SignupForm/SignupErrors in templates package** (not handlers or separate forms pkg) — avoids import cycle between templates and internal/web +- **setupTestDB duplicated into web package test file** — Go prohibits importing _test.go files across packages; duplication chosen over non-test shared infra +- **nil-Store guard in auth.ResolveSession** — enables Phase 1 unit tests to pass AuthDeps{} zero value without panic +- **errInvalidCreds const** (not inline string) — satisfies D-20 single-source-of-truth grep gate; both credential failure paths use the constant +- **NewLimiterStoreWithClock exported** — cross-package integration tests inject a frozen clock without a test-helper shim +- **Status 401 for non-HTMX credential failures** — consistent with HTTP semantics; HTMX path returns 200+fragment +- **Layout takes *auth.User explicitly** (not thread-through-ctx) — easier to test templates in isolation; no magic context values +- **TestIndex_RendersHxGet rewritten to TestIndex_UnauthRedirects** — GET / is now protected; Phase 1 test was a false positive after this plan +- **csrf.PlaintextHTTPRequest(r) in dev mode** — skips TLS Referer check in plain-HTTP local dev/httptest; production unaffected +- **trustedOrigins variadic arg in auth.Mount + NewRouter** — test routers pass "localhost"; production passes none +- **sqlc-generated files not committed** — backend/.gitignore excludes internal/db/sqlc/*.go; just generate reproduces them (03-01) +- **NewRouter extended with TablosDeps parameter** — second explicit deps param before csrfKey; all call sites updated (03-01) +- **index_templ.go deleted manually after emptying index.templ** — templ generates broken import when .templ has no components (03-02) +- **Non-HTMX validation error re-fetches tablos and renders full dashboard** — no form state threading; simpler than threading form through full page (03-02) +- **_zone hidden field (title|desc) in edit fragments** — unified POST /tablos/{id} handler reads _zone to select which display fragment to return; misuse only affects response body, not DB state (03-03) +- **loadOwnedTablo helper for all parametric handlers** — uuid.Parse + GetTabloByID + ownership check factored into single function; 404 on any failure, 403 never used (D-04) (03-03) +- **TabloDeleteButtonFragment canonical delete-zone** — TabloCard delegates to it; single source of truth for zone HTML across card and detail-page contexts (03-03) +- **task_status ENUM order matches visual column order** (todo, in_progress, in_review, done) — ENUM ordinal sorting aligns with kanban column display order per Pitfall 6 (04-01) +- **Down migration drops TABLE before TYPE** — tasks table dropped before task_status type per Pitfall 3; type cannot be dropped while still referenced (04-01) +- **TasksDeps stub in test file** — declared in handlers_tasks_test.go; moved to handlers_tasks.go in Plan 02 to avoid file dependency before handlers exist (04-01) +- **TaskColumns/TaskColumnLabels in templates package** — moved from web package to templates to avoid web↔templates import cycle; handlers reference via templates.TaskColumns (04-02) +- **TabloDetailPage accepts tasks []sqlc.Task** — kanban board embedded below tablo header; TabloDetailHandler fetches tasks via ListTasksByTablo before rendering (04-02) +- **Dual reorder payload** — TaskReorderHandler supports array form (task_id[]/task_col[]) and single-value form (task_id/status/position) for test scaffold + Sortable.js compatibility (04-03) +- **GetTaskByID before UpdateTask in reorder** — preserves title+description (T-04-08), validates task-to-tablo ownership at fetch time (T-04-10) (04-03) +- **fileQuerier interface in OrphanCleanupWorker** — enables mock injection for pure unit tests without real DB; pool field retained for production (06-01) +- **river deps as // indirect until Plan 02** — cmd/worker wiring in Plan 02 will promote river to direct dependency; expected Go module behavior (06-01) +- **signal.NotifyContext after rivermigrate and S3 init** — startup I/O uses context.Background(); signal context created after all I/O succeeds; prevents river.Client.Start receiving a pre-cancelled context (RESEARCH Pitfall 2) (06-02) +- **pool.Close() explicit after StopAndCancel, not via defer** — StopAndCancel fully returns before pool is closed; matches PATTERNS.md pool close ordering (06-02) +- **Social-only users use NULL password_hash** — email/password login rejects them through the generic invalid-credentials path while provider sign-in owns the session path (08-01) +- **Verified provider identity links by provider subject first, then verified email** — prevents duplicate local users while keeping provider subject as the strongest identity key (08-02) +- **Apple callback uses GET/query response mode for the first working version** — keeps the callback inside existing CSRF middleware boundaries; Apple dashboard configuration must match (08-03) +- **Provider buttons degrade to disabled controls when config is missing** — auth pages remain deployable without provider credentials (08-04) +- **Account providers page is read-only in Phase 8** — linked identity visibility shipped before unlink/add-password account management (08-05) +- **Apple sign-in disabled after UAT scope change** — Apple controls are hidden, Apple auth routes are not mounted, and provider docs only cover Google for now (08-UAT) +- **Google button width: max-content (not 100%)** — matches Google Material Design branding guidelines; pill button naturally sized, not stretched to card width (14-02) +- **Roboto font loaded in auth_layout.templ only** — auth pages are the only gsi-material-button context; global stylesheet pollution avoided (14-02) +- **AuthProviderButtonsBlock/AuthProviderButtonControl removed** — superseded by GoogleButton from auth_components.templ; single-provider design, components consolidated (14-02) +- **Air dev .css watching added** — .css in include_ext + Tailwind in rebuild cmd; CSS edits now trigger reload without manual restart (14-02) + +## Performance Metrics + +| Phase | Plan | Duration | Tasks | Files | +|-------|------|----------|-------|-------| +| 02-authentication | 01 | ~10min | 3 | 9 | +| 02-authentication | 02 | ~8min | 2 | 4 | +| 02-authentication | 03 | ~15min | 2 | 5 | +| 02-authentication | 04 | ~25min | 2 | 11 | +| 02-authentication | 05 | ~7min | 2 | 9 | +| 02-authentication | 06 | ~12min | 1 | 9 | +| 02-authentication | 07 | ~25min | 1 | 18 | +| 03-tablos-crud | 01 | ~15min | 3 | 10 | +| 03-tablos-crud | 02 | ~4min | 3 | 8 | +| 03-tablos-crud | 03 | ~30min | 3 | 5 | +| 04-tasks-kanban | 01 | ~4min | 3 | 7 | +| 04-tasks-kanban | 02 | ~20min | 3 | 12 | +| Phase 04-tasks-kanban P03 | ~15min | 3 tasks | 3 files | +| 06-background-worker | 01 | ~15min | 2 | 9 | +| Phase 06-background-worker P01 | ~15min | 2 tasks | 9 files | +| 06-background-worker | 02 | ~10min | 2 | 3 | +| Phase 08 P01 | 25min | 2 tasks | 8 files | +| Phase 08 P02 | 35min | 2 tasks | 9 files | +| Phase 08 P03 | 30min | 2 tasks | 7 files | +| Phase 08 P04 | 20min | 2 tasks | 8 files | +| Phase 08 P05 | 20min | 2 tasks | 7 files | +| Phase 11 P01 | ~15min | 2 tasks | 15 files | +| Phase 11 P02 | ~20min | 2 tasks | 4 files | +| Phase 12 P01 | ~25min | 3 tasks | 22 files | +| Phase 12 P02 | ~25min | 2 tasks | 5 files | +| Phase 12 P03 | ~45min | 4 tasks | 9 files | +| Phase 14 P01 | 4min | 2 tasks | 5 files | +| Phase 14 P02 | ~30min | 2 tasks | 4 files | +| Phase 17-chat-planning P02 | 15min | 3 tasks | 6 files | + +## Notes + +- Existing `go-backend/` is set aside; new code lives in a fresh `backend/` Go package. +- DB schema is changing from the JS/Supabase version — user is in the loop on every schema decision (Phases 2–5). +- Phase 2 Plan 01 SUMMARY: `.planning/phases/02-authentication/02-01-SUMMARY.md` +- Phase 2 Plan 02 SUMMARY: `.planning/phases/02-authentication/02-02-SUMMARY.md` +- Phase 2 Plan 03 SUMMARY: `.planning/phases/02-authentication/02-03-SUMMARY.md` +- Commits (02-01): 513044d (migration), 799c260 (sqlc), 2c84f42 (auth package + test harness) +- Commits (02-02): 3bb3828 (RED tests), ee36a5c (GREEN implementation) +- Commits (02-03): fd2301d (session store + cookie helpers), 1d07830 (middleware) +- Commits (02-04): 73935ed (signup templates + smoke tests), efdc16b (handler + router + integration tests) +- Phase 2 Plan 04 SUMMARY: `.planning/phases/02-authentication/02-04-SUMMARY.md` +- Commits (02-05): b5c20c7 (LimiterStore + tests), 7d8c498 (login handler + integration tests) +- Phase 2 Plan 05 SUMMARY: `.planning/phases/02-authentication/02-05-SUMMARY.md` +- Commits (02-06): b5c3fc4 (RED tests), 8b54ff4 (logout + protected routes + layout) +- Phase 2 Plan 06 SUMMARY: `.planning/phases/02-authentication/02-06-SUMMARY.md` +- Commits (02-07): ae2d356 (RED tests), 389e1bc (GREEN csrf implementation) +- Phase 2 Plan 07 SUMMARY: `.planning/phases/02-authentication/02-07-SUMMARY.md` +- Phase 3 Plan 01 SUMMARY: `.planning/phases/03-tablos-crud/03-01-SUMMARY.md` +- Commits (03-01): f1b8d6e (migration + queries), c8f44b1 (TablosDeps stub + test scaffold), 2c1b186 (button CSS variants) +- Phase 3 Plan 02 SUMMARY: `.planning/phases/03-tablos-crud/03-02-SUMMARY.md` +- Commits (03-02): 43ddf25 (tablos templates + layout footer), 5db9215 (tablo handlers + router wiring), c08da7f (delete retired index.templ) +- 03-02 complete — all 3 tasks done including human-verify checkpoint approval +- Phase 3 Plan 03 SUMMARY: `.planning/phases/03-tablos-crud/03-03-SUMMARY.md` +- Commits (03-03): 6f167e2 (detail/edit/delete templates), ab6937c (handlers + router + all 10 TABLO tests green), b5fa318 (checkpoint approved) +- **Phase 3 complete** — All 3 plans done; TABLO-01..06 closed; 10/10 TABLO tests green; full browser verify passed 2026-05-15 +- Phase 4 Plan 01 SUMMARY: `.planning/phases/04-tasks-kanban/04-01-SUMMARY.md` +- Commits (04-01): c9c8262 (migration + sqlc queries), 8b9543d (RED test scaffold + form structs), 55fb32f (Sortable.js + soft-danger CSS) +- Phase 4 Plan 02 SUMMARY: `.planning/phases/04-tasks-kanban/04-02-SUMMARY.md` +- Commits (04-02): 181ae79 (handlers + router + main.go), 889164b (templates + tablos.templ + layout.templ), 92ebb5f (activate task tests) +- Phase 4 Plan 03 SUMMARY: `.planning/phases/04-tasks-kanban/04-03-SUMMARY.md` +- Commits (04-03): 2b299e2 (TaskEditHandler + TaskUpdateHandler + TaskEditFragment + Sortable.js init), 5f87d7e (TaskReorderHandler + reorder test skips removed), f6deb87 (TestTaskOrderPersists active — full suite green) +- Phase 6 Plan 01 SUMMARY: `.planning/phases/06-background-worker/06-01-SUMMARY.md` +- Commits (06-01): 62e5e3e (river dep + ListOrphanFiles sqlc query), a1c2828 (internal/jobs package + unit tests) +- Phase 6 Plan 02 SUMMARY: `.planning/phases/06-background-worker/06-02-SUMMARY.md` +- Commits (06-02): 6e70478 (cmd/worker full river wiring), e202ad3 (just worker target + README docs) +- Phase 8 Plan 01 SUMMARY: `.planning/phases/08-social-sign-in/08-01-SUMMARY.md` +- Phase 8 Plan 02 SUMMARY: `.planning/phases/08-social-sign-in/08-02-SUMMARY.md` +- Phase 8 Plan 03 SUMMARY: `.planning/phases/08-social-sign-in/08-03-SUMMARY.md` +- Phase 8 Plan 04 SUMMARY: `.planning/phases/08-social-sign-in/08-04-SUMMARY.md` +- Phase 8 Plan 05 SUMMARY: `.planning/phases/08-social-sign-in/08-05-SUMMARY.md` +- Commits (08): 2d004cd (social identity schema foundation), 6779663 (Google sign-in), a8b6a03 (Apple implementation later disabled), 59fd6b1 (auth page provider controls), 6e65836 (account providers view + docs) +- Phase 9 Plan 01 SUMMARY: `.planning/phases/09-etapes/09-01-SUMMARY.md` +- Phase 9 Plan 02 SUMMARY: `.planning/phases/09-etapes/09-02-SUMMARY.md` +- Phase 9 Plan 03 SUMMARY: `.planning/phases/09-etapes/09-03-SUMMARY.md` +- Phase 9 Plan 04 SUMMARY: `.planning/phases/09-etapes/09-04-SUMMARY.md` +- Phase 9 UAT: `.planning/phases/09-etapes/09-UAT.md` — 5/5 checkpoints passed after selected-etape create fix. +- Commits (09): a8a3e5f/565bb88 (first etape slice), 9b89282/4af623a (management), 9f6c7eb/b22d79d (assignment selector), 55263e4/3a3ecf5/cf07c29 (reorder hardening), 0c95049/ee62ff9/f9fc7a1 (UAT fixes) + +- Phase 14 Plan 01 SUMMARY: `.planning/phases/14-auth-pages/14-01-SUMMARY.md` +- Phase 14 Plan 02 SUMMARY: `.planning/phases/14-auth-pages/14-02-SUMMARY.md` +- Commits (14-01): cf116ff (logo assets + auth.css), e4d5f96 (auth_components.templ + auth_layout.templ) +- Commits (14-02): 808eaec (auth_login.templ), 65e3dbf (auth_signup.templ), 4624fb3 (Roboto + icon fix), 70fe384 (button width fix), 6e64cfb (air CSS watch) + +--- +*Last updated: 2026-05-16 after Phase 14 Plan 02 execution complete* + +## Operator Next Steps + +- Start the next milestone with /gsd-new-milestone diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..d237a89 --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,162 @@ +# Architecture + +_Last updated: 2026-05-14_ + +This document describes the high-level architecture of the `xtablo-source` monorepo: how apps, packages, and external services fit together, the dominant data-flow patterns, and where the key abstractions live. + +## High-Level Diagram + +``` + +-----------------------------------------------------+ + | Frontend Apps | + | | + | apps/main apps/external apps/clients | + | (dashboard) (booking widget) (client portal)| + | apps/admin | + | (internal admin) | + +------+----------------+--------------------+--------+ + | | | + | shared packages (source-only) | + | @xtablo/shared @xtablo/ui | + | @xtablo/shared-types | + | @xtablo/auth-ui @xtablo/chat-ui | + | @xtablo/tablo-views | + | | + v v + +-----------------+ +---------------------+ + | apps/api |<------------>| apps/chat-worker | + | (Hono REST) | | (CF Durable Obj) | + +--------+--------+ +----------+----------+ + | | + +----------------+----------------+-----------------+ + | | | | + v v v v + Supabase Stripe Cloudflare R2 Stream Chat + (Postgres+Auth) (payments) (file storage) (messaging) + | + + Datadog (RUM/APM) + + Google Secret Manager +``` + +## Application Layers + +- **Frontend dashboard** (`apps/main`): primary authenticated SPA. Tablos, planning, events, chat, notes, billing. Entry: `apps/main/src/main.tsx`, root component `apps/main/src/App.tsx`. +- **Public booking widget** (`apps/external`): embeddable / floating booking widget. Entry: `apps/external/src/main.tsx`. Query params drive mode (`?mode=embed&eventTypeId=...`). +- **Client portal** (`apps/clients`): public-facing client portal experience. Entry: `apps/clients/src/main.tsx`, routes in `apps/clients/src/routes.tsx`. +- **Admin app** (`apps/admin`): internal admin tools. Entry: `apps/admin/src/main.tsx`, routes in `apps/admin/src/routes.tsx`. +- **API** (`apps/api`): Hono-based REST API serving all frontends. Entry: `apps/api/src/index.ts` (compiled to Node, deployed to Google Cloud Run). +- **Chat worker** (`apps/chat-worker`): Cloudflare Worker with Durable Objects for real-time chat presence. Entry: `apps/chat-worker/src/index.ts`. + +## Data Flow Patterns + +### React Query (TanStack Query v5) — primary server-state tool + +All server state flows through React Query. Default cache time is 5 minutes. Query keys follow a hierarchical convention so that mutations can invalidate just the affected sub-tree: + +```ts +["tablos"] // list +["tablos", tabloId] // single +["tablo-files", tabloId] // related collection +``` + +Hooks live in: +- `apps/main/src/hooks/` — feature hooks (`tablos.ts`, `events.ts`, `tasks.ts`, `availabilities.ts`, `stripe.ts`, `notes.ts`, ...). +- `packages/shared/src/hooks/` — cross-app hooks (`auth.ts`, `book.ts`, `public.ts`). + +### Zustand — global client state + +Used sparingly, primarily for the current user. The user is fetched via React Query then mirrored into a Zustand store so any component can read it synchronously: +- `useUser()` — throws if no session (use inside protected routes). +- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public-aware components). + +Provider: `apps/main/src/providers/UserStoreProvider.tsx` (mirrored in `apps/external/src/UserStoreProvider.tsx`). + +### Direct Supabase queries vs API calls + +Two parallel data-access patterns coexist: + +1. **Direct Supabase** (`supabase.from("table").select()...`) — used from frontend hooks when row-level security is sufficient and no server-side logic is required. Client lives in `packages/shared/src/lib/supabase.ts` (re-exported via `apps/main/src/lib/supabase.ts`). +2. **API calls** (`api.get("/api/v1/...")`) — used when the operation needs the service role key, must run server-side logic (Stripe, file ops, email, multi-tenant integrity), or aggregates data. The HTTP client wrapper lives in `packages/shared/src/lib/api.ts` (re-exported via `apps/main/src/lib/api.ts`) and attaches the Supabase JWT as a Bearer token. + +File operations use specialized mutation hooks (e.g. `useUploadTabloFile`, `useDeleteTabloFile`) that invalidate `["tablo-files", tabloId]` automatically. + +## Authentication Flow + +- **Supabase Auth** issues JWTs on login / passwordless flows. +- **SessionContext** (`packages/shared/src/contexts/SessionContext.tsx`) subscribes to `supabase.auth.onAuthStateChange()` and exposes the current session. +- Frontend HTTP client reads the session token and sends `Authorization: Bearer ` on every API call. +- The API's `supabase` middleware validates the JWT and attaches the resolved user / supabase clients to the Hono context. +- **Passwordless onboarding** generates temporary accounts flagged with `is_temporary: true`. +- **Protected routes** check `useMaybeUser()` and redirect to landing when null. +- **Client portal** has a parallel auth path: magic-link based, signed cookies issued by `apps/api/src/routers/clientAuth.ts`. Configurable TTLs, cookie domain, JWT secret are passed into the router factory. + +## API Architecture (Hono) + +Entry: `apps/api/src/index.ts`. Flow: + +1. `loadSecrets()` pulls secrets (locally from env, in staging/prod from Google Secret Manager) — `apps/api/src/secrets.ts`. +2. `createConfig(secrets)` produces the typed `AppConfig` — `apps/api/src/config.ts`. +3. `MiddlewareManager.initialize(config)` constructs the middleware singleton — `apps/api/src/middlewares/middleware.ts`. +4. The root Hono app applies `logger()` and a CORS middleware that only accepts `*.xtablo.com` and `localhost` origins. +5. All routes mount under `/api/v1` via `getMainRouter(config)` (`apps/api/src/routers/index.ts`). + +### Middleware Manager (singleton pattern) + +`MiddlewareManager` builds each piece of middleware once on init and exposes them as instance properties. The main router pulls them via `MiddlewareManager.getInstance()` and chains them in this fixed order: + +``` +supabase -> r2 -> transporter -> stripe -> stripeSync +``` + +Auxiliary middleware modules: +- `apps/api/src/middlewares/middleware.ts` — central singleton and supabase / r2 / email / stripe middlewares. +- `apps/api/src/middlewares/stripeSync.ts` — bidirectional Supabase <-> Stripe sync engine. +- `apps/api/src/middlewares/transporter.ts` — email transporter. + +### Router ordering + +Public-first, then authenticated. From `apps/api/src/routers/index.ts`: + +1. `/public` — unauthenticated (`public.ts`). +2. `/tasks` — task router (`tasks.ts`). +3. `/revenuecat-webhook`, `/stripe-webhook` — webhook receivers. +4. `/admin` — admin-only routes (`admin.ts`, `adminAuth.ts`, ...). +5. `/client-auth`, `/client-portal`, `/client-invites` — client portal stack. +6. `/` — `maybeAuthRouter.ts` (optional auth — must come before authed to allow public booking). +7. `/` — `authRouter.ts` (requires JWT). + +The exported `ApiRoutes` type (`ReturnType`) enables Hono RPC clients to consume the API in a type-safe way. + +## Key Abstractions + +- **`packages/shared`** is the central runtime sharing point. Re-exports cover contexts (`SessionContext`, `ThemeContext`), cross-app hooks, the API client, the Supabase client, toast helpers, and shared types. Public surface in `packages/shared/src/index.ts`. +- **`packages/ui`** — Radix + Tailwind component library. Source-only. Components in `packages/ui/src/components/` (`button.tsx`, `dialog.tsx`, `select.tsx`, ...). +- **`packages/shared-types`** — zero-runtime-dependency TypeScript types. Auto-generated `database.types.ts` + hand-written domain layers (`tablos.types.ts`, `tablo-data.types.ts`, `events.types.ts`, `kanban.types.ts`, `stripe.types.ts`, `admin.types.ts`). +- **`packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views`** — feature-scoped UI packages, also source-only. +- **Query keys** — convention enforced by colocation in `apps/main/src/hooks/` and feature naming (`["tablos", id]`, `["tablo-files", id]`, etc.). + +## Source-Only Package Pattern + +`@xtablo/shared`, `@xtablo/ui`, `@xtablo/shared-types`, `@xtablo/auth-ui`, `@xtablo/chat-ui`, and `@xtablo/tablo-views` export TypeScript directly with no build step. Consumers import source files; Vite handles transpilation and HMR. Benefits: instant updates, no watch processes, simpler dependency graph. Constraint: no circular dependencies between packages, and the API can only depend on `@xtablo/shared-types` (pure types, no React). + +## Entry Points + +| App / package | Entry file | +|----------------------|---------------------------------------------------------| +| `apps/main` | `apps/main/src/main.tsx` -> `App.tsx` | +| `apps/external` | `apps/external/src/main.tsx` -> `routes.tsx` | +| `apps/clients` | `apps/clients/src/main.tsx` -> `App.tsx` / `routes.tsx` | +| `apps/admin` | `apps/admin/src/main.tsx` -> `routes.tsx` | +| `apps/api` | `apps/api/src/index.ts` -> `routers/index.ts` | +| `apps/chat-worker` | `apps/chat-worker/src/index.ts` | +| `@xtablo/shared` | `packages/shared/src/index.ts` | +| `@xtablo/ui` | `packages/ui/src/components/index.ts` | +| `@xtablo/shared-types` | `packages/shared-types/src/index.ts` | + +## Build & Deployment Notes + +- **Turborepo** orchestrates tasks with caching (`turbo.json` at repo root). +- `apps/main` and other Vite apps deploy to **Cloudflare Workers** via `wrangler.toml` and the bundled `worker/` folder. +- `apps/api` compiles TypeScript and deploys to **Google Cloud Run**; secrets resolved via Google Secret Manager. +- `apps/chat-worker` deploys as a **Cloudflare Worker with Durable Objects**. +- Observability: Datadog RUM on frontends (`apps/main/src/lib/rum.ts`), `dd-trace` on the API (initialized at the top of `apps/api/src/index.ts`). diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..6b0b97a --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,126 @@ +# Codebase Concerns Map + +> Generated 2026-05-14. Catalogs tech debt, security considerations, fragile zones, and known gotchas across the `xtablo-source` Turborepo. Pointers use repo-relative paths; line numbers are accurate as of this scan. + +## TODOs / FIXMEs / HACKs / XXX + +Grep of `apps/` + `packages/` (excluding `node_modules`, `dist`) — TODO markers are sparse, suggesting either healthy hygiene or undocumented debt. + +- `apps/api/src/index.ts:67` — `// TODO: Add health check endpoint`. No `/healthz` for Cloud Run, which complicates liveness/readiness probes. +- `apps/api/src/routers/invite.ts:26` — `// TODO: Verify that the owner_id is correct`. Authorization gap on invite creation. +- `apps/api/src/routers/invite.ts:128` — `// TODO: Verify that the event start and end correspond to a slot`. Booking integrity not enforced server-side. +- `apps/main/src/components/WebcalModal.tsx:125` — `{/* TODO: Add webcal URL */}` — feature placeholder shipping incomplete. +- `apps/main/src/lib/rum.ts:25,31` — Datadog RUM session sampling rates are commented `// TODO: Uncomment when we have enough data` — observability runs in a partial state. +- `apps/api/src/routers/user.ts:54` — `// Deprecated: name field is deprecated, use first_name and last_name instead` — dual fields still flowing through APIs. + +No `FIXME`, `HACK`, or `XXX` markers found in `apps/` or `packages/` source — but absence is not assurance; many concerns live under euphemisms (see `@deprecated` below). + +`@deprecated` markers: +- `apps/main/src/hooks/user.ts:16` — `useUser` hook deprecated in favor of `useSession` from `SessionContext`. Likely still has callers. + +## Known issues (from docs) + +The `docs/` directory contains 35+ retrospective `*_FIX.md`, `*_SETUP.md`, and migration notes. No single `TROUBLESHOOTING.md` exists; institutional knowledge is scattered. Notable items: + +- `docs/MIDDLEWARE_INITIALIZATION_FIX.md` — November 2025 incident: routers called `MiddlewareManager.getInstance()` at module-load time before `initialize()` ran. Fixed by passing the manager into router factories, but the singleton's eager-throw API (`getInstance()` throws if not initialized) remains a footgun for any new router that forgets the pattern. +- `docs/STRIPE_SECURITY_FIX.md` — Migration 37: `public.active_subscriptions` view exposed all users' subscription data without RLS. Replaced with `SECURITY DEFINER` function `get_my_active_subscription()`. Future views need similar audit. +- `docs/STRIPE_MIGRATION_36.md`, `STRIPE_WITH_SYNC_ENGINE.md`, `STRIPE_FINAL_SETUP.md`, `STRIPE_IMPLEMENTATION_SUMMARY.md`, `STRIPE_CLEANUP_*` — eleven Stripe documents indicate repeated rework on the billing surface. +- `docs/TEST_FIXES.md`, `docs/TEST_ROUTER_REFACTOR.md`, `docs/API_TESTS.md`, `docs/MIDDLEWARE_TESTS.md` — test setup has needed periodic refactoring; mocks are tightly coupled to the singleton. +- `SECURITY_NOTICE.md` (repo root) — `.env` files were previously committed to git history. The checklist of credentials to rotate (Supabase service role, Stripe, Stream, Google OAuth, R2) is in the file; verification that rotation occurred is not tracked here. + +## Security considerations + +### JWT handling +- Validation occurs in `apps/api/src/helpers/auth.ts` via `validateAuthHeader` + Supabase `auth.getUser(token)`. Wired into `authMiddleware` / `maybeAuthenticatedMiddleware` in `apps/api/src/middlewares/middleware.ts`. +- `apps/main/src` uses `jwt-decode` (^4.0.0) to inspect tokens client-side — purely decode, no verification (correct), but any code path that *trusts* decoded claims for authorization would be a bug. +- Client portal sessions use a separate JWT (`CLIENT_AUTH_JWT_SECRET`, `apps/api/src/helpers/clientSessions.ts`) with cookie storage (`CLIENT_AUTH_COOKIE_NAME`). Two independent token systems = two attack surfaces. +- Admin sessions live in `apps/api/src/helpers/adminTokens.ts` signed by `ADMIN_TOKEN_SIGNING_SECRET` — a third token surface. + +### Service role key usage (RLS bypass) +The Supabase service role key is created exactly once in `apps/api/src/middlewares/middleware.ts:164` and injected into every authenticated request via `supabaseMiddleware`. This means **every API handler operates with full RLS bypass**. Authorization logic therefore must live in handler code; any forgotten ownership check is a data-exposure bug. Notable areas relying on handler-side checks: +- `apps/api/src/routers/tablo.ts`, `tablo_data.ts` +- `apps/api/src/routers/admin*.ts` (six admin routers) +- `apps/api/src/routers/clientPortal.ts` +- The two `TODO: Verify ... owner_id`/`slot` comments in `invite.ts` are direct evidence of this risk. + +### Stripe webhook verification +- `apps/api/src/routers/stripe.ts:167-191` — verification is delegated to `@supabase/stripe-sync-engine`'s `processWebhook(rawBody, signature)`. The route does retrieve raw body via `c.req.text()` (correct — verification needs unmodified bytes). Note: webhook router is wired pre-auth in `routers/index.ts`, which is required by Stripe — verify any future restructuring preserves this ordering. +- Webhook secret comes from `STRIPE_WEBHOOK_SECRET` in config; rotation procedure not documented in repo. + +### Secrets / env vars +- Production/staging: loaded from Google Secret Manager via `apps/api/src/secrets.ts` and assembled in `apps/api/src/config.ts`. +- Dev: `.env` via `dotenv`. `.env*` is now gitignored (see `SECURITY_NOTICE.md`). +- `apps/api/src/config.ts:32` requires a long list of secrets — `validateEnvVar` throws on missing; good fail-fast, but means a single missing env aborts boot with no partial-feature degradation. +- Frontend env: `apps/main` reads `VITE_*` env at build time per environment (`build:staging`, `build:prod`). Anything `VITE_*` is bundled into the public JS — only public keys belong here. + +## Performance considerations + +- **React Query defaults**: `packages/shared/src/lib/api.ts:18` sets a global `staleTime` of 5 minutes. Aggressive caching is appropriate for dashboard data but can hide write-after-read bugs; mutations must explicitly invalidate hierarchical keys (`["tablos", id]`, etc.). +- **Custom stale times**: `apps/main/src/hooks/stripe.ts:114` (5 min) and `:221` (10 min) — billing data caching for 10 minutes risks displaying a stale subscription state after a webhook arrives. UI should also listen to mutation success or refresh on Stripe-portal-return paths. +- **Pagination**: ~63 hits for `.select("*")` / `.range(` / `.limit(` across `apps/main/src` + `apps/api/src` (excluding tests). Many list endpoints (`apps/api/src/routers/tablo.ts`, `admin*.ts`) appear to return full tables; no shared cursor/offset helper exists. AG-Grid in main app loads client-side which exacerbates this for orgs with large datasets. +- **Source-only packages**: `@xtablo/shared`, `@xtablo/ui`, `@xtablo/chat-ui`, `@xtablo/auth-ui`, `@xtablo/tablo-views`, `@xtablo/shared-types` export TS directly. Pros: instant HMR. Cons: every app re-typechecks and re-bundles them; tree-shaking depends on each app's bundler being able to drop unused exports (Vite generally handles this, but barrel files in `packages/shared/src/index.ts`-style modules can defeat it — worth auditing if bundle size matters). +- **Bundle size**: `apps/main` has `rollup-plugin-visualizer` available for analysis but no tracked size budgets. Heavy deps: `@blocknote/*` (rich text editor), `ag-grid-community` + `ag-grid-react`, `jspdf`, `@datadog/browser-rum*` — all in `dependencies` of `apps/main/package.json`. +- **Datadog dd-trace** (`apps/api`) is initialized in `apps/api/src/index.ts:13` before everything else; misconfigured tracing has measurable cold-start cost on Cloud Run. + +## Fragile areas + +### Stripe sync engine (Supabase ↔ Stripe) +- `apps/api/src/middlewares/stripeSync.ts` instantiates `@supabase/stripe-sync-engine@^0.45.0` with a direct Postgres connection string (`SUPABASE_CONNECTION_STRING`) and base64-encoded CA cert (`SUPABASE_CA_CERT`). Bypasses Supabase API entirely. +- `revalidateObjectsViaStripeApi: ["subscription", "customer"]` — explicit workaround for stale-data bugs in the sync engine. +- Schema is `"stripe"` (separate from `public`); migrations must be applied carefully; eleven separate Stripe docs in `docs/` evidence repeated breakage. +- Sync is *eventually consistent*: UI hooks with 5–10 min `staleTime` (above) can show pre-webhook state. + +### Auth flow (passwordless + temporary accounts) +- Passwordless flow creates accounts flagged `is_temporary: true`. Lifecycle (cleanup of abandoned temp accounts, upgrade path to permanent) is not documented in `docs/`. +- Three independent token systems (user JWT, client-portal JWT, admin token) live side by side — see Security section. Test reference: `apps/api/src/__tests__/README.md:28` documents a `test_temp@example.com` fixture. +- `SessionContext` (main app) listens to `supabase.auth.onAuthStateChange()`. Deprecated `useUser` hook at `apps/main/src/hooks/user.ts:16` still exists and likely has stragglers. + +### Middleware singleton initialization order +- `MiddlewareManager` in `apps/api/src/middlewares/middleware.ts` is a singleton with throw-on-misuse semantics. The November 2025 fix (see `docs/MIDDLEWARE_INITIALIZATION_FIX.md`) is a pattern convention, not a structural guarantee. Any new router that calls `MiddlewareManager.getInstance()` at module top-level reintroduces the bug. +- The `index.ts` router order (`apps/api/src/routers/index.ts`) is load-bearing: public routes → stripe webhook (no auth, raw body) → auth-applied routes. Refactors that reorder these break either auth or signature verification. + +## Build / deploy concerns + +- **Cloudflare Workers** (`apps/main`, possibly `apps/clients`, `apps/external`, `apps/admin`): `apps/main/wrangler.toml` sets `compatibility_date = "2025-07-09"` and no `compatibility_flags`. Notably **no `nodejs_compat`** — any dep pulling Node built-ins at runtime will fail at the edge. Watch for incidental Node imports in shared packages. +- **Type generation**: per `CLAUDE.md`, run `npx supabase gen types typescript > packages/shared-types/src/database.types.ts` after schema changes. There's no CI guard that types are up to date; drift between DB and types is silent. +- **Cache invalidation gotchas** (from `CLAUDE.md` "Important Notes"): stale builds resolved only by `pnpm clean && rm -rf node_modules/.cache && pnpm install && pnpm build`. Indicates Turborepo cache occasionally misses dependency changes — possibly because source-only packages don't declare outputs. +- **API deploy** uses Cloud Run with Cloud Build (`docs/CLOUD_BUILD_*.md`, `docs/DOCKER_*.md`). Multi-stage pnpm Docker build is documented and has needed multiple optimization passes (`DOCKER_BUILD_PERFORMANCE.md`, `DOCKER_PNPM_OPTIMIZATION.md`, `DOCKER_FIX_SUMMARY.md`). +- **API uses `tsc` only** (`apps/api/package.json` `build: tsc`) — no bundling, ships `dist/` + `node_modules`. Large image surface; dependency vulnerabilities are deploy-time concerns. +- **Pre-commit**: `.pre-commit-config.yaml` exists at root; behavior not audited here. + +## Dependencies of concern + +Pinned/notable in `apps/main/package.json`: +- `@typescript/native-preview: 7.0.0-dev.20251010.1` — a *preview/nightly* TypeScript native compiler pinned to a dated dev build. High churn risk; may break unexpectedly. +- `@types/react: 19.0.10`, `@types/react-dom: 19.0.4` — exact-pinned (no caret). `react: 19.0.0` itself also exact-pinned. Upgrades will require coordinated change. +- `vitest: ^3.2.4` (in `apps/main`) vs `vitest: ^4.0.8` (in `apps/api`) — different major versions across the monorepo; shared test utilities will be incompatible. +- `@types/react-router-dom: ^5.3.3` listed alongside `react-router-dom: ^7.9.4` — the v5 type stubs are wrong for v7; either unused or actively misleading. +- `eslint: ^9.22.0` + `@typescript-eslint/*: ^7.0.2` — typescript-eslint v7 predates flat-config-stable ESLint 9; potential plugin compat issues. Note also Biome is the primary linter (`biome.json`), so ESLint may be vestigial. +- `jest` + `jest-environment-jsdom` + `@types/jest` in `apps/main` devDeps despite using Vitest — dead dependencies inflating install. +- `pnpm.overrides`: `form-data: ^4.0.4`, `linkifyjs: ^4.3.2` — root-level overrides usually indicate working around a transitive vulnerability or bug; reason isn't documented in repo. + +In `apps/api/package.json`: +- `stripe: ^20.0.0` — Stripe SDK is currently on v17+ as of cutoff; v20 is a future major. Verify lockfile matches expected runtime version. +- `ts-node: ^10.9.2` listed in `dependencies` (not devDeps) — likely unused at runtime; should be a devDep. +- `multer: ^2.0.2` — major version 2.x is recent; ensure middleware patterns aren't using legacy 1.x APIs. + +`pnpm-lock.yaml` is ~763 KB indicating substantial dependency graph; `pnpm audit` not run as part of this scan. + +## Documentation gaps + +- **No `TROUBLESHOOTING.md`** despite 35+ retrospective fix docs — there's no central index. +- **`docs/`** is fix-log heavy and architecture-light. `STRIPE_ARCHITECTURE.md` and `MIDDLEWARE_TESTS.md` exist but most flows lack an "as-built" diagram. +- **Three token systems** (user JWT, client session JWT, admin token) — no unified auth doc; you must read `apps/api/src/helpers/{auth,clientSessions,adminTokens}.ts` separately. +- **Temporary accounts** lifecycle (creation, retention, cleanup, upgrade to permanent) — undocumented. +- **Six admin routers** (`adminActions`, `adminAuth`, `adminDatasets`, `adminOverview`, `adminTables`, `admin.ts`) — admin surface is large and the only docs are `ADMIN_APP_ACCESS_SETUP.md` for access setup, not authorization model. +- **Frontend bundle budgets** — none defined; `rollup-plugin-visualizer` available but not enforced. +- **Type-generation workflow** — only mentioned in `CLAUDE.md`; no CI check. +- **`apps/chat-worker`, `apps/clients`, `apps/admin`, `apps/external`** — minimal architectural docs; main app dominates `CLAUDE.md`. +- **Legacy directories** at repo root (`backend/` Python, `go-backend/` Go, `frontend_v2/`, `xtablo-expo/`) are present but unmentioned in `CLAUDE.md`. Status (active? abandoned?) is unclear — this itself is a debt signal. +- **`SECURITY_NOTICE.md`** lists credentials to rotate after the `.env`-in-git incident, but completion status of that rotation checklist is not tracked. + +## Tracked-but-unaddressed observations + +- 43 `console.error`/`console.warn` calls in `apps/api/src/routers/` — direct logging instead of going through `pino` (which is a devDep but not wired to the routers). +- 21 explicit `: any` annotations in `apps/api/src` (excluding tests). Two visible examples: `apps/api/src/routers/clientInvites.ts:192` (`(candidate: any) =>`) and `apps/api/src/helpers/helpers.ts:374` (`(u: any) =>`). +- Only 2 `@ts-ignore`/`@ts-expect-error` comments across `apps/` + `packages/` — TypeScript discipline appears solid where types exist. diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..7c06e8c --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,161 @@ +# Code Conventions + +**Last updated:** 2026-05-14 +**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source` +**Apps:** `apps/main`, `apps/external`, `apps/api`, `apps/admin`, `apps/chat-worker`, `apps/clients` +**Packages:** `packages/shared`, `packages/ui`, `packages/shared-types`, `packages/auth-ui`, `packages/chat-ui`, `packages/tablo-views` + +## Linter & Formatter — Biome + +The repo uses a single root [Biome](https://biomejs.dev) config: `biome.json` (Biome 2.2.5, pinned in root `package.json` devDependencies). + +Formatting rules (from `biome.json`): +- `indentStyle: "space"`, `indentWidth: 2` +- `lineEnding: "lf"`, `lineWidth: 100` +- JS: `quoteStyle: "double"`, `jsxQuoteStyle: "double"`, `semicolons: "always"`, `trailingCommas: "es5"`, `arrowParentheses: "always"`, `bracketSpacing: true`, `bracketSameLine: false` +- JSON formatter enabled with same indent settings; parser allows comments but not trailing commas + +Key lint rules turned on (Biome `recommended: false` — rules are explicit): +- `suspicious.noExplicitAny: "error"` (overridden to `off` for some files via overrides; warn in `xtablo-expo`) +- `correctness.noUnusedVariables`, `noUnusedImports`, `noUndeclaredVariables` — all `error` +- `style.useConst`, `useTemplate`, `noNamespace`, `noCommonJs` — all `error` +- `complexity.noBannedTypes`, `suspicious.noDebugger`, `suspicious.noEmptyBlockStatements` — all `error` + +Per-package scripts wrap Biome: +- `pnpm lint` → `turbo lint` → each package runs `biome check .` +- `pnpm lint:fix` → `biome check --write .` +- `pnpm format` → `biome format --write .` + +## TypeScript Conventions + +Every package/app ships its own `tsconfig.json`; there is no root TS config. + +Strictness — every config sets `strict: true`. Additional flags consistent across configs: +- `noUnusedLocals: true`, `noUnusedParameters: true`, `noFallthroughCasesInSwitch: true` +- `noUncheckedIndexedAccess: true` for `packages/shared-types`, `packages/shared` +- `isolatedModules: true`, `moduleDetection: "force"`, `skipLibCheck: true` +- API uses `verbatimModuleSyntax: true` (`apps/api/tsconfig.json`) — type-only imports must be explicit + +Module systems: +- Frontend apps use `module: ESNext` + `moduleResolution: "bundler"` (e.g. `apps/main/tsconfig.app.json`) +- API uses `module: NodeNext` (TypeScript compiled output via `tsc`) +- All packages declare `"type": "module"` in package.json + +Path aliases (defined per app, resolved by Vite via `vite-tsconfig-paths`): +- `apps/main`: `@ui/*` → `./src/*`, `@xtablo/auth-ui` → `../../packages/auth-ui/src`, `@xtablo/ui/*` → `../../packages/ui/src/*` (see `apps/main/tsconfig.app.json`) +- `apps/main/tsconfig.json`: also `@external/*` → `src/external/*` + +Package import discipline (per `CLAUDE.md`): +- No circular dependencies between packages +- `apps/api` may only import from `@xtablo/shared-types` +- Frontend apps may import from all shared packages +- `@xtablo/shared` and `@xtablo/ui` are **source-only** — TypeScript is consumed directly via `vite-tsconfig-paths`; no build step + +Types-first workflow: +- Database types are auto-generated into `packages/shared-types/src/database.types.ts` via `npx supabase gen types typescript` +- Domain types in `@xtablo/shared-types` are derived from `database.types.ts` (nulls removed, refined shapes) +- API response types live in the same package so frontends and the API agree + +## React Component Conventions + +- Functional components only (no class components observed) +- TSX files use named exports for components, e.g. `export function CustomModal(...)` (see `apps/main/src/components/CustomModal.tsx`) +- Co-located unit tests: `Foo.tsx` + `Foo.test.tsx` +- Naming suffix conventions (per `CLAUDE.md`): + - Dialogs / modals → `*Modal.tsx` (e.g. `CustomModal.tsx`) + - Page sections → `*Section.tsx` (e.g. `TabloEventsSection.tsx`) + - Card surfaces → `*Card.tsx` (e.g. `EventTypeCard.tsx`, `AvailabilityCard.tsx`, `EventTypeCard.test.tsx`) +- Shared primitives (Radix + Tailwind) live in `packages/ui/src` +- Cross-app business hooks/contexts live in `packages/shared/src` + +## Hook Patterns + +React Query (TanStack Query v5) is the primary server-state tool. Standard return shapes: + +```ts +// Queries +const { data, isLoading, error } = useMyQuery(); + +// Mutations +const { mutate, isPending } = useMyMutation(); +``` + +- Default cache time: 5 minutes (configured in `packages/shared`'s QueryClient) +- Mutations invalidate targeted keys explicitly rather than blowing away the whole cache + +Zustand handles global client state (notably the authenticated user store): +- `useUser()` — throws if no session (use inside protected routes) +- `useMaybeUser()` — returns null if unauthenticated (use in route guards / public surfaces) + +## Query Key Conventions + +Hierarchical keys, with the resource name first and identifiers cascading deeper: + +```ts +["tablos"] // list +["tablos", tabloId] // single +["tablo-files", tabloId] // related collection +``` + +Invalidations should match the deepest key that needs to be refreshed. + +## Error Handling + +- User-facing: `toast.add({ ... })` (toast system in `packages/shared` / `packages/ui`) — messages should be friendly and actionable +- Technical: `console.error` for developer-only context (stack traces, raw API errors) +- API errors are caught at the React Query hook layer and surfaced via `error` from `useQuery`/`useMutation`; UI components branch on `isError` +- Server side (`apps/api`): Hono routers return `c.json({ error: ... }, statusCode)`; middleware handles auth failures with 401s (see `docs/MIDDLEWARE_TESTS.md`) + +## Loading States — three levels + +1. **Route level** — `ProtectedRoute` shows a full-page spinner while the session resolves +2. **Feature level** — React Query `isLoading` / `isPending` drives section-level skeletons or spinners +3. **Action level** — Buttons set `disabled` during the related mutation's `isPending` + +Empty / error states are explicit branches (no silent fallbacks). + +## Import / Export Patterns + +- ESM throughout (`"type": "module"` in every package.json; Biome enforces `noCommonJs`) +- Source-only packages (`@xtablo/shared`, `@xtablo/ui`) export from `src/index.ts` and are consumed via TS path aliases — no `dist/` involved +- Compiled packages (`@xtablo/shared-types` and `apps/api`) emit `dist/` via `tsc`; `shared-types` includes `declaration: true` and `declarationMap: true` +- Type-only imports preferred where supported (`verbatimModuleSyntax` is on for the API) +- Biome's `noUnusedImports` rule will flag dead imports at lint time + +## File Organization + +``` +apps//src/ + components/ # UI components, *.tsx + *.test.tsx + contexts/ # React contexts (e.g. UpgradeBlockContext.tsx) + providers/ # Store / provider wrappers (e.g. UserStoreProvider.tsx) + hooks/ # App-specific hooks + pages/ or routes/ # Route entry points + lib/ # Utilities, route table, api client setup + utils/testHelpers # Render-with-providers wrappers for tests +packages//src/ + index.ts # Single barrel export + ... # Domain folders mirror app layout +``` + +API layout (`apps/api/src`): +- `routers/` — Hono routers grouped by concern (`public.ts`, `authRouter.ts`, `tablo.ts`, `stripe.ts`, etc.) +- `middlewares/` — Auth, Supabase, R2, Stream, email injection +- `helpers/` — Pure logic (testable in isolation, e.g. `helpers/orgIcons.ts` + `orgIcons.test.ts`) +- `__tests__/` — Test-only fixtures, setup, globalSetup, route + middleware suites + +## Type Safety + +- `noExplicitAny` is on as `error` in the root config (relaxed to `warn` in `xtablo-expo` and `off` for select API/legacy file overrides) +- Prefer derived types from `@xtablo/shared-types` over inline shape literals +- `Database` table types are generated; domain types should narrow them rather than redeclare from scratch +- `noUncheckedIndexedAccess: true` in shared packages — index access returns `T | undefined`; handle the undefined branch explicitly +- API enforces `verbatimModuleSyntax`, so `import type { ... }` is required for type-only imports — relevant for compiled API code + +## Reference files + +- `biome.json` — single source for lint + format +- `apps/main/tsconfig.app.json` — canonical frontend TS config +- `apps/api/tsconfig.json` — canonical backend TS config +- `packages/shared-types/tsconfig.json` — type-only package config +- `CLAUDE.md` — high-level conventions (this doc expands it with verified specifics) diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..211e4a6 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,140 @@ +# INTEGRATIONS + +External systems wired into the xtablo monorepo. Last updated 2026-05-14. + +## Database — Supabase Postgres + +- **Hosted Postgres** managed by Supabase. The frontend talks to it via the JS SDK; the API talks to it via the service-role key and (for `stripe-sync-engine`) a direct `postgres://` connection string. +- **Service-role client** is constructed per-request inside the API in `apps/api/src/middlewares/middleware.ts` (`supabaseMiddleware`): + ```ts + createClient(config.SUPABASE_URL, config.SUPABASE_SERVICE_ROLE_KEY) + ``` + Mounted globally for every route in `apps/api/src/routers/index.ts`. +- **Browser client** factory at `packages/shared/src/lib/supabase.ts` re-exports `createClient`; instances are produced inside `@xtablo/shared` consumers and used directly (`supabase.from("table")...`). +- **RLS** — bypassed by the API using the service-role key when needed (see `CLAUDE.md` notes); browser clients use the anon key plus the user's JWT and rely on RLS policies. +- **Direct Postgres** — `SUPABASE_CONNECTION_STRING` (with base64-encoded `SUPABASE_CA_CERT`) is used by the Stripe sync engine `poolConfig` in `apps/api/src/middlewares/stripeSync.ts`. +- **Schema, migrations, snippets, tests** — `supabase/config.toml`, `supabase/migrations/`, `supabase/snippets/`, `supabase/tests/`. +- **Type codegen** — regenerate `packages/shared-types/src/database.types.ts` with: + ```bash + npx supabase gen types typescript > packages/shared-types/src/database.types.ts + ``` + These types are then refined into domain/API types inside `@xtablo/shared-types` (`tablos.types.ts`, `tablo-data.types.ts`, `events.types.ts`, `stripe.types.ts`, `kanban.types.ts`). + +## Authentication + +### Supabase Auth (primary users) +- JWT-based, with sessions managed by `SessionContext` (`packages/shared/src/contexts/SessionContext.tsx`) listening to `supabase.auth.onAuthStateChange()`. +- API extracts and validates the bearer token via `authenticateFromHeader` in `apps/api/src/helpers/auth.ts`, wired in `authMiddleware` (`apps/api/src/middlewares/middleware.ts`). +- `maybeAuthenticatedMiddleware` (same file) sets `c.var.user = null` when the header is missing — used by booking and other "maybe authed" endpoints in `apps/api/src/routers/maybeAuthRouter.ts`. +- Passwordless temporary accounts (`is_temporary: true`) created via the public booking flow. +- Front-end auth UI lives in `@xtablo/auth-ui`. + +### Admin auth (internal) +- Custom JWTs signed with `ADMIN_TOKEN_SIGNING_SECRET` (Google Secret Manager). +- Helpers in `apps/api/src/helpers/adminTokens.ts`, verified by `adminAuthMiddleware` (Bearer scheme) in `apps/api/src/middlewares/middleware.ts`. +- Routes: `apps/api/src/routers/admin.ts`, `adminActions.ts`, `adminAuth.ts`, `adminDatasets.ts`, `adminOverview.ts`, `adminTables.ts`. + +### Client portal auth (read-only client users) +- Magic-link → cookie-based session JWT, stored in cookie `xtablo_client_session` on domain `clients.xtablo.com`. +- Issuance: `apps/api/src/helpers/clientMagicLinks.ts`, `apps/api/src/helpers/clientSessions.ts`. +- Middlewares `clientAuthMiddleware` / `maybeClientAuthMiddleware` (`apps/api/src/middlewares/middleware.ts`) verify cookies using `CLIENT_AUTH_JWT_SECRET`. +- Routers: `apps/api/src/routers/clientAuth.ts`, `clientInvites.ts`, `clientPortal.ts`. + +### Task auth (cron / job runners) +- HTTP Basic with `TASKS_SECRET`, enforced by `basicAuthMiddleware` for `apps/api/src/routers/tasks.ts`. + +### Chat worker auth +- Independent JWT verification using `jose` against `JWT_SECRET` Wrangler secret. WebSocket connections receive the token via `?token=` query string; REST via Authorization header (`apps/chat-worker/src/index.ts`, `apps/chat-worker/src/lib/auth.ts`). + +## Payments — Stripe + +- **Server SDK** — `stripe ^20.0.0` instantiated per-request inside `stripeMiddleware` with `apiVersion: "2025-11-17.clover"` (`apps/api/src/middlewares/middleware.ts`). +- **Browser SDK** — `@stripe/stripe-js` in `apps/main`. +- **Webhook router** — `apps/api/src/routers/stripe.ts`, mounted at `/api/v1/stripe-webhook` in `apps/api/src/routers/index.ts`. Plan tiers (`solo`, `team`, `founder`) keyed off `STRIPE_*_PRICE_ID` env vars. +- **Sync engine** — `@supabase/stripe-sync-engine` writes Stripe objects directly into the `stripe` schema of Supabase via a direct Postgres pool (`apps/api/src/middlewares/stripeSync.ts`). Refetches `subscription` and `customer` objects to avoid stale state. +- **Secrets** — `stripe-secret-key`, `stripe-webhook-secret` (prod) and `…-staging` variants pulled from Google Secret Manager (`apps/api/src/secrets.ts`). +- **Billing helpers** — `apps/api/src/helpers/billing.ts`. Active-plan enforcement via `activePlanAccessMiddleware`; only the org's billing owner can manage billing. +- **Docs** — `docs/STRIPE_ARCHITECTURE.md`, `docs/STRIPE_README.md`, `docs/STRIPE_WITH_SYNC_ENGINE.md`, `docs/STRIPE_INTEGRATION_COMPLETE.md` plus several setup/testing guides. + +## Mobile Payments — RevenueCat + +- Webhook router `apps/api/src/routers/revenuecat.ts`, mounted at `/api/v1/revenuecat-webhook`. +- Maps Apple in-app product IDs to plans via `apps/api/src/helpers/appleBilling.ts`. +- Env: `REVENUECAT_WEBHOOK_AUTH_HEADER`, `REVENUECAT_SOLO_PRODUCT_ID`, `REVENUECAT_ANNUAL_PRODUCT_ID`. + +## Storage — Cloudflare R2 (S3 SDK) + +- `@aws-sdk/client-s3` pointed at `https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com` with `region: "auto"`. +- Instantiated per-request in `r2Middleware` (`apps/api/src/middlewares/middleware.ts`) and exposed as `c.get("s3_client")`. +- Credentials: `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` from Secret Manager. +- Upload pipelines use `multer` + `sharp` for image processing (declared in `apps/api/package.json`). + +## Email — Gmail OAuth2 (Nodemailer) + +- SMTP through `smtp.gmail.com:465` using Google OAuth2 tokens. +- Built in `apps/api/src/middlewares/transporter.ts` via `nodemailer.createTransport` with `EMAIL_USER`, `EMAIL_CLIENT_ID`, `EMAIL_CLIENT_SECRET`, `EMAIL_REFRESH_TOKEN`. +- Exposed as `c.get("transporter")` everywhere `transporterMiddleware` is applied (mounted globally in `apps/api/src/routers/index.ts`). + +## Chat — Custom Cloudflare Durable Objects + +The CLAUDE.md mentions Stream Chat historically, but the current implementation is a **custom WebSocket chat** running on Cloudflare Workers + Durable Objects (no `stream-chat` dependency found anywhere in the repo). + +- Worker entry: `apps/chat-worker/src/index.ts` (Hono). +- Durable Object class: `apps/chat-worker/src/durable-objects/ChatRoom.ts`. Declared in `apps/chat-worker/wrangler.toml`: + ```toml + [durable_objects] + bindings = [{ name = "CHAT_ROOM", class_name = "ChatRoom" }] + [[migrations]] + tag = "v1" + new_sqlite_classes = ["ChatRoom"] + ``` +- Membership enforced by querying Supabase via PostgREST (`apps/chat-worker/src/lib/supabase.ts`). +- UI consumed via `@xtablo/chat-ui` (used by `apps/main`, `apps/clients`, `@xtablo/tablo-views`). +- Custom domain: `chat.xtablo.com`. + +## Observability + +### Frontend — Datadog RUM +- `apps/main/src/lib/rum.ts` initialises `@datadog/browser-rum` with `applicationId: "8e268e1a-1be0-44c6-b12a-978530d497c7"`, `service: "xtablo-ui"`, `sessionSampleRate: 100`, `sessionReplaySampleRate: 80`, `defaultPrivacyLevel: "mask-user-input"`, and the React plugin with router instrumentation. +- `apps/clients/src/lib/rum.ts` does the same for the clients app. +- User is set in `apps/main/src/providers/UserStoreProvider.tsx` (`datadogRum.setUser` on login, `clearUser` on logout). +- Manual view names via `apps/main/src/hooks/useDatadogRumViewName.tsx`. + +### Backend — dd-trace +- `tracer.init({ logInjection: true })` is the very first call in `apps/api/src/index.ts`. +- Datadog CI tooling (`@datadog/datadog-ci`, `@datadog/datadog-ci-plugin-cloud-run`) is wired for Cloud Run instrumentation uploads. +- Static analysis is configured at the repo root in `static-analysis.datadog.yml`. + +## Secrets Management — Google Secret Manager + +- `apps/api/src/secrets.ts` fetches every sensitive value from `projects/xtablo/secrets/{name}/versions/latest` using `@google-cloud/secret-manager`. +- Secrets loaded: `supabase-service-role-key`, `supabase-connection-string`, `supabase-ca-cert`, `admin-token-signing-secret`, `client-auth-jwt-secret`, `email-client-secret`, `email-refresh-token`, `r2-access-key-id`, `r2-secret-access-key`, `stripe-secret-key`, `stripe-webhook-secret`, plus their `…-staging` variants. +- Setup guide: `docs/GOOGLE_SECRET_MANAGER_SETUP.md`. + +## Deployment Platforms + +### Cloudflare Workers (web apps + chat) +- All Vite-built frontends are deployed to Workers via Wrangler (see `apps/*/wrangler.toml`). +- `apps/main/wrangler.toml` binds `app.xtablo.com` (prod) and `app-staging.xtablo.com` (staging), uses `not_found_handling = "single-page-application"`. +- `apps/external/wrangler.toml`, `apps/admin/wrangler.toml`, `apps/clients/wrangler.toml` each have their own custom domain. +- `apps/chat-worker/wrangler.toml` declares the `ChatRoom` Durable Object SQLite class and binds `chat.xtablo.com`. +- Worker scripts include observability blocks (`[observability] enabled = true`). + +### Google Cloud Run (API) +- Containerised by `apps/api/Dockerfile` (multi-stage Node 20 Alpine, pnpm-driven). +- CI/CD via Google Cloud Build using `apps/api/cloudbuild.yaml`. +- Runtime configuration uses Cloud Run env vars + Secret Manager (no env files inside the image). +- Deployment guides: `docs/CLOUD_BUILD_SETUP.md`, `docs/CLOUD_BUILD_ENV_CONFIG.md`, `docs/DOCKER_BUILD.md`, `docs/DOCKER_PNPM_OPTIMIZATION.md`, `docs/DOCKER_BUILD_PERFORMANCE.md`. + +### Go backend (separate) +- `go-backend/` deploys via its own `compose.yaml` / `justfile` and is independent of the TypeScript build pipeline (uses `chi`, `templ`, `sqlc`, `pgx`). + +### Infra utilities +- `infra/docker-compose.yaml`, `infra/docker-compose.traefik.yaml`, `infra/Dockerfile` — generic infra/traefik setup. `infra/app/main.py` is a small Python helper. + +## Other Integrations / Tooling + +- **Google APIs** (`googleapis ^161.0.0`) — currently used to mint OAuth2 access tokens for the Gmail SMTP transport (`apps/api/src/middlewares/transporter.ts`). +- **PWA** — `vite-plugin-pwa` in `apps/main` generates the service worker; `workbox-window` registers it. +- **Sourcemaps to Datadog** — `@datadog/datadog-ci` is available at the workspace root for `datadog-ci sourcemaps upload`. +- **Chromatic** — `chromatic ^11.5.0` listed in `apps/main` dev deps for visual regression (no CI workflow inspected here). diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..61b523d --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,142 @@ +# STACK + +Technology stack reference for the xtablo monorepo. Last updated 2026-05-14. + +## Languages & Runtimes + +- **TypeScript** — pinned at `^5.7.0` across most workspaces (api uses `^5.8.3`). Root devDependency in `package.json`. +- **Node.js** — `>=20.0.0` enforced via the `engines` field in the root `package.json`. The API Dockerfile builds on `node:20-alpine` (`apps/api/Dockerfile`). +- **Package manager** — pnpm `10.19.0` (declared as `packageManager` in the root `package.json`). Cloudflare Workers builds and the API container use `corepack` to install pnpm. +- **Go** — a parallel `go-backend/` service exists alongside the TS monorepo (`go-backend/go.mod`, `go 1.26.0`) using `chi`, `templ`, `pgx/v5`, and `sqlc`. It is included in the pnpm workspace but is otherwise its own toolchain. +- **Python** — a small infra utility at `infra/app/main.py` with `infra/requirements.txt` (not part of the runtime stack of the apps). + +## Frameworks + +### Frontend (React) +- **React 19.0.0** + **React DOM 19.0.0** — used by `apps/main`, `apps/external`, `apps/admin`, `apps/clients`, and all React packages. +- **React Router** — `react-router-dom ^7.9.4`. +- **Vite 6.2** — bundler/dev server for every web app (`apps/main/vite.config.ts`, `apps/external/vite.config.ts`, `apps/admin/vite.config.ts`, `apps/clients/vite.config.ts`). +- **TailwindCSS 4.1** — utility CSS via `@tailwindcss/vite`, with `tw-animate-css` and `tailwind-merge`. +- **Radix UI** + **React Aria** — primitives composed in `@xtablo/ui` (see `packages/ui/package.json`). +- **BlockNote** — rich-text editor (`@blocknote/core`, `@blocknote/mantine`, `@blocknote/react`) used in `apps/main`. +- **AG Grid Community** — data grid in `apps/main` (`ag-grid-community`, `ag-grid-react`). +- **React Hook Form** + **Zod** — forms and validation, wired through `@hookform/resolvers`. +- **i18next** + `react-i18next` — translations (`apps/main/src/i18n.ts`, plus `external`, `clients`, `tablo-views`). +- **react-day-picker** — calendar UI. +- **PWA** — `vite-plugin-pwa` + `workbox-window` in `apps/main`. + +### Backend (Hono) +- **Hono ^4.7.7** — HTTP framework for both `apps/api` and `apps/chat-worker`. +- **@hono/node-server** — Node adapter that drives `apps/api` (`apps/api/src/index.ts`). +- **hono-sessions** — session helpers in the API. +- **Cloudflare Workers + Durable Objects** — `apps/chat-worker` uses Hono with a `ChatRoom` SQLite-backed DO class (`apps/chat-worker/wrangler.toml`, `apps/chat-worker/src/durable-objects/ChatRoom.ts`). + +## Core Dependencies + +### Server State / Data Fetching +- `@tanstack/react-query ^5.69.0` — primary server-state cache. Hierarchical query keys; 5-minute default cache. Used in `apps/main`, `apps/clients`, `apps/admin`, `apps/external`, and `@xtablo/tablo-views`. +- `axios ^1.12.2` — HTTP client wrapper at `packages/shared/src/lib/api.ts`. + +### Client State +- `zustand ^5.0.5` — global stores (notably user). Lives in `@xtablo/shared` and is consumed via `useUser` / `useMaybeUser`. + +### Auth & JWT +- `@supabase/supabase-js ^2.49.x` — front-end and API client. +- `jwt-decode ^4.0.0` — decode access tokens on the client. +- `jose ^6.0.0` — JWT verification in the chat worker (`apps/chat-worker/src/lib/auth.ts`). + +### UI Primitives +- Radix: `react-avatar`, `react-checkbox`, `react-collapsible`, `react-dialog`, `react-dropdown-menu`, `react-label`, `react-popover`, `react-select`, `react-separator`, `react-slider`, `react-slot`, `react-switch`, `react-tabs`, `react-tooltip`, `react-radio-group` (`packages/ui/package.json`). +- `react-aria` / `react-aria-components ^1.7.0`, `@react-stately/*`, `@react-aria/*`. +- `lucide-react ^0.460.0` — iconography. +- `class-variance-authority`, `clsx`, `tailwind-merge` — class composition. +- `sonner ^2.0.7` — toast notifications (re-exported via `packages/shared/src/lib/toast.ts`). + +### Dates, IDs, Utilities +- `date-fns ^4.1.0`, `luxon ^3.7.2` (API only), `@internationalized/date`. +- `uuid ^11.1.0`, `pluralize ^8.0.0`, `ts-pattern ^5.6.2`. +- `jspdf ^3.0.3` — PDF export (main + shared). + +### Payments / Billing +- `stripe ^20.0.0` — server SDK (`apps/api`). +- `@stripe/stripe-js ^8.2.0` — browser SDK (`apps/main`). +- `@supabase/stripe-sync-engine ^0.45.0` — Stripe ↔ Supabase sync (`apps/api/src/middlewares/stripeSync.ts`). + +### Storage / Email +- `@aws-sdk/client-s3 ^3.850.0` — used against Cloudflare R2 in `apps/api/src/middlewares/middleware.ts` (`r2Middleware`). +- `multer ^2.0.2`, `sharp ^0.34.5` — file upload handling and image processing. +- `nodemailer ^7.0.4` + `googleapis ^161.0.0` — Gmail OAuth2 SMTP (`apps/api/src/middlewares/transporter.ts`). + +### Observability +- `@datadog/browser-rum ^6.13.0` + `@datadog/browser-rum-react ^6.13.0` — initialised in `apps/main/src/lib/rum.ts` and `apps/clients/src/lib/rum.ts`. +- `dd-trace ^5.74.0` — APM tracer started at the top of `apps/api/src/index.ts`. +- `@datadog/datadog-ci`, `@datadog/datadog-ci-base`, `@datadog/datadog-ci-plugin-cloud-run` — CI source-map upload and Cloud Run integration. +- `static-analysis.datadog.yml` — repo-level Datadog static analysis config. + +## Build / Dev Tooling + +- **Turborepo `^2.5.8`** — pipeline orchestration (`turbo.json`). Tasks: `build`, `dev`, `deploy(:staging|:prod)`, `build:staging`, `build:prod`, `lint`, `lint:fix`, `typecheck`, `test`, `test:watch`, `format`, `clean`. Caches `dist/**` and `tsconfig.tsbuildinfo`. +- **Biome `2.2.5`** — formatter + linter, config at `biome.json` with explicit per-package `files.includes`. +- **Vite `^6.2.2`** with `@vitejs/plugin-react ^4.3.4`, `vite-tsconfig-paths`, `@tailwindcss/vite`, `@cloudflare/vite-plugin`, `rollup-plugin-visualizer`, `vite-plugin-pwa`. +- **Vitest** — `^3.2.4` in frontend apps, `^4.0.8` in `apps/api`. Browser env via `happy-dom` (main, admin) or `jsdom` (clients). +- **Testing Library** — `@testing-library/react`, `@testing-library/jest-dom`, `@testing-library/user-event`. +- **tsc** — every package has its own `tsconfig.json` and runs `tsc -b` or `tsc --noEmit` for typecheck. +- **Wrangler `^4.24.3`** — Cloudflare Workers CLI used by `main`, `external`, `admin`, `clients`, `chat-worker`. +- **tsx `^4.7.1`** — dev runner for the API (`pnpm dev` invokes `tsx watch src/index.ts`). + +## Configuration + +### Environment loading +- API: `dotenv.config({ path: `.env.${NODE_ENV}` })` in `apps/api/src/config.ts`. `createConfig(secrets)` synthesizes a typed `AppConfig` from env + Google Secret Manager values. +- Frontend: Vite `import.meta.env.*`; modes `dev`, `staging`, `production` selected via `vite build --mode`. + +### Secret loading +- `apps/api/src/secrets.ts` pulls all sensitive values from `projects/xtablo/secrets/*/versions/latest` using `@google-cloud/secret-manager`. +- Test mode bypasses Secret Manager and uses raw env vars. + +### Build targets per app +| App | Bundler | Output | Deploy target | +| --- | --- | --- | --- | +| `apps/main` | Vite + `@cloudflare/vite-plugin` | `dist/` + worker | Cloudflare Workers (`apps/main/wrangler.toml`, routes `app.xtablo.com`, `app-staging.xtablo.com`) | +| `apps/external` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/external/wrangler.toml`) | +| `apps/admin` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/admin/wrangler.toml`) | +| `apps/clients` | Vite + Cloudflare plugin | `dist/` | Cloudflare Workers (`apps/clients/wrangler.toml`) | +| `apps/chat-worker` | Wrangler-native | Worker bundle | Cloudflare Workers + Durable Objects, `chat.xtablo.com` | +| `apps/api` | `tsc` → `dist/` | Node 20 container | Google Cloud Run (`apps/api/Dockerfile`, `apps/api/cloudbuild.yaml`) | +| `go-backend` | `go build` | Binary | Separate (see `go-backend/justfile`) | + +### Static configs +- `biome.json` — single source of truth for formatting/linting scope. +- `turbo.json` — task graph; `globalDependencies` include `**/.env.*local`. +- `tsconfig.json` per workspace; project references across packages. + +## Workspace Structure + +`pnpm-workspace.yaml` defines: +```yaml +packages: + - 'apps/*' + - 'go-backend' + - 'packages/*' +``` + +### Apps (`apps/`) +- `@xtablo/main` — authenticated dashboard (port 5173). +- `@xtablo/external` — embeddable public booking widget (port 5174). +- `@xtablo/clients` — read-only client portal (port 5175, `clients.xtablo.com`). +- `@xtablo/admin` — internal admin app (port 5176). +- `@xtablo/api` — Hono REST API (port 8080). +- `@xtablo/chat-worker` — Cloudflare Worker hosting Durable-Object chat (`chat.xtablo.com`). + +### Packages (`packages/`) +- `@xtablo/shared` — React contexts, hooks, supabase wrapper, axios client, toast helper. **Source-only** (no build step; consumers import TS directly — see `packages/shared/package.json` `"main": "./src/index.ts"`). +- `@xtablo/ui` — Radix + Tailwind + react-aria component library. **Source-only**. +- `@xtablo/shared-types` — pure TS types including Supabase-generated `database.types.ts`. **Source-only**, zero runtime deps. +- `@xtablo/auth-ui` — shared auth screens. **Source-only**. +- `@xtablo/chat-ui` — chat UI components consumed by main, clients, tablo-views. **Source-only**. +- `@xtablo/tablo-views` — tablo view components shared between main and clients. **Source-only**. + +All shared packages export `./src/*` directly, so Vite HMR / tsc project references pick up changes instantly without a build step. + +### pnpm overrides +Root `package.json` pins `form-data ^4.0.4` and `linkifyjs ^4.3.2`, plus a `packageExtensions` entry adding `zod` as a peer of `@hookform/resolvers`. diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..938553d --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,296 @@ +# Structure + +_Last updated: 2026-05-14_ + +Directory layout, conventions, and where to look for things in the `xtablo-source` monorepo. + +## Top-Level Layout + +``` +xtablo-source/ +├── apps/ # Deployable applications +│ ├── main/ # Authenticated dashboard SPA (Vite + CF Workers) +│ ├── external/ # Public booking widget (Vite) +│ ├── clients/ # Client portal (Vite + CF Workers) +│ ├── admin/ # Internal admin tools (Vite + CF Workers) +│ ├── api/ # Hono REST API (Node, GCP Cloud Run) +│ └── chat-worker/ # Cloudflare Durable Object worker for chat +├── packages/ # Shared workspace packages (source-only) +│ ├── shared/ # Contexts, hooks, API client, toast, supabase client +│ ├── ui/ # Radix + Tailwind component library +│ ├── shared-types/ # Pure TypeScript types (zero runtime deps) +│ ├── auth-ui/ # Auth screens (login/register shells) +│ ├── chat-ui/ # Chat-specific UI components +│ └── tablo-views/ # Tablo detail / sections components +├── backend/ # Legacy / auxiliary backend code +├── go-backend/ # Go services (separate stack) +├── frontend_v2/ # Frontend v2 prototype +├── xtablo-expo/ # React Native (Expo) app +├── supabase/ # Supabase project (migrations, config) +├── infra/ # Infrastructure-as-code +├── docs/ # Architecture, deployment, integration docs +├── prompts/ # Prompt templates +├── scripts/ # Repo scripts +├── CLAUDE.md # Claude Code guidance (root) +├── DEVELOPMENT.md # Dev guide +├── turbo.json # Turborepo pipeline config +├── pnpm-workspace.yaml +└── package.json +``` + +## Apps + +### `apps/main` (`@xtablo/main`) + +Primary dashboard. Internal layout: + +``` +apps/main/src/ +├── main.tsx # Vite entry +├── App.tsx # Root component, providers +├── i18n.ts # i18next setup +├── main.css # Tailwind entry +├── components/ # UI components (Modals, Sections, Cards, ...) +├── contexts/ # App-local React contexts (UpgradeBlockContext, ...) +├── hooks/ # Feature hooks (tablos, events, tasks, stripe, ...) +├── lib/ +│ ├── api.ts # API client wrapper +│ ├── supabase.ts # Supabase client re-export +│ ├── routes.tsx # Protected routes +│ ├── publicRoutes.tsx # Public/auth routes +│ ├── billing.ts # Stripe-related helpers +│ ├── env.ts # Env validation +│ └── rum.ts # Datadog RUM init +├── pages/ # Route page components +├── providers/ +│ └── UserStoreProvider.tsx # Zustand user store +├── locales/ # i18n JSON +├── utils/ +└── assets/ +``` + +### `apps/external` (`@xtablo/external`) + +Embeddable booking widget. Internal layout: + +``` +apps/external/src/ +├── main.tsx +├── routes.tsx +├── EmbeddedBookingPage.tsx +├── FloatingBookingWidget.tsx +├── CustomModal.tsx +├── UserStoreProvider.tsx +├── lib/ +└── locales/ +``` + +Mode is driven by query string: `?mode=embed&eventTypeId=...` or `?mode=floating`. + +### `apps/clients` (`@xtablo/clients`) + +Public-facing client portal. + +``` +apps/clients/src/ +├── main.tsx +├── App.tsx +├── routes.tsx +├── components/ +├── hooks/ +├── lib/ +├── pages/ +├── locales/ +└── test/ +``` + +### `apps/admin` (`@xtablo/admin`) + +Internal admin app. + +``` +apps/admin/src/ +├── main.tsx +├── App.tsx +├── routes.tsx +├── components/ +├── hooks/ +├── lib/ +├── pages/ +└── registry/ +``` + +### `apps/api` (`@xtablo/api`) + +Hono REST API. Internal layout: + +``` +apps/api/src/ +├── index.ts # Entry: tracer, secrets, server start +├── config.ts # createConfig(secrets) -> AppConfig +├── secrets.ts # loadSecrets() (env or GCP Secret Manager) +├── client.ts # Supabase admin client factory +├── middlewares/ +│ ├── middleware.ts # MiddlewareManager singleton + supabase/r2/stripe +│ ├── stripeSync.ts # Stripe<->Supabase sync engine +│ └── transporter.ts # Email transport middleware +├── routers/ +│ ├── index.ts # getMainRouter — composition + ordering +│ ├── public.ts # Unauthenticated endpoints +│ ├── authRouter.ts # Requires JWT +│ ├── maybeAuthRouter.ts # Optional auth (booking-aware) +│ ├── tablo.ts # Tablo CRUD +│ ├── tablo_data.ts # Tablo content blocks +│ ├── tasks.ts # Tasks +│ ├── notes.ts # Notes +│ ├── events.ts (in user.ts/tablo_data.ts) +│ ├── stripe.ts # Stripe webhook + ops +│ ├── revenuecat.ts # RevenueCat webhook +│ ├── invite.ts # Tablo invites +│ ├── user.ts # User profile / settings +│ ├── admin*.ts # admin.ts, adminActions.ts, adminAuth.ts, +│ │ # adminDatasets.ts, adminOverview.ts, adminTables.ts +│ ├── clientAuth.ts # Client portal magic-link auth +│ ├── clientPortal.ts # Client portal endpoints +│ └── clientInvites.ts # Public client invites +├── helpers/ +├── types/ # BaseEnv, app.types.ts +└── __tests__/ +``` + +### `apps/chat-worker` + +``` +apps/chat-worker/src/ +├── index.ts # Worker entry +├── durable-objects/ # Durable Object classes +└── lib/ +``` + +## Packages + +### `packages/shared` (`@xtablo/shared`) + +Public surface via `packages/shared/src/index.ts`. + +``` +packages/shared/src/ +├── index.ts # Barrel +├── contexts/ +│ ├── SessionContext.tsx # Supabase session listener +│ └── ThemeContext.tsx +├── hooks/ +│ ├── auth.ts # useSignIn / useSignOut / useSession ... +│ ├── book.ts # Booking hooks +│ ├── public.ts # Public data hooks +│ └── useClickOutside.ts +├── lib/ +│ ├── api.ts # HTTP client w/ Bearer token +│ ├── supabase.ts # Supabase JS client +│ ├── toast.ts # toast.add() wrapper (sonner) +│ └── cn.ts # clsx + tailwind-merge +├── types/ # Re-exported domain types +└── utils/ + └── helpers.ts +``` + +### `packages/ui` (`@xtablo/ui`) + +Radix UI + Tailwind component library. Source-only. + +``` +packages/ui/src/ +├── components/ # button.tsx, dialog.tsx, select.tsx, +│ # popover.tsx, tabs.tsx, dropdown-menu.tsx, ... +├── hooks/ +└── styles/ +``` + +### `packages/shared-types` (`@xtablo/shared-types`) + +Zero-dependency TypeScript types — safe to import from API and frontend. + +``` +packages/shared-types/src/ +├── index.ts +├── database.types.ts # Auto-generated by supabase gen types +├── tablos.types.ts +├── tablo-data.types.ts +├── events.types.ts +├── kanban.types.ts +├── stripe.types.ts +├── admin.types.ts +└── utils.ts +``` + +### `packages/auth-ui` (`@xtablo/auth-ui`) + +Auth screens shared between apps: `AuthCardShell.tsx`, `AuthEmailPasswordForm.tsx`, `AuthInfoBanner.tsx`. + +### `packages/chat-ui` (`@xtablo/chat-ui`) + +Chat-specific UI (`components/`, `hooks.ts`, `security.ts`, `types.ts`, `chat-ui.css`). + +### `packages/tablo-views` (`@xtablo/tablo-views`) + +Tablo detail sections and shell: `TabloDetailsShell.tsx`, `TabloEventsSection.tsx`, `TabloFilesSection.tsx`, `TabloTasksSection.tsx`, `TabloDiscussionSection.tsx`, `TabloHeaderActions.tsx`, `EtapesSection.tsx`, `RoadmapSection.tsx`, plus `single-tablo/`, `components/`, `hooks/`, `styles/`. + +## Naming Conventions + +- **Modals**: `*Modal.tsx` — e.g. `apps/main/src/components/CreateTabloModal.tsx`, `EventDetailsModal.tsx`. +- **Sections**: `*Section.tsx` — e.g. `packages/tablo-views/src/TabloFilesSection.tsx`. +- **Cards**: `*Card.tsx` — e.g. `apps/main/src/components/EventTypeCard.tsx`, `AvailabilityCard.tsx`. +- **Pages**: live in `apps//src/pages/`, lowercased file names matching the route (`planning.tsx`, `events.tsx`, `login.tsx`). +- **Tests**: co-located as `*.test.tsx` / `*.test.ts` next to the source (`AvailabilityCard.test.tsx`, `auth.signup.test.ts`). +- **Hooks**: lowercase domain file in `hooks/` (`tablos.ts`, `events.ts`, `tasks.ts`); each exports multiple named hooks. +- **UI primitives**: lowercase in `packages/ui/src/components/` (`button.tsx`, `select.tsx`). + +## Key File Locations (Cheatsheet) + +| Concern | Path | +|-------------------------------|---------------------------------------------------------| +| Main app routes (protected) | `apps/main/src/lib/routes.tsx` | +| Main app public routes | `apps/main/src/lib/publicRoutes.tsx` | +| External app routes | `apps/external/src/routes.tsx` | +| Clients app routes | `apps/clients/src/routes.tsx` | +| Admin app routes | `apps/admin/src/routes.tsx` | +| Session context | `packages/shared/src/contexts/SessionContext.tsx` | +| User store (Zustand) | `apps/main/src/providers/UserStoreProvider.tsx` | +| HTTP API client | `packages/shared/src/lib/api.ts` | +| Supabase client (browser) | `packages/shared/src/lib/supabase.ts` | +| API entry | `apps/api/src/index.ts` | +| API router composition | `apps/api/src/routers/index.ts` | +| API middleware singleton | `apps/api/src/middlewares/middleware.ts` | +| API config | `apps/api/src/config.ts` | +| API secrets loader | `apps/api/src/secrets.ts` | +| Auto-generated DB types | `packages/shared-types/src/database.types.ts` | +| Shared barrel export | `packages/shared/src/index.ts` | +| UI component barrel | `packages/ui/src/components/index.ts` | +| Turborepo pipeline | `turbo.json` | +| Workspace definition | `pnpm-workspace.yaml` | +| Root Claude guidance | `CLAUDE.md` | + +## Adding New Features (5-Step Flow) + +From `CLAUDE.md`, the canonical workflow for a new feature is: + +1. **Define types** in `packages/shared-types/src/` (or run `npx supabase gen types typescript > packages/shared-types/src/database.types.ts` if you changed the DB schema). +2. **Add API endpoint** in `apps/api/src/routers/` — pick the right router (`public.ts`, `maybeAuthRouter.ts`, `authRouter.ts`, or a domain router like `tasks.ts`). Mount it in `apps/api/src/routers/index.ts` if it's new. +3. **Create a React Query hook** in either `packages/shared/src/hooks/` (cross-app) or `apps/main/src/hooks/` (app-local). Use the hierarchical query-key convention so mutations can invalidate predictably. +4. **Build the UI** using primitives from `@xtablo/ui` and patterns from `@xtablo/tablo-views` / `@xtablo/chat-ui` where applicable. Follow the `*Modal.tsx` / `*Section.tsx` / `*Card.tsx` naming convention and co-locate a `*.test.tsx`. +5. **Wire the route** in `apps/main/src/lib/routes.tsx` (or the equivalent in the relevant app) so the new page is reachable. + +## Testing Locations + +- **API tests**: `apps/api/src/__tests__/` (Vitest, mock Supabase). +- **Frontend tests**: co-located `*.test.tsx` next to source, run via `pnpm test` per app (`apps/main/src/setupTests.ts` configures happy-dom + Testing Library). +- **Docs on testing**: `docs/API_TESTS.md`, `docs/MIDDLEWARE_TESTS.md`. + +## Documentation Index (in `docs/`) + +- `DEVELOPMENT.md` — broad dev guide. +- `API_*.md` — API testing and integration. +- `STRIPE_*.md` — Stripe integration deep-dives. +- `AUTH_*.md` — Authentication patterns. +- `DOCKER_*.md` — Docker build optimization. +- `CLOUD_BUILD_*.md` — GCP Cloud Build setup. diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..22a6602 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,141 @@ +# Testing + +**Last updated:** 2026-05-14 +**Scope:** Turborepo monorepo at `/Users/arthur.belleville/Documents/perso/projects/xtablo-source` + +## Frameworks + +- **Vitest** is the only JS/TS test runner across every app and package (no Jest) +- **React Testing Library** (`@testing-library/react`) for component tests; `@testing-library/jest-dom` matchers loaded via setup files +- **happy-dom / jsdom** for DOM emulation in frontend tests (`apps/main/vite.config.ts` configures `environment: "jsdom"`) +- **Vitest with mocked Supabase** for `apps/api` — node environment, real Hono router exercised, Supabase client stubbed (`apps/api/src/__tests__/setup.ts` + `globalSetup.ts`) +- **pgTAP** SQL tests under `supabase/tests/database/*.test.sql` for schema/RLS/trigger correctness (run via the Supabase CLI, separate from Vitest) + +## Test Commands + +From the repo root (all dispatched by Turborepo via `turbo.json`): + +```bash +pnpm test # Run every package's `test` task +pnpm test:watch # Watch mode across the workspace +pnpm test:api # Run only @xtablo/api (turbo --filter) +cd apps/main && pnpm test # Run a single package directly +cd apps/api && pnpm test:watch +``` + +Per-package script conventions (see e.g. `apps/api/package.json`): +- `"test": "NODE_ENV=test vitest run"` +- `"test:watch": "NODE_ENV=test vitest"` +- Frontend apps similarly use `vitest run` / `vitest` (no `NODE_ENV=test` prefix required) + +## Test File Location + +Tests are **co-located** with the source they exercise: + +- React components: `Foo.tsx` + `Foo.test.tsx` in the same folder + - e.g. `apps/main/src/components/CustomModal.test.tsx`, `apps/main/src/components/NavigationBar.test.tsx` +- Hooks / contexts: `useFoo.ts` + `useFoo.test.ts(x)` + - e.g. `apps/main/src/contexts/UpgradeBlockContext.test.tsx` +- API: tests under `apps/api/src/__tests__//.test.ts` (grouped by router/concern), plus co-located helper tests like `apps/api/src/helpers/orgIcons.test.ts` +- Expo app sometimes uses `__tests__/` folders: `xtablo-expo/components/__tests__/BillingPaywall.test.tsx` + +## Vitest Configs + +Two distinct flavors live in the repo: + +1. **Frontend (Vite + Vitest)** — config embedded inside `vite.config.ts`. Example, `apps/main/vite.config.ts`: + ```ts + test: { + globals: true, + environment: "jsdom", + setupFiles: "./src/setupTests.ts", + } + ``` + The Cloudflare plugin is skipped when `process.env.VITEST === "true"`. `VITE_SUPABASE_URL` / `VITE_SUPABASE_ANON_KEY` are stubbed via `define` when running under Vitest. + + Other apps with the same pattern: + - `apps/admin/vite.config.ts` + `apps/admin/src/setupTests.ts` + - `apps/external/vite.config.ts` + `apps/external/src/setupTests.ts` + - `apps/clients/vite.config.ts` + `apps/clients/src/setupTests.ts` + +2. **API (standalone Vitest)** — dedicated `apps/api/vitest.config.ts`: + ```ts + test: { + globals: true, + environment: "node", + setupFiles: ["./src/__tests__/setup.ts"], + globalSetup: ["./src/__tests__/globalSetup.ts"], + testTimeout: 30000, + hookTimeout: 60000, + include: ["src/__tests__/**/*.test.ts", "src/**/*.test.ts"], + pool: "forks", + fileParallelism: false, + } + ``` + `fileParallelism: false` is deliberate — tests share initialized middleware state and can't safely run concurrently across files. + +## Setup Files + +- `apps/main/src/setupTests.ts` — imports `@testing-library/jest-dom`, registers an `afterEach(cleanup)`, mocks `ResizeObserver`, `Element.prototype.scrollIntoView`, `Element.prototype.scrollTo`, and `window.matchMedia`. Also imports `./i18n.test` to bootstrap i18next for tests. +- `apps/admin/src/setupTests.ts`, `apps/external/src/setupTests.ts`, `apps/clients/src/setupTests.ts` — analogous per-app setup +- `apps/api/src/__tests__/setup.ts` — minimal per-test-file setup (DB init handled by `globalSetup.ts`) +- `apps/api/src/__tests__/globalSetup.ts` — boots the test middleware manager via `createConfig()` reading `.env.test` + +## Mocking Patterns + +- **Frontend component tests** import the component and render it with a `renderWithProviders` helper (see `apps/main/src/utils/testHelpers.tsx` and `apps/clients/src/test/testHelpers.test.tsx`) that wires QueryClient, router, i18n, and Zustand stores. +- **React Query mocking**: integration tests mock the hook return values directly with `vi.mock(...)` and inject `{ data, isLoading, error }` shapes. +- **Supabase client**: API tests stub `supabase.from(...)` chains via `vi.fn()` — fixtures live in `apps/api/src/__tests__/fixtures/`. +- **Auth**: API middleware tests bypass real Supabase auth by overriding the Bearer-token validator and asserting on 401 paths (see `docs/MIDDLEWARE_TESTS.md`). +- **Toast / window APIs**: stubbed globally in `setupTests.ts` so individual tests don't have to. +- **i18n**: each frontend test setup imports `./i18n.test` so `useTranslation()` works without network. + +## API Test Patterns + +Documented in `docs/API_TESTS.md` and `docs/MIDDLEWARE_TESTS.md`. Key conventions: + +- Tests live under `apps/api/src/__tests__//.test.ts` (e.g. `notes/notes.test.ts`, `tasks/tasks.test.ts`) +- Each router has at minimum a smoke test that verifies the endpoint is reachable and returns the right shape +- Middleware is tested independently in `apps/api/src/__tests__/middlewares/middlewares.test.ts` — auth header validation, Supabase client injection, R2/Stream/email middleware behavior +- Test mode is detected via `NODE_ENV=test` in `createConfig()` so secrets load from `.env.test` instead of Google Secret Manager — no GCP credentials required to run the suite +- Fixtures (sample DB rows, sample Stripe payloads) live in `apps/api/src/__tests__/fixtures/` +- Helpers (token mocking, app builders) live in `apps/api/src/__tests__/helpers/` +- The `__tests__/README.md` documents the suite layout in-tree + +Snapshot from `docs/MIDDLEWARE_TESTS.md`: 116 passing tests across the API suite at last documented run (2025-11-10), with explicit coverage for the Supabase, Auth, Stream, R2, and email middlewares. + +## Coverage + +- **No coverage threshold is enforced** in CI today — `vitest.config.ts` files do not declare `coverage` blocks and `package.json` has no `test:coverage` script at the root. +- Coverage can still be produced ad-hoc with `vitest run --coverage` in any individual package, but it's not part of the normal workflow. +- The repo relies on (a) Biome lint errors blocking CI, (b) `pnpm typecheck` (`tsc -b`) blocking CI, and (c) `pnpm test` blocking CI — coverage % is currently advisory only. + +## Existing Test Inventory + +Total Vitest/RTL test files (`*.test.ts(x)`, excluding `node_modules`, `dist`): **147** + +Breakdown by area (approximate): +- `apps/main/src/**/*.test.tsx` — ~64 (components, contexts, providers, hooks) +- `apps/api/src/**/*.test.ts` — ~31 (routers, middlewares, helpers) +- `apps/admin/src/**/*.test.tsx` — pages, components, lib (e.g. `AnalyticsStudioPage.test.tsx`, `PrivilegedGate.test.tsx`) +- `apps/clients/src/**/*.test.ts(x)` — env + Vite config sanity checks plus page tests +- `apps/external/src/viteConfig.test.ts`, `apps/main/src/viteConfig.test.ts` — Vite build config smoke tests +- `xtablo-expo/**/*.test.ts(x)` — React Native (Expo) tests (e.g. `auth.test.ts`, `BillingPaywall.test.tsx`) +- `supabase/tests/database/*.test.sql` — 13 pgTAP files covering schema, RLS, triggers, indexes, Stripe + Apple billing functions + +Representative examples worth reading first: +- `apps/main/src/components/CustomModal.test.tsx` — minimal RTL test +- `apps/main/src/components/NavigationBar.test.tsx` — uses `renderWithProviders` +- `apps/main/src/providers/UserStoreProvider.test.tsx` — Zustand + React Query store test +- `apps/main/src/contexts/UpgradeBlockContext.test.tsx` — context provider test +- `apps/api/src/helpers/orgIcons.test.ts` — pure-helper unit test +- `apps/api/src/__tests__/middlewares/middlewares.test.ts` — middleware integration suite +- `supabase/tests/database/02_rls_policies_core.test.sql` — pgTAP RLS coverage + +## Reference Documents + +- `docs/API_TESTS.md` — API router test catalog, env setup, known limitations +- `docs/MIDDLEWARE_TESTS.md` — middleware-by-middleware coverage notes +- `docs/ENV_TEST_SETUP.md` — `.env.test` structure +- `docs/TEST_FIXES.md`, `docs/TEST_ROUTER_REFACTOR.md` — historical notes on test infrastructure changes +- `docs/TESTING_WITH_FAKE_ACCOUNTS.md` — temporary-account flow for manual + integration testing diff --git a/.planning/config.json b/.planning/config.json new file mode 100644 index 0000000..c91200b --- /dev/null +++ b/.planning/config.json @@ -0,0 +1,43 @@ +{ + "model_profile": "quality", + "commit_docs": true, + "parallelization": false, + "search_gitignored": false, + "brave_search": false, + "firecrawl": false, + "exa_search": false, + "git": { + "branching_strategy": "none", + "phase_branch_template": "gsd/phase-{phase}-{slug}", + "milestone_branch_template": "gsd/{milestone}-{slug}", + "quick_branch_template": null + }, + "workflow": { + "research": true, + "plan_check": true, + "verifier": true, + "nyquist_validation": true, + "auto_advance": true, + "node_repair": true, + "node_repair_budget": 2, + "ui_phase": true, + "ui_safety_gate": true, + "text_mode": false, + "research_before_questions": false, + "discuss_mode": "discuss", + "skip_discuss": false, + "code_review": true, + "code_review_depth": "standard", + "_auto_chain_active": true + }, + "hooks": { + "context_warnings": true + }, + "project_code": null, + "phase_naming": "sequential", + "agent_skills": {}, + "features": {}, + "resolve_model_ids": "omit", + "mode": "yolo", + "granularity": "standard" +} diff --git a/.planning/milestones/v2.0-MILESTONE-AUDIT.md b/.planning/milestones/v2.0-MILESTONE-AUDIT.md new file mode 100644 index 0000000..783ebf4 --- /dev/null +++ b/.planning/milestones/v2.0-MILESTONE-AUDIT.md @@ -0,0 +1,131 @@ +--- +milestone: v2.0 +milestone_name: Collaboration, planning, and social sign-in +audited: 2026-05-16T08:40:45Z +status: gaps_found +scores: + requirements: 9/27 satisfied + phases: 2/5 phase-verifications passed + integration: 5/5 phase interfaces checked + flows: 5/5 core flows checked +gaps: + requirements: + - id: "AUTH-08..13" + status: "partial" + phase: "8" + claimed_by_plans: ["08-01-SUMMARY.md", "08-02-SUMMARY.md", "08-03-SUMMARY.md", "08-04-SUMMARY.md", "08-05-SUMMARY.md"] + completed_by_plans: ["08-05-SUMMARY.md"] + verification_status: "missing" + evidence: "Phase 8 has 08-VALIDATION.md marked verified, but no 08-VERIFICATION.md for milestone audit aggregation." + - id: "ETAPE-01..06" + status: "partial" + phase: "9" + claimed_by_plans: ["09-01-SUMMARY.md", "09-02-SUMMARY.md", "09-03-SUMMARY.md", "09-04-SUMMARY.md"] + completed_by_plans: ["09-04-SUMMARY.md"] + verification_status: "missing" + evidence: "Phase 9 has completed summaries and tests, but no 09-VERIFICATION.md; REQUIREMENTS.md traceability still says Pending." + - id: "CHAT-01..06" + status: "partial" + phase: "12" + claimed_by_plans: ["12-01-SUMMARY.md", "12-02-SUMMARY.md", "12-03-SUMMARY.md"] + completed_by_plans: ["12-01-SUMMARY.md", "12-02-SUMMARY.md", "12-03-SUMMARY.md"] + verification_status: "missing" + evidence: "Phase 12 has complete summaries and 12-VALIDATION.md, but no 12-VERIFICATION.md for milestone audit aggregation." + integration: [] + flows: [] +tech_debt: + - phase: "09-etapes" + items: + - "09-VALIDATION.md remains draft with pending rows and wave_0_complete=false despite Phase 9 completion." + - "REQUIREMENTS.md traceability row for ETAPE-01..06 remains Pending." + - phase: "10-events" + items: + - "10-VALIDATION.md remains draft with pending rows and wave_0_complete=false even though 10-VERIFICATION.md passed." + - phase: "11-individual-planning" + items: + - "11-VALIDATION.md remains draft with pending rows even though 11-VERIFICATION.md passed." + - "REQUIREMENTS.md traceability row for PLAN-01..04 remains Pending." + - phase: "12-native-tablo-chat" + items: + - "Create 12-VERIFICATION.md to mirror the completed 12-VALIDATION.md and browser UAT evidence." +nyquist: + compliant_phases: ["08-social-sign-in", "12-native-tablo-chat"] + partial_phases: ["09-etapes", "10-events", "11-individual-planning"] + missing_phases: [] + overall: partial +--- + +# Milestone v2.0 Audit + +## Result + +**GAPS FOUND** + +The v2.0 implementation appears broadly complete from summaries, tests, and available verification evidence, but the milestone cannot be archived under the strict audit workflow yet because required phase-level verification artifacts are missing or stale. + +## Milestone Scope + +| Phase | Name | Plans | Disk Status | Verification | +|-------|------|-------|-------------|--------------| +| 8 | Social Sign-in | 5/5 | complete | MISSING `08-VERIFICATION.md` | +| 9 | Etapes | 4/4 | complete | MISSING `09-VERIFICATION.md` | +| 10 | Events | 4/4 | complete | PASS | +| 11 | Individual Planning | 2/2 | complete | PASS | +| 12 | Native Tablo Chat | 3/3 | complete | MISSING `12-VERIFICATION.md` | + +## Requirements Coverage + +| Requirement Set | Phase | REQUIREMENTS.md | SUMMARY Frontmatter | VERIFICATION.md | Final Status | +|-----------------|-------|-----------------|---------------------|-----------------|--------------| +| AUTH-08..13 | 8 | checked | listed | missing | PARTIAL | +| ETAPE-01..06 | 9 | pending | listed in `09-04-SUMMARY.md` | missing | PARTIAL | +| EVENT-01..05 | 10 | checked | listed | PASS in `10-VERIFICATION.md` | SATISFIED | +| PLAN-01..04 | 11 | traceability pending, checkboxes checked | listed | PASS in `11-VERIFICATION.md` | SATISFIED; traceability should be updated | +| CHAT-01..06 | 12 | checked | listed | missing | PARTIAL | + +### Unsatisfied / Partial Requirements + +- **AUTH-08..13** (Phase 8) + - Reason: all requirements are checked and validation is green, but no `08-VERIFICATION.md` exists. Milestone audit requires phase verification aggregation. +- **ETAPE-01..06** (Phase 9) + - Reason: implementation summaries claim completion, but no `09-VERIFICATION.md` exists and REQUIREMENTS.md traceability still says Pending. +- **CHAT-01..06** (Phase 12) + - Reason: Phase 12 has complete summaries, Nyquist validation, and UAT evidence, but no `12-VERIFICATION.md` exists. + +## Cross-Phase Integration + +Inline integration check found no concrete broken wiring in the v2.0 runtime surface: + +| Flow | Phases | Status | Evidence | +|------|--------|--------|----------| +| Social sign-in issues local sessions | 8 -> auth/router | OK | `handlers_social_test.go`, `handlers_auth_test.go`, `08-VALIDATION.md` | +| Etape organization preserves task board behavior | 9 -> tasks/tablos | OK but verification artifact missing | `handlers_etapes_test.go`, `handlers_tasks_test.go`, `09-04-SUMMARY.md` | +| Events feed individual planning | 10 -> 11 | OK | `10-VERIFICATION.md`, `11-VERIFICATION.md`, `TestListUserEventsRangeReturnsOnlyOwnedTablos`, `TestPlanningListsOwnedEventsChronologically` | +| Planning links return to source tablo Events tab | 10 -> 11 | OK | `11-VERIFICATION.md`, `PlanningEventURL` evidence | +| Native discussion stays owner-only and local-infrastructure-only | 12 -> tablos/router/static | OK but verification artifact missing | `12-VALIDATION.md`, discussion tests, browser UAT summary | + +## Broken Flows + +No broken cross-phase flow was identified from available tests and artifacts. + +## Nyquist Coverage + +| Phase | VALIDATION.md | Compliant | Action | +|-------|---------------|-----------|--------| +| 8 | exists | true | none | +| 9 | exists | partial | `$gsd-validate-phase 9` | +| 10 | exists | partial | `$gsd-validate-phase 10` | +| 11 | exists | partial | `$gsd-validate-phase 11` | +| 12 | exists | true | none | + +## Required Closure Before Archive + +1. Create or regenerate `08-VERIFICATION.md`, `09-VERIFICATION.md`, and `12-VERIFICATION.md`. +2. Update `REQUIREMENTS.md` traceability rows for `ETAPE-01..06` and `PLAN-01..04` if the verification artifacts confirm completion. +3. Run `$gsd-validate-phase 9`, `$gsd-validate-phase 10`, and `$gsd-validate-phase 11` to bring stale `VALIDATION.md` files from pending/draft to audited status, or explicitly accept them as tech debt at milestone close. + +## Audit Notes + +- Agents are not installed in this workspace, so the `gsd-integration-checker` step was performed inline. +- No code changes were made by this audit. +- The audit status is `gaps_found` because missing `VERIFICATION.md` files are blockers under the workflow. diff --git a/.planning/milestones/v2.0-REQUIREMENTS.md b/.planning/milestones/v2.0-REQUIREMENTS.md new file mode 100644 index 0000000..0a7e12c --- /dev/null +++ b/.planning/milestones/v2.0-REQUIREMENTS.md @@ -0,0 +1,101 @@ +# Requirements Archive: Xtablo v2.0 Collaboration, Planning, and Social Sign-in + +**Archived:** 2026-05-16 +**Source commit:** `58a9ce0` +**Outcome:** Shipped with acknowledged audit debt + +## Core Value + +A user can sign in and run the Tablos workflow - organize work, attach files, discuss, and plan scheduled events - without a JS framework or managed chat provider. + +## Completed Requirements + +### Authentication + +- [x] **AUTH-08**: User can start a Google sign-in flow from the login/signup page +- [x] **AUTH-09**: Google callback validates state, exchanges the authorization code, verifies the ID token, and creates or links a local Xtablo user +- [x] **AUTH-10**: Apple sign-in is disabled and hidden from the login/signup page +- [x] **AUTH-11**: Direct Apple sign-in routes are unavailable while Apple sign-in is disabled +- [x] **AUTH-12**: Social sign-in issues the same server-managed Xtablo session cookie used by email/password login +- [x] **AUTH-13**: Existing email/password login, signup, logout, CSRF, and rate limiting continue to work after social sign-in is added + +### Chat + +- [x] **CHAT-01**: Each tablo has a discussion view where authenticated authorized users can see persisted message history +- [x] **CHAT-02**: User can post a text message to a tablo discussion with validation and CSRF protection +- [x] **CHAT-03**: Messages are stored in Postgres with tablo, author, body, created timestamp, and deletion/edit metadata +- [x] **CHAT-04**: Open tablo discussion views receive new messages in real time without a manual refresh +- [x] **CHAT-05**: Real-time delivery uses Xtablo-owned infrastructure only; no managed chat or realtime provider is introduced +- [x] **CHAT-06**: Message rendering escapes user content and enforces a server-side maximum body length + +### Etapes + +- [x] **ETAPE-01**: User can create an etape inside a tablo with a title and optional description +- [x] **ETAPE-02**: User can edit, delete, and reorder etapes inside a tablo +- [x] **ETAPE-03**: User can assign a task to zero or one etape +- [x] **ETAPE-04**: Deleting an etape unassigns its tasks by default rather than deleting the tasks +- [x] **ETAPE-05**: The task UI can show or filter tasks by etape while preserving existing kanban status and ordering behavior +- [x] **ETAPE-06**: The data model prevents nested etapes; an etape cannot have a parent etape + +### Events + +- [x] **EVENT-01**: User can create a scheduled event attached to a tablo with title, start time, optional end time, optional description, and optional location +- [x] **EVENT-02**: User can edit and delete tablo events +- [x] **EVENT-03**: Tablo detail page includes an events view listing that tablo's scheduled events +- [x] **EVENT-04**: Event validation requires an end time to be empty or after the start time +- [x] **EVENT-05**: Event authorization follows tablo access rules so users cannot read or mutate events for inaccessible tablos + +### Planning + +- [x] **PLAN-01**: Each authenticated user can open an individual planning page +- [x] **PLAN-02**: Planning page lists the user's scheduled events across tablos in chronological order +- [x] **PLAN-03**: Planning page links each event back to its tablo context +- [x] **PLAN-04**: Planning page supports a functional empty state and date navigation/filtering suitable for the first working version + +## Traceability + +| Requirement | Phase | Final Status | Notes | +|-------------|-------|--------------|-------| +| AUTH-08..13 | Phase 8 | Complete with audit artifact debt | Implementation and validation passed; strict milestone audit wanted `08-VERIFICATION.md`. | +| ETAPE-01..06 | Phase 9 | Complete with archive correction | Source requirements were stale; `09-VALIDATION.md` and UAT evidence confirm completion. | +| EVENT-01..05 | Phase 10 | Complete | `10-VERIFICATION.md` and `10-VALIDATION.md` passed. | +| PLAN-01..04 | Phase 11 | Complete with validation draft debt | `11-VERIFICATION.md` passed; `11-VALIDATION.md` remained draft. | +| CHAT-01..06 | Phase 12 | Complete with audit artifact debt | Implementation, validation, and UAT passed; strict milestone audit wanted `12-VERIFICATION.md`. | + +## Future Requirements + +### Chat + +- **CHAT-FUT-01**: Typing indicators +- **CHAT-FUT-02**: Read receipts +- **CHAT-FUT-03**: Message reactions +- **CHAT-FUT-04**: Threads or replies +- **CHAT-FUT-05**: File previews in chat messages + +### Planning + +- **PLAN-FUT-01**: Recurring events +- **PLAN-FUT-02**: Reminder notifications +- **PLAN-FUT-03**: ICS import/export +- **PLAN-FUT-04**: External calendar sync + +### Etapes + +- **ETAPE-FUT-01**: Etape progress rollups +- **ETAPE-FUT-02**: Etape templates +- **ETAPE-FUT-03**: Etape dependencies + +## Out of Scope + +| Feature | Reason | +|---------|--------| +| Managed chat/realtime providers | User explicitly does not want third-party chat. | +| Managed auth platforms | Google is an identity provider only; Xtablo owns users and sessions. Apple sign-in is disabled for now. | +| WebSocket-first chat protocol | SSE receive + HTMX POST send shipped for v2. | +| Nested etapes or arbitrary task hierarchy | User requested one parent per task and no parent-of-parent. | +| Notes / rich documents | Not part of the requested v2 feature set. | +| Billing / Stripe | Still deferred until product loop is validated. | +| Public booking widget | Separate product surface, not part of this milestone. | +| Client portal | Separate product surface, not part of this milestone. | +| Admin tooling | Separate product surface, not part of this milestone. | +| Mobile / Expo app | Web rewrite remains the current product surface. | diff --git a/.planning/milestones/v2.0-ROADMAP.md b/.planning/milestones/v2.0-ROADMAP.md new file mode 100644 index 0000000..f34fb16 --- /dev/null +++ b/.planning/milestones/v2.0-ROADMAP.md @@ -0,0 +1,129 @@ +# Milestone v2.0: Collaboration, Planning, and Social Sign-in + +**Status:** SHIPPED 2026-05-16 +**Phases:** 8-12 +**Total Plans:** 18 +**Source commit:** `58a9ce0` + +## Overview + +v2.0 added the collaboration and scheduling layer around tablos while keeping the Go + HTMX architecture: Google sign-in, one-level etapes for task organization, tablo-owned events, a personal planning agenda, and native persisted realtime discussions. + +The milestone was closed after acknowledging 7 legacy/open artifact items from earlier phase UAT and verification records. Runtime v2.0 flows were kept as shipped; the acknowledged items are tracked in `STATE.md` and the archived audit. + +## Phases + +### Phase 8: Social Sign-in + +**Goal:** Users can sign in with Google while Xtablo keeps owning user accounts and sessions; Apple sign-in is disabled and hidden for now. +**Plans:** 5 +**Requirements:** AUTH-08, AUTH-09, AUTH-10, AUTH-11, AUTH-12, AUTH-13 + +Plans: + +- [x] 08-01: Social identity schema foundation +- [x] 08-02: Google OAuth/OIDC sign-in flow +- [x] 08-03: Apple sign-in implementation, later disabled by UAT scope change +- [x] 08-04: Social sign-in controls and missing-config states +- [x] 08-05: Account provider visibility and final docs/regression coverage + +Outcome: + +- Google social sign-in starts from auth pages, validates callback state, links by provider subject or verified email, and issues existing Xtablo sessions. +- Apple sign-in was intentionally disabled after UAT; routes are not mounted and controls are hidden. +- Provider configuration is documented without committing secrets. + +### Phase 9: Etapes + +**Goal:** A user can organize tasks under one-level etapes inside a tablo while preserving existing task status and ordering behavior. +**Plans:** 4 +**Requirements:** ETAPE-01, ETAPE-02, ETAPE-03, ETAPE-04, ETAPE-05, ETAPE-06 + +Plans: + +- [x] 09-01: Etape create/filter/assigned-task slice +- [x] 09-02: Etape edit, delete-unassign, and reorder management +- [x] 09-03: Task assignment and filter-aware task loading +- [x] 09-04: Reorder preservation, count refresh fixes, and UAT closure + +Outcome: + +- Etapes are tablo-owned one-level wrappers; tasks can belong to zero or one etape. +- Deleting an etape unassigns its tasks instead of deleting them. +- Task assignment, filtering, and reorder behavior remain compatible with the existing kanban model. + +### Phase 10: Events + +**Goal:** A user can schedule events that belong to tablos, with the same authorization expectations as tasks and files. +**Plans:** 4 +**Requirements:** EVENT-01, EVENT-02, EVENT-03, EVENT-04, EVENT-05 + +Plans: + +- [x] 10-01: Event creation and month calendar listing +- [x] 10-02: Inline edit, update validation, and hard delete +- [x] 10-03: Month navigation, day prefill, overflow display, and planning query +- [x] 10-04: Final regression coverage, full backend verification, and browser UAT + +Outcome: + +- Tablo events support title, date, start time, optional end time, location, and description. +- Invalid end times are rejected by handler validation and database constraint. +- Event access follows tablo ownership; inaccessible events cannot be read or mutated. + +### Phase 11: Individual Planning + +**Goal:** Each authenticated user can open a personal planning view that aggregates their scheduled events across tablos. +**Plans:** 2 +**Requirements:** PLAN-01, PLAN-02, PLAN-03, PLAN-04 + +Plans: + +- [x] 11-01: Protected planning agenda with owned cross-tablo event aggregation +- [x] 11-02: Full verification and UAT-approved 14-day planning agenda + +Outcome: + +- `/planning` is authenticated and lists the user's owned events chronologically. +- Event rows link back to their source tablo event context. +- Empty state, invalid-date fallback, and 14-day navigation were verified. + +### Phase 12: Native Tablo Chat + +**Goal:** Each tablo has a native persisted discussion stream with real-time updates and no managed chat/realtime provider. +**Plans:** 3 +**Requirements:** CHAT-01, CHAT-02, CHAT-03, CHAT-04, CHAT-05, CHAT-06 + +Plans: + +- [x] 12-01: Discussion schema, history, posting, validation, and templates +- [x] 12-02: Persistent unread state and dashboard/discussion read paths +- [x] 12-03: Native realtime discussion delivery with Go-owned SSE receive and HTMX POST send + +Outcome: + +- Discussion history and sends are persisted in Postgres with ownership, CSRF, escaping, and body length validation. +- Realtime receive uses local Go SSE infrastructure; no managed chat or realtime provider was added. +- Duplicate sender rows and composer reset behavior were fixed during UAT. + +## Milestone Summary + +**Key accomplishments:** + +- Added Google social sign-in while preserving Xtablo-owned sessions and disabling Apple sign-in for the shipped scope. +- Added one-level etapes for organizing tasks without changing the kanban status model. +- Added tablo-owned events and a personal planning agenda that aggregates owned events across tablos. +- Added native persisted realtime discussions using Postgres and Go-owned SSE. +- Kept the milestone inside the low-dependency Go + HTMX architecture. + +**Known deferred items at close:** 7 acknowledged open artifact items. See `STATE.md` Deferred Items and `v2.0-MILESTONE-AUDIT.md`. + +**Technical debt carried forward:** + +- Phase 8, 9, and 12 lacked strict `*-VERIFICATION.md` aggregation artifacts in the archived milestone audit. +- Phase 11 validation remained draft even though `11-VERIFICATION.md` passed. +- Earlier v1 UAT/verification records still contain acknowledged human-needed or partial statuses. + +--- + +For current project status, see `.planning/ROADMAP.md`. diff --git a/.planning/milestones/v3.0-REQUIREMENTS.md b/.planning/milestones/v3.0-REQUIREMENTS.md new file mode 100644 index 0000000..8deac6f --- /dev/null +++ b/.planning/milestones/v3.0-REQUIREMENTS.md @@ -0,0 +1,110 @@ +# Requirements Archive: v3.0 Design System & Visual Polish + +**Archived:** 2026-05-17 +**Status:** SHIPPED + +For current requirements, see `.planning/REQUIREMENTS.md`. + +--- + +# Requirements: Xtablo v3.0 Design System & Visual Polish + +**Defined:** 2026-05-16 +**Core Value:** A user can sign in and run the Tablos workflow — organize work, attach files, discuss, and plan scheduled events — without a JS framework or managed chat provider. + +## v3.0 Requirements + +Requirements for milestone v3.0. Each maps to exactly one roadmap phase. + +### Design System + +- [x] **DS-01**: CSS design tokens (colors, spacing, typography, shadows, gradients) are defined in `backend/internal/web/ui/base.css` matching the go-backend token vocabulary +- [x] **DS-02**: Button component with primary, secondary, ghost, and danger variants +- [x] **DS-03**: Input, Textarea, and Select form field components replace inline raw HTML in all templates +- [x] **DS-04**: Card component is used across tablo list, detail, and content views +- [x] **DS-05**: Badge component supports semantic tones (primary, warning, success, danger) +- [x] **DS-06**: Modal component is available for create/edit dialogs +- [x] **DS-07**: Empty-state component is available for zero-data views +- [x] **DS-08**: Table component is available for list views (files, events) +- [x] **DS-09**: Icon-button component replaces inline icon-button patterns across templates + +### Auth Views + +- [x] **AUTH-UI-01**: Login page matches the JS app's auth-card layout (gradient background, centered card, brand logo, status banner) +- [x] **AUTH-UI-02**: Signup page matches the same visual treatment as login +- [x] **AUTH-UI-03**: Google sign-in button uses the Material Design button style + +### Dashboard & Tablos + +- [ ] **DASH-01**: Sidebar uses the go-backend sidebar design (brand section, nav items with icons, tablo list, user/account footer) +- [ ] **DASH-02**: Tablo list uses project-card layout with color accents, creation date, and action controls +- [ ] **DASH-03**: Dashboard empty state uses the empty-state component + +### Tablo Detail + +- [x] **DETAIL-01**: Tablo detail header area matches the project-card-top design +- [x] **DETAIL-02**: Task kanban board uses the tasks-section design (section header, task rows, add control) +- [x] **DETAIL-03**: Etapes section is visually consistent with the task section +- [x] **DETAIL-04**: Files section uses the table component + +### Chat & Planning + +- [x] **CHAT-UI-01**: Discussion view uses consistent card/surface design with message bubbles distinguishing own vs. others +- [x] **PLAN-UI-01**: Planning page uses the overview-section layout for event aggregation + +## Future Requirements + +### Responsive / Mobile + +- **RESP-01**: Sidebar collapses on small screens (hamburger or bottom-nav pattern) +- **RESP-02**: Kanban board scrolls horizontally on mobile +- **RESP-03**: Auth pages adapt gracefully to narrow viewports + +### Dark Mode + +- **DARK-01**: All CSS tokens have dark-mode counterparts via `prefers-color-scheme` or a class toggle + +## Out of Scope + +| Feature | Reason | +|---------|--------| +| New pages or features | v3.0 is purely visual — no new routes or data models | +| Billing / Stripe | Deferred from v1.0, still out of scope | +| Client portal / Admin | Deferred from v1.0, still out of scope | +| Mobile app | Out of scope for this rewrite milestone | +| Dark mode | Future requirement — tokens will be structured to enable it, not implement it | + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| DS-01 | Phase 13 | Complete | +| DS-02 | Phase 13 | Complete | +| DS-03 | Phase 13 | Complete | +| DS-04 | Phase 13 | Complete | +| DS-05 | Phase 13 | Complete | +| DS-06 | Phase 13 | Complete | +| DS-07 | Phase 13 | Complete | +| DS-08 | Phase 13 | Complete | +| DS-09 | Phase 13 | Complete | +| AUTH-UI-01 | Phase 14 | Complete | +| AUTH-UI-02 | Phase 14 | Complete | +| AUTH-UI-03 | Phase 14 | Complete | +| DASH-01 | Phase 15 | Pending | +| DASH-02 | Phase 15 | Pending | +| DASH-03 | Phase 15 | Pending | +| DETAIL-01 | Phase 16 | Complete | +| DETAIL-02 | Phase 16 | Complete | +| DETAIL-03 | Phase 16 | Complete | +| DETAIL-04 | Phase 16 | Complete | +| CHAT-UI-01 | Phase 17 | Complete | +| PLAN-UI-01 | Phase 17 | Complete | + +**Coverage:** +- v3.0 requirements: 21 total +- Mapped to phases: 21 +- Unmapped: 0 ✓ + +--- +*Requirements defined: 2026-05-16* +*Last updated: 2026-05-16 after initial definition* diff --git a/.planning/milestones/v3.0-ROADMAP.md b/.planning/milestones/v3.0-ROADMAP.md new file mode 100644 index 0000000..6e4be10 --- /dev/null +++ b/.planning/milestones/v3.0-ROADMAP.md @@ -0,0 +1,169 @@ +# Roadmap: Xtablo v3.0 Design System & Visual Polish + +**Created:** 2026-05-16 +**Project mode:** Brownfield milestone on existing Go+HTMX backend +**Milestone:** v3.0 — Design System & Visual Polish + +5 phases, sequential. Phase numbering continues from v2.0, so v3.0 starts at Phase 13. + +--- + +## Previous Milestones + +- [x] **v2.0 Collaboration, Planning, and Social Sign-in** — Phases 8-12, shipped 2026-05-16. Archive: [`milestones/v2.0-ROADMAP.md`](milestones/v2.0-ROADMAP.md) + +--- + +## Phase Summary + +| # | Phase | Goal | Requirements | +|---|-------|------|--------------| +| 13 | 5/5 | Complete | 2026-05-16 | +| 14 | 1/2 | In Progress| | +| 15 | 3/3 | Complete | 2026-05-16 | +| 16 | 4/4 | Complete | 2026-05-16 | +| 17 | 2/2 | Complete | 2026-05-17 | + +--- + +## Phase Details + +### Phase 13: Design System Foundation +**Goal:** Replace the minimal `backend/internal/web/ui` with a full design system ported from `go-backend/internal/web/ui` — tokens, components, and Tailwind integration — so every subsequent phase has a stable component library to consume. +**Mode:** mvp +**Status:** Pending +**Requirements:** DS-01, DS-02, DS-03, DS-04, DS-05, DS-06, DS-07, DS-08, DS-09 +**Plans:** 5/5 plans complete +**Success Criteria:** +1. `backend/internal/web/ui/base.css` defines all CSS custom properties (color, spacing, typography, shadows, gradients) matching the go-backend token vocabulary +2. All component types are implemented as templ components: button, input, textarea, select, card, badge, modal, empty-state, table, icon-button +3. `backend/tailwind.input.css` imports all component CSS files and the app shell CSS +4. A component catalog page (`/ui-catalog`, dev-only) renders all components for visual verification +5. All existing templates compile and unit tests pass with no regressions + +Plans: +**Wave 1** +- [x] 13-01-PLAN.md — Token vocabulary + enum/helper foundation (base.css, auth.css extraction, variants.go, helpers.go) + +**Wave 2** *(blocked on Wave 1 completion)* +- [x] 13-02-PLAN.md — Migrate existing components to go-backend API (button multi-class, badge pill, card typed API, template hardcodes) + +**Wave 3** *(blocked on Wave 2 completion)* +- [x] 13-03-PLAN.md — Port form-input components: input, textarea, select, form-field (CSS + templ + tests) + +**Wave 4** *(blocked on Wave 3 completion)* +- [x] 13-04-PLAN.md — Port surface components: modal, empty-state, table, icon-button, space + tailwind.input.css manifest + +**Wave 5** *(blocked on Wave 4 completion)* +- [x] 13-05-PLAN.md — Catalog route (build-tag gated) + visual sign-off checkpoint + +**User-in-loop:** Review the catalog page before proceeding to per-view application phases. Confirm token choices (brand color, radius, shadow levels) match what you want the product to look like. + +### Phase 14: Auth Pages +**Goal:** Restyle login and signup pages to match the JS app's auth-card layout using the design system from Phase 13. +**Mode:** mvp +**Status:** Pending +**Requirements:** AUTH-UI-01, AUTH-UI-02, AUTH-UI-03 +**Plans:** 1/2 plans executed +**Success Criteria:** +1. Login page has gradient background with animated background layer, centered auth card with brand logo, and status banner using design tokens +2. Signup page matches the same visual treatment as login +3. Google sign-in button uses the Material Design button style from the go-backend design +4. All existing auth handler tests pass unchanged +5. Browser walkthrough of login and signup matches the go-backend app.css auth-card design + +Plans: +**Wave 1** +- [x] 14-01-PLAN.md — Auth foundation: logo assets + auth.css replacement + auth_components.templ + auth_layout.templ + +**Wave 2** *(blocked on Wave 1 completion)* +- [ ] 14-02-PLAN.md — Page migration: update auth_login.templ and auth_signup.templ to use AuthLayout + FormField inputs + nav links + browser verify checkpoint + +**User-in-loop:** Browser walkthrough checkpoint in Plan 02 — approve visual result before considering the phase complete. + +### Phase 15: Dashboard & Tablos +**Goal:** Restyle the layout shell (sidebar + main) and tablo list/dashboard to match the JS app's sidebar + project-card layout. +**Mode:** mvp +**Status:** Pending +**Requirements:** DASH-01, DASH-02, DASH-03 +**Plans:** 3/3 plans complete +**Success Criteria:** +1. Sidebar has brand section, nav items with icons, tablo list section, and user/account footer using sidebar-nav-shell classes +2. Tablo list uses project-card layout with color avatars, creation date, and action controls +3. Dashboard empty state uses the empty-state component +4. All existing tablo CRUD handler tests pass unchanged +5. Browser walkthrough of tablos list matches the go-backend project-card / sidebar design + +Plans: +**Wave 0** +- [x] 15-01-PLAN.md — Wave 0 test stubs: TestTablosDashboard_Sidebar, TestTablosDashboard_ProjectCards, TestTablosDashboard_EmptyState (RED baseline for DASH-01/02/03) + +**Wave 1** *(blocked on Wave 0 completion)* +- [x] 15-02-PLAN.md — CSS foundation + AppLayout: app.css ported from go-backend, tailwind.input.css updated, app_layout.templ + app_layout_helpers.go created + +**Wave 2** *(blocked on Wave 1 completion)* +- [x] 15-03-PLAN.md — Dashboard wiring: tablos.templ restyled (project-card grid, ui.EmptyState), handlers updated, planning + account_providers switched to AppLayout, browser verify checkpoint + +**User-in-loop:** Approve sidebar shape (nav items, tablo list section) and tablo card layout before implementation. + +### Phase 16: Tablo Detail +**Goal:** Restyle the tablo detail view — header, task kanban, etapes, and files — using design system components. +**Mode:** mvp +**Status:** Pending +**Requirements:** DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04 +**Plans:** 4/4 plans complete +**Success Criteria:** +1. Tablo detail header uses project-card-top layout with title, avatar, and action controls +2. Task kanban uses tasks-section design: section header with add button, task rows with checkbox and meta +3. Etapes section uses the same card/section visual pattern as tasks +4. Files section uses the table component with consistent row actions +5. All existing task, etape, and file handler tests pass unchanged + +Plans: +**Wave 1** +- [x] 16-01-PLAN.md — CSS foundation + icons: append CSS Sections 19–25 to app.css; add download + chat icon cases to UIIcon switch + +**Wave 2** *(blocked on Wave 1 completion)* +- [x] 16-02-PLAN.md — Header + tab nav + overview tab: restyle TabloDetailPage header (project-card-top), metadata row, tab nav (design token classes), move desc to overview tab, remove EtapeStrip call from TasksTabFragment + +**Wave 3** *(blocked on Wave 2 completion)* +- [x] 16-03-PLAN.md — Kanban + etape grouping: groupTasksByEtape helper, restyled KanbanBoard/Column/TaskCard, EtapeStrip OOB removal, handlers_tasks.go call sites updated + +**Wave 4** *(blocked on Wave 3 completion)* +- [x] 16-04-PLAN.md — Files section: @ui.Table, @ui.EmptyState, FileListRow as tr, FileDeleteConfirmFragment as tr, browser verify checkpoint + +**User-in-loop:** Browser verify checkpoint in Plan 04 — approve visual result before considering the phase complete. + +### Phase 17: Chat & Planning +**Goal:** Restyle the discussion view and planning page using design system components to make them visually consistent with the rest of the app. +**Mode:** mvp +**Status:** Pending +**Requirements:** CHAT-UI-01, PLAN-UI-01 +**Plans:** 2/2 plans complete +**Success Criteria:** +1. Discussion view uses card/surface design; own messages vs. others are visually differentiated +2. Planning page uses overview-section layout with chronological event list +3. All existing chat and planning handler tests pass unchanged +4. Browser walkthrough confirms both views look consistent with the Phase 15–16 restyled surfaces + +Plans: +**Wave 1** *(both plans parallel — no shared files)* +- [x] 17-01-PLAN.md — Discussion view: CSS message-bubble classes + DiscussionTabData view model + ChatMainContent() component + handler wiring + browser verify +- [x] 17-02-PLAN.md — Planning view: h1 selector fix + PlanningTabData view model + PlanningShowDaySeparator + PlanningMainContent() component + handler wiring + browser verify + +**User-in-loop:** Browser verify checkpoints in both plans — approve visual result before considering the phase complete. + +--- + +## Coverage + +**21 requirements mapped across 5 phases** + +| Phase | Requirements | Count | +|-------|-------------|-------| +| 13 | DS-01, DS-02, DS-03, DS-04, DS-05, DS-06, DS-07, DS-08, DS-09 | 9 | +| 14 | AUTH-UI-01, AUTH-UI-02, AUTH-UI-03 | 3 | +| 15 | DASH-01, DASH-02, DASH-03 | 3 | +| 16 | DETAIL-01, DETAIL-02, DETAIL-03, DETAIL-04 | 4 | +| 17 | CHAT-UI-01, PLAN-UI-01 | 2 | +| **Total** | | **21 / 21 ✓** | diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md new file mode 100644 index 0000000..f40b8d8 --- /dev/null +++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-PLAN.md @@ -0,0 +1,258 @@ +--- +phase: 13-design-system-foundation +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - backend/internal/web/ui/base.css + - backend/internal/web/ui/auth.css + - backend/internal/web/ui/variants.go + - backend/internal/web/ui/helpers.go + - backend/tailwind.input.css +autonomous: true +requirements: + - DS-01 + +must_haves: + truths: + - "base.css contains the full 223-line CSS custom property vocabulary from go-backend" + - "auth-provider button styles are preserved in auth.css (not lost when button.css is replaced)" + - "variants.go declares ButtonVariantGhost, BadgeVariantPrimary, IconButtonVariant, IconButtonTone, and SpacingStep enums" + - "helpers.go declares buttonType, inputType, inputID, and textareaRows helper functions" + - "tailwind.input.css imports auth.css so the login page retains provider button styling" + - "go test ./internal/web/ui/... passes (no regressions from enum additions)" + artifacts: + - path: "backend/internal/web/ui/base.css" + provides: "Full CSS custom property token vocabulary" + contains: "--color-brand-primary" + - path: "backend/internal/web/ui/auth.css" + provides: "Auth provider button CSS extracted from button.css" + contains: ".auth-provider-button" + - path: "backend/internal/web/ui/variants.go" + provides: "All variant enums including Ghost, Primary, IconButton, SpacingStep" + contains: "ButtonVariantGhost" + - path: "backend/internal/web/ui/helpers.go" + provides: "Helper functions for templ components" + contains: "buttonType" + key_links: + - from: "backend/tailwind.input.css" + to: "backend/internal/web/ui/auth.css" + via: "@import" + pattern: "auth\\.css" + - from: "backend/internal/web/ui/variants.go" + to: "ButtonVariantGhost" + via: "const block" + pattern: "ButtonVariantGhost" +--- + +## Phase Goal + +**As a** developer, **I want to** have a complete CSS token vocabulary and Go variant enums in the backend, **so that** every subsequent plan can port component CSS and templ files against a stable foundation without losing any existing styling. + + +Replace the 28-line backend/base.css stub with the full 223-line token vocabulary from go-backend, +extract auth-provider CSS from button.css into auth.css before button.css is replaced, +and extend variants.go + helpers.go with all new enums and helper functions. + +Purpose: This is the mandatory prerequisite wave. Without the full token vocabulary, component CSS +ported in Plans 02–04 cannot reference var(--...) tokens. Without auth.css extraction, replacing +button.css in Plan 02 silently destroys the login page's provider button styling. + +Output: +- backend/internal/web/ui/base.css — 223-line token vocabulary (verbatim port, per D-T01/T02/T03) +- backend/internal/web/ui/auth.css — auth-provider selectors extracted from current button.css +- backend/internal/web/ui/variants.go — extended with Ghost/Primary variants + new enums +- backend/internal/web/ui/helpers.go — extended with buttonType/inputType/inputID/textareaRows +- backend/tailwind.input.css — auth.css import added (button.css replacement in Plan 02) + + + +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/REQUIREMENTS.md +@.planning/phases/13-design-system-foundation/13-CONTEXT.md +@.planning/phases/13-design-system-foundation/13-RESEARCH.md +@.planning/phases/13-design-system-foundation/13-PATTERNS.md + + + + + + Task 1: Extract auth.css and replace base.css + + backend/internal/web/ui/auth.css, + backend/internal/web/ui/base.css, + backend/tailwind.input.css + + + - backend/internal/web/ui/button.css (lines 121–180 contain the auth-provider CSS to extract verbatim) + - backend/internal/web/ui/base.css (current 28-line stub to understand what is being replaced) + - go-backend/internal/web/ui/base.css (the 223-line source to port verbatim) + - backend/tailwind.input.css (current 4-import manifest to update) + + + Step 1 — Create backend/internal/web/ui/auth.css: copy lines 121–180 from the current + backend/internal/web/ui/button.css verbatim. The first line must be a comment: + "/* auth.css — sign-in provider controls, extracted from button.css in Phase 13 */". + The file must contain the selectors: .auth-provider-stack, .auth-provider-button, + .auth-provider-button:hover, .auth-provider-button:focus-visible, + .auth-provider-button-disabled, .auth-provider-separator, .auth-provider-separator span, + .auth-provider-separator em. + + Step 2 — Replace backend/internal/web/ui/base.css entirely with the contents of + go-backend/internal/web/ui/base.css (verbatim — per D-T01, D-T02, D-T03). + Do NOT merge with the existing 28-line stub. The target file is 223 lines. + + Step 3 — Update backend/tailwind.input.css: add the line + "@import "./internal/web/ui/auth.css";" immediately after the base.css import line. + Do not yet add imports for the component CSS files being created in Plans 02–04 — those + are added in Plan 04. + + + grep -c 'color-brand-primary' backend/internal/web/ui/base.css + Expected output: at least 3 (the token appears in multiple rules). + Also: grep -c 'auth-provider-button' backend/internal/web/ui/auth.css — must return at least 3. + Also: grep 'auth\.css' backend/tailwind.input.css — must match. + + + - backend/internal/web/ui/base.css is exactly 223 lines (or close — the exact line count from go-backend) + - backend/internal/web/ui/base.css contains "--color-brand-primary: #804eec" + - backend/internal/web/ui/base.css contains "--color-text-primary" + - backend/internal/web/ui/base.css contains "--shadow-surface-md" + - backend/internal/web/ui/base.css does NOT contain "box-sizing: border-box" at the top (the stub had that; go-backend version puts it inside :root or omits it) + - backend/internal/web/ui/auth.css exists and contains ".auth-provider-button {" + - backend/internal/web/ui/auth.css contains ".auth-provider-separator {" + - backend/tailwind.input.css contains "@import "./internal/web/ui/auth.css";" + - The auth.css import appears after the base.css import in tailwind.input.css + + base.css replaced with 223-line token vocabulary; auth.css created with extracted provider styles; tailwind.input.css updated with auth.css import + + + + Task 2: Extend variants.go with new enums and helpers.go with new helper functions + + backend/internal/web/ui/variants.go, + backend/internal/web/ui/helpers.go, + backend/internal/web/ui/ui_test.go + + + - backend/internal/web/ui/variants.go (current file — full read to understand existing enum patterns) + - backend/internal/web/ui/helpers.go (current file — full read to understand existing helper pattern) + - backend/internal/web/ui/ui_test.go (current test file — understand existing test structure) + - go-backend/internal/web/ui/variants.go (source for new enums and class functions) + - go-backend/internal/web/ui/helpers.go (source for new helper functions) + - 13-PATTERNS.md (Pattern for variants.go and helpers.go changes) + - 13-RESEARCH.md (Code Examples section for exact function signatures) + + + - Test: NormalizedButtonVariant(ButtonVariantGhost) returns ButtonVariantGhost (not ButtonVariantDefault) + - Test: ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost" + - Test: NormalizedBadgeVariant(BadgeVariantPrimary) returns BadgeVariantPrimary + - Test: BadgeClass(BadgeVariantPrimary) == "ui-badge ui-badge-primary" + - Test: IconButtonClass(IconButtonVariantNeutral, IconButtonToneGhost) contains "borderless-icon-button" + - Test: IconButtonClass(IconButtonVariantNeutral, IconButtonToneSolid) contains "ui-icon-button-solid" + - Test: SpaceXClass(SpacingStepMD) == "ui-space-x ui-space-x-md" + - Test: SpaceYClass(SpacingStepLG) == "ui-space-y ui-space-y-lg" + + + Step 1 — Write the failing tests in ui_test.go first (RED). Add test functions for: + TestButtonVariantGhost_Normalizer, TestButtonClass_GhostVariant, + TestBadgeVariantPrimary_Normalizer, TestBadgeClass_PrimaryVariant, + TestIconButtonClass_GhostNeutral, TestIconButtonClass_SolidNeutral, + TestSpaceXClass_MD, TestSpaceYClass_LG. + Run go test ./internal/web/ui/... and confirm failures (RED). + + Step 2 — Update variants.go (GREEN): + a. Add "ButtonVariantGhost ButtonVariant = "ghost"" to the ButtonVariant const block. + b. Add "ButtonVariantGhost" case to NormalizedButtonVariant switch: "case ButtonVariantGhost: return variant". + c. Add "BadgeVariantPrimary BadgeVariant = "primary"" to the BadgeVariant const block. + d. Add "BadgeVariantPrimary" case to NormalizedBadgeVariant switch. + e. Add IconButtonVariant type with constants: IconButtonVariantNeutral="neutral", + IconButtonVariantWarning="warning", IconButtonVariantSuccess="success", + IconButtonVariantDanger="danger". + f. Add IconButtonTone type with constants: IconButtonToneSolid="solid", IconButtonToneGhost="ghost". + g. Add SpacingStep type with constants: SpacingStepXS="xs", SpacingStepSM="sm", + SpacingStepMD="md", SpacingStepLG="lg", SpacingStepXL="xl". + h. Add NormalizedIconButtonVariant function (defaults to IconButtonVariantNeutral). + i. Add NormalizedIconButtonTone function (defaults to IconButtonToneSolid). + j. Add NormalizedSpacingStep function (defaults to SpacingStepMD). + k. Add IconButtonClass(variant IconButtonVariant, tone IconButtonTone) string function. + Ghost tone path: returns "borderless-icon-button ui-icon-button-ghost ui-icon-button-" + normalizedVariant. + Solid tone path: returns "ui-icon-button ui-icon-button-solid ui-icon-button-" + normalizedVariant. + l. Add SpaceXClass(step SpacingStep) string — returns "ui-space-x ui-space-x-" + normalizedStep. + m. Add SpaceYClass(step SpacingStep) string — returns "ui-space-y ui-space-y-" + normalizedStep. + + Step 3 — Update helpers.go (GREEN): + Add four new unexported helper functions (following the existing mergeAttrs pattern): + a. buttonType(value string) string — returns "button" if value is empty, otherwise value. + b. inputType(value string) string — returns "text" if value is empty, otherwise value. + c. inputID(id string, name string) string — returns id if non-empty, otherwise name. + d. textareaRows(rows int) string — returns strconv.Itoa(rows) if rows > 0, else "4". + Add import "strconv" to helpers.go. + + Do NOT change ButtonClass() output yet — that is Plan 02's work. + Run go test ./internal/web/ui/... and confirm all tests pass (GREEN). + + + cd backend && go test ./internal/web/ui/... -run "TestButtonVariantGhost|TestBadgeVariantPrimary|TestIconButtonClass|TestSpaceXClass|TestSpaceYClass" -v + + + - All 8 new test functions pass + - go test ./internal/web/ui/... (full suite) is green — no existing tests broken + - variants.go contains "ButtonVariantGhost ButtonVariant = "ghost"" + - variants.go contains "BadgeVariantPrimary BadgeVariant = "primary"" + - variants.go contains type IconButtonVariant string + - variants.go contains type SpacingStep string + - variants.go contains func IconButtonClass + - variants.go contains func SpaceXClass + - helpers.go imports "strconv" + - helpers.go contains func buttonType + - helpers.go contains func textareaRows + - ButtonClass() still returns the OLD compound pattern (ui-button-solid-default-md) — this is intentional; Plan 02 migrates it + + variants.go extended with all new enums and class functions; helpers.go extended with component helper functions; all tests green + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Static build → browser | CSS compiled by Tailwind CLI; no runtime input | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-13-01-01 | Information Disclosure | base.css token values | accept | Token values are design constants (brand colors, spacing). No secrets. Acceptable public visibility. | +| T-13-01-02 | Tampering | variants.go normalizer | accept | Normalizers return safe defaults for unknown inputs; no user-controlled data flows through variant enums at runtime. | + + + +After this plan completes: +- cd backend && go test ./internal/web/ui/... — must be green +- grep -c 'color-brand-primary' backend/internal/web/ui/base.css — must return >= 3 +- grep 'ButtonVariantGhost' backend/internal/web/ui/variants.go — must match +- grep 'auth-provider-button' backend/internal/web/ui/auth.css — must match +- grep 'auth\.css' backend/tailwind.input.css — must match + + + +1. base.css contains the full go-backend token vocabulary (223 lines, --color-brand-primary: #804eec present) +2. auth.css exists with .auth-provider-button and is imported in tailwind.input.css +3. variants.go has ButtonVariantGhost, BadgeVariantPrimary, IconButtonVariant, IconButtonTone, SpacingStep +4. variants.go has IconButtonClass(), SpaceXClass(), SpaceYClass() functions +5. helpers.go has buttonType(), inputType(), inputID(), textareaRows() +6. go test ./internal/web/ui/... is green + + + +After completion, create `.planning/phases/13-design-system-foundation/13-01-SUMMARY.md` + diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md new file mode 100644 index 0000000..24b01d2 --- /dev/null +++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-01-SUMMARY.md @@ -0,0 +1,92 @@ +--- +phase: 13-design-system-foundation +plan: "01" +subsystem: backend/internal/web/ui +tags: [css-tokens, go-enums, design-system, foundation] +dependency_graph: + requires: [] + provides: [css-token-vocabulary, variant-enums, helper-functions, auth-css] + affects: [backend/tailwind.input.css, backend/internal/web/ui/variants.go, backend/internal/web/ui/helpers.go] +tech_stack: + added: [] + patterns: [verbatim-port, tdd-red-green, enum-extension] +key_files: + created: + - backend/internal/web/ui/auth.css + modified: + - backend/internal/web/ui/base.css + - backend/internal/web/ui/variants.go + - backend/internal/web/ui/helpers.go + - backend/internal/web/ui/ui_test.go + - backend/tailwind.input.css +decisions: + - "ButtonClass() retains compound format (ui-button-solid-ghost-md) until Plan 02 migrates it to multi-class" + - "TestButtonClass_GhostVariant asserts 'ghost' substring rather than 'ui-button-ghost' standalone class, matching the preserved compound format" + - "auth-provider CSS extracted verbatim from button.css Phase 8 block (lines 121-180) into standalone auth.css" +metrics: + duration: "~4 minutes" + completed_date: "2026-05-16" + tasks: 2 + files: 5 +--- + +# Phase 13 Plan 01: Design System Foundation — Token Vocabulary and Enum Extension Summary + +Full CSS custom property vocabulary (223-line go-backend port) plus auth-provider CSS extraction, Ghost/Primary variant enums, IconButton/SpacingStep enum types, and component helper functions added to backend. + +## Tasks Completed + +| Task | Name | Commit | Key Files | +|------|------|--------|-----------| +| 1 | Extract auth.css and replace base.css | 59e39fe | auth.css (new), base.css (replaced), tailwind.input.css (updated) | +| 2 | Extend variants.go and helpers.go (TDD) | 8602eb1 (RED), d149965 (GREEN) | variants.go, helpers.go, ui_test.go | + +## What Was Built + +**Task 1 — CSS foundation:** +- Replaced the 28-line `backend/internal/web/ui/base.css` stub with the full 223-line `:root` CSS custom property vocabulary verbatim-ported from `go-backend/internal/web/ui/base.css`. Contains `--color-brand-primary: #804eec`, `--color-text-primary`, `--shadow-surface-md`, and all token categories (text, surfaces, borders, brand, status, effects, gradients, legacy aliases). +- Created `backend/internal/web/ui/auth.css` with the 8 auth-provider selectors (`.auth-provider-stack`, `.auth-provider-button`, `.auth-provider-button:hover`, `.auth-provider-button:focus-visible`, `.auth-provider-button-disabled`, `.auth-provider-separator`, `.auth-provider-separator span`, `.auth-provider-separator em`) extracted verbatim from `button.css` lines 121-180. +- Added `@import "./internal/web/ui/auth.css";` to `backend/tailwind.input.css` after the base.css import line. + +**Task 2 — Go enum and helper extension (TDD):** +- Extended `variants.go` with `ButtonVariantGhost`, `BadgeVariantPrimary`, `IconButtonVariant` type (Neutral/Warning/Success/Danger), `IconButtonTone` type (Solid/Ghost), `SpacingStep` type (XS/SM/MD/LG/XL). +- Added exported normalizer functions: `NormalizedIconButtonVariant`, `NormalizedIconButtonTone`, `NormalizedSpacingStep`. +- Added exported class functions: `IconButtonClass`, `SpaceXClass`, `SpaceYClass`. +- Updated `NormalizedButtonVariant` to pass through `ButtonVariantGhost`; `NormalizedBadgeVariant` to pass through `BadgeVariantPrimary`. +- Extended `helpers.go` with `buttonType`, `inputType`, `inputID`, `textareaRows` (with `strconv` import). +- All 18 tests in `ui_test.go` pass (10 existing + 8 new). + +## Verification Results + +- `go test ./internal/web/ui/... PASS` (18/18 tests) +- `grep -c 'color-brand-primary' backend/internal/web/ui/base.css` returns 4 +- `grep 'ButtonVariantGhost' backend/internal/web/ui/variants.go` matches +- `grep 'auth-provider-button' backend/internal/web/ui/auth.css` returns 5 matches +- `grep 'auth.css' backend/tailwind.input.css` matches + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 1 - Bug] Fixed TestButtonClass_GhostVariant test assertion** +- **Found during:** Task 2 GREEN phase +- **Issue:** The plan's behavior spec stated `ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost"`, but the plan also explicitly requires `ButtonClass()` to retain the old compound format (`ui-button-solid-ghost-md`) until Plan 02 migrates it. The compound format produces `"ui-button-solid-ghost-md"` which does NOT contain the substring `"ui-button-ghost"` — only `"ghost"`. +- **Fix:** Changed the test assertion from `strings.Contains(got, "ui-button-ghost")` to `strings.Contains(got, "ghost")` with a comment explaining the compound format is intentionally preserved for Plan 02. +- **Files modified:** `backend/internal/web/ui/ui_test.go` +- **Commit:** d149965 + +## Known Stubs + +None — this plan creates foundational infrastructure only (CSS tokens, Go enums, helpers). No UI rendering or data wiring involved. + +## Threat Flags + +No new network endpoints, auth paths, file access patterns, or schema changes introduced. + +## TDD Gate Compliance + +- RED gate: commit 8602eb1 (`test(13-01): add failing tests for new variant enums and class functions (RED)`) +- GREEN gate: commit d149965 (`feat(13-01): extend variants.go with new enums and helpers.go with helper functions (GREEN)`) +- REFACTOR gate: Not needed — implementation was clean on first pass. + +## Self-Check: PASSED diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md new file mode 100644 index 0000000..8de45b9 --- /dev/null +++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-PLAN.md @@ -0,0 +1,314 @@ +--- +phase: 13-design-system-foundation +plan: 02 +type: execute +wave: 2 +depends_on: + - 13-01 +files_modified: + - backend/internal/web/ui/button.css + - backend/internal/web/ui/button.templ + - backend/internal/web/ui/badge.css + - backend/internal/web/ui/badge.templ + - backend/internal/web/ui/card.css + - backend/internal/web/ui/card.templ + - backend/internal/web/ui/variants.go + - backend/internal/web/ui/ui_test.go + - backend/templates/planning.templ + - backend/templates/tasks.templ + - backend/templates/events.templ + - backend/templates/etapes.templ +autonomous: true +requirements: + - DS-02 + - DS-04 + - DS-05 + +must_haves: + truths: + - "ButtonClass() emits four separate classes: ui-button, ui-button-{tone}, ui-button-{variant}, ui-button-{size}" + - "All hardcoded compound button class strings in templates are updated to multi-class equivalents" + - "ButtonVariantGhost class combo renders with transparent background and brand color text" + - "BadgeVariantPrimary renders with brand-purple surface" + - "Card uses typed Header/Body/Footer templ.Component fields (not children passthrough)" + - "go test ./... passes — no regressions in any package" + - "just generate succeeds — all templ files compile" + artifacts: + - path: "backend/internal/web/ui/button.css" + provides: "Multi-class button selectors + ghost variant" + contains: ".ui-button-solid.ui-button-default {" + - path: "backend/internal/web/ui/card.templ" + provides: "Typed Header/Body/Footer Props API" + contains: "CardProps" + - path: "backend/internal/web/ui/variants.go" + provides: "ButtonClass() emitting multi-class output" + contains: "ui-button-" + key_links: + - from: "backend/templates/planning.templ" + to: "multi-class button pattern" + via: "class attribute strings" + pattern: "ui-button ui-button-soft ui-button-neutral" + - from: "backend/internal/web/ui/button.templ" + to: "ButtonClass()" + via: "ButtonClass(props.Variant, props.Tone, props.Size)" + pattern: "ButtonClass" +--- + +## Phase Goal + +**As a** developer, **I want to** migrate existing Button/Badge/Card components to go-backend's API (multi-class button pattern, typed Card fields, new variants), **so that** buttons render correctly from the ported button.css and templates remain functional with no visual regressions. + + +Migrate the three existing components (Button, Badge, Card) to match go-backend's API and CSS. +The critical task is the button multi-class migration — both the CSS selectors and the class +generation function must change atomically, and all hardcoded compound class strings in templates +must be updated in the same wave. + +Purpose: This wave closes the gap between the current compound-class pattern and go-backend's +multi-class pattern. Without this, porting button.css (which uses compound CSS selectors like +.ui-button-solid.ui-button-default) would silently break all button rendering. + +Output: +- button.css replaced with go-backend's multi-class selector version + new ghost rules +- ButtonClass() updated to emit "ui-button ui-button-solid ui-button-default ui-button-md" +- button.templ updated with Icon field and buttonType() helper (from Plan 01 helpers.go) +- badge.css replaced with go-backend's pill-shape version + new primary variant +- card.css replaced with go-backend's token-based version +- card.templ migrated from children to typed Header/Body/Footer fields +- All compound class strings in planning.templ, tasks.templ, events.templ, etapes.templ updated +- ui_test.go updated to match new patterns (multi-class assertions, card typed API) + + + +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/13-design-system-foundation/13-CONTEXT.md +@.planning/phases/13-design-system-foundation/13-RESEARCH.md +@.planning/phases/13-design-system-foundation/13-PATTERNS.md +@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md + + + +From backend/internal/web/ui/variants.go (after Plan 01): + func ButtonClass(variant ButtonVariant, tone ButtonTone, size Size) string + // Currently returns: "ui-button ui-button-solid-default-md" — Plan 02 changes this output + // Target: "ui-button ui-button-solid ui-button-default ui-button-md" + +From backend/internal/web/ui/helpers.go (after Plan 01): + func buttonType(value string) string // returns "button" if empty + func UIIcon(kind string) templ.Component // does NOT exist yet — created in Plan 04 + +Compound class → multi-class mapping (all occurrences to replace in templates): + "ui-button ui-button-solid-default-md" → "ui-button ui-button-solid ui-button-default ui-button-md" + "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md" + "ui-button ui-button-soft-danger-md" → "ui-button ui-button-soft ui-button-danger ui-button-md" + + + + + + Task 1: Update ButtonClass() multi-class output + button.templ + update ui_test.go button assertions + + backend/internal/web/ui/variants.go, + backend/internal/web/ui/button.templ, + backend/internal/web/ui/ui_test.go + + + - backend/internal/web/ui/variants.go (current — confirm ButtonClass() still uses old pattern after Plan 01) + - backend/internal/web/ui/button.templ (current — full file) + - backend/internal/web/ui/ui_test.go (current — find TestButton_DefaultSolidMD, TestButtonClass_String assertions to update) + - go-backend/internal/web/ui/button.templ (source for updated Props struct with Icon field) + - 13-PATTERNS.md (button.templ target pattern section) + - 13-RESEARCH.md (Multi-Class Button Pattern section, Code Examples section) + + + - Test: ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) == "ui-button ui-button-solid ui-button-default ui-button-md" + - Test: ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) contains "ui-button-ghost" and does NOT contain "ui-button-solid" + - Test: Button(ButtonProps{Label: "x"}) renders class attribute containing "ui-button ui-button-solid" (not "ui-button-solid-default-md") + + + Step 1 — Update the test assertions in ui_test.go (they will now FAIL — this is intentional RED state): + a. TestButton_DefaultSolidMD: change wantClass from "ui-button ui-button-solid-default-md" + to a for-loop checking for each of: "ui-button", "ui-button-solid", "ui-button-default", "ui-button-md" + using the multi-assertion slice pattern from 13-PATTERNS.md. + b. TestButtonClass_String: change want from "ui-button ui-button-solid-default-md" + to "ui-button ui-button-solid ui-button-default ui-button-md". + Run go test ./internal/web/ui/... — confirm these two tests FAIL (RED). + + Step 2 — Update ButtonClass() in variants.go (GREEN): + Change the return statement from: + "ui-button ui-button-" + string(t) + "-" + string(v) + "-" + string(s) + to: + "ui-button ui-button-" + string(t) + " ui-button-" + string(v) + " ui-button-" + string(s) + The ghost variant is a special case: when variant is ButtonVariantGhost, the tone class is + omitted. Add a conditional: if v == ButtonVariantGhost, return "ui-button ui-button-ghost ui-button-" + string(s). + + Step 3 — Update button.templ: + a. Add "Icon string" field to ButtonProps struct (after the Size field). + b. Replace the inline btnType variable logic with: type={ buttonType(props.Type) }. + c. Replace the class variable lookup with: class={ ButtonClass(props.Variant, props.Tone, props.Size) }. + d. Add icon rendering inside the button: if props.Icon != "" { — leave + a placeholder comment "// UIIcon added in Plan 04" because UIIcon does not exist yet. + DO NOT reference UIIcon yet — it does not compile until Plan 04. Instead, skip the icon rendering + entirely in the templ for now: the Icon field is present in the struct, but icon rendering is + wired in Plan 04 when icon_button.templ creates UIIcon. + + Run go test ./internal/web/ui/... — confirm all tests pass (GREEN). + Run cd backend && just generate — confirm templ generates without errors. + + + cd backend && just generate && go test ./internal/web/ui/... -run "TestButton" -v + + + - ButtonClass(ButtonVariantDefault, ButtonToneSolid, SizeMD) returns "ui-button ui-button-solid ui-button-default ui-button-md" (not the old compound form) + - ButtonClass(ButtonVariantGhost, ButtonToneSolid, SizeMD) returns "ui-button ui-button-ghost ui-button-md" + - TestButton_DefaultSolidMD passes with multi-class assertions + - TestButtonClass_String passes with new expected string + - button.templ uses buttonType(props.Type) helper + - ButtonProps struct contains "Icon string" field + - just generate succeeds (no templ compile errors) + - All other existing tests still pass (TestButton_PassesThroughAttrs, TestButton_ExplicitTypeSubmit) + + ButtonClass() emits multi-class output; button.templ updated with Icon field and buttonType helper; all button tests green + + + + Task 2: Replace button.css + badge.css + card.css; migrate card.templ; update template hardcodes; extend ui_test.go + + backend/internal/web/ui/button.css, + backend/internal/web/ui/badge.css, + backend/internal/web/ui/card.css, + backend/internal/web/ui/card.templ, + backend/internal/web/ui/ui_test.go, + backend/templates/planning.templ, + backend/templates/tasks.templ, + backend/templates/events.templ, + backend/templates/etapes.templ + + + - backend/internal/web/ui/button.css (current — 180 lines; lines 1–120 are button CSS, lines 121–180 are auth-provider CSS that was extracted in Plan 01 but the file still needs full review) + - backend/internal/web/ui/badge.css (current — see current selectors) + - backend/internal/web/ui/card.css (current — see current selectors) + - backend/internal/web/ui/card.templ (current — children-based API to replace) + - backend/internal/web/ui/ui_test.go (current — TestCard_RendersChildren to rewrite) + - go-backend/internal/web/ui/button.css (source for multi-class selectors) + - go-backend/internal/web/ui/badge.css (source for pill shape + token colors) + - go-backend/internal/web/ui/card.css (source for header/body/footer pattern) + - go-backend/internal/web/ui/card.templ (source for typed Props API) + - 13-PATTERNS.md (button.css, badge.css, card.css, card.templ, template hardcode sections) + - 13-RESEARCH.md (Pitfall 1 — template compound class strings; Pitfall 3 — Card API break) + + + - Test (rewritten): Card(CardProps{Body: textComponent("hello")}) renders "ui-card-body" in HTML and "hello" inside it + - Test (new): BadgeClass(BadgeVariantPrimary) returns "ui-badge ui-badge-primary" + - Test (new): Badge(BadgeProps{Label: "x", Variant: BadgeVariantPrimary}) HTML contains "ui-badge-primary" + + + Step 1 — Rewrite TestCard_RendersChildren in ui_test.go to TestCard_RendersTypedRegions: + Add textComponent helper function (from 13-PATTERNS.md) and add imports "io". + New test passes CardProps{Header: textComponent("header"), Body: textComponent("body")} + and asserts "ui-card-header", "header", "ui-card-body", "body" are all in the output. + Add test for nil footer: CardProps{Body: textComponent("x")} must NOT contain "ui-card-footer". + Add TestBadge_PrimaryVariant: Badge(BadgeProps{Label: "x", Variant: BadgeVariantPrimary}) + must contain "ui-badge-primary". + Run go test — these will fail RED (card.templ not yet migrated, PrimaryVariant normalizer not yet applied to BadgeClass). + + Step 2 — Replace button.css with go-backend's multi-class version: + Port go-backend/internal/web/ui/button.css verbatim (multi-class selectors like + ".ui-button-solid.ui-button-default { background: var(--color-brand-primary); }"). + Do NOT include the auth-provider selectors (those are in auth.css from Plan 01). + After the go-backend content, append the ghost variant rules (per D-CA01): + .ui-button-ghost { background: transparent; color: var(--color-brand-primary); } + .ui-button-ghost:hover { background: var(--color-surface-brand-soft); } + .ui-button-ghost:focus-visible { box-shadow: 0 0 0 3px var(--color-focus-ring); outline: none; } + + Step 3 — Replace badge.css with go-backend's version: + Port go-backend/internal/web/ui/badge.css verbatim (pill shape, border-radius: 999px). + After the go-backend content, append the primary variant (per D-CA02): + .ui-badge-primary { background: var(--color-surface-brand-soft); border-color: rgba(128, 78, 236, 0.3); color: var(--color-text-brand); } + + Step 4 — Replace card.css with go-backend's version: + Port go-backend/internal/web/ui/card.css verbatim (ui-card, ui-card-header, ui-card-body, ui-card-footer). + + Step 5 — Migrate card.templ to typed Props API: + Replace the current children-based Card(attrs templ.Attributes) with: + type CardProps struct { Header templ.Component; Body templ.Component; Footer templ.Component } + templ Card(props CardProps) — with nil-guard conditionals for each region (see 13-PATTERNS.md). + + Step 6 — Update compound class strings in templates. For each file, replace all occurrences: + planning.templ: "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md", + "ui-button ui-button-solid-default-md" → "ui-button ui-button-solid ui-button-default ui-button-md" + tasks.templ: "ui-button ui-button-soft-danger-md flex-shrink-0 text-xs" → "ui-button ui-button-soft ui-button-danger ui-button-md flex-shrink-0 text-xs", + "ui-button ui-button-soft-neutral-md w-full text-left text-sm mt-2" → "ui-button ui-button-soft ui-button-neutral ui-button-md w-full text-left text-sm mt-2" + events.templ: "ui-button ui-button-soft-neutral-md" → "ui-button ui-button-soft ui-button-neutral ui-button-md" + etapes.templ: "ui-button ui-button-soft-neutral-md px-2" → "ui-button ui-button-soft ui-button-neutral ui-button-md px-2", + "ui-button ui-button-soft-danger-md px-2" → "ui-button ui-button-soft ui-button-danger ui-button-md px-2", + "ui-button ui-button-soft-neutral-md flex-shrink-0" → "ui-button ui-button-soft ui-button-neutral ui-button-md flex-shrink-0" + Do NOT strip Tailwind utility classes that follow the button classes — keep them as-is. + + Run just generate && go test ./... — all tests must pass GREEN. + + + cd backend && just generate && go test ./... -count=1 + + + - go test ./... passes with no failures + - just generate produces no errors + - button.css contains ".ui-button-solid.ui-button-default {" (multi-class compound selector) + - button.css contains ".ui-button-ghost {" (new ghost variant) + - button.css does NOT contain ".auth-provider-button" (that is in auth.css) + - badge.css contains "border-radius: 999px" (pill shape from go-backend) + - badge.css contains ".ui-badge-primary {" + - card.css contains ".ui-card-header," + - card.templ contains "type CardProps struct" + - card.templ contains "if props.Header != nil" + - planning.templ contains "ui-button ui-button-soft ui-button-neutral ui-button-md" and does NOT contain "ui-button-soft-neutral-md" + - tasks.templ does NOT contain "ui-button-soft-danger-md" + - TestCard_RendersTypedRegions passes (card renders typed regions correctly) + - TestBadge_PrimaryVariant passes (ui-badge-primary in output) + + button.css/badge.css/card.css replaced; card.templ migrated to typed API; all template compound class strings updated; full test suite green + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| Template HTML → browser | templ auto-escapes string interpolations; CSS class changes are compile-time constants | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-13-02-01 | Tampering | card.templ typed API migration | accept | No user input flows through CardProps fields at runtime; templ auto-escapes all string interpolations | +| T-13-02-02 | Information Disclosure | button.css ghost variant | accept | CSS is a public static asset; ghost variant is visual styling only, no sensitive data | + + + +After this plan completes: +- cd backend && just generate — must succeed (no templ errors) +- cd backend && go test ./... -count=1 — must be green across all packages +- grep -r 'ui-button-soft-neutral-md\|ui-button-solid-default-md\|ui-button-soft-danger-md' backend/templates/ — must return nothing +- grep 'ui-button-solid.ui-button-default' backend/internal/web/ui/button.css — must match +- grep 'ui-badge-primary' backend/internal/web/ui/badge.css — must match +- grep 'type CardProps struct' backend/internal/web/ui/card.templ — must match + + + +1. ButtonClass() emits "ui-button ui-button-solid ui-button-default ui-button-md" (multi-class) +2. button.css uses multi-class compound selectors, includes ghost variant, excludes auth-provider CSS +3. badge.css is pill-shape, includes primary variant +4. card.templ uses typed Header/Body/Footer CardProps (no children passthrough) +5. All 4 template files have multi-class button strings (no compound "ui-button-solid-default-md") +6. Full test suite (go test ./...) is green + + + +After completion, create `.planning/phases/13-design-system-foundation/13-02-SUMMARY.md` + diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md new file mode 100644 index 0000000..1b216d6 --- /dev/null +++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-02-SUMMARY.md @@ -0,0 +1,127 @@ +--- +phase: 13-design-system-foundation +plan: "02" +subsystem: backend/internal/web/ui +tags: [css-migration, multi-class-pattern, component-api, design-system] +dependency_graph: + requires: [13-01] + provides: [multi-class-button-css, typed-card-api, pill-badge-css, ghost-button-variant] + affects: + - backend/internal/web/ui/button.css + - backend/internal/web/ui/button.templ + - backend/internal/web/ui/badge.css + - backend/internal/web/ui/card.css + - backend/internal/web/ui/card.templ + - backend/internal/web/ui/variants.go + - backend/internal/web/ui/ui_test.go + - backend/templates/planning.templ + - backend/templates/tasks.templ + - backend/templates/events.templ + - backend/templates/etapes.templ + - backend/templates/tablos.templ + - backend/templates/auth_login.templ + - backend/templates/auth_signup.templ +tech_stack: + added: [] + patterns: [multi-class-css-compound-selectors, typed-templ-component-props, tdd-red-green] +key_files: + created: [] + modified: + - backend/internal/web/ui/variants.go + - backend/internal/web/ui/button.templ + - backend/internal/web/ui/button.css + - backend/internal/web/ui/badge.css + - backend/internal/web/ui/card.css + - backend/internal/web/ui/card.templ + - backend/internal/web/ui/ui_test.go + - backend/templates/planning.templ + - backend/templates/tasks.templ + - backend/templates/events.templ + - backend/templates/etapes.templ + - backend/templates/tablos.templ + - backend/templates/auth_login.templ + - backend/templates/auth_signup.templ +decisions: + - "TabloCard wraps ui.Card in a
instead of passing id via Attrs — typed CardProps has no Attrs field; outer div preserves HTMX id-targeting without changing the delete/confirm zone selectors" + - "auth_login.templ and auth_signup.templ extract loginCardBody/signupCardBody as private templ components — cleaner than ComponentFunc inline; matches the typed Props pattern" + - "button.css adds .ui-button-soft.ui-button-neutral rule (not in go-backend) — original button.css had soft-neutral-md compound; must exist in new multi-class form to avoid visual regression" +metrics: + duration: "~15 minutes" + completed_date: "2026-05-16" + tasks: 2 + files: 14 +--- + +# Phase 13 Plan 02: Component API Migration — Multi-Class Button, Typed Card, New Variants Summary + +Multi-class button CSS and ButtonClass() output migration (compound to multi-class), card.templ rewritten to typed Header/Body/Footer Props API, badge.css and card.css replaced with go-backend token-based versions, all 4 template files updated from compound to multi-class button class strings. + +## Tasks Completed + +| Task | Name | Commit | Key Files | +|------|------|--------|-----------| +| 1 | Update ButtonClass() multi-class + button.templ + ui_test.go button assertions | 66f23bb | variants.go, button.templ, ui_test.go | +| 2 | Replace CSS files, migrate card.templ, update template hardcodes | a30a6f9 | button.css, badge.css, card.css, card.templ, ui_test.go, 6 template files | + +## What Was Built + +**Task 1 — ButtonClass() multi-class migration (TDD):** +- Updated `ButtonClass()` in `variants.go` from compound format (`ui-button ui-button-solid-default-md`) to multi-class format (`ui-button ui-button-solid ui-button-default ui-button-md`). +- Ghost variant special case: omits tone class, emits `ui-button ui-button-ghost ui-button-md`. +- Updated `button.templ`: added `Icon string` field to `ButtonProps`, replaced inline `btnType` variable with `buttonType(props.Type)` helper from `helpers.go`. +- Updated `ui_test.go`: `TestButton_DefaultSolidMD` now uses multi-class slice assertion pattern; `TestButtonClass_String` updated to new expected string; `TestButtonClass_GhostVariant` updated to assert standalone ghost format. +- TDD RED gate: tests failed before variants.go change (3 failures). GREEN: all 18 tests pass after. + +**Task 2 — CSS replacement + card.templ migration + template hardcode updates (TDD):** +- **button.css**: Replaced 180-line compound-class version with go-backend multi-class selector version (`.ui-button-solid.ui-button-default { ... }`). Added ghost variant rules (`.ui-button-ghost`). Added `.ui-button-soft.ui-button-neutral` (required for existing templates; not in go-backend). Auth-provider CSS correctly excluded (already in `auth.css` from Plan 01). +- **badge.css**: Replaced with go-backend pill-shape version (`border-radius: 999px`). Added `.ui-badge-primary` variant with brand-purple surface. +- **card.css**: Replaced with go-backend token-based version with `.ui-card-header`, `.ui-card-body`, `.ui-card-footer` padding rules. +- **card.templ**: Migrated from `Card(attrs templ.Attributes)` with children to `Card(CardProps)` with typed `Header/Body/Footer templ.Component` fields. Nil-guard conditionals for each region. +- **ui_test.go**: Removed `TestCard_RendersChildren` (old children API). Added `textComponent()` helper, `TestCard_RendersTypedRegions`, `TestBadge_PrimaryVariant`. Added `"io"` import. +- **Template hardcodes**: Updated all compound button class strings across 6 template files. +- **auth_login.templ / auth_signup.templ**: Migrated `@ui.Card(nil) { ... }` to `@ui.Card(ui.CardProps{Body: loginCardBody(...)})` with extracted private templ components. +- **tablos.templ**: `TabloCard` migrated from `@ui.Card(templ.Attributes{id:...}) { ... }` to `
@ui.Card(ui.CardProps{Body: tabloCardBody(...)})` with extracted `tabloCardBody` component. + +## Verification Results + +- `just generate`: succeeds (no templ compile errors) +- `go test ./... -count=1`: all packages pass (auth, db, files, jobs, web, web/ui, templates) +- `grep ".ui-button-solid.ui-button-default" backend/internal/web/ui/button.css`: matches +- `grep "ui-badge-primary" backend/internal/web/ui/badge.css`: matches +- `grep "type CardProps struct" backend/internal/web/ui/card.templ`: matches +- `grep -r "ui-button-soft-neutral-md|ui-button-solid-default-md|ui-button-soft-danger-md" backend/templates/`: no matches + +## Deviations from Plan + +### Auto-fixed Issues + +**1. [Rule 2 - Missing critical functionality] Added .ui-button-soft.ui-button-neutral to button.css** +- **Found during:** Task 2, Step 2 +- **Issue:** The original button.css had a `.ui-button-soft-neutral-md` compound rule. After migrating to multi-class, `.ui-button-soft.ui-button-neutral` must exist in button.css for the neutral soft buttons in planning.templ, events.templ, and etapes.templ to render correctly. The go-backend button.css only has `.ui-button-solid.ui-button-neutral`, not `.ui-button-soft.ui-button-neutral`. +- **Fix:** Added `.ui-button-soft.ui-button-neutral` with hover/active rules to button.css using the same token variable pattern as other soft variants. +- **Files modified:** `backend/internal/web/ui/button.css` +- **Commit:** a30a6f9 + +**2. [Rule 3 - Blocking issue] Migrated auth_login.templ, auth_signup.templ, tablos.templ Card call sites** +- **Found during:** Task 2, Step 5 +- **Issue:** The plan stated "No production templates call Card with children" but 3 templates used the old `Card(attrs)` API: auth_login.templ, auth_signup.templ, and tablos.templ. These were breaking build failures. +- **Fix:** Extracted content into private templ components (`loginCardBody`, `signupCardBody`, `tabloCardBody`) and passed them as `CardProps.Body`. For TabloCard, the `id` attribute moved to a wrapping `
` since `CardProps` has no Attrs field. HTMX targeting via `tablo-delete-zone` class is unaffected. +- **Files modified:** `backend/templates/auth_login.templ`, `backend/templates/auth_signup.templ`, `backend/templates/tablos.templ` +- **Commit:** a30a6f9 + +## Known Stubs + +None — all CSS changes are complete multi-class implementations. All template call sites fully migrated. + +## Threat Flags + +No new network endpoints, auth paths, file access patterns, or schema changes introduced. + +## TDD Gate Compliance + +- RED gate Task 1: 3 test failures (TestButton_DefaultSolidMD, TestButtonClass_String, TestButtonClass_GhostVariant) before variants.go update +- GREEN gate Task 1: commit 66f23bb — all 18 tests pass +- RED gate Task 2: compile failure (`undefined: CardProps`) + badge test fail before card.templ migration +- GREEN gate Task 2: commit a30a6f9 — full test suite (all packages) green + +## Self-Check: PASSED diff --git a/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md new file mode 100644 index 0000000..54d71f7 --- /dev/null +++ b/.planning/milestones/v3.0-phases/13-design-system-foundation/13-03-PLAN.md @@ -0,0 +1,317 @@ +--- +phase: 13-design-system-foundation +plan: 03 +type: execute +wave: 3 +depends_on: + - 13-01 + - 13-02 +files_modified: + - backend/internal/web/ui/input.css + - backend/internal/web/ui/input.templ + - backend/internal/web/ui/textarea.css + - backend/internal/web/ui/textarea.templ + - backend/internal/web/ui/select.css + - backend/internal/web/ui/select.templ + - backend/internal/web/ui/select_helpers.go + - backend/internal/web/ui/form-field.css + - backend/internal/web/ui/form_field.templ + - backend/internal/web/ui/ui_test.go +autonomous: true +requirements: + - DS-03 + +must_haves: + truths: + - "Input component renders with class ui-input and respects ID/Name/Placeholder/Type props" + - "Textarea component renders with class ui-textarea and respects Rows default (4 when 0)" + - "Select component renders with class ui-select-control, inline JS for open/close, and HTMX re-init listener" + - "FormField wraps any component with label, hint, and error regions" + - "All four CSS files exist and contain their primary .ui-* selectors" + - "go test ./internal/web/ui/... passes for all new component tests" + - "just generate succeeds — all new .templ files compile" + artifacts: + - path: "backend/internal/web/ui/input.templ" + provides: "Input component with InputProps" + contains: "type InputProps struct" + - path: "backend/internal/web/ui/textarea.templ" + provides: "Textarea component with TextareaProps" + contains: "type TextareaProps struct" + - path: "backend/internal/web/ui/select.templ" + provides: "Select component with inline JS" + contains: "SelectProps" + - path: "backend/internal/web/ui/select_helpers.go" + provides: "Select helper functions" + contains: "selectPlaceholder" + - path: "backend/internal/web/ui/form_field.templ" + provides: "FormField wrapper component" + contains: "type FormFieldProps struct" + key_links: + - from: "backend/internal/web/ui/select.templ" + to: "select_helpers.go" + via: "selectPlaceholder(), selectOptionSelected() calls" + pattern: "selectPlaceholder" + - from: "backend/internal/web/ui/textarea.templ" + to: "helpers.go" + via: "textareaRows(), inputID() calls" + pattern: "textareaRows" +--- + +## Phase Goal + +**As a** developer, **I want to** have Input, Textarea, Select, and FormField components available as templ components with matching CSS, **so that** form-heavy views in Phases 14–17 can use these components instead of raw HTML form elements. + + +Port all four form-input component types from go-backend to backend. Each component delivers +a complete vertical slice: CSS file → templ Props struct → templ rendering → test coverage. + +Purpose: These are the form primitives that all subsequent phases need. Auth pages (Phase 14) +need Input and FormField. Tablo create/edit dialogs (Phase 15-16) need all four. + +Output: +- input.css + input.templ (update existing stub to match go-backend API) +- textarea.css + textarea.templ (new files — port from go-backend) +- select.css + select.templ + select_helpers.go (new files — complex, includes inline JS) +- form-field.css + form_field.templ (new files — wrapper component) +- ui_test.go extended with tests for all four component types + + + +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/workflows/execute-plan.md +@/Users/arthur.belleville/Documents/perso/projects/xtablo-source/.claude/get-shit-done/templates/summary.md + + + +@.planning/phases/13-design-system-foundation/13-CONTEXT.md +@.planning/phases/13-design-system-foundation/13-RESEARCH.md +@.planning/phases/13-design-system-foundation/13-PATTERNS.md +@.planning/phases/13-design-system-foundation/13-01-SUMMARY.md +@.planning/phases/13-design-system-foundation/13-02-SUMMARY.md + + + +From backend/internal/web/ui/helpers.go (after Plan 01): + func inputType(value string) string // returns "text" if empty + func inputID(id string, name string) string // returns id if non-empty, else name + func textareaRows(rows int) string // returns "4" if rows <= 0 + +From go-backend/internal/web/ui/select_helpers.go (source to port): + func selectPlaceholder(props SelectProps) string + func selectNativeID(id, name string) string + func selectMenuID(id, name string) string + func selectBoolData(b bool) string + func selectOptionSelected(option SelectOption, values []string) bool + func selectSelectedLabels(props SelectProps) string + func selectSelectedLabel(props SelectProps) string + func selectMenuOptionClass(option SelectOption, values []string) string + func selectIsDisabled(props SelectProps, option SelectOption) bool + + + + + + Task 1: Port input.templ, input.css, textarea.templ, textarea.css + tests + + backend/internal/web/ui/input.css, + backend/internal/web/ui/input.templ, + backend/internal/web/ui/textarea.css, + backend/internal/web/ui/textarea.templ, + backend/internal/web/ui/ui_test.go + + + - backend/internal/web/ui/input.templ (existing stub — see current InputProps fields to understand what is already there) + - go-backend/internal/web/ui/input.templ (source Props struct and rendering pattern) + - go-backend/internal/web/ui/input.css (source CSS — port verbatim) + - go-backend/internal/web/ui/textarea.templ (source Props struct and rendering pattern) + - go-backend/internal/web/ui/textarea.css (source CSS — port verbatim) + - 13-PATTERNS.md (input.templ, textarea.templ, input.css, textarea.css sections) + - 13-RESEARCH.md (Props Struct Alignment Detail — Input and Textarea sections) + - 13-UI-SPEC.md (Input and Textarea component inventory sections) + + + - Test: Input(InputProps{Name: "email", Type: "email"}) HTML contains 'class="ui-input"' and 'type="email"' + - Test: Input(InputProps{Name: "x"}) HTML contains 'type="text"' (default via inputType helper) + - Test: Input(InputProps{ID: "my-id", Name: "x"}) HTML contains 'id="my-id"' + - Test: Input(InputProps{Name: "x"}) with no ID — HTML contains 'id="x"' (inputID fallback to name) + - Test: Textarea(TextareaProps{Name: "body"}) HTML contains 'class="ui-textarea"' + - Test: Textarea(TextareaProps{Name: "x", Rows: 0}) HTML contains 'rows="4"' (default via textareaRows) + - Test: Textarea(TextareaProps{Name: "x", Rows: 6}) HTML contains 'rows="6"' + + + Step 1 — Write failing tests in ui_test.go (RED): + Add TestInput_DefaultType, TestInput_EmailType, TestInput_IDFallback, + TestTextarea_DefaultRows, TestTextarea_ExplicitRows. + Run go test ./internal/web/ui/... — confirm failures. + + Step 2 — Create/update input.css: port go-backend/internal/web/ui/input.css verbatim. + The file must contain .ui-input { } selector with: appearance: none, background, border, + border-radius: 0.75rem, color, font: inherit, line-height: 1.4, min-height: 44px, + padding: 0.75rem 0.95rem, width: 100%. + Strip any page-level selectors (body, :root). + + Step 3 — Update input.templ to match go-backend's Props API: + InputProps must have: Name string, ID string, Type string, Placeholder string, Value string, Attrs templ.Attributes. + The templ function uses: inputID(props.ID, props.Name) for id, inputType(props.Type) for type. + Per D-CA03 (Claude's discretion on props alignment) and UI-SPEC explicit fields: also add + Disabled bool and Required bool with conditional attribute rendering: + if props.Disabled { disabled } and if props.Required { required } in the element. + + Step 4 — Create textarea.css: port go-backend/internal/web/ui/textarea.css verbatim. + Must contain .ui-textarea { } with: same appearance/border/border-radius/color/font as input, + min-height: 7rem, resize: vertical, width: 100%. + + Step 5 — Create textarea.templ: new file in backend/internal/web/ui/textarea.templ. + package ui declaration at top. TextareaProps must have: Name string, ID string, + Placeholder string, Value string, Rows int, Disabled bool, Required bool, Attrs templ.Attributes. + The templ function renders +} +``` + +**Helpers used:** `inputID()` and `textareaRows()` from `helpers.go` (add these in Wave 1). + +--- + +### `backend/internal/web/ui/textarea.css` (config, static — NEW FILE) + +Port verbatim from go-backend. File mirrors input.css styling with `min-height: 7rem` and `resize: vertical`. + +--- + +### `backend/internal/web/ui/select.templ` + `select_helpers.go` (component, event-driven — NEW FILES) + +**Analog:** `go-backend/internal/web/ui/select.templ` + `go-backend/internal/web/ui/select_helpers.go` + +**Props pattern** (`go-backend/internal/web/ui/select.templ` lines 1–18): +```go +type SelectOption struct { + Value string + Label string + Disabled bool +} +type SelectProps struct { + ID string + Name string + Placeholder string + Value string + Values []string + Multiple bool + Options []SelectOption + Attrs templ.Attributes +} +``` + +**Inline script pattern** (`go-backend/internal/web/ui/select.templ` lines 114–249): +The ` + + +} +``` + +AuthLayout signature (from PATTERNS.md): +```go +templ AuthLayout(title string, csrfToken string) { ... } +// No auth.User param — auth pages are always unauthenticated +// No sortable.min.js or discussion-sse.js — not needed on auth pages +``` + +From go-backend/internal/web/views/auth_components.templ (reference — package name changes): +- AnimatedBackground: 35 .background-logo elements; single img per element (Phase 14 uses logo_dark.png only, no light-only/dark-only pair) +- GoogleButton: accepts (href string, configured bool); when configured, + } +} +``` + +Note: go-backend `GoogleButton` is a ` +
+ + @SidebarOrganization(user) + + +} +``` + +**Note:** go-backend's sidebar nav links use `hx-get` + `hx-target="#app-main-content"` for HTMX partial swaps. In the backend, per D-N02, routes that don't exist render as visual-only items (no href, cursor: default). For existing routes, use standard `` without HTMX (full-page navigation is fine for Phase 15 — no SPA swap is required). + +**OOB constraint** (`tablos.templ` lines 172–179 — established pattern to follow): +```go +// OOB fragments MUST be top-level siblings, never nested inside AppLayout: +templ TabloCardWithOOBFormClear(tablo sqlc.Tablo, csrfToken string) { + @TabloCard(TabloCardFromTablo(tablo), csrfToken) +
+} +``` + +--- + +### `backend/templates/app_layout_helpers.go` (new — utility, pure function) + +**Analog:** `go-backend/internal/web/views/home.go` lines 17–39 + +**Exact pattern to copy** (go-backend `home.go` lines 1–39): +```go +package templates // NOTE: package is "templates" not "views" + +import "strings" + +func sidebarNavItemClass(active bool) string { + if active { + return "sidebar-nav-item is-active" + } + return "sidebar-nav-item" +} + +func isActivePath(activePath string, href string) bool { + return strings.TrimSpace(activePath) != "" && activePath == href +} + +func sidebarNavItemID(href string) string { + switch href { + case "/": + return "sidebar-nav-home" + default: + slug := strings.Trim(strings.ReplaceAll(href, "/", "-"), "-") + if slug == "" { + slug = "item" + } + return "sidebar-nav-" + slug + } +} +``` + +**sidebarNavItem struct** (go-backend `home.go` lines 47–53): +```go +type sidebarNavItem struct { + Href string + Label string + Icon string + Active bool + DividerAfter bool +} +``` + +**Nav items slice builder** (go-backend `home.go` lines 158–167 — adapt to English labels + backend routes): +```go +func sidebarPrimaryNavItems(activePath string) []sidebarNavItem { + return []sidebarNavItem{ + {Href: "/", Label: "Dashboard", Icon: "panels", Active: isActivePath(activePath, "/"), DividerAfter: true}, + // Tasks, Chat, Files — visual-only (no Href, or Href: "" per D-N02) + {Href: "/planning", Label: "Planning", Icon: "planning", Active: isActivePath(activePath, "/planning")}, + } +} +``` + +--- + +### `backend/templates/tablos.templ` (modified — components, request-response) + +**Analog:** `backend/templates/tablos.templ` (current file, lines 1–526) — same file, pattern evolution + +**Signature change pattern** — follow the call-forwarding convention in `tablos.templ` line 13: +```go +// BEFORE (line 12–13): +templ TablosDashboard(user *auth.User, csrfToken string, tablos []TabloCardView) { + @Layout("Tablos — Xtablo", user, csrfToken) { + +// AFTER — new signature forwards activePath + tablos slice to AppLayout: +templ TablosDashboard(user *auth.User, csrfToken string, activePath string, tablos []sqlc.Tablo, cards []TabloCardView) { + @AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos) { +``` + +**Section heading pattern** (from go-backend `dashboard_components.templ` lines 238–244 — `.overview-section-heading`): +```go +
+
+

Your Tablos

+ // New tablo button + #create-form-slot go here (D-C05) +
+
+ // cards or empty state +
+
+``` + +**Project card pattern** (adapted from go-backend `tablos.templ` lines 178–214, preserving backend HTMX attrs): +```go +templ TabloProjectCard(card TabloCardView, csrfToken string) { +
+
+
+ @ui.IconButton(ui.IconButtonProps{ + Label: "Edit tablo", + Icon: "pencil", + Variant: ui.IconButtonVariantNeutral, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + // PRESERVE existing HTMX attrs from current tablos.templ (Pitfall 3) + }, + }) + @ui.IconButton(ui.IconButtonProps{ + Label: "Delete tablo", + Icon: "trash", + Variant: ui.IconButtonVariantDanger, + Tone: ui.IconButtonToneGhost, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/" + card.Tablo.ID.String() + "/delete-confirm", + "hx-target": "closest .tablo-delete-zone", + "hx-swap": "outerHTML", + }, + }) +
+
+
+ + +

{ card.Tablo.Title }

+
+
+ { card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") } +
+
+} +``` + +**Color null-safety pattern** — already established in current `tablos.templ` lines 85–90: +```go +// ESTABLISHED: always guard pgtype.Text with .Valid check +if card.Tablo.Color.Valid && card.Tablo.Color.String != "" { + style={ "background-color: " + card.Tablo.Color.String } +} +``` + +**CreatedAt formatting** — from Pitfall 6 in RESEARCH.md (pgtype.Timestamptz requires `.Time` accessor): +```go +// CORRECT: +card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") +// WRONG (compiler error): +card.Tablo.CreatedAt.Format("Jan 2, 2006") +``` + +**EmptyState pattern** (`backend/internal/web/ui/empty_state.templ` lines 10–27): +```go +// Replace TablosEmptyState raw HTML with: +@ui.EmptyState(ui.EmptyStateProps{ + Title: "No tablos yet", + Description: "Create your first tablo to get started.", + Action: ui.Button(ui.ButtonProps{ + Label: "New tablo", + Variant: ui.ButtonVariantDefault, + Tone: ui.ButtonToneSolid, + Size: ui.SizeMD, + Type: "button", + Attrs: templ.Attributes{ + "hx-get": "/tablos/new", + "hx-target": "#create-form-slot", + "hx-swap": "innerHTML", + }, + }), +}) +``` + +**SidebarProjectsSection sub-component** (adapted from go-backend `dashboard_components.templ` lines 103–115 — use `sqlc.Tablo` not `tablomodel.Record`): +```go +templ SidebarProjectsSection(tablos []sqlc.Tablo) { +
+} +``` + +**SidebarOrganization footer** (adapted from go-backend `dashboard_components.templ` lines 131–143 — use `user.Email` and include logout): +```go +templ SidebarOrganizationFooter(user *auth.User, csrfToken string) { + +} +``` + +**TabloDetailPage / TabloNotFoundPage layout switch** (current `tablos.templ` lines 187–188, 515–516): +```go +// BEFORE: +@Layout("Tablos — Xtablo", user, csrfToken) { +// AFTER (D-L01 — all authenticated pages switch): +@AppLayout("Tablos — Xtablo", user, csrfToken, activePath, tablos) { +``` + +--- + +### `backend/internal/web/ui/app.css` (new — CSS) + +**Analog:** `backend/internal/web/ui/auth.css` — same pattern: verbatim port of CSS sections from go-backend into a new file + +**Go-backend source:** `go-backend/internal/web/ui/app.css` + +**Sections to port verbatim** (line ranges from go-backend app.css): + +| Section | Lines | Classes | +|---|---|---| +| Dashboard shell grid | 455–465 | `.dashboard-shell`, `.dashboard-sidebar` | +| Sidebar nav shell | 467–479 | `.sidebar-nav-shell` | +| Sidebar brand | 481–509 | `.sidebar-brand`, `.sidebar-brand-link`, `.sidebar-brand-logo`, `.sidebar-brand-title` | +| Sidebar collapse button | 511–527 | `.sidebar-collapse-button` (non-functional in Phase 15) | +| Sidebar primary + list | 533–558 | `.sidebar-primary`, `.sidebar-list`, `.sidebar-divider` | +| Sidebar nav items | 560–605 | `.sidebar-nav-item`, `.sidebar-nav-item.is-active`, `.sidebar-nav-link`, `.sidebar-nav-link-inner`, `.sidebar-nav-icon`, `.sidebar-nav-label` | +| Sidebar projects | 607–668 | `.sidebar-projects`, `.sidebar-section-label`, `.sidebar-project-list`, `.sidebar-project-link`, `.sidebar-project-icon`, `.sidebar-project-label` | +| Sidebar footer links | 670–673 | `.sidebar-footer-links` | +| Sidebar organization | 675–732 | `.sidebar-organization`, `.organization-button`, `.organization-avatar`, `.organization-name`, `.organization-meta` | +| Dashboard main | 734–741 | `.dashboard-main` | +| Overview section heading | 875–892 | `.overview-section`, `.overview-section-heading` | +| Project grid | 894–899 | `.project-grid` | +| Project card | 900–914 | `.project-card`, `.project-card-top` | +| Project card icon button overrides | 945–963 | **Adapt** go-backend's `.borderless-icon-button` overrides to use backend class names (see Shared Patterns below) | +| Project title/avatar | 986–1005 | `.project-card-title-row`, `.project-avatar` | +| Project date row | 1039–1046 | `.project-date-row` | + +**Critical:** All color values in go-backend app.css already use `var(--...)` tokens — port verbatim without substitution. + +--- + +### `backend/tailwind.input.css` (modified — config) + +**Analog:** `backend/tailwind.input.css` (self, current — lines 1–20) + +**Pattern to follow** (current file lines 7–20): +```css +@import "./internal/web/ui/base.css"; +@import "./internal/web/ui/auth.css"; +/* ... existing imports ... */ +@import "./internal/web/ui/spacing.css"; +``` + +**Change:** Add one new line after the existing imports: +```css +@import "./internal/web/ui/app.css"; +``` + +--- + +### `backend/internal/web/handlers_tablos.go` (modified — handler) + +**Analog:** `backend/internal/web/handlers_tablos.go` lines 39–55 (current `TablosListHandler`) + +**Current pattern** (lines 39–55): +```go +func TablosListHandler(deps TablosDeps) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, user, _ := auth.Authed(r.Context()) + + tabloRows, err := deps.Queries.ListTablosByUserWithDiscussionUnread(r.Context(), user.ID) + if err != nil { + slog.Default().Error("tablos list: query failed", "user_id", user.ID, "err", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return + } + if tabloRows == nil { + tabloRows = []sqlc.ListTablosByUserWithDiscussionUnreadRow{} + } + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + _ = templates.TablosDashboard(user, csrf.Token(r), templates.TabloCardsFromUnreadRows(tabloRows)).Render(r.Context(), w) + } +} +``` + +**Change pattern** — derive `[]sqlc.Tablo` from already-fetched cardViews (no second query per RESEARCH.md Pattern 5): +```go +cardViews := templates.TabloCardsFromUnreadRows(tabloRows) + +// Derive sidebar tablos from already-fetched data (no extra DB query) +tablos := make([]sqlc.Tablo, 0, len(cardViews)) +for _, cv := range cardViews { + tablos = append(tablos, cv.Tablo) +} + +_ = templates.TablosDashboard(user, csrf.Token(r), "/", tablos, cardViews).Render(r.Context(), w) +``` + +**Error handling pattern** — unchanged, copy from lines 44–48 verbatim. + +--- + +### `backend/internal/web/handlers_planning.go` (modified — handler) + +**Analog:** `backend/internal/web/handlers_planning.go` lines 34–58 (self, current) + +**Current pattern** (lines 34–58): +```go +func PlanningPageHandler(deps PlanningDeps) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + _, user, _ := auth.Authed(r.Context()) + // ... fetch events ... + _ = templates.PlanningPage(user, csrf.Token(r), agenda).Render(r.Context(), w) + } +} +``` + +**Change pattern** — add tablos fetch before template render: +```go +// Fetch tablos for sidebar (PlanningDeps already has Queries *sqlc.Queries) +tablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID) +if err != nil { + slog.Default().Error("planning: ListTablosByUser failed", "user_id", user.ID, "err", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return +} +if tablos == nil { + tablos = []sqlc.Tablo{} +} + +// Pass tablos + activePath to template +_ = templates.PlanningPage(user, csrf.Token(r), "/planning", tablos, agenda).Render(r.Context(), w) +``` + +--- + +## Shared Patterns + +### CSS @import registration +**Source:** `backend/tailwind.input.css` lines 7–20 +**Apply to:** New `app.css` file +```css +/* Add to tailwind.input.css after existing imports: */ +@import "./internal/web/ui/app.css"; +``` + +### pgtype.Text null-safety +**Source:** `backend/templates/tablos.templ` lines 85–90 +**Apply to:** All color-rendering spans in `app_layout.templ` and `tablos.templ` +```go +if card.Tablo.Color.Valid && card.Tablo.Color.String != "" { + style={ "background-color: " + card.Tablo.Color.String } +} +``` + +### pgtype.Timestamptz formatting +**Source:** RESEARCH.md Pitfall 6 (verified against `sqlc.Tablo` type) +**Apply to:** Project card date row in `tablos.templ` +```go +// Always use .Time to unwrap pgtype.Timestamptz: +card.Tablo.CreatedAt.Time.Format("Jan 2, 2006") +``` + +### CSRF field +**Source:** `backend/templates/tablos.templ` line 121 / `layout.templ` lines 41–42 +**Apply to:** All form fragments inside `app_layout.templ` (logout form) +```go +@ui.CSRFField(csrfToken) +``` + +### IconButton class names (VERIFIED) +**Source:** `backend/internal/web/ui/variants.go` lines 179–187 +**Apply to:** `app.css` project-card icon button overrides + +``` +IconButtonClass(IconButtonVariantNeutral, IconButtonToneGhost) + → "borderless-icon-button ui-icon-button-ghost ui-icon-button-neutral" + +IconButtonClass(IconButtonVariantDanger, IconButtonToneGhost) + → "borderless-icon-button ui-icon-button-ghost ui-icon-button-danger" +``` + +The `.borderless-icon-button` class is already in `icon-button.css`. So `app.css` project-card overrides should target: +```css +/* Adapted from go-backend app.css lines 945–963 — using verified backend class names */ +.project-card-top .borderless-icon-button { + padding: 0; +} +.project-card-top .ui-icon-button-ghost.ui-icon-button-neutral:hover { + color: var(--color-text-primary); +} +.project-card-top .ui-icon-button-ghost.ui-icon-button-danger:hover { + color: var(--color-status-danger-icon-hover); +} +``` + +### Handler error + render pattern +**Source:** `backend/internal/web/handlers_tablos.go` lines 44–54 +**Apply to:** Any new tablos-fetch added to other handlers +```go +tablos, err := deps.Queries.ListTablosByUser(r.Context(), user.ID) +if err != nil { + slog.Default().Error(": ListTablosByUser failed", "user_id", user.ID, "err", err) + http.Error(w, "internal server error", http.StatusInternalServerError) + return +} +if tablos == nil { + tablos = []sqlc.Tablo{} +} +``` + +### templ.SafeURL for dynamic links +**Source:** `backend/templates/tablos.templ` line 98 +**Apply to:** All dynamic `href` attributes in `app_layout.templ` +```go +href={ templ.SafeURL("/tablos/" + tablo.ID.String()) } +``` + +--- + +## No Analog Found + +All Phase 15 files have analogs in the existing codebase. No file requires building from RESEARCH.md patterns alone. + +--- + +## Key Notes for Planner + +1. **`account_providers.templ`** also calls `@Layout(...)` (verified via grep in RESEARCH.md) — planner should include it in the layout-switch task if Phase 15 scope covers all authenticated pages. + +2. **SVG icons:** The `SidebarNavItem` templ calls `@SidebarIcon(item.Icon)` in go-backend. The backend's equivalent is `@ui.UIIcon(kind)` (already in `icon_button.templ` lines 18–74). The nav items can call `@ui.UIIcon(item.Icon)` directly, or a new `SidebarIcon` wrapper can delegate to it. The icon names from go-backend (`"panels"`, `"tasks"`, `"layers"`, `"planning"`, `"chat"`, `"files"`) must either match a `case` in `UIIcon` or be added. + +3. **Visual-only nav items** (D-N02): For Tasks, Files, Settings — render as `