Files
collective-lists/README.md
Oier Bravo Urtasun 2174d2c2c0 feat(fase-5): mobile UX shell + masthead + swipe-delete undo (224 tests green)
5.0 Tests primero
  Playwright: mobile-shell.test.ts (4 — sidebar hidden on mobile, bottom tab
    bar visible with aria-current, hamburger opens drawer, desktop shows
    sidebar), mobile-masthead.test.ts (4 — every top-level route pairs
    masthead-label + masthead-title), mobile-swipe-delete.test.ts (M-06/M-07
    describe.skip — Chromium touch emulation flaky; needs `playwright install
    webkit`)
  Vitest: undoQueue.test.ts (5 — schedule+commit, undo+restore, TTL expiry,
    double-undo no-op, custom ttlMs)

5.1 Responsive shell
  DesktopSidebar.svelte (hidden md:flex)
  MobileTopBar.svelte (md:hidden sticky backdrop-blur-xl)
  BottomTabBar.svelte (md:hidden glassmorphism, safe-area-inset-bottom)
  MobileDrawer.svelte (backdrop + collective switcher + settings + logout)
  (app)/+layout.svelte — CRITICAL FIX: flex flex-col md:flex-row. Was
    horizontal-only, which pushed masthead off-screen on mobile. UndoToast
    mounted globally here.

5.2 Masthead + responsive padding
  ScreenMasthead.svelte — label (13px uppercase) + title (26px/32px).
  Applied to /lists, /tasks, /notes, /search. px-8 → px-4 md:px-6 everywhere.
  messages/{en,es}.json: masthead_{lists,tasks,notes,search}_label +
    masthead_search_title, undo, undo_deleted_item, list_item_delete_aria.

5.4 Red-zone swipe-delete on /lists/[id]
  SWIPE_MAX=96 (red zone rest width), SWIPE_THRESHOLD=48 (latch-open),
  SWIPE_COMMIT_THRESHOLD=200 (full-swipe commit). Red zone button exposes
  aria-label="Delete item"/"Eliminar producto". handleDelete uses
  scheduleUndoable — optimistic local removal, 4-s TTL, commit to Supabase
  on timeout or restore from snapshot on Undo.

5.5 UndoQueue + UndoToast
  src/lib/sync/undoQueue.ts — writable store + Map-backed actions with TTL.
    __flushUndoQueueForTests + __pendingUndoCount for Vitest.
  src/lib/components/UndoToast.svelte — renders latest undoable, offset
    above bottom tab bar (bottom: calc(env(safe-area-inset-bottom) + 4.5rem)),
    Undo button.

5.6 Scrollable frequency chips
  ItemSuggestions.svelte — mobile: flex overflow-x-auto with
    [scrollbar-width:none] [&::-webkit-scrollbar]:hidden; desktop: wraps.
    Chips shrink-0 so they don't compress.

5.7 Shell polish on tasks/notes/search
  Search group headers now carry "{n} MATCH / MATCHES" badges.
  Grid in /lists goes 2-col → 3-col at md+.
  Sticky create input offset for the bottom tab bar on mobile.

5.Z Verification
  just test-all → 224 verdes, 4 skipped
    34 pgTAP (unchanged)
    140 Vitest integration + 2 skipped presence (unchanged)
    11 Vitest unit (was 6, +5 undoQueue)
    39 Playwright + 2 skipped swipe-touch (was 31, +8 new mobile tests)

Deferred (explicitly):
  - Presence avatars + item counts on list cards (needs per-list count RPC)
  - Integrate undoQueue in task delete + note trash (trivial when needed)
  - WebKit install to unskip M-06/M-07 swipe gesture tests
  - max-w-2xl reading-width constraint on notes/search
  - Manual visual QA in DevTools iPhone/Pixel/Desktop viewports

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:09:17 +02:00

16 KiB
Raw Blame History

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/push/rate-limit/JWT rotation diferidos a sprint de deploy
Fase 5 — Rediseño UX Mobile 🟡 Shell responsive + masthead + swipe-delete red zone + undoQueue shipped; presence avatars en cards + touch-gesture E2E diferidos
Suite de tests (pgTAP + Vitest + Playwright) 224 tests verdes (34 pgTAP + 140 integración + 11 unit + 39 E2E), 4 skipped, ejecutables con just test-all

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 finaljust 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 12 semanas
Fase 1 fase-1-auth-colectivo.md 23 semanas
Fase 2a fase-2a-lista-compra-crud.md 2 semanas
Fase 2b fase-2b-realtime-modo-compra.md 23 semanas
Fase 3 fase-3-tareas-notas.md 12 semanas
Fase 4 fase-4-busqueda-pulido.md 1 semana
Fase 5 fase-5-mobile-ux.md 12 semanas

Total estimado: 1015 semanas (con Fase 5 mobile)

Los mockups de referencia viven en 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

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 12 semanas
Fase 1 — Auth y Colectivo 23 semanas
Fase 2a — Lista de Compra CRUD 2 semanas
Fase 2b — Realtime y Modo Compra 23 semanas
Fase 3 — Tareas y Notas 12 semanas
Fase 4 — Búsqueda y Pulido 1 semana
Total 913 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.