84 Commits

Author SHA1 Message Date
markosbenitez
cf09346f29 feat(auth): recuperación de contraseña self-service en /login
Reusa /auth/confirm sin tocarlo — ya escucha PASSWORD_RECOVERY (lo
armamos para invitaciones, Supabase emite el mismo evento para
recovery). Antes, cualquiera que olvidara su contraseña dependía de
que el director la reseteara a mano por SQL.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 13:09:42 -03:00
markosbenitez
9461ee1e2e feat(admin): menú de navegación compartido — dashboard, respuestas, usuarios
AdminNavBar reemplaza los links sueltos y duplicados entre páginas
(cada una solo enlazaba a una de las otras dos). Ahora las tres
pantallas detrás del login (/dashboard, /admin/responses,
/admin/usuarios) comparten la misma barra con las 3 secciones +
cerrar sesión, sin importar por dónde se entre primero.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 12:43:28 -03:00
markosbenitez
3bf92f015c docs: guía de prueba del formulario + lectura del dashboard, seed Fundación
Guía dirigida a dos audiencias: equipo técnico (plan de pruebas del
wizard contra la encuesta sandbox Demo) y Maria Mersan (cómo leer los
7 widgets de Mirada Empresa, k-anonimato, datos sintéticos vs reales).
Incluye también el seed de datos sintéticos de Fundación Solidaridad
ya aplicado en producción (versionado para trazabilidad).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 12:19:05 -03:00
markosbenitez
67042b0941 fix(auth): parsear error_code del fragmento de GoTrue en /auth/confirm
GoTrue redirige con #error=...&error_code=... cuando el token de
invitación/recovery ya fue usado o venció, en vez de emitir
PASSWORD_RECOVERY. Antes esto se descubría recién a los 8s (timeout
genérico); ahora se lee el fragmento al montar y se muestra un mensaje
específico (otp_expired, access_denied, etc.) al instante.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 11:18:37 -03:00
markosbenitez
7efab390da fix(admin): layout 3 columnas en invite form, banner de confirmación visible
Email/Organización/botón en una fila en desktop, apilado en mobile.
El mensaje de éxito/error ahora es un banner (no texto inline) visible
al menos 4s o hasta que el usuario vuelva a tocar el formulario.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:56:27 -03:00
markosbenitez
4611fb2363 feat(admin): onboarding por invitación — /admin/usuarios + /auth/confirm
Platform admin invita usuarios de empresa desde /admin/usuarios.
inviteUserByEmail + insert inmediato en user_roles (org_admin).
Rollback explícito si falla el insert. /auth/confirm maneja el link
de invitación con PASSWORD_RECOVERY event. Middleware no requirió
cambios — /auth/confirm ya queda fuera del matcher de rutas protegidas.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 14:54:30 -03:00
markosbenitez
d211382e4d fix(usabilidad): required questions, prompt min-height, autocomplete max-width
Activa required=true en todas las preguntas excepto barrio (2.3) y secciones.
El motor ya valida solo preguntas visibles — las condicionales ocultas no se
validan. Elimina min-height fijo del prompt block para mejor UX mobile.
Agrega max-width de seguridad al dropdown autocomplete en viewports angostos.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 09:26:10 -03:00
markosbenitez
e29752644a fix(wizard): tests e2e sprint6 — cache geo, usabilidad, draft
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 09:03:49 -03:00
markosbenitez
704d2407e8 fix(wizard): cache geo singleton, feedback carga, debounce draft
geo_paraguay_full.json se cargaba dos veces sin cache compartido.
Módulo lib/geo-cache.ts singleton: un fetch para toda la sesión.
Feedback visual mientras carga (disabled + placeholder) y si falla
(mensaje + texto libre). debounce 500ms en saveDraft para no escribir
en localStorage en cada keystroke.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 22:04:12 -03:00
markosbenitez
5472043a33 fix(ops): setup_first_admin.sql usa email_confirmed_at (GoTrue v2.189.0)
Esquema real verificado en producción (information_schema.columns):
auth.users tiene tanto confirmed_at como email_confirmed_at — ambas son
columnas reales, así que el INSERT con confirmed_at nunca falló por
columna inexistente (por eso el usuario test-e2e se creó sin error).

