docs(fase-10): history doc + CLAUDE.md status + plan closed

Records the closing state of Fase 10: spec coverage now 100% on the
H-series CUs, full FK on-delete audit table, 5 architectural decisions
(Keycloak not cleaned up on account delete, sole-member must dissolve,
type-the-name dissolve confirmation, client-side language detection,
trash via RPC). Bumps the MVP2 progress to 2/6 and the test totals
to 360 active + 3 skipped.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 02:43:29 +02:00
parent 378fcbe5f4
commit a868b5a314
3 changed files with 180 additions and 2 deletions

View File

@@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
**Fase 05 complete. Fase 6 complete (deploy readiness: PWA injectManifest SW + manifest + iOS meta tags + placeholder icons + Kong rate-limit on invitations + dev JWT rotation + prod env template + rls-audit.sh + Justfile lighthouse/rls-audit/test-rate-limit). Production stack deployed to ambrosio (OVH VPS) at https://erosi.limonia.net (app + Supabase, external Keycloak); edge proxy now off-stack — internal Caddy fans kong/app on host port 3000, whatever external proxy fronts the host terminates TLS and forwards plain HTTP to ambrosio:3000 (originally NetBird's self-hosted Traefik, now proxy-agnostic); PWA strategy switched to generateSW to unblock the prod build (see the "injectManifest hardcoded filename" gotcha below). Fase 7 complete: 12 new Playwright tests fill the collective-flow UI gap (O-01..O-03 onboarding, I-01..I-05 invitation acceptance, MC-02..MC-05 admin-manage) — writing them surfaced + fixed three latent bugs in the product code (atomic `create_collective()` RPC, pendingInvitationToken restore after login, manage page not reloading on late $currentCollective hydration). Plus migration 013 (auth.users role/aud default trigger) — found after the first real self-registration on prod via Keycloak→GoTrue left the user with empty `role`/`aud` columns, breaking every REST call with `role "" does not exist`. Post-fase-7 prod auth hardening (2026-04-20..21, see `docs/history/fase-8-auth-logout.md`): migration 014 extends the role/aud guard to UPDATE (GoTrue's pop-ORM resends role='' right after the INSERT and clobbered the INSERT trigger's backfill); `logout()` now does RP-initiated Keycloak end-session + lands on a new public `/logged-out` route, which both actually ends the Keycloak SSO cookie (so the next login re-prompts) and closes a PKCE-verifier race that produced "PKCE code verifier not found in storage" on prod. 259 tests green: 45 pgTAP + 140 Vitest integration + 15 Vitest unit + 59 Playwright + 1 Vitest rate-limit (gated, run via `just test-rate-limit`). 3 skipped in `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). Push notifications + CI Docker + final production icons deliberately out of scope. ✅**
**MVP2 — in progress (1/6 ✅).** Branch `mvp2`. **Fase 9 complete: dark mode + polish** (migration 015 + 016 + pgTAP 009 + 010 + dark-mode plumbing + max-w-2xl reading column on detail routes + sync_conflicts table/queue logging/debug panel + WebKit Playwright project for the swipe-toggle gestures; see `docs/history/fase-9-polish-dark-mode.md`). Test totals: 65 pgTAP + 144 Vitest integration + 26 Vitest unit + 65 Playwright + 1 Vitest rate-limit (gated) = **300 tests green**; 3 skipped in `just test-all` (2 Realtime presence — upstream `handle_out/3` bug `supabase/realtime#1617` still open as of 2026-05-18 / latest v2.86.3; 1 rate-limit — gated). WebKit specs (2 new in Fase 9.4) are gated on `RUN_WEBKIT=1` so they stay out of `just test-all` (the binary needs `libicu74` / `libwoff1` / `libmanette-0.2-0` system deps via `sudo pnpm exec playwright install-deps`). Remaining fases (📋): Fase 10 spec completion — CU-H06/H07/H08, trash restore, account hard-delete, sidebar create, list reset, lang detect (`plan/fase-10-spec-completion.md`) | Fase 11 item tags + drag-reorder (`plan/fase-11-item-tags-importance.md`, migration 021) | Fase 12 section visibility — JSONB `feature_flags` on `users`+`collectives`, `section_enabled()` SQL helper (`plan/fase-12-section-visibility.md`, migration 022) | Fase 13 server administration — `/admin` area, `server_admins` table, audit log, server-level visibility defaults, depends on Fase 12 (`plan/fase-13-server-admin.md`, migrations 023+024) | Fase 14 PWA hardening — update toast, offline polish, version chip, depends on Fase 9.3 (`plan/fase-14-pwa-hardening.md`, no migration). Fase 7+8 already shipped as MVP cleanup, not MVP2.
**MVP2 — in progress (2/6 ✅).** Branch `mvp2`. **Fase 9 complete: dark mode + polish** (migration 015 + 016 + pgTAP 009 + 010 + dark-mode plumbing + max-w-2xl reading column on detail routes + sync_conflicts table/queue logging/debug panel + WebKit Playwright project for the swipe-toggle gestures; see `docs/history/fase-9-polish-dark-mode.md`). **Fase 10 complete: spec completion (H06/H07/H08 + trash restore + account deletion + sidebar-create + reset-from-detail + Accept-Language)** — 5 new migrations (017 leave_collective, 018 dissolve_collective, 019 user-deletion FKs, 020 restore/hard-delete RPCs, 021 delete_account) + 4 new pgTAP suites + 6 new Playwright specs + Accept-Language parser; see `docs/history/fase-10-spec-completion.md`. Test totals: 96 pgTAP + 151 Vitest integration + 38 Vitest unit + 74 Playwright + 1 Vitest rate-limit (gated) = **360 tests green**; 3 skipped in `just test-all` (2 Realtime presence — upstream `handle_out/3` bug `supabase/realtime#1617` still open as of 2026-05-18 / latest v2.86.3; 1 rate-limit — gated). WebKit specs (2 in Fase 9.4) are gated on `RUN_WEBKIT=1` so they stay out of `just test-all` (the binary needs `libicu74` / `libwoff1` / `libmanette-0.2-0` system deps via `sudo pnpm exec playwright install-deps`). Remaining fases (📋): Fase 11 item tags + drag-reorder (`plan/fase-11-item-tags-importance.md`, migration 022) | Fase 12 section visibility — JSONB `feature_flags` on `users`+`collectives`, `section_enabled()` SQL helper (`plan/fase-12-section-visibility.md`, migration 023) | Fase 13 server administration — `/admin` area, `server_admins` table, audit log, server-level visibility defaults, depends on Fase 12 (`plan/fase-13-server-admin.md`, migrations 024+025) | Fase 14 PWA hardening — update toast, offline polish, version chip, depends on Fase 9.3 (`plan/fase-14-pwa-hardening.md`, no migration). Fase 7+8 already shipped as MVP cleanup, not MVP2.
## Documentation Map
@@ -15,6 +15,8 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- `docs/deployment.md` — production deploy to ambrosio (erosi.limonia.net) + prod-specific fixes
- `docs/history/fase-{1,2a,2b,3,4,5,6,7}.md` — per-phase record of what was built
- `docs/history/fase-8-auth-logout.md` — post-fase-7 prod auth hardening (migration 014 + RP-initiated logout)
- `docs/history/fase-9-polish-dark-mode.md` — MVP2 dark mode + reading width + sync-conflicts debug panel
- `docs/history/fase-10-spec-completion.md` — MVP2 CU-H06/H07/H08 + trash RPCs + account delete + sidebar create + reset from detail + auto language
- `analysis/analisis-funcional.md` — complete functional specification (domain model, use cases, data models, business rules)
- `plan/fase-*.md` — phase-by-phase implementation plans (Fase 0 through Fase 7)
- `design/slate_collective/DESIGN.md` + `design/*/screen.png` — "Monolith Editorial" direction + per-screen Stitch mockups. Consult before changing UI.

