docs(fase-7): plan — collective-flow E2E coverage + ambrosio deploy notes
- plan/fase-7-collective-flow-tests.md: 14 new Playwright tests (O-01..O-04 onboarding, I-01..I-05 invitation acceptance, MC-01..MC-05 admin manage) plus shared fixtures/db.ts helpers (resetEva, seedExpiredInvitation, seedUsedInvitation, restoreSeedMembership). Risks + scope-out documented. - CLAUDE.md: add "Fase 6+ production deploy to ambrosio" section with the files / config / discovered fixes for the prod stack; link to the new plan from the Project Status line; add gotchas #16 (injectManifest filename hardcoded in @vite-pwa/sveltekit 0.6.8) and #17 (supabase/postgres doesn't bootstrap its internal roles on a fresh volume).
This commit is contained in:
28
CLAUDE.md
28
CLAUDE.md
@@ -8,11 +8,11 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
|
||||
## Project Status
|
||||
|
||||
**Fase 0–5 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). 236 tests green: 34 pgTAP + 140 Vitest integration + 15 Vitest unit + 46 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. ✅**
|
||||
**Fase 0–5 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.oier.ovh (app + Supabase) + https://auth.oier.ovh (Keycloak); Let's Encrypt certs via host Caddy; PWA strategy switched to generateSW to unblock the prod build (see gotcha #16). Fase 7 is the active plan: 14 Playwright tests filling the collective-flow UI gap (onboarding / invitation acceptance / admin-manage) — see `plan/fase-7-collective-flow-tests.md`. 236 tests green: 34 pgTAP + 140 Vitest integration + 15 Vitest unit + 46 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. ✅**
|
||||
|
||||
- `README.md` — full development plan, confirmed tech stack, Justfile reference, and technical warnings
|
||||
- `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 6)
|
||||
- `plan/fase-*.md` — phase-by-phase implementation plans (Fase 0 through Fase 7; Fase 7 is the active plan)
|
||||
- `design/slate_collective/DESIGN.md` + `design/*/screen.png` — "Monolith Editorial" direction + per-screen Stitch mockups (desktop + mobile variants). Consult before changing UI.
|
||||
|
||||
### Fase 1 — what has been built
|
||||
@@ -74,6 +74,26 @@ just test-e2e # Playwright E2E tests (requires just dev running)
|
||||
just test-e2e-headed # Playwright with visible browser (debugging)
|
||||
```
|
||||
|
||||
### Fase 6+ — production deploy to ambrosio (2026-04-14)
|
||||
|
||||
Live URLs: **https://erosi.oier.ovh** (app + Supabase API, single-domain) + **https://auth.oier.ovh** (Keycloak). OVH VPS, Ubuntu 24, Docker 29.
|
||||
|
||||
- `infra/docker-compose.erosi.yml` — full stack bound to `127.0.0.1` only (db, auth, rest, realtime, storage, imgproxy, kong, keycloak, app). `studio` + `meta` intentionally dropped for prod.
|
||||
- `infra/caddy/erosi.Caddyfile` — snippet appended to the host's `/etc/caddy/Caddyfile` between `# BEGIN colectivo-erosi` / `# END colectivo-erosi` markers. Host Caddy 2.11.2 handles TLS (Let's Encrypt via tls-alpn-01). `erosi.oier.ovh` routes `/auth/v1/*`, `/rest/v1/*`, `/realtime/v1/*`, `/storage/v1/*`, `/graphql/v1/*`, `/pg/*` → Kong :8000; everything else → SvelteKit :3000. `auth.oier.ovh` → Keycloak :8090 with `X-Forwarded-Port` explicitly (Caddy auto-sets Proto/Host/For).
|
||||
- `infra/scripts/deploy-erosi.sh` — idempotent redeploy: rsync → generate secrets on first run (via `infra/scripts/rotate-jwt.sh`) → substitute `__KEYCLOAK_CLIENT_SECRET__` in `keycloak/realm-export.erosi.json` → docker compose build + up → run pending migrations → re-install Caddy snippet → reload.
|
||||
- `keycloak/realm-export.erosi.json` — prod realm: `registrationAllowed: true`, redirect URIs scoped to `https://erosi.oier.ovh/*`, client secret placeholder that the deploy script substitutes with a per-install random value. No dev users.
|
||||
- `.env.erosi.example` — committed template. The real `/opt/colectivo/.env` on ambrosio is 600 and stays on the server.
|
||||
- UFW allows `80/tcp`, `443/tcp`, `22/tcp`. All other ports remain DENY. Docker services listen on `127.0.0.1` only, so Kong / Keycloak / the app are unreachable from the public internet except via Caddy.
|
||||
|
||||
**Prod-specific fixes that had to be made for the stack to boot cleanly on a fresh volume:**
|
||||
- `infra/db-init/00-role-passwords.sh` — rewrote as idempotent bootstrap (see gotcha #17): creates all Supabase service roles if absent, enables `pg_cron` + `pgcrypto` + `uuid-ossp`, creates `auth` / `storage` / `graphql_public` / `_realtime` / `realtime` schemas + empty `supabase_realtime` publication + `keycloak` database + role, sets `supabase_auth_admin` search_path to `auth`. The old `00-role-passwords.sql` (ALTER-only) has been removed.
|
||||
- `apps/web/vite.config.ts` — PWA strategy switched `injectManifest` → `generateSW` (gotcha #16).
|
||||
- Keycloak container uses `start --import-realm` (no `--optimized`) so Quarkus re-builds with the Postgres driver on first boot; with `--optimized` it silently falls back to H2 and fails with "jdbc:h2 URL format error".
|
||||
|
||||
**Keycloak admin password:** generated per-install — retrieve with `ssh ambrosio 'grep KEYCLOAK_ADMIN_PASSWORD /opt/colectivo/.env'`. Console at https://auth.oier.ovh/admin.
|
||||
|
||||
**Not yet configured on ambrosio:** SMTP (Resend), automated backups (script exists at `infra/scripts/backup.sh`, not scheduled), Lighthouse run against prod URL, final production icons.
|
||||
|
||||
### Fase 6 — what has been built
|
||||
|
||||
**6.1 PWA `injectManifest` migration**
|
||||
@@ -111,6 +131,10 @@ just test-e2e-headed # Playwright with visible browser (debugging)
|
||||
|
||||
15. **`@vite-pwa/sveltekit` does not auto-register the service worker.** The plugin provides a virtual module (`virtual:pwa-register`, `virtual:pwa-register/svelte`) but you have to call `registerSW()` (or `useRegisterSW()` for Svelte bindings) yourself. Without that call, the SW file is generated + served but never registered, and `navigator.serviceWorker.getRegistration()` returns `undefined`. The canonical place is `apps/web/src/routes/+layout.svelte`'s `onMount()`.
|
||||
|
||||
16. **`@vite-pwa/sveltekit` 0.6.8 `injectManifest` hardcodes the source SW filename.** The plugin looks for the compiled SW at `.svelte-kit/output/client/service-worker.js` regardless of `filename: 'sw.ts'`. SvelteKit blocks Workbox imports from `src/service-worker.[jt]s` (gotcha #10), so there's no way to actually feed the plugin. **Workaround:** use `strategies: 'generateSW'` instead — the plugin auto-generates a precache-only SW equivalent to our minimal `src/sw.ts`. `vite.config.ts` currently uses this. Found while building for the ambrosio deploy (2026-04-14); same build was broken on `main` silently.
|
||||
|
||||
17. **supabase/postgres v15.1.1.78 does NOT create the Supabase internal roles on a fresh volume.** The image ships with custom extensions + configs but no role-bootstrap script. Dev "works" because its volume was initialised long ago, grandfathering the roles (`authenticator`, `anon`, `authenticated`, `service_role`, `supabase_admin`, `supabase_auth_admin`, `supabase_storage_admin`, `supabase_replication_admin`, `supabase_realtime_admin`, `pgbouncer`, `dashboard_user`). On a fresh volume only `postgres` exists. `infra/db-init/00-role-passwords.sh` now bootstraps them idempotently (create-if-missing) + enables `pg_cron` + creates the empty `supabase_realtime` publication + sets `supabase_auth_admin` search_path to `auth` (without this GoTrue migrations create `factor_type` in `public` and later migrations explode). See `plan/fase-6-deploy-prep.md` §6+ context for the full list of deltas discovered during the ambrosio bring-up.
|
||||
|
||||
### Fase 5 — what has been built (complete in scope)
|
||||
|
||||
**5.8 big-button create (match /notes)**
|
||||
|
||||
97
plan/fase-7-collective-flow-tests.md
Normal file
97
plan/fase-7-collective-flow-tests.md
Normal file
@@ -0,0 +1,97 @@
|
||||
### Fase 7 — Collective creation flow: fill the E2E gap
|
||||
**Estado: 📝 Plan (2026-04-14).**
|
||||
**Objetivo: cerrar la laguna de cobertura UI en el ciclo completo de un colectivo — onboarding, invitación y gestión — que hoy sólo vive en RLS / pgTAP.**
|
||||
|
||||
**Contexto:** el audit de 2026-04-14 (ver mensaje de planificación en el chat) muestra que el backend del dominio "colectivo" está bien testado (13 casos RLS + 7 pgTAP), pero la UI sólo tiene 2 asserts (sidebar tras login en `auth.test.ts` A-04/A-06). Ninguna prueba recorre `/onboarding`, `/invitation/[token]` ni `/collective/manage`.
|
||||
|
||||
Esta fase no introduce features — sólo tests. Si alguno falla en el primer run es porque hay un bug real a corregir (los flujos están en producción desde Fase 1).
|
||||
|
||||
---
|
||||
|
||||
#### 7.0 Helpers compartidos
|
||||
|
||||
**`apps/web/tests/fixtures/db.ts`** (nuevo) — usa el admin service-role client de `@colectivo/test-utils` para manipulación directa (bypass RLS):
|
||||
|
||||
- [ ] `resetEva()` — borra todos los `collective_members` de Eva **y** los `collectives` que Eva haya creado. Se invoca en `beforeEach` de `onboarding.test.ts` y `invitation.test.ts` (ambos asumen Eva = cero colectivos al empezar).
|
||||
- [ ] `seedExpiredInvitation(collectiveId: string, adminId: string): Promise<string>` — inserta una fila en `collective_invitations` con `expires_at = now() - 1 hour`; devuelve el token. Usado por I-04.
|
||||
- [ ] `seedUsedInvitation(collectiveId: string, adminId: string, acceptorId: string): Promise<string>` — inserta con `used_at = now()` y `used_by = acceptorId`. Usado por I-05.
|
||||
- [ ] `restoreSeedMembership()` — restituye los roles base de la semilla (Borja = member, Carmen = member, David = guest). `afterAll` de `manage-collective.test.ts`, imprescindible para que los tests posteriores (lists / items / tasks / notes) vean el estado sembrado esperado.
|
||||
|
||||
Patrón de importación para evitar que helpers se filtren al bundle del cliente: el helper vive bajo `tests/fixtures/`, no `src/`, y sólo se importa desde ficheros `*.test.ts`.
|
||||
|
||||
---
|
||||
|
||||
#### 7.1 `/onboarding` — O-series (4 tests, fichero nuevo)
|
||||
|
||||
**Fichero:** `apps/web/tests/e2e/onboarding.test.ts`
|
||||
**Fixture:** Eva (`USERS.eva`), sin colectivo al iniciar (garantizado por `resetEva()` en `beforeEach`).
|
||||
|
||||
- [ ] **O-01** — Eva se loguea, no tiene colectivos → el layout la redirige automáticamente a `/onboarding`. Asserta `page.url()` contiene `/onboarding` y la pantalla muestra el formulario de creación.
|
||||
- [ ] **O-02** — Camino feliz: escribe un nombre ("Familia Eva"), selecciona un emoji (🏠), envía → redirección a `/lists` + sidebar muestra el nuevo colectivo con nombre y emoji correctos + `localStorage.getItem('activeCollectiveId')` es el id devuelto por el INSERT.
|
||||
- [ ] **O-03** — Input de nombre vacío → botón de submit deshabilitado (o error inline). Ninguna petición POST sale (network stub o count `collective_members` antes/después sin cambios).
|
||||
- [ ] **O-04** — Tras O-02, Eva crea un segundo colectivo desde la UI (botón "+" en el switcher o entrada del sidebar). El switcher muestra ambos; seleccionar el segundo persiste `activeCollectiveId` tras recargar.
|
||||
|
||||
**Criterio de aceptación:** los 4 tests pasan; Eva queda sin colectivos al final (via `afterAll → resetEva()`).
|
||||
|
||||
---
|
||||
|
||||
#### 7.2 `/invitation/[token]` — I-series (5 tests, fichero nuevo)
|
||||
|
||||
**Fichero:** `apps/web/tests/e2e/invitation.test.ts`
|
||||
**Fixtures:** Ana (admin del colectivo semilla) + Eva (no-miembro). `resetEva()` en `beforeEach`.
|
||||
|
||||
- [ ] **I-01** — Ana abre `/collective/manage`, pulsa "Generar invitación" → un URL con token aparece y es copiable; la fila correspondiente en `collective_invitations` tiene `expires_at` > ahora y `used_at IS NULL`.
|
||||
- [ ] **I-02** — Eva (deslogueada) abre el URL de invitación activo → el guard redirige a login; tras autenticarse vuelve a la página de aceptación; pulsa "Aceptar" → queda registrada como `member` en `collective_members` + redirigida al colectivo (`/lists` del nuevo collective_id).
|
||||
- [ ] **I-03** — Eva (ya logueada, aún no-miembro) abre el URL → un solo click de "Aceptar" la añade como member, sin pasar por login.
|
||||
- [ ] **I-04** — Ana genera invitación, Eva abre con token expirado (vía `seedExpiredInvitation`) → la UI muestra estado "expirada" sin botón de aceptar; no se inserta fila en `collective_members`.
|
||||
- [ ] **I-05** — Invitación ya usada (`seedUsedInvitation`) → la UI muestra estado "ya aceptada" / "token inválido"; igual, no hay INSERT.
|
||||
|
||||
**Captura del token en I-02/I-03:** Ana abre `/collective/manage` en la misma browser context, pulsa "Generar", el token se lee del `href` del link copiable (selector `data-testid="invite-link"`). La parte de Eva se hace en un segundo browser context (`browser.newContext()`), aislando cookies/localStorage.
|
||||
|
||||
**Criterio de aceptación:** los 5 tests pasan; Eva y sus membresías quedan reseteadas al terminar.
|
||||
|
||||
---
|
||||
|
||||
#### 7.3 `/collective/manage` — MC-series (5 tests, fichero nuevo)
|
||||
|
||||
**Fichero:** `apps/web/tests/e2e/manage-collective.test.ts`
|
||||
**Fixtures:** Ana (admin), Borja (member), Carmen (member) sobre el colectivo semilla. `restoreSeedMembership()` en `afterAll`.
|
||||
|
||||
- [ ] **MC-01** — Ana renombra el colectivo a "Familia Bravo-Urtasun" → el sidebar actualiza optimistamente + la fila en `collectives.name` coincide tras reload.
|
||||
- [ ] **MC-02** — Ana promueve a Borja de `member` a `admin` → tras reload, Borja ve el botón "Generar invitación" y el input editable de nombre (afordances reservadas a admin).
|
||||
- [ ] **MC-03** — Ana degrada a Borja de vuelta a `member`. Precondición: Ana sigue siendo admin (no dispara el trigger de auto-promoción). Asserta que el cambio persiste.
|
||||
- [ ] **MC-04** — Ana elimina a Carmen → la fila desaparece de la lista inmediatamente. En un segundo context, Carmen (logueada antes de la eliminación) intenta abrir `/collective/manage` → debe recibir redirección / 403 / estado vacío (el RLS ya bloquea; hay que comprobar que la UI no crashea).
|
||||
- [ ] **MC-05** — Ana genera una invitación, luego pulsa "Revocar" en esa fila → la fila se marca como revocada (o desaparece); abrir el URL revocado muestra el mismo estado de "inválida" que I-05.
|
||||
|
||||
**Criterio de aceptación:** los 5 tests pasan; `restoreSeedMembership()` deja Borja=member, Carmen=member, David=guest antes de que se ejecute cualquier otro test.
|
||||
|
||||
---
|
||||
|
||||
#### 7.Z Verificación
|
||||
|
||||
- [ ] `just test-e2e` → 14 tests nuevos verdes, 0 regresiones en los 46 existentes (total: 60 Playwright).
|
||||
- [ ] El fichero `restoreSeedMembership()` no deja cambios residuales — verificación manual: `docker exec colectivo-dev-db-1 psql -U postgres -c "SELECT user_id, role FROM collective_members ORDER BY user_id"` debe devolver los 4 miembros de la semilla con sus roles originales.
|
||||
- [ ] Actualizar `CLAUDE.md` con el nuevo total de tests + una línea describiendo la cobertura añadida.
|
||||
|
||||
**Criterio de aceptación:** 14 tests verdes, seed restaurada, doc actualizada.
|
||||
|
||||
---
|
||||
|
||||
### Riesgos / notas
|
||||
|
||||
1. **Eva como fixture compartido entre `onboarding.test.ts` e `invitation.test.ts`.** Si Playwright paraleliza ambos ficheros, el `resetEva()` del segundo puede borrar los datos mientras el primero está a medias. **Mitigación:** los 3 ficheros nuevos deben ejecutarse secuencialmente. `playwright.config.ts` ya usa `workers: 1` por defecto en CI; confirmar que sigue así al añadir estos tests.
|
||||
|
||||
2. **`restoreSeedMembership()` es un punto único de fallo.** Si se olvida el `afterAll` o falla, toda la suite aguas abajo falla con "member not found". **Mitigación:** envolver los tests MC dentro de un `test.describe.serial(...)` y registrar el `afterAll` como hook inline, no importado.
|
||||
|
||||
3. **I-04 e I-05 manipulan tablas directamente vía admin client.** Esto bypassa el trigger de auditoría si se añade en el futuro. **Mitigación:** documentar en el helper que es la única vía aceptable; cualquier nuevo trigger auditor debe tolerarlo (o generar eventos equivalentes manualmente).
|
||||
|
||||
4. **Captura del token de invitación desde la UI (I-02/I-03).** Depende de que el input del link tenga un `data-testid` estable. Si hoy no existe, hay que añadirlo a `apps/web/src/routes/(app)/collective/manage/+page.svelte` como parte del trabajo (cambio mínimo, no es scope creep).
|
||||
|
||||
---
|
||||
|
||||
### Scope explícito fuera
|
||||
|
||||
- **No** se tocan tests de shopping / tasks / notes.
|
||||
- **No** se cubre la flow de "rechazar invitación" (no existe en la UI).
|
||||
- **No** se cubre el trigger auto-promote-oldest-admin desde E2E — pgTAP ya lo testa (003_promote_on_admin_leave.sql), duplicarlo en Playwright no aporta.
|
||||
- **No** se cubren notificaciones push al aceptar invitación — fuera de MVP.
|
||||
Reference in New Issue
Block a user