Pero el login seguía rechazando con "Email o contraseña incorrectos"
incluso después de confirmar que el usuario existía — GoTrue valida
email_confirmed_at para permitir login por password, no confirmed_at.
Dejar confirmed_at solo creaba un usuario que nunca queda "confirmado"
para efectos de auth.

Agregado comentario documentando la versión de GoTrue verificada y cómo
re-verificar el schema si esto vuelve a cambiar.
2026-06-28 02:04:36 -03:00
markosbenitez
0fdee073a6 en el medio del kilombo de la db 2026-06-28 01:36:45 -03:00
markosbenitez
6a09ce9ee5 fix(seed): consolidar en un único bloque DO $$ (DELETE+UPDATE adentro)
La versión anterior (af751d7) ya resolvía los question_id por code en
runtime, pero DELETE y el primer UPDATE quedaban como statements sueltos
ANTES del bloque DO $$ — si psql corre sin -v ON_ERROR_STOP=1, un fallo
dentro del DO no impide que esos statements sueltos ya se hayan
ejecutado, y deja el "único bloque" pedido incompleto.

Ahora DELETE, los dos UPDATE y los 50 PERFORM viven todos dentro de un
solo DO $$ ... END $$. Si algo falla, aborta el bloque completo (un
solo error, no cincuenta) — nada de hardcodeado salvo el uuid del
survey Demo (necesario para inicializar la variable s_id).

Mismos 50 datos, verificado byte a byte contra la versión anterior
(diff vacío salvo nombres de variable).
2026-06-28 00:43:05 -03:00
markosbenitez
af751d771d fix(seed): resolver question_id por code en runtime, no hardcodeado
Los UUIDs de questions cambian cada vez que la DB se reinicializa
(supabase/seed.sql usa gen_random_uuid()) — el seed con UUIDs literales
se desactualizaba cada vez, ya pasó dos veces. Reescrito como un bloque
DO $$ que resuelve los 13 question_id usados (4.1, 2.7, 6.1, 6.2, 6.4,
7.2, 7.3, 7.5, 7.6, 9.1, 9.2, 9.3, 9.4) por code + survey_id al momento
de ejecutarse, con RAISE EXCEPTION explícito si alguno no se encuentra
(en vez de fallar más abajo con invalid_answer_reference).

Mismas 50 respuestas, misma distribución (seed=42), mismo DELETE
idempotente acotado a ip_hash LIKE 'seed-prueba-%'. Generado a partir
del archivo anterior con un script de conversión (no transcripto a
mano) para no introducir errores de transcripción en 50 filas.
2026-06-28 00:30:57 -03:00
markosbenitez
cd87ddb18e fix(ops): setup_first_admin.sql usa confirmed_at, no email_confirmed_at
GoTrue en esta versión de producción no tiene la columna
email_confirmed_at en auth.users — falla con "column does not exist".
La columna correcta es confirmed_at.
2026-06-28 00:17:22 -03:00
markosbenitez
6d9cf918f9 feat(ops): scripts de recuperación tras reinicialización de DB
scripts/apply_all_migrations.sh: aplica todas las migraciones de
supabase/migrations/ en orden de timestamp, una por una, con
ON_ERROR_STOP=1 (sin esto psql sigue tras un error SQL y devuelve exit
0 igual — set -e nunca lo vería).

scripts/setup_all.sh: orquesta la recuperación completa — migraciones,
catálogo de encuestas (supabase/seed.sql, paso agregado: sin él
surveys/questions quedan vacías y seed_prueba_50.sql no tiene nada que
referenciar), usuario platform_admin vía ADMIN_EMAIL/ADMIN_PASSWORD, y
datos de prueba (docs/dashboard/seed_prueba_50.sql).

