From 30cd83ffc1f50418b749583bd5c7d08f96f5172a Mon Sep 17 00:00:00 2001 From: markosbenitez Date: Sat, 27 Jun 2026 15:27:54 -0300 Subject: [PATCH] =?UTF-8?q?ETAPA=5FCALIDAD:=20Nota=20de=20discrepancia:=20?= =?UTF-8?q?las=205=20clases=20de=20P1=20usan=20var(--color-primary),=20no?= =?UTF-8?q?=20var(--color-re-teal)=20como=20dec=C3=ADa=20el=20prompt=20?= =?UTF-8?q?=E2=80=94=20ambas=20resuelven=20al=20mismo=20hex=20#43BBC8.=20H?= =?UTF-8?q?ice=20el=20reemplazo=20por=20nombre=20de=20clase=20(igual=20int?= =?UTF-8?q?enci=C3=B3n:=20texto=20teal=20=E2=86=92=20carb=C3=B3n),=20no=20?= =?UTF-8?q?por=20el=20literal=20del=20grep.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/estado/ESTADO_PROYECTO_26JUN2026.md | 169 ++++++++++++++++++ sprints/sprint-4/ETAPA_4I_PROMPT.md | 113 ++++++++++++ sprints/sprint-4/ETAPA_AUTH_PROMPT.md | 148 +++++++++++++++ sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md | 124 +++++++++++++ sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md | 123 +++++++++++++ .../sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md | 47 +++++ sprints/sprint-5/INSUMOS_AUDIT_LOG.md | 106 +++++++++++ 7 files changed, 830 insertions(+) create mode 100644 docs/estado/ESTADO_PROYECTO_26JUN2026.md create mode 100644 sprints/sprint-4/ETAPA_4I_PROMPT.md create mode 100644 sprints/sprint-4/ETAPA_AUTH_PROMPT.md create mode 100644 sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md create mode 100644 sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md create mode 100644 sprints/sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md create mode 100644 sprints/sprint-5/INSUMOS_AUDIT_LOG.md diff --git a/docs/estado/ESTADO_PROYECTO_26JUN2026.md b/docs/estado/ESTADO_PROYECTO_26JUN2026.md new file mode 100644 index 0000000..f79d0c3 --- /dev/null +++ b/docs/estado/ESTADO_PROYECTO_26JUN2026.md @@ -0,0 +1,169 @@ +# ESTADO DEL PROYECTO — Datos País sobre el Cuidado +## Documento de continuidad — 26 junio 2026 + +> Director (Product Owner): Marcos · Claude (Projects) = arquitecto/copiloto · Claude Code = implementación +> Metodología: Maria Mersan · Contacto Fundación: Valeria Arce + +--- + +## 1. Versionado y ramas +- **Producción:** **3.0.0** — wizard v3 + dashboard tres miradas desplegado · tag `v3.0.0` · commit `fcf93ea` +- **Preservación:** `v2.1.0-stable` + tag `v2.1.0` +- **3 surveys en `live`** — reactivados al desplegar v3 (26 jun 2026) +- SemVer + CHANGELOG + tags. ADRs en `docs/adr/`. + +--- + +## 2. Sprint 4 — Wizard v3 — COMPLETO Y EN PRODUCCIÓN + +| Etapa | Estado | +|---|---| +| 4.0 Preservación v2 | ✅ | +| 4A instance_id | ✅ | +| 4B esquema declarativo + seed v3 | ✅ | +| 4C motor de flujo (`lib/form-engine.ts`, puro, 20+ tests) | ✅ | +| 4D wizard renderer (layout estable, header/footer fijos) | ✅ | +| 4E instancias familiares (mini-wizard emergente del motor) | ✅ | +| 4F autocomplete barrios + filtro ciudad por departamento | ✅ | +| 4G submit + validación server-side (RPC `rpc_submit_response`) | ✅ | +| 4I validación e2e + tag v3.0.0 + deploy + surveys live | ✅ | + +### Decisiones arquitectónicas clave (no reabrir) +- **ADR-005:** lógica de flujo en datos (esquema), nunca en frontend. +- Motor puro en cliente; reusado server-side para validación de submit. +- Único camino de escritura: RPC `rpc_submit_response` (SECURITY DEFINER). +- `instance_id` por answer — instancias familiares persistidas correctamente. +- Validación server-side: 7 checks (preguntas visibles, valores válidos, instancias legítimas, + consentimiento, requeridas presentes). Rechaza el submit completo si algo falla. + +### Autocomplete geo (4F) +- `public/data/geo_paraguay_full.json` — 8152 barrios, jerarquía depto → ciudad → barrios. +- Pregunta 2.3 (barrio): autocomplete con `position: fixed` + `getBoundingClientRect()`. +- Pregunta 2.2 (ciudad): filtra por departamento (2.1) usando el mismo JSON. Limpia valor + al cambiar departamento. Texto libre siempre permitido como fallback. + +--- + +## 3. Dashboard v3 — Mirada Empresa en producción + +**Decisión vigente:** NO migrar RPCs v2. Dashboard nuevo de tres miradas +(OIT 5R, CEPAL, PNCUPA Paraguay). + +### Estado +- **Mirada Empresa:** ✅ COMPLETA EN PRODUCCIÓN + - 11 RPCs (`rpc_me_*`) + helper `_dash_es_cuidador` verificados contra 50 respuestas reales. + - 9 widgets funcionando: Talento en riesgo, Prevalencia, Punto ciego, Señales de impacto + (presentismo 41.7%, ausentismo 36.1%, riesgo 38.9%, carrera congelada 47.2%), + Apoyos demandados (ordenados por demanda), Segmentación (nivel y modalidad), + Disposición a red de apoyo. + - Tema claro marca Re: fondo `#E7E6E5`, cards `#FFFFFF`, texto `#494F4F`. + - Migraciones aplicadas: `20260625000001` (fix `_dash_responses`) + + `20260625000002` (11 RPCs) + `20260626000001` (fix bug `cnt` Opción B). +- **Mirada País y Mirada Humana:** propuesta + mockup listos, implementación DIFERIDA + a sprint de dashboard post-deadline. + +### Bug conocido abierto +- `rpc_me_carrera_congelada`: "Ninguna de las anteriores" aparece en `top_opciones` + a pesar del filtro en el subquery. El filtro opera sobre el array completo, no sobre + los elementos individuales del `jsonb_array_elements_text`. No bloqueante para el + release — registrado para próximo sprint. + +### Documentos (en `docs/dashboard/`) +- `PROPUESTA_DASHBOARD_v3.md` — las 3 miradas, ~30 métricas, marcos citados +- `dashboard_tres_miradas_mockup.html` — mockup funcional 3 pestañas +- `SPEC_MIRADA_EMPRESA.md` — contrato técnico 10 métricas + estrella + +### Decisiones del dashboard (no reabrir) +- CUIDADOR_ACTIVO incluye "hijo menor sin discapacidad" (criterio UNFPA, validado por Maria jun 2026). +- Mirada Empresa NO expone datos sensibles (3.x, 4.2, 4.3). +- k-anonimato k≥5 sobre `count(DISTINCT response_id)` — también en métricas por instancia. +- 2 gaps de datos para sprint dashboard: horas cuidado no remunerado (País), + escolarización niñez (Humana). + +--- + +## 4. Datos de prueba en VPS +- **51 respuestas** en survey Demo (`30000000-0000-0000-0000-000000000001`) — 50 del seed + determinista (seed=42) + 1 del e2e de 4I. +- Survey Demo en `live`. Datos NO limpiados — el director los necesita para mostrar al equipo. +- Script de limpieza cuando corresponda: +```bash +docker exec supabase-db psql -U postgres -d postgres -c " +TRUNCATE answers, responses, consent_records CASCADE; +UPDATE surveys SET status='draft' WHERE id='30000000-0000-0000-0000-000000000001';" +``` +- ⚠️ El seed corrió dos veces en sesión anterior (detectado y corregido). Agregar guard + anti-duplicación al `seed_prueba_50.sql` — anotado en BACKLOG. + +--- + +## 5. Deuda técnica y seguridad + +| ID | Descripción | Prioridad | +|---|---|---| +| BUG-001 | "Ninguna de las anteriores" aparece en `rpc_me_carrera_congelada` | Próximo sprint dashboard | +| TECH-DEBT-006 | Backup remoto (S3 o VPS alternativo o Google Drive) | Antes de datos reales no desechables | +| TECH-DEBT-009 | Rotar secrets post-CVE | Antes de datos reales no desechables | +| TECH-DEBT-010 | E9 lee modalidad por JOIN a answers (2.7) en vez de `qi_modalidad` | Baja | +| SEC-002 | Sprint compliance (audit log, hashing, cifrado en reposo) | Sprint dedicado | +| SEC-003 | TRUNCATE evade inmutabilidad ADR-003 | Antes de datos reales no desechables | +| SEC-004 | RPC bypass REST (coherencia de flujo en Server Action) | Sprint futuro | +| BACKLOG-001 | `seed_prueba_50.sql` idempotente (guard anti-duplicación) | Baja | +| BACKLOG-002 | Fase 2: motor genérico de formularios | Post-deadline | + +**Riesgo aceptado explícitamente (26 jun 2026):** TECH-DEBT-006, TECH-DEBT-009 y SEC-003 +aplican cuando haya datos reales no desechables. Los datos actuales son de empresas voluntarias +y grupos de control — desechables si es necesario. + +--- + +## 6. Infraestructura + +- Next.js 15.3.8 · Supabase self-hosted · Dokploy v0.29.4 · VPS inq-mart-2 (172.20.5.11) +- App: `economia-cuidado.inqualityhq.com` · API: `db.inqualityhq.com` (Kong + SSL) +- Repo: `git.inquality.com.py/marcos/motor-de-encuestas` (rama `main`) +- Repo local: `/Users/marcos/InQ/Motor de encuestas` +- Migraciones SIEMPRE vía CLI del VPS: + `docker exec -i supabase-db psql -U postgres -d postgres < archivo.sql` +- Deploy: Dokploy → Rebuild + "Clean cache" +- Anon key Kong (diagnóstico): + `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjIwMDAwMDAwMDB9.6D79NWu1KfeOzEZXT6Amf-NmG0T9uk8SK9xNd32oLaY` + +--- + +## 7. Contexto comercial +- 2 clientes inminentes. Mirada Empresa es la que le habla al cliente que paga. +- InQuality y Fundación Solidaridad en joint venture. +- Datos actuales: empresas voluntarias y grupos de control (desechables). +- Datos reales no desechables: avisará el director — en ese momento activar deuda de seguridad. + +--- + +## 8. Próximos sprints sugeridos + +1. **Sprint dashboard** — Miradas País y Humana, fix BUG-001 carrera congelada, + preguntas faltantes (horas no remuneradas, escolarización), decisión `qi_modalidad`. +2. **Sprint compliance** — SEC-002 (audit log, hashing, cifrado), SEC-003, TECH-DEBT-006/009. +3. **Sprint motor genérico** — BACKLOG-002 Fase 2, extracción del intérprete. + +--- + +## 9. Prompt para nueva sesión + +``` +Sos el arquitecto/copiloto técnico de "Datos País sobre el Cuidado", motor de formularios +soberano (Paraguay). Yo soy el product owner (Marcos). +Claude Code (VS Code, contexto separado) hace la implementación; vos planificás, +especificás y auditás. + +Estado completo: docs/estado/ESTADO_PROYECTO_26JUN2026.md + +Producción: v3.0.0 desplegada · 3 surveys en live · tag v3.0.0 (commit fcf93ea) +Formulario v3: COMPLETO (motor, wizard, instancias, submit server-side, autocomplete geo) +Dashboard: Mirada Empresa funcional en producción · País/Humana diferidas +Datos en VPS: 51 respuestas en survey Demo (no limpiar — para mostrar al equipo) + +Bug abierto: "Ninguna de las anteriores" en rpc_me_carrera_congelada (no bloqueante) + +Próximo paso: [COMPLETAR] +``` diff --git a/sprints/sprint-4/ETAPA_4I_PROMPT.md b/sprints/sprint-4/ETAPA_4I_PROMPT.md new file mode 100644 index 0000000..165622d --- /dev/null +++ b/sprints/sprint-4/ETAPA_4I_PROMPT.md @@ -0,0 +1,113 @@ +# ETAPA 4I — Validación e2e + Release v3.0.0 + +## 1. Objetivo +Validar el wizard v3 end-to-end y hacer el release a producción. Los 3 surveys +pasan de `draft` a `live`. Esta etapa es irreversible en varios pasos — seguir +el orden estricto de fases y detenerse si algo falla. + +## 2. Estado de entrada +- Último commit: `3e7109a0` (4F ciudad-select) +- Commits no desplegados desde último deploy: + `8dea04c` fix cnt RPCs · `dbcc60a` tema claro · `3ec5da0` autocomplete barrios · + `3e7109a0` ciudad-select +- VPS: seed de 50 respuestas activo (Demo en `live`) — NO limpiar, el director + quiere mostrar el dashboard al equipo con esos datos +- Los 3 surveys están en `draft` + +## 3. DECISIONES YA TOMADAS — no consultar +- Deploy vía Dokploy (rebuild + clean cache) +- Migraciones SIEMPRE vía CLI del VPS — ninguna migración pendiente en esta etapa +- Los 3 surveys se reactivan a `live` solo DESPUÉS de verificar producción +- Tag `v3.0.0` solo DESPUÉS de reactivar surveys +- No hay migración de datos v2 → v3 (arranque limpio) + +## 4. Fases — orden estricto, detenerse si algo falla + +### FASE 1 — Validación e2e local (antes de tocar producción) +Correr el wizard completo localmente y verificar: +- [ ] Consentimiento → flujo completo → submit exitoso +- [ ] Una respuesta con al menos 1 familiar (instancia) persiste con instance_id +- [ ] El motor no muestra preguntas que el flujo oculta +- [ ] El autocomplete de barrio funciona (dropdown visible, no tapado) +- [ ] El filtro ciudad-departamento funciona (2.1 → 2.2 filtra, cambiar 2.1 limpia 2.2) +- [ ] Submit devuelve confirmación, no error + +Si CUALQUIER item falla: DETENER. Reportar el fallo y esperar instrucción. + +### FASE 2 — Deploy en Dokploy +- Trigger rebuild en Dokploy con "Clean cache" +- Verificar que el BUILD_ID nuevo aparece en los logs +- Verificar que la app responde en `economia-cuidado.inqualityhq.com` +- Verificar que el dashboard sigue mostrando los 50 datos sembrados +- Verificar que el wizard abre y llega a la pregunta 2.3 sin errores de consola + +Si CUALQUIER item falla: DETENER. Reportar el fallo y esperar instrucción. + +### FASE 3 — Reactivar surveys + Tag +Solo ejecutar si Fase 1 y Fase 2 están completas y verificadas. + +Reactivar los 3 surveys en el VPS: +```sql +UPDATE surveys SET status = 'live' +WHERE id IN ( + SELECT id FROM surveys WHERE status = 'draft' +); +``` + +Verificar que quedaron en `live`: +```sql +SELECT id, name, status FROM surveys ORDER BY name; +``` + +Tag de release en el repo local y push: +```bash +git tag -a v3.0.0 -m "Release v3.0.0 — wizard declarativo + dashboard tres miradas" +git push origin v3.0.0 +``` + +Actualizar CHANGELOG.md con las etapas 4C→4I completadas. + +## 5. DoD completo +- [ ] `git log --name-only -1` antes de empezar — confirmar commit de entrada +- [ ] Fase 1 completa sin fallos +- [ ] Fase 2 completa — app en producción respondiendo +- [ ] Surveys en `live` — verificado con SELECT +- [ ] Tag `v3.0.0` pusheado a Gitea +- [ ] CHANGELOG.md actualizado +- [ ] `git log --name-only -1` del commit de CHANGELOG + +## 6. Qué reportar +Por fase, en orden: +FASE 1 — e2e local: + +Submit con familiar: [response_id y instance_id del registro insertado] +Items del checklist: ✓/✗ cada uno + +FASE 2 — Deploy: + +BUILD_ID nuevo: [valor] +App responde: ✓/✗ +Dashboard con datos: ✓/✗ +Wizard sin errores de consola: ✓/✗ + +FASE 3 — Release: + +Surveys en live: [output del SELECT] +Tag v3.0.0: [output de git tag] +CHANGELOG: [secciones agregadas] + +## 7. Qué NO hacer +- ❌ No reactivar surveys antes de verificar producción (Fase 2 primero) +- ❌ No taggear antes de reactivar surveys (Fase 3 es la última) +- ❌ No limpiar el seed de 50 respuestas — el director los necesita para mostrar al equipo +- ❌ No aplicar migraciones — no hay ninguna pendiente en esta etapa +- ❌ No tocar `v2.1.0-stable` +- ❌ No continuar si algo falla — detenerse y reportar + +## 8. Versionado +chore(release): CHANGELOG v3.0.0 — wizard declarativo + dashboard tres miradas +Etapas 4C→4I completadas. Motor de flujo puro, wizard renderer, + +instancias familiares, submit con validación server-side, dashboard + +tres miradas (Mirada Empresa funcional), autocomplete barrios/ciudad. \ No newline at end of file diff --git a/sprints/sprint-4/ETAPA_AUTH_PROMPT.md b/sprints/sprint-4/ETAPA_AUTH_PROMPT.md new file mode 100644 index 0000000..8f3aa65 --- /dev/null +++ b/sprints/sprint-4/ETAPA_AUTH_PROMPT.md @@ -0,0 +1,148 @@ +# ETAPA AUTH — Supabase Auth + protección /admin + +## 1. Objetivo +Implementar autenticación email/password con Supabase Auth y proteger +la ruta /admin/*. El dashboard y las vistas de admin quedan detrás de login. +Sin esta etapa no se avanza a vista admin mejorada ni vista detallada. + +## 2. Contexto +- `current_app_role()` y `current_org_id()` ya leen del JWT — el esquema está listo. +- No hay middleware.ts ni sistema de auth hoy — /admin/responses está abierto. +- Organizations existentes: Empresa Demo (…0001), InQuality (…0002), + Fundación Solidaridad (…0003). +- No hay usuarios en auth.users — hay que crear el primero como parte de esta etapa. +- La app usa anon key para el formulario público — eso NO cambia. +- /admin/* usa service_role hoy — eso SÍ cambia: pasa a usar sesión del usuario. + +## 3. DECISIONES YA TOMADAS — no consultar +- Supabase Auth nativo, email/password. Sin OAuth por ahora. +- Dos roles: `platform_admin` (acceso total) y `org_admin` (acceso a su org). +- Roles van como custom claims en el JWT: `app_role` y `org_id`. +- Middleware Next.js protege /admin/* — redirige a /login si no hay sesión válida. +- Página de login en /login — simple, solo email/password, sin registro público. +- El formulario público (/encuesta/*) NO se toca — sigue con anon key. +- Las RPCs del dashboard usan current_app_role() y current_org_id() del JWT — + cuando el usuario esté autenticado, RLS funciona automáticamente. + +## 4. Tareas + +### 4.1 Diagnóstico previo — mecanismo de custom claims +ANTES de implementar, reportar qué mecanismo de JWT customization está disponible +en esta versión de Supabase self-hosted. Puede ser: +- Hook SQL en `auth.hooks` +- Override de función `auth.jwt()` +- Edge Function hook +No implementar nada hasta confirmar el mecanismo y reportarlo. + +### 4.2 Migración — tabla user_roles + custom claims hook +Migración `20260627000001_auth_user_roles.sql`: + +```sql +-- Tabla de roles +CREATE TABLE public.user_roles ( + user_id uuid PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, + app_role text NOT NULL CHECK (app_role IN ('platform_admin','org_admin')), + org_id uuid REFERENCES public.organizations(id) +); + +-- RLS: solo el propio usuario puede ver su rol (y platform_admin ve todos) +ALTER TABLE public.user_roles ENABLE ROW LEVEL SECURITY; +``` + +Implementar el hook de custom claims según el mecanismo disponible confirmado en 4.1. +El JWT resultante debe incluir: +```json +{ "app_role": "platform_admin", "org_id": "10000000-0000-0000-0000-000000000002" } +``` + +### 4.3 Script de setup — primer usuario admin +Script operativo (NO migración) en `scripts/setup_first_admin.sql`: +- Crea el usuario en auth.users vía INSERT directo o Supabase admin API +- Inserta en user_roles con app_role='platform_admin' y org_id de InQuality +- NO hardcodear credenciales — usar variables `:email` y `:password` con instrucciones + de cómo ejecutarlo: + ```bash + docker exec -i supabase-db psql -U postgres -d postgres \ + -v email='admin@ejemplo.com' -v password='CAMBIAR' \ + < scripts/setup_first_admin.sql + ``` +- Documentar en el script cómo cambiar la contraseña después del primer login + +### 4.4 Middleware Next.js +`middleware.ts` en la raíz del proyecto: +- Protege `/admin/*` y `/dashboard/*` +- Si no hay sesión válida → redirect a `/login` +- Si hay sesión → deja pasar +- `/`, `/encuesta/*`, `/login` — sin restricción + +### 4.5 Página de login +`app/login/page.tsx` — Client Component: +- Formulario email/password +- Llama `supabase.auth.signInWithPassword()` +- En éxito: redirect a `/admin/responses` +- En error: mensaje claro ("Email o contraseña incorrectos"), nunca trace crudo +- Sin registro público +- Usar clases Tailwind existentes — sin nuevo sistema de diseño + +### 4.6 Actualizar /admin/responses +- Reemplazar `getSupabaseAdmin()` (service_role) por cliente de sesión del usuario +- RLS de Postgres filtra según app_role y org_id del JWT automáticamente +- Agregar botón "Cerrar sesión" (`supabase.auth.signOut()` → redirect a `/login`) + +## 5. DoD antes de reportar +- [ ] `git log --name-only -1` — listar todos los archivos tocados +- [ ] Diagnóstico de mecanismo de custom claims reportado (4.1) antes de implementar +- [ ] Migración `20260627000001` aplicada en VPS — verificar con: + ```bash + docker exec -i supabase-db psql -U postgres -d postgres -c "\d user_roles" + ``` +- [ ] Script `scripts/setup_first_admin.sql` existe y está documentado +- [ ] Hook JWT verificado — el token incluye `app_role` (cómo verificarlo: reportar el método) +- [ ] `/login` funciona — email/password → sesión activa → redirect a /admin/responses +- [ ] `/admin/responses` sin sesión → redirect a `/login` +- [ ] `/admin/responses` con sesión → carga datos correctamente +- [ ] Formulario público sin cambios (verificar que /encuesta/* sigue funcionando) +- [ ] Build sin errores: `npm run build` +- [ ] Commit + push a Gitea + +## 6. Qué reportar +``` +Diagnóstico previo (4.1): +- Mecanismo de custom claims disponible: [cuál] +- Alternativas descartadas: [por qué] + +Verificado: +- Migración user_roles aplicada: ✓ — output de \d user_roles +- Hook JWT: app_role presente en token ✓/✗ — método de verificación usado +- /login funciona: ✓/✗ +- /admin sin sesión → /login: ✓/✗ +- /admin con sesión → datos: ✓/✗ +- Formulario público sin cambios: ✓/✗ +- Build: ✓/✗ +- git log --name-only -1: [lista completa de archivos] + +Asumido / decisiones: +- [mecanismo de hook y razonamiento] + +Hallazgos: +- [lo que aparezca] +``` + +## 7. Qué NO hacer +- ❌ No tocar el formulario público ni la anon key +- ❌ No hardcodear credenciales en ningún archivo commiteado +- ❌ No implementar registro público de usuarios +- ❌ No usar service_role en rutas que ya tienen sesión de usuario +- ❌ No avanzar a vista admin mejorada — eso es la etapa siguiente +- ❌ No hacer deploy — solo commit y push a Gitea +- ❌ No implementar el hook sin reportar primero el diagnóstico de 4.1 + +## 8. Versionado +``` +feat(auth): Supabase Auth email/password + middleware + login [auth] + +Protege /admin/* con Supabase Auth. Tabla user_roles con custom claims +(app_role, org_id) en JWT. Middleware Next.js redirige a /login sin sesión. +Página /login con email/password. /admin/responses migrado de service_role +a sesión de usuario autenticado. +``` \ No newline at end of file diff --git a/sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md b/sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md new file mode 100644 index 0000000..665e880 --- /dev/null +++ b/sprints/sprint-5/ETAPA_AUDIT_LOG_PROMPT.md @@ -0,0 +1,124 @@ +# ETAPA AUDIT LOG — Registro de auditoría append-only (Sprint 5, SEC-002) + +## 1. Objetivo +Implementar un registro de auditoría append-only en Postgres que loggee eventos +de configuración crítica: cambios en `user_roles` (altas/bajas/modificaciones de +acceso) y cambios de `status` en `surveys` (activación/desactivación de encuestas). +Opcionalmente, si la implementación es simple y sin fricción, agregar también un +trigger DDL para loggear migraciones aplicadas. + +## 2. Contexto +- Migración de referencia para patrones: `20260627000001_auth_user_roles.sql` + (patrón SECURITY DEFINER + GRANT puntual a reutilizar). +- Helpers RLS existentes a reusar: `public.current_app_role()` y + `public.current_org_id()` — no reinventar. +- Gap conocido y aceptado (SEC-003): esto NO protege contra TRUNCATE/DROP ejecutado + por superusuario directo en el VPS. Documentar en comentario de la migración, no resolver. +- Acceso a datos sensibles: NO loggear en esta etapa — hoy no existe código que + lea columnas `is_sensitive=true`. Deferido a cuando ese código exista. +- Naming de migración: después de `20260627000001` — usar timestamp del día de + ejecución, formato `YYYYMMDDHHMMSS`. + +## 3. DECISIONES YA TOMADAS — no consultar +- Tabla nueva: `public.audit_log`, append-only real. +- Ningún rol (`anon`, `authenticated`, `service_role` vía API) tiene INSERT/UPDATE/DELETE + directo en `audit_log`. Solo escriben funciones SECURITY DEFINER de los triggers. +- Segunda barrera: trigger `BEFORE UPDATE OR DELETE ON audit_log` que aborta con + excepción — defensa en profundidad. +- Lectura de audit_log: RLS, solo `platform_admin` via `current_app_role()`. +- Patrón de función trigger: SECURITY DEFINER, propiedad de `postgres`, + GRANT EXECUTE solo al rol que lo necesita (no a PUBLIC). +- Columnas de audit_log: + ```sql + id bigserial PRIMARY KEY, + occurred_at timestamptz NOT NULL DEFAULT now(), + actor_role text, + actor_user_id uuid, + action text NOT NULL, + target_table text NOT NULL, + target_id text, + detail jsonb + ``` +- Triggers obligatorios (Prioridad 1): + - AFTER INSERT/UPDATE/DELETE en `user_roles` + - AFTER UPDATE en `surveys` cuando cambia `status` +- Trigger DDL (Prioridad 2 — opcional): solo implementar si es simple y sin fricción. + Si requiere extensión nueva o configuración de infraestructura, NO incluir y reportarlo. + +## 4. Tareas + +### 4.1 Diagnóstico previo — DDL trigger +Antes de implementar, verificar si el DDL trigger (`ddl_command_end`) está disponible +sin extensiones adicionales en esta versión de Postgres self-hosted: +```bash +docker exec -i supabase-db psql -U postgres -d postgres -c " +SELECT event_object_schema, trigger_name +FROM information_schema.triggers +WHERE trigger_name LIKE '%ddl%' LIMIT 5;" +``` +Reportar si es viable sin fricción. Si requiere configuración extra, documentarlo +y omitirlo de esta etapa. + +### 4.2 Migración principal +Archivo: `supabase/migrations/YYYYMMDDHHMMSS_audit_log.sql` + +Contenido en orden: +1. Crear tabla `audit_log` con columnas definidas arriba. +2. RLS en `audit_log`: solo `platform_admin` puede SELECT. +3. Trigger `BEFORE UPDATE OR DELETE ON audit_log` → función que lanza excepción + (append-only enforcement). +4. Función `log_user_roles_change()` SECURITY DEFINER → INSERT en audit_log. +5. Trigger AFTER INSERT/UPDATE/DELETE en `user_roles` → llama a la función. +6. Función `log_survey_status_change()` SECURITY DEFINER → INSERT en audit_log + solo cuando `NEW.status IS DISTINCT FROM OLD.status`. +7. Trigger AFTER UPDATE en `surveys` → llama a la función. +8. Si DDL trigger es viable (4.1): función + trigger DDL. +9. Comentario al final documentando el gap SEC-003. + +### 4.3 Verificación en VPS +NO aplicar la migración — solo generar el archivo y reportar el SQL completo para +revisión del director. El director aplica manualmente después de aprobar. + +## 5. DoD antes de reportar +- [ ] `git log --name-only -1` — solo el archivo de migración +- [ ] SQL completo de la migración en el reporte (no resumido — el director + necesita leerlo antes de aplicar) +- [ ] Diagnóstico DDL trigger reportado (4.1) +- [ ] Build sin errores: `npm run build` +- [ ] Commit + push a Gitea +- [ ] NO aplicado en VPS — esperar aprobación del director + +## 6. Qué reportar +``` +Diagnóstico DDL trigger (4.1): +- ¿Viable sin extensiones? ✓/✗ +- Si no: razón y alternativa + +SQL completo de la migración: +[pegar el SQL íntegro — no resumido] + +Verificado: +- Build: ✓/✗ +- git log --name-only -1: [lista] +- Commit: [hash] + +NO aplicado en VPS — pendiente aprobación del director. +``` + +## 7. Qué NO hacer +- ❌ No aplicar la migración en el VPS sin aprobación explícita del director +- ❌ No implementar logging de accesos a datos sensibles (deferido) +- ❌ No agregar extensiones nuevas a Postgres +- ❌ No tocar el formulario público, las RPCs de dashboard, ni el middleware de auth +- ❌ No usar GRANT EXECUTE a PUBLIC — solo al rol específico que necesita cada función +- ❌ No omitir el comentario del gap SEC-003 en la migración + +## 8. Versionado +``` +feat(compliance): audit_log append-only — user_roles y surveys.status [SEC-002] + +Tabla audit_log con RLS (solo platform_admin), trigger BEFORE UPDATE/DELETE +que aborta (append-only enforcement), triggers AFTER en user_roles y +surveys.status. Patrón SECURITY DEFINER reutilizado de auth_user_roles. +Gap SEC-003 (TRUNCATE por superusuario) documentado y aceptado. +``` \ No newline at end of file diff --git a/sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md b/sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md new file mode 100644 index 0000000..4e06f4b --- /dev/null +++ b/sprints/sprint-5/ETAPA_CALIDAD_PROMPT.md @@ -0,0 +1,123 @@ +# ETAPA CALIDAD — Contraste, mobile, conectividad, touch (Sprint 5) + +## 1. Objetivo +Resolver los 4 problemas de calidad identificados en la auditoría del 27 jun 2026, +en orden de impacto. Un solo commit al terminar todo. + +## 2. Contexto +Diagnóstico base: auditoría de solidez/usabilidad (27 jun 2026, sin modificaciones). +Todos los problemas tienen archivo y línea exacta identificados. + +## 3. DECISIONES YA TOMADAS — no consultar +- **Contraste teal:** Opción B — `#43BBC8` queda SOLO para decoración (fondos, + bordes, barras, elementos gráficos). Cuando se usa como COLOR DE TEXTO, reemplazar + por `#494F4F` (carbón de marca). No crear variante oscura del teal. +- **Mobile bottombar:** flex-wrap en el contenedor, no width fijo en los botones. +- **Submit sin try/catch:** agregar manejo de error de red que muestre el estado + 'error' propio del wizard, no la pantalla cruda de React. +- **Autocomplete touch:** agregar `onTouchEnd` como fallback junto al `onMouseDown` + existente — no reemplazarlo. +- Sin librerías nuevas en ningún fix. + +## 4. Tareas en orden + +### P1 — Contraste teal como texto (CRÍTICO — sistemático) +En `app/globals.css`, reemplazar `color: var(--color-re-teal)` por +`color: #494F4F` en estas clases específicas: +- `.btn-secondary` (botón "Anterior") — línea ~380 +- `.wizard-section-number` ("Sección X de Y") — línea ~569 +- `.summary-code` — línea ~431 +- `.barrio-autocomplete-item--active` (texto del ítem activo) — línea ~319 +- `.consent-required-badge` — línea ~490 + +NO tocar usos de `var(--color-re-teal)` como `background-color`, `border-color`, +`fill`, o en las clases `.db-*` del dashboard — esos son usos decorativos correctos. + +Verificar después del cambio que no quedó ningún `color: var(--color-re-teal)` en +clases del wizard (grep en globals.css sección wizard): +```bash +grep -n "color: var(--color-re-teal)" app/globals.css +``` +Los únicos resultados aceptables son en sección `.db-*` del dashboard. + +### P2 — Submit sin try/catch (red se cae al enviar) +En `components/wizard-form.tsx`, función `handleSubmit` (líneas ~212-226): +Envolver el `await submitSurvey(...)` en try/catch. Si el catch recibe un error +de red (fetch failed, network error), setear el estado del wizard a 'error' con +mensaje accionable: "No pudimos enviar tu respuesta. Verificá tu conexión e +intentá de nuevo." El botón de reintento ya existe en el estado 'error' +(wizard-form.tsx:136-152) — solo hay que llegar a ese estado correctamente. +No crear `app/error.tsx` — el estado de error propio del wizard es suficiente. + +### P3 — Botones bottombar en mobile ≤480px +En `app/globals.css`, sección `.wizard-bottombar` (~línea 653): +Agregar `flex-wrap: wrap` al contenedor `.wizard-bottombar`. +En el breakpoint `@media (max-width: 480px)`: +- `.wizard-bottombar` → `flex-direction: column-reverse` (Siguiente arriba, Anterior abajo) +- `.btn` dentro del bottombar → `width: 100%` +- El span hint entre los botones → `order: 3` para que quede al final +`column-reverse` porque el botón primario (Siguiente/Enviar) debe estar arriba +en mobile — el pulgar llega más fácil al centro que a la base. + +### P4 — Autocomplete touch frágil +En `components/widgets/barrio-autocomplete-field.tsx` y +`components/widgets/ciudad-select-field.tsx`: +En el handler de selección de ítem del dropdown, agregar `onTouchEnd` junto +al `onMouseDown` existente (no reemplazarlo). El `onTouchEnd` debe llamar +`e.preventDefault()` antes de ejecutar la selección para evitar el blur +sintético que cierra la lista antes de que se procese la selección. + +## 5. DoD antes de reportar +- [ ] `git log --name-only -1` — listar todos los archivos tocados +- [ ] grep de verificación P1: + ```bash + grep -n "color: var(--color-re-teal)" app/globals.css + ``` + Solo resultados en sección `.db-*` +- [ ] Build sin errores: `npm run build` +- [ ] P2 verificado: en `handleSubmit`, el catch existe y setea estado 'error' + con mensaje accionable (pegar el bloque try/catch completo en el reporte) +- [ ] P3 verificado: describir el CSS resultante del bottombar en mobile +- [ ] P4 verificado: describir el handler resultante de selección en autocomplete +- [ ] Commit: `fix(quality): contraste teal, submit red, bottombar mobile, autocomplete touch` +- [ ] Push a Gitea + +## 6. Qué reportar +``` +P1 — Contraste: +- Clases modificadas: [lista con línea antes → después] +- grep verificación: [output] + +P2 — Submit try/catch: +- Bloque handleSubmit completo después del fix: [código] + +P3 — Bottombar mobile: +- CSS resultante de .wizard-bottombar y el breakpoint: [código] + +P4 — Autocomplete touch: +- Handler de selección resultante (onMouseDown + onTouchEnd): [código] + +Verificado: +- Build: ✓/✗ +- git log --name-only -1: [lista] +- Commit: [hash] +``` + +## 7. Qué NO hacer +- ❌ No tocar `var(--color-re-teal)` en usos decorativos (background, border, fill) +- ❌ No tocar clases `.db-*` del dashboard +- ❌ No crear `app/error.tsx` +- ❌ No reemplazar `onMouseDown` — agregar `onTouchEnd` junto a él +- ❌ No agregar librerías nuevas +- ❌ No hacer deploy — solo commit y push a Gitea +- ❌ No mezclar con los fixes rápidos de la etapa anterior + +## 8. Versionado +``` +fix(quality): contraste teal, submit red, bottombar mobile, autocomplete touch + +P1: color de texto teal → #494F4F en 5 clases del wizard (WCAG AA). +P2: try/catch en handleSubmit — error de red muestra estado error del wizard. +P3: flex-wrap + column-reverse en bottombar ≤480px. +P4: onTouchEnd fallback en autocomplete barrio y ciudad. +``` diff --git a/sprints/sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md b/sprints/sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md new file mode 100644 index 0000000..5cadc8f --- /dev/null +++ b/sprints/sprint-5/ETAPA_FIXES_RAPIDOS_PROMPT.md @@ -0,0 +1,47 @@ +# ETAPA — Fixes rápidos: toggle password + card Talento en riesgo + +## 1. Objetivo +Dos fixes visuales independientes y quirúrgicos. Scope estricto. + +## 2. DECISIONES YA TOMADAS — no consultar +- Toggle de contraseña: solo en el campo password del login, sin librería nueva. +- Card Talento en riesgo: eliminar el gradiente, quedar igual que los demás cards + (background blanco, sin distinción especial de fondo). + +## 3. Tareas + +### Fix A — Toggle "ver contraseña" en /login +En `app/login/page.tsx`: +- Agregar estado `showPassword` (useState). +- El input password alterna entre `type="password"` y `type="text"`. +- Botón inline dentro del campo (position absolute o flex) con ícono SVG + simple de ojo (inline, sin librería). Accesible: `aria-label="Mostrar contraseña"` / + `aria-label="Ocultar contraseña"` según estado. `type="button"` para no submitear. +- Estilo consistente con el formulario existente. + +### Fix B — Card Talento en riesgo sin gradiente +En `app/globals.css`, clase `.db-widget-star`: +- Eliminar la propiedad `background` (el gradiente actual). +- El card hereda `background: #FFFFFF` de `.db-widget` — mismo que los demás. +- NO tocar `border-color: var(--color-re-teal)` — mantener el borde distintivo. +- NO tocar `.db-widget-star .db-widget-title` — mantener el título en teal. + +## 4. DoD antes de reportar +- [ ] `git log --name-only -1` — solo `app/login/page.tsx` y `app/globals.css` +- [ ] Toggle password funciona: click muestra/oculta, aria-label cambia +- [ ] Card Talento en riesgo: fondo blanco igual que los demás cards +- [ ] Build sin errores: `npm run build` +- [ ] Commit: `fix(ui): toggle password en login + fondo blanco card talento` +- [ ] Push a Gitea + +## 5. Qué reportar +- `git log --name-only -1` +- Línea exacta eliminada en `.db-widget-star` +- Build: ✓/✗ + +## 6. Qué NO hacer +- ❌ No tocar otros campos del formulario de login +- ❌ No tocar otros widgets del dashboard +- ❌ No agregar librerías de iconos +- ❌ No modificar el borde teal ni el título teal del card estrella +- ❌ No hacer deploy — solo commit y push \ No newline at end of file diff --git a/sprints/sprint-5/INSUMOS_AUDIT_LOG.md b/sprints/sprint-5/INSUMOS_AUDIT_LOG.md new file mode 100644 index 0000000..d4f66ba --- /dev/null +++ b/sprints/sprint-5/INSUMOS_AUDIT_LOG.md @@ -0,0 +1,106 @@ +# INSUMOS — Etapa Audit Log append-only (Sprint compliance, SEC-002) + +> Material de contexto para que Claude (Projects, arquitecto) redacte el ETAPA_PROMPT.md +> de esta etapa. No es el prompt en sí — es lo que el arquitecto necesita para escribirlo +> bien, sin tener que releer todo el repo. + +--- + +## 1. Por qué esta etapa, y por qué este alcance + +`BACKLOG.md` (SEC-002) y `COMPLIANCE.md` §5.1 piden, dentro del sprint de compliance, +un "registro de auditoría append-only (log inmutable de operaciones sensibles)". El +director priorizó esta pieza primero, antes de hashing de integridad, trazabilidad de +cambios o cifrado en reposo (esas quedan para etapas futuras del mismo sprint). + +COMPLIANCE.md §5.1 pide loggear 3 cosas: accesos a datos sensibles, cambios de +configuración, y ejecución de migraciones. Evalué las 3 y propongo **acotar el alcance +de esta etapa a 2 de las 3**: + +1. **Migraciones (DDL)** — factible con un event trigger nativo de Postgres + (`ddl_command_end`), sin tocar el flujo de deploy. +2. **Cambios de configuración** — triggers `AFTER INSERT/UPDATE/DELETE` en + `user_roles` (altas/bajas/cambios de `app_role`/`org_id`) y en `surveys` cuando + cambia `status` (activación/desactivación de encuestas). +3. **Acceso a datos sensibles — propongo DEFERIR explícitamente.** Verificado en + COMPLIANCE.md §3.2: hoy ningún camino de código (ni `/admin/responses` ni las RPCs + `rpc_me_*`) lee columnas `is_sensitive=true`. Instrumentar "acceso" sin que exista + un acceso real sería simular cobertura. Cuando se construya el primer camino que sí + lea sensibles, ESE PR debe agregar su propio log de acceso junto con el código que + lo necesita — no antes. + +El arquitecto puede confirmar o ajustar este recorte; lo presento como punto de partida, +no como cerrado (esto todavía no se aprobó con el director, se está armando el prompt). + +--- + +## 2. Decisiones de diseño que ya tienen forma (para que el prompt las fije) + +- **Tabla nueva:** `public.audit_log` — append-only real: + - Ningún rol (`anon`, `authenticated`, ni `service_role` vía API) tiene + `INSERT`/`UPDATE`/`DELETE` directo. Solo escriben funciones `SECURITY DEFINER` + de los triggers, propiedad de `postgres` (mismo patrón que + `custom_access_token_hook`, ver `supabase/migrations/20260627000001_auth_user_roles.sql`). + - Trigger `BEFORE UPDATE OR DELETE ON audit_log` que aborta con excepción — segunda + barrera, defensa en profundidad (no a prueba de un operador con `psql -U postgres` + en el VPS — ver gap conocido abajo). + - Lectura: RLS, solo `platform_admin` (reusar `current_app_role()`, ver + `supabase/migrations/20260531000003_rls_policies.sql:17`). +- **Columnas sugeridas (a validar por el arquitecto):** `id bigserial pk`, + `occurred_at timestamptz default now()`, `actor_role text`, `actor_user_id uuid`, + `action text`, `target_table text`, `target_id text`, `detail jsonb`. +- **Gap conocido a documentar, no a resolver en esta etapa:** esto no protege contra + `TRUNCATE`/`DROP` ejecutado por un superusuario directo en el VPS — mismo gap ya + registrado en `SEC-003` (BACKLOG.md). Append-only a nivel de aplicación/RLS, no a + prueba de acceso root a la base. + +--- + +## 3. Convenciones del repo que el prompt debe respetar + +- **Migraciones:** `supabase/migrations/`, naming `YYYYMMDDHHMMSS_descripcion.sql`. + Última migración aplicada: `20260627000001_auth_user_roles.sql`. La próxima debe + fecharse después de esa. +- **Patrón SECURITY DEFINER + GRANT EXECUTE puntual:** ver + `custom_access_token_hook` en `20260627000001_auth_user_roles.sql` — función + `SECURITY DEFINER STABLE`, `GRANT EXECUTE ... TO supabase_auth_admin` (rol + específico, no `PUBLIC`). Mismo patrón a replicar para las funciones de trigger + de `audit_log`. +- **Helpers RLS existentes a reusar, no reinventar:** `public.current_app_role()` + y `public.current_org_id()` (`20260531000003_rls_policies.sql:17-30`). +- **Verificación obligatoria antes de aplicar en VPS:** mostrar el SQL final completo + y esperar OK explícito del director antes de correrlo en producción (ya es práctica + establecida en este proyecto — ver incidente VPS del 27 jun, recuperación documentada + en `docs/estado/ESTADO_PROYECTO_JUNIO_2026.md` §2.1). +- **Formato esperado del archivo de etapa:** seguir la estructura de + `sprints/sprint-4/ETAPA_AUTH_PROMPT.md` o `ETAPA_4G_PROMPT.md` — Objetivo, + Decisiones ya tomadas (no consultar), Tareas, DoD, Qué reportar (datos concretos, + no "funcionó"), Qué NO hacer, Versionado (mensaje de commit sugerido con tag + `[compliance]` o similar). + +--- + +## 4. Qué NO debería pedir el prompt (fuera de alcance de esta etapa) + +- Hashing de integridad SHA-256 (siguiente pieza del mismo sprint, etapa separada). +- Cifrado en reposo (es trabajo de VPS/infraestructura, no de migración SQL). +- Trazabilidad histórica completa de cambios de datos (triggers de historial / WAL) — + etapa separada también. +- Logging de acceso a datos sensibles (ver §1, punto 3 — deferido a cuando exista + código que lea sensibles). +- pgaudit o cualquier extensión nueva — no está en el Supabase self-hosted actual, + agregar una extensión es Nivel 3 aparte (afecta infraestructura/dependencias). + +--- + +## 5. Referencias + +- `COMPLIANCE.md` §5 (roadmap auditabilidad), §5.2 (por qué no blockchain), §3.2 + (qué es sensible y cómo se protege hoy). +- `BACKLOG.md` → SEC-002, SEC-003 (gap de TRUNCATE), SEC-004 (bypass REST, contexto + de otro riesgo residual ya aceptado, mismo espíritu de "documentar en vez de + sobre-construir"). +- `TECH_DEBT.md` → TECH-DEBT-008 (resuelto 27 jun, ejemplo de cómo se cierra un ítem). +- `docs/estado/ESTADO_PROYECTO_JUNIO_2026.md` — estado general post-release v3.0.0 + + incidente VPS, para que el arquitecto tenga el panorama completo antes de escribir + el prompt.