View File

@@ -0,0 +1,176 @@
# Fase 10 — Spec completion (MVP2 2/6)
**Status: ✅ complete · 2026-05-18.**
Closes the 8 functional-spec gaps audited on 2026-04-22:
1. CU-H06 Abandonar colectivo
2. CU-H07 Renombrar colectivo + cambiar emoji
3. CU-H08 Disolver colectivo
4. Trash drawer — restore + hard delete
5. Eliminación de cuenta (hard delete)
6. "Crear nuevo colectivo" desde la sidebar
7. Botón "Reiniciar lista" en vista de detalle
8. Detección automática de idioma (Accept-Language)
Spec coverage on the CU-H\* axis (collective management + user lifecycle)
is now 100 %.
---
## Migrations introduced
| # | File | Purpose |
|---|---|---|
| 017 | `017_leave_collective_rpc.sql` | `public.leave_collective(uuid)` — sole-admin auto-promote inline; P0001 for sole-member |
| 018 | `018_dissolve_collective_rpc.sql` | `public.dissolve_collective(uuid)` — admin only (P0002), cascade-driven |
| 019 | `019_user_deletion_fks.sql` | Relax every `created_by` / `updated_by` FK from `NOT NULL RESTRICT``NULL SET NULL` |
| 020 | `020_restore_list_rpc.sql` | `public.restore_list(uuid)` + `public.hard_delete_list(uuid)` with explicit role guards |
| 021 | `021_delete_account_rpc.sql` | `public.delete_account()` — two-step (`public.users` then `auth.users`); P0003 for sole-admin-stuck |
Migration **019** was held back from the trash work (10.4) and used here in 10.5 — that's why
the trash RPC migration is 020, leaving 019 as the FK-normalization seat.
---
## FK on-delete audit (Fase 10.5.1)
Before migration 019, deleting a `public.users` row would have been blocked
by every table where the user had ever created content. The relaxed
behaviour after 019:
| Table | Column | Before | After |
|---|---|---|---|
| `collectives` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `collective_members` | `user_id` | `NOT NULL CASCADE` | (unchanged — membership belongs to the user) |
| `collective_invitations` | `created_by` | `NOT NULL CASCADE` | `NULL SET NULL` |
| `collective_invitations` | `accepted_by` | `NULL SET NULL` | (unchanged) |
| `shopping_lists` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `shopping_items` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `shopping_items` | `checked_by` | `NULL SET NULL` | (unchanged) |
| `task_lists` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `tasks` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `tasks` | `completed_by` | `NULL SET NULL` | (unchanged) |
| `notes` | `created_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `notes` | `updated_by` | `NOT NULL RESTRICT` | `NULL SET NULL` |
| `sync_conflicts` | `user_id` | `NOT NULL CASCADE (→ auth.users)` | (unchanged — log entries vanish with the user) |
Implication: any UI that renders the author of a row must defensively
handle `NULL`. The intended label is "Deleted user" — the visual polish
is deferred to a later cosmetic pass; the data layer is correct now.
---
## Decisions documented
1. **Keycloak is NOT cleaned up on `delete_account()`.** The Keycloak
admin API requires an admin token (separate auth realm), and is the
external operator's domain. After `delete_account()` the row is gone
from GoTrue's `auth.users` and the identity link in `auth.identities`,
but the Keycloak account itself survives. The next OIDC login with
the same email reaches GoTrue with no matching identity, GoTrue
provisions a new `auth.users` row with a fresh UUID, the
`handle_new_user` trigger creates the corresponding `public.users`
row, and the user is effectively a brand-new profile. The settings
modal makes this explicit.
2. **Sole-member "leave" is rejected**, not auto-converted to dissolve.
`leave_collective` raises errcode P0001 in that case; the UI surfaces
the "use Dissolve instead" hint with no follow-up navigation. This
keeps the destructive paths explicit (per plan §Riesgo 4).
3. **Dissolve confirmation by typing the collective name**, not a
checkbox — pattern from GitHub/Vercel for destructive operations.
Plan §10.3 confirmed; D-02 enforces it.
4. **Auto-language detection lives client-side** (`navigator.languages`)
not server-side (`Accept-Language` header). The OIDC dance (browser
↔ Keycloak ↔ GoTrue) bypasses SvelteKit entirely, so a
`hooks.server.ts` would never see those requests. The bootstrap is
gated to the post-`SIGNED_IN` path on the layout, fires only once
per account (60-second-old + `language='en'` heuristic), and skips
any user who has explicitly set a language before. Hispanic-locale
users land in Spanish without touching anything; existing users are
not retroactively re-detected.
5. **Trash UI uses RPCs even though the existing PostgREST UPDATE/DELETE
already works.** The RPC route closes a `is_active_member` ambiguity
(guest is excluded today via WITH CHECK, but the surface area was
broader than the two intents) and pre-emptively flags the case where
someone tries to hard-delete a still-active list. Direct UPDATEs
continue to pass C-07..C-12 in `rls-lists.test.ts` — the policies
are unchanged.
---
## Test deltas
| Suite | Baseline (Fase 9) | After Fase 10 | Δ |
|---|---|---|---|
| pgTAP | 65 | 96 | +31 (010 leave +7, 011 dissolve +8, 012 trash-rpcs +8, 013 delete-account +8) |
| Vitest integration | 144 | 151 | +7 (trash-rpcs +5, language-bootstrap +2) |
| Vitest unit | 26 | 38 | +12 (accept-language) |
| Playwright E2E | 65 | 74 | +9 (L-01, MC-01a/b, D-01/02/03, O-04, RST-01, DEL-01) |
| Rate-limit (gated) | 1 | 1 | — |
| **Total active** | **301** | **360** | **+59** |
| Skipped | 3 | 3 | — |
All suites green at end of fase. RLS audit clean.
---
## Files added
* `supabase/migrations/017_leave_collective_rpc.sql`
* `supabase/migrations/018_dissolve_collective_rpc.sql`
* `supabase/migrations/019_user_deletion_fks.sql`
* `supabase/migrations/020_restore_list_rpc.sql`
* `supabase/migrations/021_delete_account_rpc.sql`
* `supabase/tests/010_leave_collective.sql`
* `supabase/tests/011_dissolve_collective.sql`
* `supabase/tests/012_trash_rpcs.sql`
* `supabase/tests/013_delete_account.sql`
* `apps/web/src/lib/utils/accept-language.ts`
* `apps/web/src/lib/utils/accept-language.test.ts`
* `apps/web/src/lib/components/CreateCollectiveModal.svelte`
* `apps/web/tests/e2e/leave-collective.test.ts`
* `apps/web/tests/e2e/rename-collective.test.ts`
* `apps/web/tests/e2e/dissolve-collective.test.ts`
* `apps/web/tests/e2e/account-deletion.test.ts`
* `apps/web/tests/e2e/sidebar-create-collective.test.ts`
* `apps/web/tests/e2e/reset-from-detail.test.ts`
* `packages/test-utils/tests/trash-rpcs.test.ts`
* `packages/test-utils/tests/language-bootstrap.test.ts`
## Files modified
* `apps/web/src/routes/(app)/settings/+page.svelte` — Collectives section, Danger zone, delete-account modal
* `apps/web/src/routes/(app)/collective/manage/+page.svelte` — rename + emoji picker, Dissolve danger zone, dissolve confirm modal
* `apps/web/src/routes/(app)/lists/[id]/+page.svelte` — prominent Reset button on the completed-list view
* `apps/web/src/lib/stores/lists.ts` — restore/hard-delete go through the new RPCs
* `apps/web/src/lib/components/layout/DesktopSidebar.svelte` — switcher always-open, "+ New collective" entry
* `apps/web/src/lib/components/layout/MobileDrawer.svelte` — matching "+ New collective" entry
* `apps/web/src/routes/+layout.svelte``maybeBootstrapLanguage()` after SIGNED_IN
* `apps/web/messages/en.json` + `apps/web/messages/es.json` — ~30 new keys
---
## Spec coverage table (analysis/analisis-funcional.md)
| CU | Description | Status before | Status after |
|---|---|---|---|
| CU-H01 | Crear cuenta | ✅ | ✅ |
| CU-H02 | Crear colectivo | ✅ (onboarding only) | ✅ + sidebar |
| CU-H03 | Invitar miembros | ✅ | ✅ |
| CU-H04 | Aceptar invitación | ✅ | ✅ |
| CU-H05 | Cambiar de colectivo | ✅ | ✅ |
| **CU-H06** | **Abandonar colectivo** | ❌ | **✅** |
| **CU-H07** | **Renombrar / cambiar emoji** | ❌ | **✅** |
| **CU-H08** | **Disolver colectivo** | ❌ | **✅** |
| §6.3 | Eliminación de cuenta | ❌ | **✅** |
| §5.1 | Trash restore | partial (no UI) | **✅** |
| §3.4 | Reset list from detail | partial (overview only) | **✅** |
| §6.2 | Accept-Language detection | ❌ | **✅** |
All H-series CUs and the audited completion-debt items are now covered
end-to-end.

View File

@@ -2,7 +2,7 @@
**MVP2 · 2/6.** Segunda fase del ciclo MVP2 (rama `mvp2`, fases 9 → 14).
**Estado: 📋 Planificada. Cubre las 8 piezas de "completion debt" detectadas en el audit del 2026-04-22: 3 CU de gestión de colectivo (H06/H07/H08), 3 afordances de contenido ya construido (trash restore, reset-from-detail, sidebar create-collective), eliminación de cuenta, y detección automática de idioma.**
**Estado: ✅ Completa (2026-05-18). Cierra las 8 piezas de "completion debt" del audit 2026-04-22. Migraciones 017021, pgTAP 010013, 6 specs E2E + 2 integración + 12 unit. Ver `docs/history/fase-10-spec-completion.md`.**
**Objetivo: llevar la paridad con `analysis/analisis-funcional.md` al 100% en el eje de gestión de colectivo y ciclo de vida de usuario/contenido — todo lo que los usuarios empiezan a pedir desde el primer día de uso real.**