ETAPA_CALIDAD: Nota de discrepancia: las 5 clases de P1 usan var(--color-primary), no var(--color-re-teal) como decía el prompt — ambas resuelven al mismo hex #43BBC8. Hice el reemplazo por nombre de clase (igual intención: texto teal → carbón), no por el literal del grep.

This commit is contained in:
markosbenitez
2026-06-27 15:27:54 -03:00
parent ca3af1a750
commit 30cd83ffc1
7 changed files with 830 additions and 0 deletions

View File

@@ -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]
```

View File

@@ -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.

View File

@@ -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.
```

View File

@@ -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.
```

View File

@@ -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.
```

View File

@@ -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

View File

@@ -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.