No ejecutado — solo los archivos, a la espera de uso manual en el VPS.
2026-06-27 23:51:51 -03:00
markosbenitez
a0db888f2e fix(seed): regenerar seed_prueba_50.sql con UUIDs reales de questions
dashboard-propuesta/seed_prueba_50.sql quedó con question_id de una
instancia anterior de la DB — rpc_submit_response rechazaba todo con
invalid_answer_reference. Mapeo code→question_id reconstruido por
vocabulario de opciones contra supabase/seed.sql (sin ambigüedad,
incluye desambiguar 6.1/6.2/6.4 por sus opciones únicas), UUIDs reales
tomados de la DB de producción. Mismas 50 filas, misma distribución
(seed=42), mismo formato de llamada — solo cambian los 13 question_id.

Idempotencia: DELETE acotado a survey_id + ip_hash LIKE 'seed-prueba-%'
en vez de TRUNCATE de las tablas completas (answers/responses pueden
tener datos reales de otras encuestas). Survey Demo queda en 'live' al
inicio y al final, explícito.

NO aplicado en VPS — solo el archivo, a la espera de aprobación.
2026-06-27 17:22:12 -03:00
markosbenitez
a0253b4f90 feat(admin): vista respuestas mejorada + vista detallada [admin]
/admin/responses: filtro survey dinámico (select, no hardcodeado),
paginación 50/página, link a vista detallada, conteos consistentes con
el filtro de preguntas permitidas (evita página fantasma al final).
/admin/responses/[id]: respuestas agrupadas por sección (derivado de
type='section', igual que el wizard — no existen section_number/
section_title, ver TECH-DEBT-011), instancias bajo "Familiar N".
RLS y exclusión de sensibles intactos en ambas vistas.

Fixes de revisión antes de cerrar: toggle expandir/colapsar movido a
un <button> real (evita anidar el link "Ver" dentro de un role=button),
formatDate/renderValue unificados en admin-format.ts, queries
independientes en paralelo (Promise.all).
2026-06-27 16:48:32 -03:00
markosbenitez
a660928085 fix(ui): título card talento en riesgo a carbón #494F4F 2026-06-27 15:46:48 -03:00
markosbenitez
30cd83ffc1 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. 2026-06-27 15:27:54 -03:00
markosbenitez
ca3af1a750 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.
2026-06-27 15:25:16 -03:00
markosbenitez
b3f039fbd5 fix(ui): toggle password en login + fondo blanco card talento
Fix A: botón inline con ícono SVG (ojo/ojo tachado) en el campo
password de /login, alterna type password/text, aria-label según
estado. Sin librería nueva.
Fix B: .db-widget-star pierde el gradiente, hereda background blanco
de .db-widget igual que los demás cards. Borde y título teal intactos.
2026-06-27 15:18:22 -03:00
markosbenitez
430bf7a5da 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.
Event trigger DDL incluido (confirmado viable sin extensiones, ver
diagnóstico en ETAPA_AUDIT_LOG_PROMPT.md §4.1). Gap SEC-003 (TRUNCATE por
superusuario) documentado y aceptado, no resuelto.

