Files
collective-lists/docs/history/fase-10-spec-completion.md
Oier Bravo Urtasun a868b5a314 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>
2026-05-18 02:43:29 +02:00

8.2 KiB

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 RESTRICTNULL 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.sveltemaybeBootstrapLanguage() 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.