Final test totals after MVP2:
- pgTAP: 166 (was 45 → +121)
- Vitest integration: 173 (was 140 → +33)
- Vitest unit: 47 (was 15 → +32)
- Playwright e2e: 89 (was 59 → +30)
- Rate-limit gated: 1
- Total active: 476 (was 259 → +217)
- Skipped: 3 (unchanged: 2 Realtime presence + 1 gated)
Cycle summary:
- Fase 9 — dark mode + max-w-2xl + sync_conflicts + WebKit project
- Fase 10 — H06/H07/H08 + trash restore + account delete + sidebar create
+ reset-from-detail + auto language detection
- Fase 11 — item tags + drag-reorder
- Fase 12 — section visibility (feature flags, JSONB + section_enabled())
- Fase 13 — server admin (/admin, server_admins, audit log, server-layer
section_enabled extension)
- Fase 14 — PWA auto-update toast + offline polish + version chip
Migrations 015 → 025 (11 new). pgTAP 009 → 017 (9 new files).
Per-fase history docs under docs/history/fase-{9,10,11,12,13,14}-*.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
301 lines
19 KiB
Markdown
301 lines
19 KiB
Markdown
# Plan de Desarrollo — App Gestión del Colectivo
|
||
> **Stack:** SvelteKit + Supabase self-hosted + PWA
|
||
> **Infraestructura:** Docker Compose (dev local + producción VPS)
|
||
> **Última actualización:** Abril 2026
|
||
|
||
---
|
||
|
||
## Estado actual
|
||
|
||
| Fase | Estado |
|
||
|------|--------|
|
||
| Fase 0 — Infraestructura | ✅ Completa |
|
||
| Fase 1 — Auth y Colectivo | ✅ Completa |
|
||
| Fase 2a — Lista de Compra CRUD | ✅ Completa |
|
||
| Fase 2b — Realtime y Modo Compra | ✅ Completa (presence bloqueado por bug upstream de `supabase/realtime`) |
|
||
| Fase 3 — Tareas y Notas | ✅ Completa |
|
||
| Fase 4 — Búsqueda y Pulido | ✅ Búsqueda + RLS audit completos; PWA install/rate-limit/JWT rotation cerrados en Fase 6 |
|
||
| Fase 5 — Rediseño UX Mobile | ✅ Completa en scope automatizable (shell + masthead + big-button create + row redesign + selection mode + sticky detail chrome); presence avatars en cards + M-swipe/SEL-long-press touch E2E diferidos a WebKit install |
|
||
| Fase 6 — Deploy readiness | ✅ Completa (PWA generateSW + manifest + iconos placeholder + Kong rate-limit en invitaciones + rotación JWT dev + rotate-jwt.sh + `.env.erosi.example` + rls-audit.sh + `just lighthouse`). Rate-limit auth retirado — Keycloak brute-force nativa cubre el caso. Push notifications y iconos finales fuera de scope. |
|
||
| Fase 7 — Collective-flow E2E tests | ✅ Completa (12 Playwright nuevos O-01..MC-05; 3 bugs latentes corregidos: RPC atómica `create_collective`, restore de `pendingInvitationToken` tras login, hidratación tardía de `$currentCollective` en manage; migración 013 trigger `role/aud` default). |
|
||
| Fase 8 — Prod auth hardening | ✅ Completa (migración 014 extiende `role/aud` guard a UPDATE; `logout()` RP-initiated Keycloak end-session + ruta pública `/logged-out`). |
|
||
| **MVP2** — Fase 9 → 14 | ✅ Completa (rama `mvp2`, 2026-05-18) |
|
||
| ↳ Fase 9 — Polish + Dark Mode | ✅ |
|
||
| ↳ Fase 10 — Spec completion | ✅ |
|
||
| ↳ Fase 11 — Item tags + importance UX | ✅ |
|
||
| ↳ Fase 12 — Section visibility | ✅ |
|
||
| ↳ Fase 13 — Server administration | ✅ |
|
||
| ↳ Fase 14 — PWA hardening | ✅ |
|
||
| Suite de tests (pgTAP + Vitest + Playwright) | ✅ **476 tests verdes** (166 pgTAP + 173 integración + 47 unit + 89 E2E) + 1 rate-limit gated (`just test-rate-limit`). 3 skipped en `just test-all` (2 Realtime presence — upstream `handle_out/3` bug; 1 rate-limit — gated). |
|
||
|
||
---
|
||
|
||
## Metodología por fase (TDD)
|
||
|
||
Desde la Fase 1 en adelante, cada fase está estructurada así:
|
||
|
||
1. **`X.0 Tests primero`** — se escriben los tests (Vitest + pgTAP + Playwright) que definen el éxito de la fase, **antes** de cualquier implementación.
|
||
2. **`X.1 … X.N`** — implementación en el orden que mejor sirva a los tests.
|
||
3. **`X.Z Verificación final`** — `just test-all` debe devolver 0 failures. Sólo entonces la fase se considera cerrada.
|
||
|
||
Las fases ya cerradas (0, 1, 2a) reflejan este patrón retroactivamente: la sección `X.0` enumera los tests que hoy cubren la fase, y `X.Z` confirma que `just test-all` pasa.
|
||
|
||
## Índice de Fases
|
||
|
||
| Fase | Archivo | Duración estimada |
|
||
|------|---------|-------------------|
|
||
| Fase 0 | [fase-0-infraestructura.md](plan/fase-0-infraestructura.md) | 1–2 semanas |
|
||
| Fase 1 | [fase-1-auth-colectivo.md](plan/fase-1-auth-colectivo.md) | 2–3 semanas |
|
||
| Fase 2a | [fase-2a-lista-compra-crud.md](plan/fase-2a-lista-compra-crud.md) | 2 semanas |
|
||
| Fase 2b | [fase-2b-realtime-modo-compra.md](plan/fase-2b-realtime-modo-compra.md) | 2–3 semanas |
|
||
| Fase 3 | [fase-3-tareas-notas.md](plan/fase-3-tareas-notas.md) | 1–2 semanas |
|
||
| Fase 4 | [fase-4-busqueda-pulido.md](plan/fase-4-busqueda-pulido.md) | 1 semana |
|
||
| Fase 5 | [fase-5-mobile-ux.md](plan/fase-5-mobile-ux.md) | 1–2 semanas |
|
||
| Fase 6 | [fase-6-deploy-prep.md](plan/fase-6-deploy-prep.md) | 3–5 días |
|
||
| Fase 7 | [fase-7-collective-flow-tests.md](plan/fase-7-collective-flow-tests.md) | 1 semana |
|
||
| **MVP2 — Fase 9** | [fase-9-polish-dark-mode.md](plan/fase-9-polish-dark-mode.md) | 1–2 semanas |
|
||
| **MVP2 — Fase 10** | [fase-10-spec-completion.md](plan/fase-10-spec-completion.md) | 2–3 semanas |
|
||
| **MVP2 — Fase 11** | [fase-11-item-tags-importance.md](plan/fase-11-item-tags-importance.md) | 1–2 semanas |
|
||
| **MVP2 — Fase 12** | [fase-12-section-visibility.md](plan/fase-12-section-visibility.md) | 1 semana |
|
||
| **MVP2 — Fase 13** | [fase-13-server-admin.md](plan/fase-13-server-admin.md) | 2–3 semanas |
|
||
| **MVP2 — Fase 14** | [fase-14-pwa-hardening.md](plan/fase-14-pwa-hardening.md) | 3–5 días |
|
||
|
||
**Total estimado MVP (Fase 0–6): 11–16 semanas. MVP2 (Fase 9–14, rama `mvp2`): 10–14 semanas. Fase 7+8 entre MVP y MVP2 como cleanup post-launch.**
|
||
|
||
Los mockups de referencia viven en [`design/`](design/) (organizados por pantalla — ver `design/slate_collective/DESIGN.md` para la dirección "Monolith Editorial").
|
||
|
||
---
|
||
|
||
## Stack Tecnológico Confirmado
|
||
|
||
| Capa | Tecnología | Justificación |
|
||
|------|-----------|---------------|
|
||
| Frontend / PWA | SvelteKit 2 | Reactividad nativa, bundle pequeño, PWA de primera clase |
|
||
| Backend / BaaS | Supabase self-hosted | Realtime, RLS, Storage — Auth delegada a Keycloak |
|
||
| Identidad / Auth | Keycloak self-hosted | IdP OIDC/OAuth2, gestión de usuarios, federación futura |
|
||
| Base de datos | PostgreSQL 15 (vía Supabase) | Full-text search nativo, `pg_cron` para jobs |
|
||
| Tiempo real | Supabase Realtime (WebSocket) | Presencia + cambios en < 1s sin infraestructura extra |
|
||
| Offline | IndexedDB vía `idb` | PWA-compatible, sin dependencias nativas |
|
||
| PWA | `@vite-pwa/sveltekit` + Workbox | Service Worker, caché offline, instalable |
|
||
| Infraestructura dev | Docker Compose + Supabase CLI | Entorno local con Studio + Keycloak + usuarios de prueba |
|
||
| Infraestructura prod | Docker Compose + nginx (existente) | Reverse proxy ya operativo en el VPS, gestionado externamente |
|
||
| CI/CD | GitHub Actions | Lint, test, deploy vía SSH |
|
||
| Email transaccional | Resend | SMTP para invitaciones (Keycloak también lo usa para reset) |
|
||
| Backups | `pg_dump` → Backblaze B2 (S3) | Retención 30 días, coste mínimo |
|
||
|
||
---
|
||
|
||
## Estructura del Repositorio
|
||
|
||
```
|
||
colectivo/
|
||
├── apps/
|
||
│ └── web/ # SvelteKit — web + PWA
|
||
│ ├── src/
|
||
│ │ ├── lib/
|
||
│ │ │ ├── supabase.ts # cliente Supabase singleton (PKCE, GoTrue auth)
|
||
│ │ │ ├── auth.ts # login() / logout() via supabase.auth.signInWithOAuth
|
||
│ │ │ ├── stores/ # Svelte stores globales
|
||
│ │ │ ├── sync/ # lógica offline + cola de operaciones
|
||
│ │ │ └── components/ # componentes UI reutilizables
|
||
│ │ ├── routes/ # rutas SvelteKit
|
||
│ │ └── service-worker.ts # SW personalizado (Workbox)
|
||
│ ├── tests/
|
||
│ │ ├── fixtures/
|
||
│ │ │ ├── users.ts # constantes de usuarios semilla
|
||
│ │ │ └── global-setup.ts # login por usuario vía Keycloak, cachea storageState
|
||
│ │ └── e2e/ # tests Playwright (A, C, D series)
|
||
│ ├── static/
|
||
│ │ ├── manifest.webmanifest
|
||
│ │ └── icons/ # iconos PWA (512, 192, 180...)
|
||
│ └── vite.config.ts
|
||
│
|
||
├── packages/
|
||
│ ├── types/ # tipos TypeScript compartidos
|
||
│ │ └── src/
|
||
│ │ ├── database.ts # tipos generados desde schema Supabase
|
||
│ │ └── domain.ts # tipos de dominio (Colectivo, Item, etc.)
|
||
│ └── test-utils/ # infraestructura de tests de integración
|
||
│ └── src/
|
||
│ ├── seed-constants.ts # UUIDs fijos de usuarios y colectivo de prueba
|
||
│ ├── supabase-clients.ts # createClientAs(userId) + createAdminClient()
|
||
│ └── db-helpers.ts # sql() vía pg.Pool (bypassa RLS para setup/teardown)
|
||
│
|
||
├── supabase/
|
||
│ ├── migrations/ # migraciones SQL versionadas
|
||
│ ├── seed.sql # datos de prueba para dev
|
||
│ ├── tests/ # tests pgTAP (SQL puro, sin mocking)
|
||
│ ├── functions/ # Edge Functions (invitaciones, etc.)
|
||
│ └── config.toml # configuración Supabase CLI + OIDC Keycloak
|
||
│
|
||
├── keycloak/
|
||
│ ├── realm-export.json # realm "colectivo" exportado — importado al arrancar dev
|
||
│ └── themes/ # tema personalizado (opcional, v2)
|
||
│
|
||
├── infra/
|
||
│ ├── docker-compose.dev.yml # Supabase + Keycloak dev (Kong en puerto 8001)
|
||
│ ├── docker-compose.prod.yml # producción en VPS
|
||
│ ├── kong.yml # Kong declarativo (rutas API, con ${VAR} placeholders)
|
||
│ ├── kong-start.sh # entrypoint Kong: sustituye ${VAR} vía perl antes de arrancar
|
||
│ ├── db-init/
|
||
│ │ └── 00-role-passwords.sh # ajusta passwords de roles internos de Supabase al arrancar
|
||
│ └── scripts/
|
||
│ ├── backup.sh # backup manual de una BD concreta
|
||
│ ├── backup-cron.sh # ejecutado por cron: decide tipo (diario/semanal/mensual) y purga
|
||
│ ├── restore.sh # restaura un dump concreto en una BD
|
||
│ └── deploy.sh # SSH + docker compose pull + up
|
||
│
|
||
├── .github/
|
||
│ └── workflows/
|
||
│ ├── ci.yml # lint + test en PR
|
||
│ └── deploy.yml # deploy en push a main
|
||
│
|
||
├── .env.example # todas las variables documentadas con valores de ejemplo
|
||
├── Justfile # comandos de desarrollo frecuentes
|
||
├── turbo.json # Turborepo pipeline
|
||
└── package.json # workspaces
|
||
```
|
||
|
||
---
|
||
|
||
## Justfile — Comandos de referencia
|
||
|
||
```bash
|
||
just setup # preparación del entorno local (idempotente, seguro relanzar)
|
||
just dev # levanta docker-compose.dev.yml + SvelteKit dev server
|
||
just serve # build + servidor Node prod en :3000 contra el stack Docker dev
|
||
just stop # para todos los servicios (Docker stack + servidor Vite)
|
||
just dev-stop # para solo el stack de Docker
|
||
just dev-clean # para Docker y elimina todos los volúmenes (reset completo)
|
||
just db-reset # drop + migrate + seed (dev)
|
||
just db-migrate # aplica migraciones pendientes (dev)
|
||
just db-seed # aplica supabase/seed.sql a la BD dev en marcha
|
||
just db-types # genera tipos TS desde schema Supabase → packages/types
|
||
just kc-export # exporta realm de Keycloak → keycloak/realm-export.json
|
||
just kc-open # abre Keycloak Admin Console en el navegador (localhost:8080)
|
||
just deploy # ejecuta infra/scripts/deploy.sh (prod)
|
||
just backup supabase # backup manual inmediato de supabase
|
||
just backup keycloak # backup manual inmediato de keycloak
|
||
just restore supabase <file> # restaura un dump concreto en supabase
|
||
just restore keycloak <file> # restaura un dump concreto en keycloak
|
||
just logs # docker compose logs -f (prod)
|
||
just test-all # ejecuta todas las suites (pgTAP + Vitest + Playwright); requiere just dev activo
|
||
just test-integration # Vitest: tests de integración RLS (requiere just dev activo)
|
||
just test-db # pgTAP: tests SQL directamente contra la BD dev
|
||
just test-e2e # Playwright: tests E2E en modo headless (requiere just dev activo)
|
||
just test-e2e-headed # Playwright: tests E2E con navegador visible (debugging)
|
||
```
|
||
|
||
---
|
||
|
||
## Endpoints y credenciales en desarrollo local
|
||
|
||
| Servicio | URL | Credenciales (solo dev) |
|
||
|----------|-----|-------------------------|
|
||
| App SvelteKit | http://localhost:5173 | — |
|
||
| Supabase Studio | http://localhost:54323 | — |
|
||
| Supabase API (Kong) | http://localhost:8001 | apikey: `PUBLIC_SUPABASE_ANON_KEY` del `.env` |
|
||
| Keycloak Admin | http://localhost:8080/admin | `admin` / `admin` |
|
||
| PostgreSQL | localhost:5432 | `postgres` / `postgres` |
|
||
|
||
**Secretos dev (generados en `.env`, nunca usar en producción):**
|
||
|
||
| Variable | Valor dev |
|
||
|----------|-----------|
|
||
| `POSTGRES_PASSWORD` | `postgres` |
|
||
| `SUPABASE_JWT_SECRET` | `super-secret-jwt-token-with-at-least-32-characters-long` |
|
||
| `KEYCLOAK_ADMIN` / `KEYCLOAK_ADMIN_PASSWORD` | `admin` / `admin` |
|
||
| `KEYCLOAK_CLIENT_SECRET` | `gotrue-dev-secret` (secreto del cliente `colectivo-web` en GoTrue) |
|
||
|
||
**Usuarios de prueba** (definidos en `keycloak/realm-export.json` y `supabase/seed.sql`):
|
||
|
||
| Usuario | Email | Contraseña | Rol en el colectivo de prueba |
|
||
|---------|-------|-----------|-------------------------------|
|
||
| `ana` | ana@dev.local | `test1234` | Admin |
|
||
| `borja` | borja@dev.local | `test1234` | Member |
|
||
| `carmen` | carmen@dev.local | `test1234` | Member |
|
||
| `david` | david@dev.local | `test1234` | Guest |
|
||
| `eva` | eva@dev.local | `test1234` | (sin colectivo — para testing de onboarding) |
|
||
|
||
---
|
||
|
||
## Decisiones Previas al Inicio
|
||
|
||
Antes de arrancar la Fase 0, confirmar:
|
||
|
||
| Decisión | Estado |
|
||
|----------|--------|
|
||
| Estrategia JWT Keycloak → Supabase (Opción A vs B) | ✅ **Opción B — GoTrue como proxy OIDC** |
|
||
| Base de datos de Keycloak en prod | ✅ **PostgreSQL independiente** |
|
||
| Entorno de producción dockerizado | ✅ **Ya operativo** |
|
||
| Reverse proxy en producción | ✅ **nginx existente, gestionado externamente** |
|
||
| Self-registration habilitado en Keycloak o invitación obligatoria | ✅ **Self-registration habilitado en Keycloak** |
|
||
| Invitaciones: email o link manual | ✅ **Link-only — admin genera link y lo comparte manualmente** |
|
||
| Proveedor SMTP | ✅ **No necesario para invitaciones** — Resend queda solo para Keycloak reset-password si se activa |
|
||
| Almacenamiento de backups | Pendiente — **Backblaze B2 recomendado** |
|
||
| Dominio y DNS del VPS | Pendiente |
|
||
| Subdominio para Keycloak | Pendiente — `auth.tudominio.com` recomendado |
|
||
| Registry de imágenes Docker | Pendiente — **GitHub Container Registry recomendado** |
|
||
| OAuth Google (federated en Keycloak) | Pendiente — configurar en Keycloak, no en Supabase |
|
||
| OAuth Apple (federated en Keycloak) | Pendiente — requiere Apple Developer Program ($99/año) |
|
||
|
||
---
|
||
|
||
## Advertencias Técnicas
|
||
|
||
**Keycloak y JWT con Supabase RLS (Opción B — GoTrue como proxy OIDC):**
|
||
- GoTrue recibe el token de Keycloak, lo valida contra el discovery endpoint de Keycloak, y emite su propio JWT de Supabase. `auth.uid()` resuelve al UUID de GoTrue (mapeado desde el `sub` de Keycloak).
|
||
- El `sub` del realm de Keycloak es un UUID v4 estándar — verificado en las pruebas de aceptación.
|
||
- No es necesario gestionar `JWT_SECRET` manualmente con la clave pública de Keycloak. GoTrue usa el endpoint OIDC de Keycloak para validar tokens.
|
||
- El logout (`supabase.auth.signOut()`) invalida la sesión de GoTrue. La sesión SSO de Keycloak persiste hasta que expire — comportamiento esperado para esta app.
|
||
|
||
**Integración GoTrue + Keycloak — requisitos no obvios (ya implementados):**
|
||
- **Cliente `colectivo-web` confidencial:** GoTrue necesita un secreto de cliente (`KEYCLOAK_CLIENT_SECRET=gotrue-dev-secret`) para el intercambio de código. Sin esto, GoTrue falla con "missing OAuth secret".
|
||
- **Scope `openid` personalizado en Keycloak:** GoTrue v2.158.1 envía `scope=profile email` a Keycloak ignorando `GOTRUE_EXTERNAL_KEYCLOAK_SCOPES`. El endpoint userinfo de Keycloak requiere `openid` en el token. Solución: scope de cliente personalizado llamado `openid` con `include.in.token.scope=true` configurado como scope por defecto en `colectivo-web`.
|
||
- **`GOTRUE_DISABLE_SIGNUP: "false"`:** El flag bloquea también la creación de usuarios vía OIDC. Debe ser `false`; Keycloak es el guardián del registro.
|
||
- **`auth.users` pre-sembrado con UUIDs de Keycloak:** `seed.sql` inserta `auth.users` (con `instance_id='00000000-0000-0000-0000-000000000000'` y campos de token como strings vacíos, no NULL) y `auth.identities`. Garantiza que `auth.uid()` coincida con las claves foráneas en tablas públicas.
|
||
- **Kong 2.8 no interpola variables de entorno:** `infra/kong-start.sh` ejecuta `perl` para sustituir `${VAR}` en `kong.yml` antes de arrancar Kong. Sin esto, Kong carga la cadena literal y rechaza todas las peticiones con 401.
|
||
|
||
**Keycloak self-hosted en producción:**
|
||
- Keycloak 24+ requiere mínimo 512MB de RAM asignados. En un VPS pequeño (2GB), monitorizar el consumo conjunto con Supabase.
|
||
- El endpoint `/realms/{realm}/.well-known/openid-configuration` debe ser accesible públicamente para que Supabase pueda validar tokens. No poner Keycloak detrás de autenticación básica.
|
||
- Habilitar `SSL Required: external requests` en el realm de producción. Nunca desactivarlo.
|
||
|
||
**OAuth federado (Google, Apple):**
|
||
- Con Keycloak, Google y Apple se configuran como Identity Providers en Keycloak, no en Supabase. El flujo es: usuario → Keycloak → Google/Apple → Keycloak → app. Supabase solo ve el token de Keycloak.
|
||
- Esto simplifica la configuración de Supabase pero requiere configurar los Identity Providers en Keycloak Admin Console.
|
||
|
||
**iOS Safari y PWA:**
|
||
- La autenticación usa el flujo PKCE de Supabase OAuth (`signInWithOAuth({ provider: 'keycloak' })`). No hay keycloak-js ni iframes de refresco silencioso — compatible con Safari de base.
|
||
- La sesión de GoTrue se persiste en `localStorage` y se refresca automáticamente; no requiere `updateToken()` manual.
|
||
- Background Sync API no está soportada en Safari. Implementar fallback manual con el evento `online`.
|
||
- Push notifications solo disponibles desde iOS 16.4 y únicamente con la PWA instalada en pantalla de inicio.
|
||
- Probar el Modo Compra en un iPhone real en paralelo al desarrollo, no solo en DevTools.
|
||
|
||
**Supabase Realtime self-hosted:**
|
||
- Revisar `MAX_REPLICATION_SLOTS` en PostgreSQL y `REALTIME_MAX_CONNECTIONS` antes de producción.
|
||
- Las suscripciones de Presence consumen más recursos que Postgres Changes. Limitar a listas con sesión activa.
|
||
|
||
**Conflictos de sincronización:**
|
||
- Last-write-wins es suficiente para este dominio. No implementar CRDT en el MVP.
|
||
- Registrar conflictos detectados en tabla `sync_conflicts` para depuración.
|
||
|
||
---
|
||
|
||
## Estimación Total
|
||
|
||
| Fase | Duración |
|
||
|------|----------|
|
||
| Fase 0 — Infraestructura | 1–2 semanas |
|
||
| Fase 1 — Auth y Colectivo | 2–3 semanas |
|
||
| Fase 2a — Lista de Compra CRUD | 2 semanas |
|
||
| Fase 2b — Realtime y Modo Compra | 2–3 semanas |
|
||
| Fase 3 — Tareas y Notas | 1–2 semanas |
|
||
| Fase 4 — Búsqueda y Pulido | 1 semana |
|
||
| **Total** | **9–13 semanas** |
|
||
|
||
---
|
||
|
||
*Plan generado como base para Claude Code. Stack: SvelteKit + Supabase self-hosted + Keycloak + PWA. Cada fase puede usarse como contexto independiente para sesiones de trabajo.*
|
||
|