NO aplicada en VPS — pendiente de aprobación.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 14:51:07 -03:00
markosbenitez
10bb7299a2 docs: actualizar estado — cierre release v3.0.0 + ETAPA_AUTH + incidente VPS
Sprint 4 completo (widgets Mirada Empresa, tema claro, 4F, 4I/release ya
estaban hechos antes de esta sesión). Documenta el incidente de colisión de
container_name en el VPS y su recuperación, y el cierre de TECH-DEBT-008
(auth de staff) en esta sesión. Próximos pasos quedan abiertos a decisión
del director (dashboard País/Humana, sprint de compliance, o mantenimiento
de dependencias).
2026-06-27 13:38:10 -03:00
markosbenitez
dda4b5320a fix(auth): agregar email_change/email_change_token_new en setup_first_admin
GoTrue fallaba con "Database error querying schema" al hacer login porque
estas dos columnas de auth.users no tienen DEFAULT en este esquema y quedaban
NULL — el driver Go espera string, no NULL. Verificado en VPS: login
funciona después de este fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 13:18:32 -03:00
markosbenitez
44d1f4b9d9 fix(auth): corregir setup_first_admin — psql vars no funcionan en DO $$
Reescribir con CTE INSERT...RETURNING para encadenar auth.users + user_roles
en una sola sentencia SQL plana donde :variable sí funciona.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 09:47:38 -03:00
markosbenitez
a484ba7849 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.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-27 09:39:52 -03:00
markosbenitez
ddaba6866a fix(rpc): filtrar elementos individuales en carrera_congelada [BUG-001]
Mover el filtro 'Ninguna de las anteriores' del WHERE sobre a.value
al WHERE del subquery expandido, donde opt es ya un elemento individual
resultado de jsonb_array_elements_text. El operador #>> '{}' aplicado
sobre un array jsonb no filtraba elementos sino la representación textual
del array completo.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 22:38:17 -03:00
markosbenitez
fcf93ea3ea chore(release): CHANGELOG v3.0.0 — wizard declarativo + dashboard tres miradas
Etapas 4A→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.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
v3.0.0
2026-06-26 22:21:08 -03:00
markosbenitez
3e7109a0f1 feat(wizard): ciudad-select filtra por departamento desde geo json [4F]
- CiudadSelectField: autocomplete sobre Object.keys(geoData[depto]),
  mismo patrón que BarrioAutocompleteField (position:fixed, getBoundingClientRect,
  scroll listener, teclado ARIA). Reutiliza clases CSS .barrio-autocomplete-*.
- Limpia el valor de 2.2 via useEffect cuando cambia departamento (2.1),
  usando un ref estable para onChange para evitar dep cíclica.
- QuestionRenderer: branch code==='2.2' dentro del bloque single_choice;
  pasa answers['2.1:default'] como prop departamento.
- Sin cambios al seed, form-engine ni RPCs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 20:07:16 -03:00
markosbenitez
6e9ad89f7b fix(wizard): z-index dropdown barrio + texto cortado opciones
Issue A: barrio-autocomplete-list cambia a position:fixed + getBoundingClientRect()
para escapar el overflow-y:auto de wizard-answer-area que clippeaba el dropdown.
z-index: 20 → 30 (clearear sticky topbar/bottombar z-index:10 en stacking context raíz).
Listener de scroll cierra el dropdown si el contenedor scrollea.

