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