Issue B: wizard-answer-area max-height 360px → 480px (desktop) / 320px → 400px (mobile)
para mostrar ~8 opciones completas. .wizard-answer-area.has-overflow { padding-bottom: 44px }
asegura que el fade ::after (28px) cubre espacio vacío, no texto de opción.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 20:03:09 -03:00
markosbenitez
3ec5da0e54 feat(wizard): autocomplete barrios desde geo_paraguay_full.json [4F]
- BarrioAutocompleteField: carga JSON estático, filtra por depto/ciudad (2.1/2.2), mín 2 chars, máx 10 sugerencias, texto libre permitido, teclado (↑↓ Enter Escape), ARIA listbox
- QuestionRenderer: branch code === '2.3' → BarrioAutocompleteField; prop answers? pasado desde WizardForm para contexto depto/ciudad
- globals.css: clases .barrio-autocomplete-* (wrapper, list, item, item--active)
- Incluye public/data/geo_paraguay_full.json (202 KB, sin commitear hasta ahora)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 19:41:55 -03:00
markosbenitez
dbcc60ad95 fix(dashboard): tema claro — widget-star, gap-brecha, lens-intro
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:59:31 -03:00
markosbenitez
474b501fff tema claro v1 2026-06-26 18:44:31 -03:00
markosbenitez
c847ef3ef5 feat(dashboard): tema claro marca Re — fondos, cards y texto
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 18:22:26 -03:00
markosbenitez
8dea04cae3 fix(rpc): expose cnt as outer column in apoyos and carrera (Opción B)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-26 17:59:35 -03:00
markosbenitez
56443a739f Etapa completada (local) — widgets de Mirada Empresa 2026-06-26 11:29:16 -03:00
markosbenitez
df9b168577 docs: actualizar estado — Mirada Empresa verificada 2026-06-25 14:10:48 -03:00
markosbenitez
83782ab6f8 feat(dashboard): RPCs Mirada Empresa v3 + fix códigos v2 en helper [dashboard]
11 RPCs de Mirada Empresa (E1-E10 + estrella Talento en riesgo) sobre
instrumento v3, k≥5 en dos niveles, helper _dash_es_cuidador (criterio
UNFPA). Fix de _dash_responses: A6/A7 → 2.5/2.7. Verificado contra 50
respuestas reales: métricas núcleo exactas, k-anonimato suprime segmentos
n<5, umbral de la estrella parametrizable. Modalidad por JOIN (TECH-DEBT-010).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-25 14:06:49 -03:00
markosbenitez
b0cf273bc8 docs: actualizar estado — cierre Etapa 4G. 2026-06-24 21:31:56 -03:00
markosbenitez
eaf71567a7 prompt 4G 2026-06-24 21:29:01 -03:00
markosbenitez
2e6fbd7801 feat(submit): submit v3 con validación de integridad server-side [4G]
RPC transaccional SECURITY DEFINER (rpc_submit_response) como único camino
de escritura. Dropea las policies WITH CHECK(true): cierra la falla donde
anon podía insertar cualquier cosa. Validación server-side (7 checks)
reusando el motor. Persistencia atómica con instance_id por instancia.
Verificado contra Postgres real: instancias separadas, atomicidad, RLS.
Riesgo residual de bypass por REST documentado (SEC-004).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-24 21:27:49 -03:00
markosbenitez
61676e1423 refactor(engine): mover limpieza de instancias y validación required al motor [4E]
applyAnswer y isRequiredAndUnanswered pasan de wizard-form.tsx al motor
como funciones puras — eran lógica de flujo en el presentador (viola ADR-005).
Limpieza de respuestas huérfanas al remover un vínculo y validación required
por instancia, ahora testeables. 20/20 tests. Verificado end-to-end con navegador.
4E sin construcción pendiente: el mini-wizard es propiedad emergente del motor.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 22:57:42 -03:00
markosbenitez
ce679ec1c3 feat(wizard): renderer pregunta-por-pantalla con layout estable [4D] 2026-06-23 22:40:01 -03:00
markosbenitez
7c8b034756 Bug corregido — duplicación de enunciado + fade mal posicionado 2026-06-23 22:12:00 -03:00
markosbenitez
c30688810a Con las mejoras visuales 2026-06-23 21:44:05 -03:00
markosbenitez
f2666c7d3c Con las mejoras visuales 2026-06-23 21:43:52 -03:00
markosbenitez
424071b97c estado de proyecto updated 2026-06-23 15:01:08 -03:00
markosbenitez
eba93db452 feat(engine): motor de resolución de flujo declarativo [4C] 2026-06-23 07:21:35 -03:00
markosbenitez
ed524af16b feat(engine): motor de resolución de flujo declarativo [4C]
Módulo puro que interpreta el esquema v3 y resuelve pantallas.
5 operadores (eq, eq_any, not_in, contains, eq_sole), acciones show/hide
(hide = veto absoluto). Instancias con ID estable por posición de opción,
secciones-pantalla. Navegación por ID estable. 16/16 tests Node en verde.
No reemplaza aún lib/conditional.ts de v2 (lo hace 4D al migrar el componente).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-23 07:21:03 -03:00
markosbenitez
5987502664 feat(schema): esquema declarativo + seed v3 del formulario [4B]
Extiende questions con instance_generator/instance_of/hint.
Instrumento v3 completo (10 secciones, 50 preguntas) como datos declarativos.
Operadores de flujo para multiple_choice: contains, eq_sole.
Flujo del formulario en el esquema, no en presentación (ADR-005).
Seed solo reemplaza catálogo; no toca evidencia de consentimiento (ADR-003).
2026-06-22 21:05:11 -03:00