Compare commits
13 Commits
2f39ce555c
...
v2.1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c17a9846a | ||
|
|
b9a3118c84 | ||
|
|
cfd2eb0624 | ||
|
|
edf82bf7fd | ||
|
|
60c9c81224 | ||
|
|
1bab6874a9 | ||
|
|
933d00ad58 | ||
|
|
059dfa071e | ||
|
|
2d13982d92 | ||
|
|
92a7923a26 | ||
|
|
af0ec65be5 | ||
|
|
157e108322 | ||
|
|
bf963e8894 |
@@ -6,6 +6,8 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=<anon-key-publica>
|
||||
|
||||
# NUNCA agregar SUPABASE_SERVICE_ROLE_KEY como variable NEXT_PUBLIC_ ni en el cliente
|
||||
# La service_role bypasa RLS — solo para Edge Functions / backend server-side
|
||||
# Usada por app/admin/responses (Server Component) para listar respuestas sin RLS
|
||||
SUPABASE_SERVICE_ROLE_KEY=
|
||||
|
||||
# IP hash — variable server-only (NUNCA NEXT_PUBLIC_)
|
||||
# Usada para deduplicación anónima. Generá un string aleatorio largo.
|
||||
|
||||
384
INSTALL_SUPABASE.md
Normal file
384
INSTALL_SUPABASE.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# INSTALL_SUPABASE.md — Supabase self-hosted en Dokploy v0.29.4
|
||||
|
||||
> Guía operativa basada en la instalación real del proyecto "Datos País sobre el Cuidado" (junio 2026).
|
||||
> Registra los pasos exactos, errores conocidos y sus soluciones.
|
||||
|
||||
---
|
||||
|
||||
## Decisiones de arquitectura
|
||||
|
||||
- **Supabase self-hosted completo** (Postgres + Auth + Storage + Realtime + Studio + Kong).
|
||||
- **Partida limpia** — no se migran datos desde Cloud, se aplican las migraciones del repo.
|
||||
- **Gestión vía Git** — el compose y los volumes viven en un repo dedicado; Dokploy clona ese repo.
|
||||
Las variables de entorno viven en Dokploy (no en el repo).
|
||||
- **Backups locales** diarios, retención 30 días. Remoto: TECH-DEBT-006.
|
||||
- **Studio** no expuesto a internet. Acceso solo vía SSH tunnel.
|
||||
|
||||
---
|
||||
|
||||
## Requisitos previos
|
||||
|
||||
- VPS Ubuntu 22.04+ con Docker 29+ instalado.
|
||||
- Dokploy v0.29.4 instalado y accesible.
|
||||
- Repo Git disponible (Gitea o GitHub) con acceso desde el VPS y desde Dokploy.
|
||||
- Acceso SSH al VPS.
|
||||
- Supabase CLI en la máquina de desarrollo local.
|
||||
|
||||
---
|
||||
|
||||
## PARTE 1 — Preparar el repo de Supabase
|
||||
|
||||
### 1.1 Descargar el stack oficial en el VPS
|
||||
|
||||
```bash
|
||||
curl -fsSL https://supabase.link/setup.sh | sh
|
||||
```
|
||||
|
||||
Crea `~/supabase-project/` con esta estructura:
|
||||
|
||||
```
|
||||
~/supabase-project/
|
||||
├── docker-compose.yml
|
||||
├── volumes/
|
||||
│ ├── api/
|
||||
│ │ ├── kong.yml <- API gateway (EDITAR: ver 1.2)
|
||||
│ │ └── kong-entrypoint.sh
|
||||
│ ├── db/
|
||||
│ │ ├── _supabase.sql <- CRITICO: crea la base _supabase
|
||||
│ │ ├── roles.sql
|
||||
│ │ ├── jwt.sql
|
||||
│ │ ├── realtime.sql
|
||||
│ │ ├── pooler.sql
|
||||
│ │ ├── webhooks.sql
|
||||
│ │ ├── logs.sql
|
||||
│ │ └── init/data.sql
|
||||
│ └── pooler/
|
||||
│ └── pooler.exs
|
||||
```
|
||||
|
||||
> Estos archivos son los scripts de inicializacion que Postgres ejecuta la primera vez.
|
||||
> Sin ellos la base _supabase no se crea y el stack no funciona.
|
||||
> Por eso NO se puede usar "docker raw" en Dokploy — ese modo no tiene contexto del directorio.
|
||||
|
||||
### 1.2 Ajustar kong.yml
|
||||
|
||||
El kong.yml define las credenciales del API gateway. Verificar que use los mismos
|
||||
nombres de variables que se definen en Dokploy:
|
||||
|
||||
```bash
|
||||
grep -n "keyauth" ~/supabase-project/volumes/api/kong.yml
|
||||
```
|
||||
|
||||
Deben quedar dos bloques con UNA sola entrada cada uno:
|
||||
|
||||
```yaml
|
||||
keyauth_credentials:
|
||||
- key: $ANON_KEY # nombre exacto de la variable en Dokploy
|
||||
|
||||
keyauth_credentials:
|
||||
- key: $SERVICE_ROLE_KEY # nombre exacto de la variable en Dokploy
|
||||
```
|
||||
|
||||
ERROR comun: si los dos bloques resuelven al mismo valor JWT, Kong falla con
|
||||
"uniqueness violation". Verificar que ANON_KEY y SERVICE_ROLE_KEY son distintos.
|
||||
|
||||
### 1.3 Crear .gitignore
|
||||
|
||||
```bash
|
||||
cat > ~/supabase-project/.gitignore << 'EOF'
|
||||
.env
|
||||
.env.local
|
||||
volumes/db/data/
|
||||
volumes/storage/
|
||||
volumes/logs/
|
||||
*.log
|
||||
EOF
|
||||
```
|
||||
|
||||
### 1.4 Push al repo Git
|
||||
|
||||
```bash
|
||||
cd ~/supabase-project
|
||||
git init
|
||||
git add docker-compose.yml volumes/api/ volumes/db/ volumes/pooler/ .gitignore
|
||||
git commit -m "chore: supabase self-hosted stack inicial"
|
||||
git remote add origin https://TU_GIT/usuario/supabase-repo.git
|
||||
git push -u origin master
|
||||
```
|
||||
|
||||
El .env con los secrets NO se sube (esta en .gitignore).
|
||||
|
||||
---
|
||||
|
||||
## PARTE 2 — Generar los secrets
|
||||
|
||||
Usar siempre openssl rand -hex (solo hex, sin caracteres especiales que rompan el escaping de Docker).
|
||||
|
||||
### 2.1 Valores base (en el VPS)
|
||||
|
||||
```bash
|
||||
echo "POSTGRES_PASSWORD=$(openssl rand -hex 24)"
|
||||
echo "JWT_SECRET=$(openssl rand -hex 40)"
|
||||
echo "DASHBOARD_PASSWORD=$(openssl rand -hex 16)"
|
||||
echo "SECRET_KEY_BASE=$(openssl rand -hex 40)"
|
||||
echo "VAULT_ENC_KEY=$(openssl rand -hex 20)"
|
||||
echo "PG_META_CRYPTO_KEY=$(openssl rand -hex 24)"
|
||||
```
|
||||
|
||||
Guardar todos en un gestor de contrasenas antes de continuar.
|
||||
|
||||
### 2.2 Generar ANON_KEY y SERVICE_ROLE_KEY
|
||||
|
||||
Con el JWT_SECRET generado, usar Node.js en el VPS:
|
||||
|
||||
```bash
|
||||
node -e "
|
||||
const crypto = require('crypto');
|
||||
const secret = 'PEGAR_AQUI_JWT_SECRET';
|
||||
|
||||
function b64url(obj) {
|
||||
return Buffer.from(JSON.stringify(obj)).toString('base64url');
|
||||
}
|
||||
|
||||
function makeJWT(role) {
|
||||
const h = b64url({alg:'HS256',typ:'JWT'});
|
||||
const p = b64url({role,iss:'supabase',iat:1700000000,exp:2000000000});
|
||||
const sig = crypto.createHmac('sha256',secret).update(h+'.'+p).digest('base64url');
|
||||
return h+'.'+p+'.'+sig;
|
||||
}
|
||||
|
||||
console.log('ANON_KEY='+makeJWT('anon'));
|
||||
console.log('SERVICE_ROLE_KEY='+makeJWT('service_role'));
|
||||
"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PARTE 3 — Configurar el servicio en Dokploy
|
||||
|
||||
### 3.1 Crear el servicio
|
||||
|
||||
1. Projects -> seleccionar proyecto -> Create Service -> Docker Compose.
|
||||
2. Provider: Gitea (o GitHub) -> seleccionar el repo del paso 1.4.
|
||||
3. Branch: master | Compose Path: ./docker-compose.yml
|
||||
4. IMPORTANTE: desactivar Autodeploy (toggle OFF).
|
||||
|
||||
### 3.2 Variables de entorno (pestaña Environment)
|
||||
|
||||
Pegar el bloque completo reemplazando los valores:
|
||||
|
||||
```env
|
||||
POSTGRES_PASSWORD=<openssl rand -hex 24>
|
||||
POSTGRES_HOST=db
|
||||
POSTGRES_DB=postgres
|
||||
POSTGRES_PORT=5432
|
||||
JWT_SECRET=<openssl rand -hex 40>
|
||||
JWT_EXPIRY=3600
|
||||
ANON_KEY=<JWT anon>
|
||||
SERVICE_ROLE_KEY=<JWT service_role>
|
||||
SUPABASE_ANON_KEY=<igual a ANON_KEY>
|
||||
SUPABASE_SERVICE_KEY=<igual a SERVICE_ROLE_KEY>
|
||||
DASHBOARD_USERNAME=admin
|
||||
DASHBOARD_PASSWORD=<openssl rand -hex 16>
|
||||
KONG_HTTP_PORT=8000
|
||||
KONG_HTTPS_PORT=8443
|
||||
SECRET_KEY_BASE=<openssl rand -hex 40>
|
||||
VAULT_ENC_KEY=<openssl rand -hex 20>
|
||||
PG_META_CRYPTO_KEY=<openssl rand -hex 24>
|
||||
SITE_URL=http://localhost:3000
|
||||
API_EXTERNAL_URL=http://localhost:8000
|
||||
SUPABASE_PUBLIC_URL=http://localhost:8000
|
||||
STUDIO_DEFAULT_ORGANIZATION=NombreOrg
|
||||
STUDIO_DEFAULT_PROJECT=NombreProyecto
|
||||
SMTP_ADMIN_EMAIL=admin@example.com
|
||||
SMTP_HOST=
|
||||
SMTP_PORT=587
|
||||
SMTP_USER=
|
||||
SMTP_PASS=
|
||||
SMTP_SENDER_NAME=
|
||||
ENABLE_EMAIL_SIGNUP=false
|
||||
ENABLE_EMAIL_AUTOCONFIRM=false
|
||||
ENABLE_PHONE_SIGNUP=false
|
||||
ENABLE_PHONE_AUTOCONFIRM=false
|
||||
DISABLE_SIGNUP=true
|
||||
ENABLE_ANONYMOUS_USERS=false
|
||||
GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false
|
||||
MAILER_URLPATHS_CONFIRMATION=/auth/v1/verify
|
||||
MAILER_URLPATHS_INVITE=/auth/v1/verify
|
||||
MAILER_URLPATHS_RECOVERY=/auth/v1/verify
|
||||
MAILER_URLPATHS_EMAIL_CHANGE=/auth/v1/verify
|
||||
ADDITIONAL_REDIRECT_URLS=
|
||||
POOLER_TENANT_ID=pooler
|
||||
POOLER_DEFAULT_POOL_SIZE=20
|
||||
POOLER_MAX_CLIENT_CONN=100
|
||||
POOLER_PROXY_PORT_TRANSACTION=6543
|
||||
POOLER_DB_POOL_SIZE=10
|
||||
STORAGE_TENANT_ID=default
|
||||
GLOBAL_S3_BUCKET=
|
||||
REGION=
|
||||
S3_PROTOCOL_ACCESS_KEY_ID=
|
||||
S3_PROTOCOL_ACCESS_KEY_SECRET=
|
||||
LOGFLARE_LOGGER_BACKEND_API_KEY=logflare-local-key
|
||||
LOGFLARE_API_KEY=logflare-local-key
|
||||
FUNCTIONS_VERIFY_JWT=true
|
||||
IMGPROXY_ENABLE_WEBP_DETECTION=true
|
||||
IMGPROXY_AUTO_WEBP=true
|
||||
PGRST_DB_SCHEMAS=public,storage,graphql_public
|
||||
```
|
||||
|
||||
### 3.3 Deploy y validacion
|
||||
|
||||
Click en Deploy. Cuando termine verificar:
|
||||
|
||||
```bash
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}" | grep supabase
|
||||
|
||||
docker exec supabase-db psql -U postgres -c 'SELECT version();'
|
||||
|
||||
curl -s http://localhost:8000/rest/v1/ -H "apikey: <ANON_KEY>" | head -c 200
|
||||
|
||||
docker exec supabase-db psql -U postgres -c '\l' | grep supabase
|
||||
```
|
||||
|
||||
Contenedores esperados healthy: supabase-db, supabase-kong, supabase-studio,
|
||||
supabase-auth, supabase-rest, supabase-storage, supabase-meta, supabase-pooler,
|
||||
supabase-imgproxy, realtime-dev.supabase-realtime.
|
||||
supabase-edge-functions puede quedar en Restarting si no se usa Edge Functions (no critico).
|
||||
|
||||
---
|
||||
|
||||
## PARTE 4 — Aplicar las migraciones del proyecto
|
||||
|
||||
El puerto 5432 no esta expuesto externamente. Las migraciones se aplican desde el VPS.
|
||||
|
||||
### 4.1 Copiar migraciones al VPS (desde la maquina de desarrollo)
|
||||
|
||||
```bash
|
||||
ssh administrator@<VPS_IP> "mkdir -p ~/migrations"
|
||||
scp -r supabase/migrations/ administrator@<VPS_IP>:~/migrations/
|
||||
```
|
||||
|
||||
### 4.2 Aplicar en orden
|
||||
|
||||
```bash
|
||||
for f in $(ls ~/migrations/*.sql | sort); do
|
||||
echo "Aplicando: $f"
|
||||
docker exec -i supabase-db psql -U postgres -d postgres < "$f"
|
||||
echo "---"
|
||||
done
|
||||
```
|
||||
|
||||
### 4.3 Verificar
|
||||
|
||||
```bash
|
||||
docker exec supabase-db psql -U postgres -d postgres -c \
|
||||
"SELECT tablename, policyname FROM pg_policies WHERE schemaname='public' ORDER BY tablename;"
|
||||
```
|
||||
|
||||
### Nota: auth.jwt() en GoTrue v2.189+
|
||||
|
||||
GoTrue v2.189+ elimino auth.jwt(). Las funciones disponibles son auth.uid(), auth.role().
|
||||
Las politicas que usen custom claims JWT (app_role, org_id) fallaran. Ver TECH-DEBT-007.
|
||||
No es bloqueante para el piloto porque el dashboard usa service_role que bypasa RLS.
|
||||
|
||||
---
|
||||
|
||||
## PARTE 5 — Backups locales
|
||||
|
||||
### 5.1 Crear el script
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/backups/supabase-prod
|
||||
sudo chmod 700 /var/backups/supabase-prod
|
||||
|
||||
sudo tee /usr/local/bin/supabase-backup.sh > /dev/null << 'SCRIPT'
|
||||
#!/usr/bin/bash
|
||||
set -euo pipefail
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
OUT=/var/backups/supabase-prod/supabase_$TS.sql.gz
|
||||
docker exec supabase-db pg_dump -U postgres postgres | gzip > "$OUT"
|
||||
chmod 600 "$OUT"
|
||||
find /var/backups/supabase-prod -name "supabase_*.sql.gz" -mtime +30 -delete
|
||||
echo "Backup OK: $OUT ($(du -h "$OUT" | cut -f1))"
|
||||
SCRIPT
|
||||
|
||||
sudo chmod +x /usr/local/bin/supabase-backup.sh
|
||||
```
|
||||
|
||||
### 5.2 Probar y configurar cron
|
||||
|
||||
```bash
|
||||
sudo bash /usr/local/bin/supabase-backup.sh
|
||||
|
||||
sudo crontab -e
|
||||
# Agregar: 0 3 * * * /usr/bin/bash /usr/local/bin/supabase-backup.sh >> /var/log/supabase-backup.log 2>&1
|
||||
```
|
||||
|
||||
### 5.3 Probar restore (obligatorio)
|
||||
|
||||
```bash
|
||||
docker exec supabase-db createdb -U postgres restore_test
|
||||
gunzip < $(ls /var/backups/supabase-prod/*.sql.gz | tail -1) | \
|
||||
docker exec -i supabase-db psql -U postgres restore_test
|
||||
docker exec supabase-db psql -U postgres -d restore_test -c \
|
||||
"SELECT count(*) FROM pg_tables WHERE schemaname='public';"
|
||||
docker exec supabase-db dropdb -U postgres restore_test
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PARTE 6 — Dominio y SSL (pendiente)
|
||||
|
||||
En Dokploy, pestaña Domains del servicio supabase:
|
||||
- Agregar dominio db.dominio.com -> apuntar a supabase-kong:8000 -> activar SSL.
|
||||
- Studio NO tiene dominio publico. Acceso solo via SSH tunnel:
|
||||
ssh -L 3001:localhost:3001 administrator@VPS_IP -> abrir http://localhost:3001
|
||||
|
||||
Actualizar variables de entorno:
|
||||
```
|
||||
API_EXTERNAL_URL=https://db.dominio.com
|
||||
SUPABASE_PUBLIC_URL=https://db.dominio.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## PARTE 7 — Conectar la app Next.js
|
||||
|
||||
Actualizar variables de entorno de la app Next.js en Dokploy:
|
||||
```
|
||||
NEXT_PUBLIC_SUPABASE_URL=https://db.dominio.com
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY=<ANON_KEY>
|
||||
SUPABASE_SERVICE_ROLE_KEY=<SERVICE_ROLE_KEY>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Errores conocidos y soluciones
|
||||
|
||||
| Error | Causa | Solucion |
|
||||
|---|---|---|
|
||||
| password authentication failed for supabase_storage_admin | POSTGRES_PASSWORD con caracteres especiales | Usar openssl rand -hex 24 |
|
||||
| database "_supabase" does not exist | volumes/db/ no accesible | Usar modo Git en Dokploy, no docker raw |
|
||||
| keyauth_credentials uniqueness violation | ANON_KEY = SERVICE_ROLE_KEY o duplicados en kong.yml | Una sola entrada por bloque en kong.yml |
|
||||
| exec: kong-entrypoint.sh is a directory | Volumen corrupto | docker volume rm del volumen afectado |
|
||||
| GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED: converting to bool | Variable vacia | Agregar GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false |
|
||||
| GOTRUE_SMTP_PORT: converting to int | SMTP_PORT vacio | Agregar SMTP_PORT=587 |
|
||||
| auth.jwt() does not exist | GoTrue v2.189+ lo elimino | Ver TECH-DEBT-007 |
|
||||
| Conflicto de nombres de contenedores | Stack manual y Dokploy corriendo en paralelo | docker compose down desde ~/supabase-project |
|
||||
|
||||
---
|
||||
|
||||
## Deuda tecnica
|
||||
|
||||
- TECH-DEBT-006: Backup remoto (S3) — bloqueante antes del primer dato real de empresa.
|
||||
- TECH-DEBT-007: Migrar auth.jwt() a alternativa compatible con GoTrue v2.189+.
|
||||
|
||||
---
|
||||
|
||||
## Datos operativos de esta instalacion
|
||||
|
||||
- VPS: inq-mart-2 - IP VPN: 172.20.5.11
|
||||
- Dokploy: https://172.20.5.11 (acceso via VPN)
|
||||
- Repo Supabase: https://git.inquality.com.py/marcos/supabase-motor-fomularios
|
||||
- Backups: /var/backups/supabase-prod/ (diarios, retencion 30 dias)
|
||||
- Studio: ssh -L 3001:localhost:3001 administrator@172.20.5.11 -> http://localhost:3001
|
||||
89
OBSERVATIONS.md
Normal file
89
OBSERVATIONS.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# OBSERVATIONS.md — Observaciones y breaking changes
|
||||
|
||||
> Registro de cambios al instrumento que rompen compatibilidad con datos existentes
|
||||
> o que requieren atención del director antes/después de re-seedear. No bloquean el
|
||||
> trabajo del agente — se documentan para que el director decida qué hacer con los
|
||||
> datos ya capturados.
|
||||
|
||||
---
|
||||
|
||||
## Etapa 3I2 — Revisión del instrumento v2.1 (feedback equipo médico)
|
||||
|
||||
### 1. Eliminación de "Prefiero no decir" (Cambio 1)
|
||||
Afecta: A5, A6, 1.1, 1.5, 1.6, 1.13, 2.5, 3.1, 3.4.
|
||||
|
||||
Si existen respuestas guardadas con el valor `"Prefiero no decir"` en alguna de estas
|
||||
preguntas, esos valores quedan **huérfanos**: ya no aparecen como opción en el
|
||||
formulario ni en `seed.sql`, pero permanecen en `responses.answers` tal cual fueron
|
||||
guardados. No se borran ni se migran automáticamente.
|
||||
|
||||
**Impacto en triggers (1.1, 1.6, 1.13):** estas preguntas ahora tienen solo 2 opciones
|
||||
(Sí/No). La lógica condicional (incluyendo el nuevo salto-triple, Cambio 4a) solo
|
||||
reacciona a `"Sí"`. Una respuesta histórica `"Prefiero no decir"` en un trigger se
|
||||
trataría como "no Sí" a efectos de la lógica condicional si se re-evaluara, pero no
|
||||
hay re-evaluación retroactiva de respuestas ya enviadas.
|
||||
|
||||
### 2. 2.1 cambia de `single_choice` a `multiple_choice` (Cambio 5)
|
||||
Respuestas existentes para 2.1 están guardadas como un string único (`"Yo soy el/la
|
||||
principal"`, etc.). El formulario nuevo espera un array de strings para preguntas
|
||||
`multiple_choice`. Las respuestas históricas de 2.1 quedan en formato de string,
|
||||
inconsistente con el nuevo esquema esperado para esa pregunta.
|
||||
|
||||
Ver `TECH_DEBT.md` → TECH-DEBT-006 (deuda de ranking pendiente sobre esta misma
|
||||
pregunta).
|
||||
|
||||
### 3. Eliminación de la pregunta 2.4 (Cambio 6)
|
||||
"¿Contás con apoyo de otras personas para el cuidado?" se elimina del seed por ser
|
||||
similar/redundante con 2.1 (feedback del equipo médico).
|
||||
|
||||
El `DELETE FROM public.questions WHERE survey_id = ...` al inicio de `seed.sql` borra
|
||||
la fila de la pregunta 2.4 (id `40000000-0000-0000-0000-000000000027`). Si existen
|
||||
filas en `responses` cuyo `answers` referencian `question_id` = 2.4 (por
|
||||
`question_id`, no por código embebido en JSON — depende del esquema de `responses`),
|
||||
esas referencias quedan apuntando a un `question_id` que ya no existe en `questions`
|
||||
tras el re-seed.
|
||||
|
||||
**No re-seedear la base de producción sin que el director confirme que no hay
|
||||
respuestas reales que dependan de 2.4**, o decidir conservar la fila de 2.4 marcada
|
||||
como `archived`/inactiva en lugar de borrarla (cambio de diseño — Nivel 3, no
|
||||
ejecutado en esta etapa).
|
||||
|
||||
### 4. Cambio de texto en opción de 3.3 (Cambio 8)
|
||||
"Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis ingresos".
|
||||
|
||||
Respuestas existentes que eligieron la opción anterior quedan guardadas con el texto
|
||||
literal `"Es difícil sostenerlo con mis ingresos"`, que ya no coincide con ninguna
|
||||
opción del seed actual. Cualquier agregación por valor exacto de opción debe
|
||||
contemplar ambos strings como equivalentes para no perder esos datos en reportes.
|
||||
|
||||
### 5. División de la primera franja salarial en 3.4 (Cambio 9)
|
||||
"Sueldo mínimo – Gs. 5.000.000" se reemplaza por dos opciones nuevas: "Sueldo mínimo"
|
||||
y "Gs. 3.000.000 a 5.000.000".
|
||||
|
||||
Respuestas existentes con el valor `"Sueldo mínimo – Gs. 5.000.000"` quedan con ese
|
||||
valor, que ya no existe como opción. No hay forma automática de reasignar esas
|
||||
respuestas a una de las dos franjas nuevas (la franja exacta del respondente no se
|
||||
puede inferir del valor agregado anterior).
|
||||
|
||||
### 6. Cambio de texto en última opción de 6.1 (Cambio 10)
|
||||
"No, nunca se ha hablado del tema" → "No, no se puede hablar del tema".
|
||||
|
||||
Respuestas existentes con el valor anterior quedan con ese texto literal, que ya no
|
||||
coincide con ninguna opción del seed actual.
|
||||
|
||||
---
|
||||
|
||||
## Resumen para el director
|
||||
|
||||
- Ningún cambio de esta etapa borra datos directamente, salvo el `DELETE` estándar de
|
||||
`questions` por `survey_id` (mecanismo de idempotencia ya existente en `seed.sql`,
|
||||
no nuevo de esta etapa) — pero ese `DELETE` ahora también elimina la fila de 2.4.
|
||||
- Los cambios 1, 8, 9 y 10 dejan **valores de texto huérfanos** en respuestas
|
||||
históricas (ya no matchean ninguna opción vigente). No afecta el funcionamiento del
|
||||
formulario hacia adelante, pero sí cualquier reporte/agregación que dependa de
|
||||
matchear el string exacto de la opción.
|
||||
- El cambio 5 (2.1) deja respuestas históricas en un formato (string) distinto al que
|
||||
el formulario nuevo produce (array). Si se construye un reporte sobre 2.1, debe
|
||||
manejar ambos formatos o normalizarse en una migración de datos aparte.
|
||||
- **No se ejecutó ningún re-seed sobre la base real** — esto aplica solo cuando el
|
||||
director apruebe re-seedear con `seed.sql` v2.1.
|
||||
75
SEED_SPEC.md
75
SEED_SPEC.md
@@ -1,23 +1,31 @@
|
||||
# SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v2
|
||||
# SEED_SPEC.md — Contrato del instrumento "Mapeo de Cuidadores" v2.1
|
||||
|
||||
> Versión 2 — incorpora revisión de la colega (Jun 2026). Reemplaza la v1 (34 preguntas).
|
||||
> 44 preguntas · 8 secciones · Códigos únicos corregidos.
|
||||
> Versión 2.1 — incorpora feedback del equipo médico, Etapa 3I2 (Jun 2026). Reemplaza la v2.
|
||||
> 44 preguntas (se elimina 2.4, se agrega 6.6) · 8 secciones · Códigos únicos corregidos.
|
||||
> Claude Code reescribe `seed.sql` a partir de ESTE contrato.
|
||||
|
||||
## Convenciones
|
||||
- Tipos: radio→`single_choice`, checkbox→`multiple_choice`, escala 1-5→`rating`,
|
||||
texto libre largo→`text_long`, texto corto→`text_short`.
|
||||
- **`conditional_rules` en la pregunta DEPENDIENTE** (cada pregunta declara su propia condición).
|
||||
- Los 3 triggers (1.1, 1.6, 1.13) son `single_choice` de 3 opciones; solo **"Sí"** dispara el bloque.
|
||||
- Los 3 triggers (1.1, 1.6, 1.13) son `single_choice` de 2 opciones (Sí · No); solo **"Sí"** dispara
|
||||
el bloque correspondiente.
|
||||
- `is_sensitive=true` en preguntas de salud/discapacidad/diagnóstico (marcadas ✅).
|
||||
- `3.4` (salary) — `is_sensitive=true` + analyst-only (ADR-001 addendum): nunca en vistas org_admin.
|
||||
- RE-INDEX: las preguntas marcadas son candidatas a índice compuesto (metodología TBD Sprint 3).
|
||||
- En preguntas `multiple_choice` el formulario muestra el subtítulo "Podés marcar más de una opción".
|
||||
- "Prefiero no decir" eliminada de todas las preguntas (Cambio 1, Etapa 3I2).
|
||||
|
||||
## Estructura condicional (3 bloques)
|
||||
## Estructura condicional (4 bloques)
|
||||
- **cond-disc-propia** — 1.1='Sí' → muestra 1.2, 1.3, 1.4, 1.5
|
||||
- **cond-familiar** — 1.6='Sí' → muestra 1.7, 1.8, 1.9, 1.10, 1.11, 1.12
|
||||
- **cond-adulto-mayor** — 1.13='Sí' → muestra 1.14, 1.15, 1.16
|
||||
- Secciones A, 2, 3, 4, 5, 6, cierre: **top-level, siempre visibles.**
|
||||
- **salto-triple** — si 1.1='No' Y 1.6='No' Y 1.13='No' (es decir, ninguno de los tres dio
|
||||
'Sí'), se ocultan completas las secciones 2, 3, 4, 5 y las preguntas 6.1, 6.2, 6.3. El
|
||||
formulario salta directo a 6.4. Implementado como `conditional_rules`
|
||||
`{"if_question":["1.1","1.6","1.13"],"op":"eq_any","value":"Sí","action":"show"}` en cada
|
||||
pregunta del bloque (equivalente: se muestra si AL MENOS uno de los tres es 'Sí').
|
||||
- Sección A, 6.4-6.6 y Sección 7 (cierre): **top-level, siempre visibles.**
|
||||
|
||||
---
|
||||
|
||||
@@ -28,49 +36,68 @@
|
||||
| A2 | single_choice | ¿En qué ciudad vivís?* | 219 ciudades (GeoSelectWidget: dependiente de A1) | qi_city — Sprint 3 |
|
||||
| A3 | text_short | ¿En qué barrio vivís? | — (texto libre, opcional) | |
|
||||
| A4 | single_choice | ¿En qué área o gerencia trabajás?* | Directorio · Gerencia · Operaciones/Producción · Comercial/Ventas · Administración/Finanzas · Recursos Humanos/Cultura · Tecnología/Sistemas · Logística/Distribución · Otra área | qi_sector |
|
||||
| A5 | single_choice | ¿Cuántos años tenés? | Menos de 25 años · 25 a 34 años · 35 a 44 años · 45 a 54 años · 55 años o más · Prefiero no decir | qi_age_bucket |
|
||||
| A6 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro · Prefiero no decir | |
|
||||
| A5 | single_choice | ¿Cuántos años tenés? | Menos de 25 años · 25 a 34 años · 35 a 44 años · 45 a 54 años · 55 años o más | qi_age_bucket |
|
||||
| A6 | single_choice | ¿Con qué género te identificás? | Femenino · Masculino · No binario/Otro | |
|
||||
| A7 | single_choice | ¿Cuál es tu modalidad de trabajo actual? | Presencial · Remoto (home office) · Híbrido | |
|
||||
|
||||
## Sección 1 — Prevalencia: cuidado y discapacidad *(sensible)*
|
||||
| Code | Tipo | Cond. | Pregunta | Opciones | is_sensitive |
|
||||
|---|---|---|---|---|---|
|
||||
| 1.1 | single_choice | trigger | ¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?* | Sí · No · Prefiero no decir | ✅ |
|
||||
| 1.1 | single_choice | trigger | ¿Vos mismo/a vivís con alguna discapacidad o condición de salud que afecte tu funcionamiento cotidiano?* | Sí · No | ✅ |
|
||||
| 1.2 | multiple_choice | 1.1=Sí | ¿Qué tipo de discapacidad o condición tenés? | Discapacidad física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica que genera limitaciones · Otra | ✅ |
|
||||
| 1.3 | multiple_choice | 1.1=Sí | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utilizo | ✅ |
|
||||
| 1.3 | multiple_choice | 1.1=Sí | ¿Usás algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utilizo | ✅ |
|
||||
| 1.4 | single_choice | 1.1=Sí | ¿Contás con certificado oficial de discapacidad? | Sí · No, pero estoy en trámite · No | ✅ |
|
||||
| 1.5 | single_choice | 1.1=Sí | ¿Tu condición afecta tu desempeño o comodidad en el trabajo? | Sí, de forma significativa · Sí, pero en aspectos puntuales · No, está bien gestionada · Prefiero no decir | ✅ |
|
||||
| 1.6 | single_choice | trigger | ¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?* | Sí · No · Prefiero no decir | ✅ |
|
||||
| 1.5 | single_choice | 1.1=Sí | ¿Tu condición afecta tu desempeño o comodidad en el trabajo? | Sí, de forma significativa · Sí, pero en aspectos puntuales · No, está bien gestionada | ✅ |
|
||||
| 1.6 | single_choice | trigger | ¿Tenés algún familiar cercano (hijo/a, pareja, hermano/a, padre/madre) que viva con discapacidad?* | Sí · No | ✅ |
|
||||
| 1.7 | multiple_choice | 1.6=Sí | ¿Cuál es tu vínculo con esa persona? | Hijo/a · Cónyuge/pareja · Padre o madre · Hermano/a · Otro familiar | ✅ |
|
||||
| 1.8 | multiple_choice | 1.6=Sí | ¿Qué tipo de discapacidad tiene? | Física/motriz · Visual · Auditiva · Intelectual · TEA · Psicosocial/salud mental · Condición crónica severa · Otra | ✅ |
|
||||
| 1.9 | multiple_choice | 1.6=Sí | ¿Qué tipo de apoyo o acompañamiento necesita principalmente? | Movilidad física · Comunicación · Apoyo escolar/educativo · Inclusión laboral · Terapias/rehabilitación · Apoyo emocional/psicosocial | ✅ |
|
||||
| 1.10 | multiple_choice | 1.6=Sí | ¿Usa algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utiliza | ✅ |
|
||||
| 1.10 | multiple_choice | 1.6=Sí | ¿Usa algún elemento de apoyo para la movilidad o autonomía? | Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utiliza | ✅ |
|
||||
| 1.11 | single_choice | 1.6=Sí | Si es niño/a o adolescente en edad escolar: ¿está escolarizado/a y enfrenta barreras de aprendizaje? | Escolarizado/a, sin barreras importantes · Escolarizado/a, enfrenta barreras o faltan ajustes · No está escolarizado/a · No aplica | ✅ |
|
||||
| 1.12 | single_choice | 1.6=Sí | ¿La persona con discapacidad se encuentra trabajando actualmente? | Sí, está trabajando · No, y está buscando empleo · No, y no está buscando · No aplica | ✅ |
|
||||
| 1.13 | single_choice | trigger | ¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?* | Sí · No · Prefiero no decir | |
|
||||
| 1.13 | single_choice | trigger | ¿Tenés algún adulto mayor (60+ años) a tu cuidado o bajo tu responsabilidad?* | Sí · No | |
|
||||
| 1.14 | single_choice | 1.13=Sí | ¿Qué nivel de dependencia tiene? (la más representativa) | Independiente, pero necesita supervisión · Dependencia leve · Dependencia moderada · Dependencia severa | ✅ |
|
||||
| 1.15 | multiple_choice | 1.13=Sí | ¿Tiene una condición de salud que genere mayor necesidad de cuidado? | Demencia/Alzheimer · ACV/Parálisis · Enfermedad crónica grave · Movilidad reducida · Deterioro cognitivo · Sin condición específica | ✅ |
|
||||
| 1.16 | single_choice | 1.13=Sí | ¿Vive con vos o en otra vivienda? | Vive en mi hogar · Vive en otra vivienda · Está en residencia/institución | |
|
||||
|
||||
## Sección 2 — Intensidad y rol del cuidado (top-level)
|
||||
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
|
||||
|
||||
| Code | Tipo | Pregunta | Opciones |
|
||||
|---|---|---|---|
|
||||
| 2.1 | single_choice | ¿Quién es el/la responsable principal del cuidado? | Yo soy el/la principal · Comparto con otro familiar · Hay cuidador/a pagado/a · Está en institución/centro de día |
|
||||
| 2.1 | multiple_choice ⚠️ TECH-DEBT | ¿Quién es el/la responsable principal del cuidado? | Yo soy el/la principal · Comparto con otro familiar · Hay cuidador/a pagado/a · Está en institución/centro de día |
|
||||
| 2.2 | single_choice | ¿Cuántas horas por día dedicás, en promedio, al cuidado directo? | Menos de 1h · Entre 1 y 3h · Entre 3 y 6h · Más de 6h · Varía mucho |
|
||||
| 2.3 | multiple_choice | ¿En qué momentos del día cuidás principalmente? | Antes del trabajo · Durante horario laboral (llamadas/emergencias) · Al llegar del trabajo · Durante la noche · Los fines de semana · Es continuo sin horario fijo |
|
||||
| 2.4 | single_choice | ¿Contás con apoyo de otras personas para el cuidado? | Sí, apoyo familiar suficiente · Algo de apoyo, no suficiente · No, soy el/la único/a responsable · Tengo apoyo contratado |
|
||||
| 2.5 | single_choice | Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico? | Sí, frecuentemente · A veces · No, lo llevo bien · Prefiero no decir |
|
||||
| 2.5 | single_choice | Como cuidador/a primario, ¿presentás signos de agotamiento o estrés crónico? | Sí, frecuentemente · A veces · No, lo llevo bien |
|
||||
|
||||
> ⚠️ TECH-DEBT (2.1): pasó de `single_choice` a `multiple_choice` — selección múltiple sin orden
|
||||
> de prioridad. Deuda: implementar ranking (Principal cuidador soy yo > secundario instituto >
|
||||
> principal pagado > secundario familiar). Ver `TECH_DEBT.md`.
|
||||
>
|
||||
> **2.4 eliminada** (Cambio 6, Etapa 3I2): "¿Contás con apoyo de otras personas para el cuidado?"
|
||||
> — feedback del equipo médico: pregunta similar/redundante con 2.1. Ver `OBSERVATIONS.md`.
|
||||
|
||||
## Sección 3 — Impacto económico del cuidado (top-level)
|
||||
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
|
||||
> 3.4: is_sensitive=true + analyst-only (no exponer a org_admin).
|
||||
|
||||
| Code | Tipo | Pregunta | Opciones | Notas |
|
||||
|---|---|---|---|---|
|
||||
| 3.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | Sin gastos significativos · Menos de Gs. 500.000 · Gs. 500.000–1.500.000 · Gs. 1.500.000–3.000.000 · Gs. 3.000.000–6.000.000 · Más de Gs. 6.000.000 · Prefiero no decir | |
|
||||
| 3.1 | single_choice | ¿Cuánto estimás que gastás por mes en el cuidado? | Menos de Gs. 500.000 · Gs. 500.000–1.500.000 · Gs. 1.500.000–3.000.000 · Gs. 3.000.000–6.000.000 · Más de Gs. 6.000.000 | |
|
||||
| 3.2 | multiple_choice | ¿Qué servicios o apoyos utilizás actualmente? | Terapias especializadas · Cuidador/a particular · Centro de día/institución · Transporte adaptado · Medicación alto costo · Equipamiento médico/tecnología asistiva · No utilizo servicios formales | |
|
||||
| 3.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Con mi empleo de tiempo completo · Con mi empleo de tiempo parcial · Con trabajo independiente/emprendimiento · Me ayudan mis familiares · Es difícil sostenerlo con mis ingresos | |
|
||||
| 3.4 | single_choice | ¿En qué franja salarial mensual te ubicás? *(CONFIDENCIAL — analyst-only)* | Sueldo mínimo – Gs. 5.000.000 · Gs. 5.000.001–8.000.000 · Gs. 8.000.001–12.000.000 · Gs. 12.000.001–15.000.000 · Gs. 15.000.001–18.000.000 · Gs. 18.000.001–25.000.000 · Gs. 25.000.001–50.000.000 · Gs. 50.000.001 o más · Prefiero no decir | is_sensitive=true; never in org_admin views |
|
||||
| 3.3 | multiple_choice | ¿Cómo sostenés los gastos y responsabilidades del cuidado? | Con mi empleo de tiempo completo · Con mi empleo de tiempo parcial · Con trabajo independiente/emprendimiento · Me ayudan mis familiares · No puedo sostenerlo con mis ingresos | |
|
||||
| 3.4 | single_choice | ¿En qué franja salarial mensual te ubicás? *(CONFIDENCIAL — analyst-only)* | Sueldo mínimo · Gs. 3.000.000 a 5.000.000 · Gs. 5.000.001–8.000.000 · Gs. 8.000.001–12.000.000 · Gs. 12.000.001–15.000.000 · Gs. 15.000.001–18.000.000 · Gs. 18.000.001–25.000.000 · Gs. 25.000.001–50.000.000 · Gs. 50.000.001 o más | is_sensitive=true; never in org_admin views |
|
||||
| 3.5 | rating | ¿Los gastos de cuidado impactan en tu situación económica personal? | 1=Sin impacto · 2=Leve · 3=Moderado · 4=Alto · 5=Muy alto | RE-INDEX |
|
||||
|
||||
> **Cambio 7** (3.1): se elimina "Sin gastos significativos" además de "Prefiero no decir".
|
||||
> **Cambio 8** (3.3): "Es difícil sostenerlo con mis ingresos" → "No puedo sostenerlo con mis
|
||||
> ingresos". Breaking change para datos existentes — ver `OBSERVATIONS.md`.
|
||||
> **Cambio 9** (3.4): "Sueldo mínimo – Gs. 5.000.000" se divide en "Sueldo mínimo" y
|
||||
> "Gs. 3.000.000 a 5.000.000". Breaking change para datos existentes — ver `OBSERVATIONS.md`.
|
||||
|
||||
## Sección 4 — Tiempo y descanso (top-level)
|
||||
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
|
||||
|
||||
| Code | Tipo | Pregunta | Opciones |
|
||||
|---|---|---|---|
|
||||
| 4.1 | single_choice | ¿Cómo describirías tu nivel de descanso habitual? | Descanso bien, me siento recuperado/a · Descanso de forma irregular, pero lo manejo · Me cuesta descansar, me siento cansado/a con frecuencia · Descanso muy poco, el agotamiento afecta mi día a día | RE-INDEX |
|
||||
@@ -78,19 +105,25 @@
|
||||
| 4.3 | rating | En una escala del 1 al 5, ¿cómo evaluás tu bienestar general en este momento? | 1=Muy mal · 2=Mal · 3=Regular · 4=Bien · 5=Muy bien | RE-INDEX |
|
||||
|
||||
## Sección 5 — Impacto en el trabajo (top-level)
|
||||
> Cond.: **salto-triple** — toda la sección se oculta si 1.1='No' Y 1.6='No' Y 1.13='No'.
|
||||
|
||||
| Code | Tipo | Pregunta | Opciones |
|
||||
|---|---|---|---|
|
||||
| 5.1 | single_choice | En los últimos 3 meses, ¿con qué frecuencia sentiste que el cuidado afectó tu concentración o rendimiento? | Nunca o casi nunca · Algunas veces al mes · Varias veces por semana · Casi todos los días | RE-INDEX |
|
||||
| 5.2 | single_choice | En el último año, ¿faltaste al trabajo por situaciones relacionadas al cuidado? | Nunca · Pocas veces (1–3 veces al año) · Varias veces (4–10 veces al año) · Con frecuencia (más de 10 veces al año) | |
|
||||
|
||||
## Sección 6 — Conciencia organizacional (top-level)
|
||||
> Cond. (6.1-6.3): **salto-triple** — se ocultan si 1.1='No' Y 1.6='No' Y 1.13='No'.
|
||||
> 6.4-6.6: siempre visibles (destino del salto).
|
||||
|
||||
| Code | Tipo | Pregunta | Opciones | Notas |
|
||||
|---|---|---|---|---|
|
||||
| 6.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?* | Sí, me siento acompañado/a · Parcialmente, hay cierta comprensión · No, es algo bastante invisible · No, nunca se ha hablado del tema | RE-INDEX |
|
||||
| 6.1 | single_choice | ¿Sentís que tu empresa conoce o comprende la realidad del cuidado y la discapacidad de su gente?* | Sí, me siento acompañado/a · Parcialmente, hay cierta comprensión · No, es algo bastante invisible · No, no se puede hablar del tema | RE-INDEX. Cambio 10: última opción reescrita (era "No, nunca se ha hablado del tema"). Breaking change — ver `OBSERVATIONS.md`. |
|
||||
| 6.2 | multiple_choice (**max_select=3**) | ¿Qué apoyos de la empresa te ayudarían más? Elegí hasta 3 | Flexibilidad horaria o trabajo remoto · Apoyo económico o convenios/descuentos · Acceso a terapias o salud · Contención/apoyo emocional o psicológico · Capacitación y autocuidado · Redes de apoyo/grupos de pares · Información sobre recursos y servicios disponibles | ⚠️ Ranking → multiple_choice para v1. Ranking widget al backlog. |
|
||||
| 6.3 | single_choice | ¿Estarías dispuesto/a a participar en un grupo de apoyo para cuidadores dentro de la empresa? | Sí, definitivamente · Tal vez, si hay más personas en la misma situación · No por ahora · Prefiero mantenerlo en privado | |
|
||||
| 6.4 | single_choice | ¿Conocés a algún compañero/a de trabajo que esté al cuidado de una persona con discapacidad o dependiente? | Sí, conozco a varias personas · Sí, conozco a una persona · No conozco a nadie · No estoy seguro/a | Código corregido (era 6.2 duplicado) |
|
||||
| 6.5 | single_choice | ¿Considerás que la empresa debería ofrecer apoyos a quienes cuidan a una persona dependiente? | Sí, definitivamente · Tal vez, dependería del caso · No lo considero necesario · No lo sé | Código corregido (era 6.3 duplicado) |
|
||||
| 6.6 | single_choice | ¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo a personas con discapacidad y/o cuidadores? | Sí · No · Tal vez | Nueva (Cambio 4b, Etapa 3I2) |
|
||||
|
||||
## Sección 7 — Para cerrar (top-level)
|
||||
| Code | Tipo | Pregunta | Notas |
|
||||
|
||||
27
TECH_DEBT.md
27
TECH_DEBT.md
@@ -58,3 +58,30 @@
|
||||
- **Para implementar:** migration futura que agregue extracción de A1/A2 en el insert
|
||||
y extienda la whitelist de `agg_segment`.
|
||||
- **Trigger:** inicio de Sprint 3 (dashboard avanzado con filtros de ciudad).
|
||||
|
||||
---
|
||||
|
||||
## [TECH-DEBT-006] Ranking widget para 2.1
|
||||
|
||||
- **Estado:** Etapa 3I2 (Cambio 5) cambió 2.1 ("¿Quién es el/la responsable principal
|
||||
del cuidado?") de `single_choice` a `multiple_choice`. Permite selección múltiple
|
||||
pero sin orden de prioridad — pierde la noción de "responsable principal" cuando
|
||||
el respondente marca más de una opción.
|
||||
- **Deuda:** implementar widget de ranking para 2.1 con el orden:
|
||||
Principal cuidador soy yo > secundario instituto > principal pagado > secundario
|
||||
familiar (mismo patrón que TECH-DEBT-001 para 6.2).
|
||||
- **Trigger:** decisión del director sobre el tipo de 2.1 en próxima versión del
|
||||
instrumento.
|
||||
|
||||
---
|
||||
|
||||
## [TECH-DEBT-008] /admin/responses sin autenticación
|
||||
|
||||
- **Estado:** ABIERTO.
|
||||
- **Descripción:** la vista de control `/admin/responses` no tiene auth. Por diseño
|
||||
explícito, excluye preguntas `is_sensitive=true` (14 preguntas de salud/
|
||||
discapacidad) y `text_long`. Para mostrar esas preguntas en la vista, primero
|
||||
implementar un gate de autenticación para staff (Basic Auth con env var como
|
||||
mínimo viable).
|
||||
- **Impacto actual:** ninguno en producción — la URL no está publicada.
|
||||
- **Bloqueante para:** ampliar visibilidad de datos en la vista de control.
|
||||
|
||||
228
app/admin/responses/page.tsx
Normal file
228
app/admin/responses/page.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
import { createClient } from '@supabase/supabase-js'
|
||||
import { ResponsesTable, type DisplayRow } from './responses-table'
|
||||
|
||||
// Server Component sin auth — herramienta interna de diagnóstico (Etapa 3V-B).
|
||||
// Usa SUPABASE_SERVICE_ROLE_KEY: vive solo acá, server-side, nunca al cliente.
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const MAX_SUBMISSIONS = 500
|
||||
const PROMPT_MAX_LEN = 60
|
||||
|
||||
const SURVEY_OPTIONS = [
|
||||
{ label: 'Todos', id: null },
|
||||
{ label: 'InQuality', id: '30000000-0000-0000-0000-000000000002' },
|
||||
{ label: 'Fundación Solidaridad', id: '30000000-0000-0000-0000-000000000003' },
|
||||
{ label: 'Demo', id: '30000000-0000-0000-0000-000000000001' },
|
||||
] as const
|
||||
|
||||
interface AllowedQuestion {
|
||||
id: string
|
||||
code: string
|
||||
prompt: string
|
||||
type: string
|
||||
position: number
|
||||
}
|
||||
|
||||
interface AnswerRow {
|
||||
value: unknown
|
||||
question_id: string
|
||||
}
|
||||
|
||||
interface ResponseRow {
|
||||
id: string
|
||||
submitted_at: string
|
||||
qi_zone: string | null
|
||||
qi_sector: string | null
|
||||
answers: AnswerRow[]
|
||||
}
|
||||
|
||||
function getSupabaseAdmin() {
|
||||
const url = process.env.NEXT_PUBLIC_SUPABASE_URL || 'https://placeholder.supabase.co'
|
||||
const serviceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || 'placeholder'
|
||||
return createClient(url, serviceKey, { auth: { persistSession: false } })
|
||||
}
|
||||
|
||||
function formatDate(iso: string): string {
|
||||
const d = new Date(iso)
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${pad(d.getDate())}/${pad(d.getMonth() + 1)}/${d.getFullYear()} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
function truncate(text: string, max = PROMPT_MAX_LEN): string {
|
||||
return text.length > max ? `${text.slice(0, max)}...` : text
|
||||
}
|
||||
|
||||
// Spec 4.3 — null/undefined → null (renderizado como badge "sin dato")
|
||||
function renderValue(value: unknown, type: string): string | null {
|
||||
if (value === null || value === undefined) return null
|
||||
|
||||
switch (type) {
|
||||
case 'single_choice':
|
||||
case 'text_short':
|
||||
return String(value).replace(/^"|"$/g, '')
|
||||
case 'multiple_choice':
|
||||
if (Array.isArray(value)) return value.join(', ')
|
||||
return String(value)
|
||||
case 'rating':
|
||||
return String(value)
|
||||
default:
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
|
||||
export default async function AdminResponsesPage({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams: Promise<{ survey_id?: string }>
|
||||
}) {
|
||||
const { survey_id: surveyId } = await searchParams
|
||||
const supabase = getSupabaseAdmin()
|
||||
|
||||
// 1. Preguntas permitidas — text_long e is_sensitive=true se excluyen ACÁ,
|
||||
// antes de tocar la tabla answers. Sus valores nunca se consultan.
|
||||
const { data: questionsData, error: questionsError } = await supabase
|
||||
.from('questions')
|
||||
.select('id, code, prompt, type, position')
|
||||
.eq('is_sensitive', false)
|
||||
.neq('type', 'text_long')
|
||||
.order('position', { ascending: true })
|
||||
|
||||
if (questionsError) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<h1 className="admin-title">Control de respuestas</h1>
|
||||
<p className="admin-error">No fue posible conectarse a la base de datos.</p>
|
||||
<pre className="admin-error-detail">{questionsError.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const allowedQuestions = (questionsData ?? []) as AllowedQuestion[]
|
||||
const allowedIds = allowedQuestions.map((q) => q.id)
|
||||
const questionMap = new Map(allowedQuestions.map((q) => [q.id, q]))
|
||||
|
||||
// 2. Submissions con sus respuestas — answers!inner + .in() filtra el ARRAY
|
||||
// embebido a solo question_id permitidos (consulta, no post-filtro).
|
||||
let responsesQuery = supabase
|
||||
.from('responses')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
submitted_at,
|
||||
qi_zone,
|
||||
qi_sector,
|
||||
answers!inner ( value, question_id )
|
||||
`
|
||||
)
|
||||
.in('answers.question_id', allowedIds)
|
||||
|
||||
if (surveyId) {
|
||||
responsesQuery = responsesQuery.eq('survey_id', surveyId)
|
||||
}
|
||||
|
||||
const { data: responsesData, error: responsesError } =
|
||||
allowedIds.length > 0
|
||||
? await responsesQuery.order('submitted_at', { ascending: false }).limit(MAX_SUBMISSIONS)
|
||||
: { data: [] as ResponseRow[], error: null }
|
||||
|
||||
if (responsesError) {
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<h1 className="admin-title">Control de respuestas</h1>
|
||||
<p className="admin-error">No fue posible conectarse a la base de datos.</p>
|
||||
<pre className="admin-error-detail">{responsesError.message}</pre>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const responses = (responsesData ?? []) as unknown as ResponseRow[]
|
||||
|
||||
// 3. Contadores totales (independientes del límite de 500 submissions),
|
||||
// filtrados por survey_id cuando hay filtro activo.
|
||||
let submissionsCountQuery = supabase
|
||||
.from('responses')
|
||||
.select('*', { count: 'exact', head: true })
|
||||
|
||||
if (surveyId) {
|
||||
submissionsCountQuery = submissionsCountQuery.eq('survey_id', surveyId)
|
||||
}
|
||||
|
||||
const { count: totalSubmissions } = await submissionsCountQuery
|
||||
|
||||
let answersCountQuery = supabase
|
||||
.from('answers')
|
||||
.select('*, responses!inner(survey_id)', { count: 'exact', head: true })
|
||||
.in('question_id', allowedIds)
|
||||
|
||||
if (surveyId) {
|
||||
answersCountQuery = answersCountQuery.eq('responses.survey_id', surveyId)
|
||||
}
|
||||
|
||||
const { count: totalAnswers } =
|
||||
allowedIds.length > 0 ? await answersCountQuery : { count: 0 }
|
||||
|
||||
// 4. Aplanar: una fila por answer, ordenadas por position dentro de cada submission
|
||||
const rows: DisplayRow[] = []
|
||||
responses.forEach((response, idx) => {
|
||||
const answers = response.answers
|
||||
.filter((a) => questionMap.has(a.question_id))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
questionMap.get(a.question_id)!.position - questionMap.get(b.question_id)!.position
|
||||
)
|
||||
|
||||
answers.forEach((a, ai) => {
|
||||
const q = questionMap.get(a.question_id)!
|
||||
const isFirstInGroup = ai === 0
|
||||
rows.push({
|
||||
key: `${response.id}-${a.question_id}`,
|
||||
submissionNum: idx + 1,
|
||||
isFirstInGroup,
|
||||
date: isFirstInGroup ? formatDate(response.submitted_at) : '',
|
||||
code: q.code,
|
||||
prompt: truncate(q.prompt),
|
||||
rendered: renderValue(a.value, q.type),
|
||||
zone: isFirstInGroup ? (response.qi_zone ?? '—') : '',
|
||||
sector: isFirstInGroup ? (response.qi_sector ?? '—') : '',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="admin-page" role="main">
|
||||
<header className="admin-header">
|
||||
<div>
|
||||
<h1 className="admin-title">Control de respuestas</h1>
|
||||
<p className="admin-subtitle">
|
||||
{totalSubmissions ?? 0} submissions · {totalAnswers ?? 0} respuestas individuales ·
|
||||
actualizado {formatDate(new Date().toISOString())}
|
||||
</p>
|
||||
<nav className="admin-survey-nav" aria-label="Selección de empresa">
|
||||
{SURVEY_OPTIONS.map((opt) => (
|
||||
<a
|
||||
key={opt.label}
|
||||
href={opt.id ? `/admin/responses?survey_id=${opt.id}` : '/admin/responses'}
|
||||
className={
|
||||
opt.id === (surveyId ?? null)
|
||||
? 'admin-survey-link admin-survey-link-active'
|
||||
: 'admin-survey-link'
|
||||
}
|
||||
aria-current={opt.id === (surveyId ?? null) ? 'page' : undefined}
|
||||
>
|
||||
{opt.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
<a href="/admin/responses" className="admin-refresh">Actualizar</a>
|
||||
</header>
|
||||
|
||||
<div className="admin-table-wrapper">
|
||||
<ResponsesTable rows={rows} />
|
||||
{rows.length === 0 && (
|
||||
<p className="admin-empty">Sin respuestas registradas todavía.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
98
app/admin/responses/responses-table.tsx
Normal file
98
app/admin/responses/responses-table.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
|
||||
export interface DisplayRow {
|
||||
key: string
|
||||
submissionNum: number
|
||||
isFirstInGroup: boolean
|
||||
date: string
|
||||
code: string
|
||||
prompt: string
|
||||
rendered: string | null
|
||||
zone: string
|
||||
sector: string
|
||||
}
|
||||
|
||||
export function ResponsesTable({ rows }: { rows: DisplayRow[] }) {
|
||||
const [expanded, setExpanded] = useState<Set<number>>(new Set())
|
||||
|
||||
function toggle(submissionNum: number) {
|
||||
setExpanded((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(submissionNum)) next.delete(submissionNum)
|
||||
else next.add(submissionNum)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<table className="admin-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Fecha</th>
|
||||
<th>Código</th>
|
||||
<th>Pregunta</th>
|
||||
<th>Valor</th>
|
||||
<th className="admin-col-optional">Departamento</th>
|
||||
<th className="admin-col-optional">Sector</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows
|
||||
.filter((row) => row.isFirstInGroup || expanded.has(row.submissionNum))
|
||||
.map((row) => {
|
||||
const groupClass =
|
||||
row.submissionNum % 2 === 0 ? 'admin-group-even' : 'admin-group-odd'
|
||||
const isExpanded = expanded.has(row.submissionNum)
|
||||
const trClass = row.isFirstInGroup
|
||||
? `${groupClass} admin-group-start admin-row-toggle`
|
||||
: groupClass
|
||||
|
||||
return (
|
||||
<tr
|
||||
key={row.key}
|
||||
className={trClass}
|
||||
onClick={row.isFirstInGroup ? () => toggle(row.submissionNum) : undefined}
|
||||
onKeyDown={
|
||||
row.isFirstInGroup
|
||||
? (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault()
|
||||
toggle(row.submissionNum)
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
role={row.isFirstInGroup ? 'button' : undefined}
|
||||
tabIndex={row.isFirstInGroup ? 0 : undefined}
|
||||
aria-expanded={row.isFirstInGroup ? isExpanded : undefined}
|
||||
>
|
||||
<td>
|
||||
{row.isFirstInGroup && (
|
||||
<span className="admin-toggle-icon" aria-hidden="true">
|
||||
{isExpanded ? '▼' : '▶'}
|
||||
</span>
|
||||
)}
|
||||
{row.submissionNum}
|
||||
</td>
|
||||
<td>{row.date}</td>
|
||||
<td>{row.code}</td>
|
||||
<td className="admin-prompt">{row.prompt}</td>
|
||||
<td>
|
||||
{row.rendered === null ? (
|
||||
<span className="admin-nodata">sin dato</span>
|
||||
) : (
|
||||
row.rendered
|
||||
)}
|
||||
</td>
|
||||
<td className="admin-col-optional">{row.zone}</td>
|
||||
<td className="admin-col-optional">{row.sector}</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,14 @@ import { FilterSidebar } from '@/components/dashboard/filter-sidebar'
|
||||
import type { DashboardFilters } from '@/lib/dashboard-types'
|
||||
|
||||
// El survey_id se pasa como query param: /dashboard?survey_id=UUID
|
||||
// '00000000-0000-0000-0000-000000000000' = flag p_all (vista agregada de todos los surveys)
|
||||
const SURVEY_OPTIONS = [
|
||||
{ label: 'Todos', id: '00000000-0000-0000-0000-000000000000' },
|
||||
{ label: 'InQuality', id: '30000000-0000-0000-0000-000000000002' },
|
||||
{ label: 'Fundación Solidaridad', id: '30000000-0000-0000-0000-000000000003' },
|
||||
{ label: 'Demo', id: '30000000-0000-0000-0000-000000000001' },
|
||||
]
|
||||
|
||||
export default function DashboardPage() {
|
||||
const searchParams = useSearchParams()
|
||||
const surveyId = searchParams.get('survey_id') ?? '30000000-0000-0000-0000-000000000001'
|
||||
@@ -40,6 +48,22 @@ export default function DashboardPage() {
|
||||
Datos agregados · k-anonimato k≥5 · Encuesta Mapeo de Cuidadores 2026
|
||||
{filters.depto && <span className="db-active-filter"> · Filtro: {filters.depto}</span>}
|
||||
</p>
|
||||
<nav className="db-survey-nav" aria-label="Selección de empresa">
|
||||
{SURVEY_OPTIONS.map((opt) => (
|
||||
<a
|
||||
key={opt.id}
|
||||
href={`/dashboard?survey_id=${opt.id}`}
|
||||
className={
|
||||
opt.id === surveyId
|
||||
? 'db-survey-link db-survey-link-active'
|
||||
: 'db-survey-link'
|
||||
}
|
||||
aria-current={opt.id === surveyId ? 'page' : undefined}
|
||||
>
|
||||
{opt.label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
{/* Fila 1: KPIs (span 12) */}
|
||||
|
||||
@@ -559,6 +559,14 @@ body {
|
||||
.db-page-title { font-size: 1.375rem; font-weight: 700; color: #fff; margin: 0 0 4px; }
|
||||
.db-page-subtitle { font-size: 0.8125rem; color: #9ca3af; margin: 0; }
|
||||
.db-active-filter { color: var(--color-re-teal); font-weight: 600; }
|
||||
.db-survey-nav { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
||||
.db-survey-link {
|
||||
font-size: 0.75rem; font-weight: 600; padding: 4px 12px; border-radius: 999px;
|
||||
border: 1px solid rgba(67,187,200,0.3); color: #9ca3af; text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.db-survey-link:hover { color: #E7E6E5; border-color: var(--color-re-teal); }
|
||||
.db-survey-link-active { background: var(--color-re-teal); color: #0F1923; border-color: var(--color-re-teal); }
|
||||
.db-section { margin-bottom: 16px; }
|
||||
.db-grid {
|
||||
display: grid;
|
||||
@@ -742,11 +750,67 @@ body {
|
||||
}
|
||||
.db-filter-reset:hover { background: rgba(67,187,200,0.2); }
|
||||
|
||||
/* ── Admin: control de respuestas (Etapa 3V) ── */
|
||||
.admin-page { max-width: 1100px; margin: 0 auto; padding: 24px; }
|
||||
.admin-header {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
gap: 16px; margin-bottom: 16px; flex-wrap: wrap;
|
||||
}
|
||||
.admin-title { font-size: 1.375rem; font-weight: 700; color: var(--color-text); margin: 0 0 4px; }
|
||||
.admin-subtitle { font-size: 0.8125rem; color: var(--color-text-muted); margin: 0; }
|
||||
.admin-survey-nav { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
|
||||
.admin-survey-link {
|
||||
font-size: 0.75rem; font-weight: 600; padding: 4px 12px; border-radius: 999px;
|
||||
border: 1px solid var(--color-border); color: var(--color-text-muted); text-decoration: none;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.admin-survey-link:hover { color: var(--color-text); border-color: var(--color-primary); }
|
||||
.admin-survey-link-active { background: var(--color-primary); color: #fff; border-color: var(--color-primary); }
|
||||
.admin-refresh {
|
||||
display: inline-block; padding: 8px 16px; border-radius: var(--radius);
|
||||
background: var(--color-primary); color: #fff; font-size: 0.8125rem;
|
||||
font-weight: 600; text-decoration: none; white-space: nowrap;
|
||||
}
|
||||
.admin-refresh:hover { background: var(--color-primary-hover); }
|
||||
.admin-table-wrapper {
|
||||
overflow-x: auto; background: var(--color-surface); border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius); box-shadow: var(--shadow-card);
|
||||
}
|
||||
.admin-table { width: 100%; border-collapse: collapse; font-size: 0.8125rem; }
|
||||
.admin-table th {
|
||||
position: sticky; top: 0; background: var(--color-re-dark); color: #fff;
|
||||
font-weight: 600; text-align: left; padding: 10px 12px; white-space: nowrap;
|
||||
}
|
||||
.admin-table td { padding: 8px 12px; border-bottom: 1px solid var(--color-border); color: var(--color-text); vertical-align: top; }
|
||||
.admin-table tbody tr:last-child td { border-bottom: none; }
|
||||
.admin-table tbody tr.admin-group-even { background: var(--color-bg); }
|
||||
.admin-table tbody tr.admin-group-start td { border-top: 2px solid var(--color-re-dark); }
|
||||
.admin-table tbody tr:hover { background: var(--color-primary-light); }
|
||||
.admin-prompt { color: var(--color-text); }
|
||||
.admin-row-toggle { cursor: pointer; }
|
||||
.admin-toggle-icon {
|
||||
display: inline-block; width: 14px; margin-right: 4px;
|
||||
color: var(--color-text-muted); font-size: 0.7rem;
|
||||
}
|
||||
.admin-nodata {
|
||||
display: inline-block; padding: 1px 8px; border-radius: 10px;
|
||||
background: var(--color-re-light); color: var(--color-re-dark);
|
||||
opacity: 0.6; font-size: 0.7rem; font-style: italic;
|
||||
}
|
||||
.admin-empty { padding: 24px; text-align: center; color: var(--color-text-muted); }
|
||||
.admin-error { color: var(--color-error); font-weight: 600; }
|
||||
.admin-error-detail {
|
||||
background: var(--color-error-bg); border-radius: var(--radius);
|
||||
padding: 12px; font-family: 'Courier New', monospace; font-size: 0.75rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 768px) {
|
||||
.db-kpi-row { grid-template-columns: repeat(2,1fr); }
|
||||
.db-sidebar { width: 100%; border-right: none; border-bottom: 1px solid rgba(67,187,200,0.15); }
|
||||
.db-page { flex-direction: column; }
|
||||
.admin-col-optional { display: none; }
|
||||
}
|
||||
|
||||
/* ── Reduced motion ── */
|
||||
|
||||
@@ -26,15 +26,17 @@ export function ConsentScreen({ brandConfig, onAccept }: ConsentScreenProps) {
|
||||
</p>
|
||||
<ol className="consent-list">
|
||||
<li>
|
||||
<strong>Diagnóstico interno de {orgName}:</strong> tus respuestas ayudan a la empresa
|
||||
a entender la realidad de las personas que cuidan a familiares con discapacidad o adultos
|
||||
mayores, para diseñar apoyos más adecuados. Solo se trabaja con{' '}
|
||||
<strong>Diagnóstico interno de {orgName}:</strong> Tus respuestas ayudan a la empresa,
|
||||
en alianza con Fundación Solidaridad, a entender la realidad de las personas que cuidan
|
||||
a familiares con discapacidad o adultos mayores, para diseñar apoyos más adecuados. Solo
|
||||
se trabaja con{' '}
|
||||
<strong>datos agregados</strong> — nunca accede a respuestas individuales.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Estudio país (opcional):</strong> tus respuestas anonimizadas contribuyen a un
|
||||
mapeo nacional sobre la economía del cuidado en Paraguay. Los datos se procesan de
|
||||
forma irreversiblemente desvinculada de cualquier empresa o persona.
|
||||
<strong>Estudio país (opcional):</strong> Tus respuestas serán{' '}
|
||||
<strong>anónimas</strong> y contribuyen a un mapeo nacional sobre la economía del
|
||||
cuidado en Paraguay. Los datos se procesan de manera que no pueden asociarse a ninguna
|
||||
empresa ni persona.
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { useCallback, useEffect, useRef, useState, useTransition } from 'react'
|
||||
import type { FormAnswers, Question, Survey } from '@/lib/types'
|
||||
import { clearDraft, loadDraft, saveDraft } from '@/lib/storage'
|
||||
import { getHiddenCodes, isQuestionVisible } from '@/lib/conditional'
|
||||
import { getHiddenCodes, isQuestionVisible, ruleQuestionCodes } from '@/lib/conditional'
|
||||
import { submitSurvey } from '@/app/actions/submit-survey'
|
||||
import { ConsentScreen } from './consent-screen'
|
||||
import { ProgressBar } from './progress-bar'
|
||||
@@ -71,7 +71,7 @@ export function SurveyForm({ survey, questions }: SurveyFormProps) {
|
||||
} else {
|
||||
const appeared = questions.some(
|
||||
(q) =>
|
||||
q.conditional_rules?.some((r) => r.if_question === code) &&
|
||||
q.conditional_rules?.some((r) => ruleQuestionCodes(r).includes(code)) &&
|
||||
isQuestionVisible(q, updated) &&
|
||||
!isQuestionVisible(q, prev)
|
||||
)
|
||||
|
||||
@@ -40,11 +40,13 @@ export function CheckboxGroup({
|
||||
{required && <span className="required-mark" aria-hidden="true"> *</span>}
|
||||
{required && <span className="sr-only"> (obligatorio)</span>}
|
||||
</legend>
|
||||
{maxSelect !== null && (
|
||||
{maxSelect !== null ? (
|
||||
<p className="max-select-hint" aria-live="polite">
|
||||
Elegí hasta {maxSelect} opciones
|
||||
{value.length > 0 && ` · ${value.length}/${maxSelect} seleccionadas`}
|
||||
</p>
|
||||
) : (
|
||||
<p className="max-select-hint">Podés marcar más de una opción</p>
|
||||
)}
|
||||
<div className="options-list" role="list">
|
||||
{options.map((opt) => {
|
||||
|
||||
155
docs/estado/ESTADO_PROYECTO_JUNIO_2026.md
Normal file
155
docs/estado/ESTADO_PROYECTO_JUNIO_2026.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# ESTADO DEL PROYECTO — Datos País sobre el Cuidado
|
||||
## Documento de continuidad — 11 junio 2026
|
||||
|
||||
> Director: Marcos · Metodología: Maria Mersan · Contacto Fundación: Valeria Arce
|
||||
|
||||
---
|
||||
|
||||
## 1. URLs operativas
|
||||
|
||||
| Recurso | URL |
|
||||
|---|---|
|
||||
| Formulario Demo | https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000001 |
|
||||
| Formulario InQuality | https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000002 |
|
||||
| Formulario Fundación Solidaridad | https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000003 |
|
||||
| Dashboard — Todos | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=00000000-0000-0000-0000-000000000000 |
|
||||
| Dashboard — InQuality | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=30000000-0000-0000-0000-000000000002 |
|
||||
| Dashboard — Fundación Solidaridad | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=30000000-0000-0000-0000-000000000003 |
|
||||
| Dashboard — Demo | https://economia-cuidado.inqualityhq.com/dashboard?survey_id=30000000-0000-0000-0000-000000000001 |
|
||||
| Control de respuestas | https://economia-cuidado.inqualityhq.com/admin/responses |
|
||||
|
||||
---
|
||||
|
||||
## 2. Infraestructura — OPERATIVA ✅
|
||||
|
||||
- **App:** Next.js 15.3.8 (CVE-2025-66478 resuelto ✅)
|
||||
- **DB:** Supabase self-hosted en Dokploy v0.29.4
|
||||
- **VPS:** inq-mart-2, IP VPN: 172.20.5.11
|
||||
- **Repos:**
|
||||
- App: `git.inquality.com.py/marcos/motor-de-encuestas` (rama main)
|
||||
- Supabase stack: `git.inquality.com.py/marcos/supabase-motor-fomularios` (rama master)
|
||||
- **API Supabase:** https://db.inqualityhq.com (Kong + SSL)
|
||||
- **Backups:** `/var/backups/supabase-prod/`, cron 3AM, GFS (3 días diarios + domingos 14 días)
|
||||
- **Studio:** solo vía SSH tunnel → `ssh -L 3001:localhost:3001 administrator@172.20.5.11`
|
||||
|
||||
---
|
||||
|
||||
## 3. Instrumento — v2.1 ACTIVO
|
||||
|
||||
- **44 preguntas**, 8 secciones
|
||||
- **Salto triple:** 1.1=No AND 1.6=No AND 1.13=No → salta a 6.4 (validado con datos reales ✅)
|
||||
- **Nueva pregunta 6.6:** voluntariado en redes de apoyo (Sí/No/Tal vez)
|
||||
- **2.4 eliminada**, **2.1 → multiple_choice**
|
||||
- "Prefiero no decir" eliminado de todas las preguntas
|
||||
- Hint "Podés marcar más de una opción" en todas las multiple_choice
|
||||
- "Tecnología asistiva" en 1.3 y 1.10
|
||||
|
||||
---
|
||||
|
||||
## 4. DB — estado actual
|
||||
|
||||
| Tabla | Filas |
|
||||
|---|---|
|
||||
| organizations | 3 (Empresa Demo, InQuality, Fundación Solidaridad) |
|
||||
| surveys | 3 (una por organización) |
|
||||
| questions | 132 (44 × 3 surveys) |
|
||||
| responses | 15 (8 InQuality + 7 Demo — todas de prueba) |
|
||||
| answers | ~500 (estimado) |
|
||||
|
||||
---
|
||||
|
||||
## 5. Dashboard — multitenante operativo ✅
|
||||
|
||||
- Selector de empresa en el header: Todos / InQuality / Fundación Solidaridad / Demo
|
||||
- "Todos" usa UUID especial `00000000-0000-0000-0000-000000000000` → pool agregado
|
||||
- Implementado via flag en `_dash_responses` (migración `20260611000001`)
|
||||
- Validado visualmente con n=15: KPIs, heatmap departamental, perfil población ✅
|
||||
|
||||
---
|
||||
|
||||
## 6. Vista de control /admin/responses
|
||||
|
||||
- Tabla plana: una fila por answer, agrupada por submission
|
||||
- Colapsar/expandir por submission (Client Component island)
|
||||
- Excluye: `text_long`, `is_sensitive=true` (TECH-DEBT-008 — sin auth)
|
||||
- Sin columna "Tipo"
|
||||
|
||||
---
|
||||
|
||||
## 7. Método — Sprint 2 cerrado ✅
|
||||
|
||||
- `method/PATTERNS.md` y `method/ANTI_PATTERNS.md` creados en el repo
|
||||
- C.8 promovido a estable con casos reales del proyecto
|
||||
- AP.10 y AP.11 documentados con casos reales
|
||||
|
||||
---
|
||||
|
||||
## 8. Deuda técnica abierta
|
||||
|
||||
| ID | Descripción | Bloqueante |
|
||||
|---|---|---|
|
||||
| TECH-DEBT-006 | Backup remoto S3 | SÍ — antes del primer dato real |
|
||||
| TECH-DEBT-007 | auth.jwt() deprecado en GoTrue v2.189+ | No (piloto) |
|
||||
| TECH-DEBT-008 | /admin/responses sin auth | No (URL no publicada) |
|
||||
| TECH-DEBT-009 | Rotar secrets Supabase post-CVE-2025-66478 | SÍ — antes del primer dato real |
|
||||
|
||||
---
|
||||
|
||||
## 9. Pendientes próxima sesión
|
||||
|
||||
1. TECH-DEBT-006 — backup remoto S3
|
||||
2. TECH-DEBT-009 — rotar secrets Supabase
|
||||
3. RE Index — confirmar mapeo G1/G2/G4/G5 → códigos seed con Marcos
|
||||
4. Informe descargable — prometido en deck comercial
|
||||
5. Onboarding empresas reales — cuando validación esté lista
|
||||
|
||||
---
|
||||
|
||||
## 10. Decisiones tomadas — no reabrir
|
||||
|
||||
1. Supabase en Dokploy siempre via modo Git
|
||||
2. POSTGRES_PASSWORD siempre con `openssl rand -hex`
|
||||
3. kong.yml: una sola entrada por bloque keyauth_credentials
|
||||
4. SMTP_PORT=587 siempre explícito aunque SMTP esté desactivado
|
||||
5. GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false siempre explícito
|
||||
6. LUA_AUTH_EXPR con prefijo "Bearer " en kong-entrypoint.sh
|
||||
7. NEXT_PUBLIC_SUPABASE_URL via Build Argument en Dokploy
|
||||
8. App conecta a https://db.inqualityhq.com
|
||||
9. /admin/responses no muestra is_sensitive=true sin auth (TECH-DEBT-008)
|
||||
10. brand_config.org_name alimenta el nombre de empresa en el formulario
|
||||
11. Organizaciones separadas para cada encuesta
|
||||
12. conditional_rules referencian códigos de pregunta, no UUIDs
|
||||
13. UUID cero `00000000-...` es el flag p_all del dashboard
|
||||
14. Next.js pinneado a versión exacta `15.3.8` (sin caret)
|
||||
|
||||
---
|
||||
|
||||
## 11. Contexto comercial
|
||||
|
||||
- 2 empresas comprometidas para piloto (InQuality y Fundación Solidaridad en espacio de pruebas)
|
||||
- Cronograma: JUN inicio · JUL-AGO relevamiento · SEP análisis · OCT informe + evento
|
||||
- Meta: 20 empresas pioneras
|
||||
- Precios: Gs. 4.5M (≤50) · 7.5M (≤200) · 12M (≤400)
|
||||
- GAPs deck vs producto: RE Index no implementado, informe descargable no existe
|
||||
- Contacto Fundación: Valeria Arce — ddff@fundacionsolidaridad.org.py · +595 991 854333
|
||||
|
||||
---
|
||||
|
||||
## 12. Prompt para nueva sesión
|
||||
|
||||
Pegá al inicio de la nueva conversación:
|
||||
|
||||
---
|
||||
|
||||
Sos el copiloto técnico del proyecto "Datos País sobre el Cuidado" — plataforma soberana
|
||||
de encuestas sobre economía del cuidado en Paraguay. Tenemos múltiples sesiones de trabajo.
|
||||
|
||||
Stack: Next.js 15.3.8 + Supabase self-hosted + Dokploy v0.29.4.
|
||||
URL: https://economia-cuidado.inqualityhq.com
|
||||
Repos en Gitea: git.inquality.com.py/marcos/
|
||||
|
||||
El estado completo está en `docs/estado/ESTADO_PROYECTO_JUNIO_2026.md` en el repo.
|
||||
Los prompts de etapas pasadas están en `sprints/`.
|
||||
Las reglas no negociables están en `CLAUDE.md`.
|
||||
|
||||
Próximos pasos: [COMPLETAR CON LO QUE VAS A HACER]
|
||||
@@ -11,9 +11,17 @@ export function isQuestionVisible(
|
||||
return rules.every((rule) => evaluateRule(rule, answers))
|
||||
}
|
||||
|
||||
/** Normalizes if_question to an array of codes regardless of rule shape. */
|
||||
export function ruleQuestionCodes(rule: ConditionalRule): string[] {
|
||||
return Array.isArray(rule.if_question) ? rule.if_question : [rule.if_question]
|
||||
}
|
||||
|
||||
function evaluateRule(rule: ConditionalRule, answers: FormAnswers): boolean {
|
||||
if (rule.op === 'eq') {
|
||||
return answers[rule.if_question] === rule.value
|
||||
return answers[rule.if_question as string] === rule.value
|
||||
}
|
||||
if (rule.op === 'eq_any') {
|
||||
return ruleQuestionCodes(rule).some((code) => answers[code] === rule.value)
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -32,8 +40,8 @@ export function getHiddenCodes(
|
||||
return questions
|
||||
.filter((q) => {
|
||||
if (!q.conditional_rules?.length) return false
|
||||
const dependsOnTrigger = q.conditional_rules.some(
|
||||
(r) => r.if_question === triggerCode
|
||||
const dependsOnTrigger = q.conditional_rules.some((r) =>
|
||||
ruleQuestionCodes(r).includes(triggerCode)
|
||||
)
|
||||
if (!dependsOnTrigger) return false
|
||||
return !isQuestionVisible(q, projected)
|
||||
@@ -46,7 +54,7 @@ export function getTriggerCodes(questions: Question[]): Set<string> {
|
||||
const triggers = new Set<string>()
|
||||
for (const q of questions) {
|
||||
for (const rule of q.conditional_rules ?? []) {
|
||||
triggers.add(rule.if_question)
|
||||
for (const code of ruleQuestionCodes(rule)) triggers.add(code)
|
||||
}
|
||||
}
|
||||
return triggers
|
||||
|
||||
@@ -19,8 +19,8 @@ export interface RatingOptions {
|
||||
}
|
||||
|
||||
export interface ConditionalRule {
|
||||
if_question: string
|
||||
op: 'eq'
|
||||
if_question: string | string[]
|
||||
op: 'eq' | 'eq_any'
|
||||
value: string
|
||||
action: 'show'
|
||||
}
|
||||
|
||||
246
method/ANTI_PATTERNS.md
Normal file
246
method/ANTI_PATTERNS.md
Normal file
@@ -0,0 +1,246 @@
|
||||
# Anti-patrones
|
||||
|
||||
> Comportamientos que producen daño concreto y deben evitarse. Cada uno está documentado con el caso real donde apareció.
|
||||
>
|
||||
> A diferencia de los patrones (que son propuestos hasta validación), los anti-patrones se documentan estables porque el daño ya está observado.
|
||||
|
||||
---
|
||||
|
||||
## AP.1 — Tests verdes como aprobación final
|
||||
|
||||
**Comportamiento:** declarar una etapa terminada porque los tests pasaron, sin abrir el artefacto final con los ojos.
|
||||
|
||||
**Daño concreto:** llegás a producción con outputs visuales rotos. Caso real (InQ ROI MVP): los tests E2E verificaban que el PDF se generaba y tenía las páginas esperadas, pero nadie abrió el PDF. El heatmap embebido estaba en blanco por un bug de timing en html2canvas. Se descubrió 2 días después cuando un consultor lo abrió.
|
||||
|
||||
**Cómo detectarlo:** el reporte del agente dice "tests verdes, build limpio, PDF generado en X path" pero no incluye observaciones visuales del PDF.
|
||||
|
||||
**Cómo evitarlo:** aplicar **Principio 2** (validación humana es ojo) y **Patrón C.1** (validación visual mandatoria).
|
||||
|
||||
---
|
||||
|
||||
## AP.2 — Pasamanos invertido (humano como operador)
|
||||
|
||||
**Comportamiento:** el humano traduce manualmente entre dos agentes o entre agente y código. Copia outputs, formatea, transporta contexto entre sesiones.
|
||||
|
||||
**Daño concreto:** el humano se vuelve cuello de botella del sistema. Lo peor: deja de pensar en arquitectura/producto y se concentra en operación. Caso real (InQ ROI Sprint 1.5, Etapa 5): el director del proyecto sintió "soy pasamanos" después de varios checkpoints transportando reportes de Claude Code al arquitecto.
|
||||
|
||||
**Cómo detectarlo:** preguntate si lo que estás haciendo lo podría hacer un script o un prompt mejor estructurado. Si la respuesta es sí, estás en este anti-patrón.
|
||||
|
||||
**Cómo evitarlo:** aplicar **Principio 5** (humano director). Mover lo operativo a archivos (prompts pre-armados, templates) y reservar el chat para decisiones reales.
|
||||
|
||||
---
|
||||
|
||||
## AP.3 — Racionalización de output anómalo
|
||||
|
||||
**Comportamiento:** cuando un output del agente tiene una métrica anómala (PDF inusualmente liviano, número muy redondo, payback "0.0 meses"), el agente o el humano racionalizan ("numéricamente correcto", "es el caso esperado") en lugar de investigar la causa raíz.
|
||||
|
||||
**Daño concreto:** bugs latentes que se descubren en producción. Caso real (InQ ROI Sprint 1.5, Etapa 4): el PDF de ROI pesaba 36 KB con 4 páginas (9 KB/página), comparado con 78 KB/3 páginas (26 KB/página) de los otros PDFs. El agente reportó "PDF generado". El humano paró y pidió investigar — había secciones que no se renderizaban.
|
||||
|
||||
**Cómo detectarlo:** cualquier número que parezca "anómalo pero técnicamente válido". Especialmente: tamaños de archivo, payback inmediato, ROI extremo, métricas con muchos ceros.
|
||||
|
||||
**Cómo evitarlo:** aplicar **Principio 3** (auditoría adversarial) y **Patrón E.3** (anti-racionalización explícita).
|
||||
|
||||
---
|
||||
|
||||
## AP.4 — Decisión recurrente no registrada
|
||||
|
||||
**Comportamiento:** el agente vuelve a preguntar lo mismo en cada sesión nueva, o miembros del equipo asumen distinto porque la decisión no está documentada.
|
||||
|
||||
**Daño concreto:** outputs inconsistentes entre etapas, tiempo perdido re-debatiendo, deriva del producto. Caso real (InQ ROI Sprint 1.5, varias etapas): cada vez que Claude Code iba a aplicar formato de cifras, había que recordarle si usaba separador europeo o americano. Hasta que se documentó en `CLAUDE.md`.
|
||||
|
||||
**Cómo detectarlo:** el agente te hace una pregunta que ya respondiste en alguna sesión anterior.
|
||||
|
||||
**Cómo evitarlo:** aplicar **Principio 4** (decisiones como contratos) y **Patrón B.3** (decisiones tomadas explícitas en cada prompt).
|
||||
|
||||
---
|
||||
|
||||
## AP.5 — Improvisación sin gobernanza
|
||||
|
||||
**Comportamiento:** un equipo (o miembros individuales) usa agentes de IA sin método compartido. Cada uno improvisa con su herramienta preferida, sin briefs, sin validación estructurada, sin documentar decisiones.
|
||||
|
||||
**Daño concreto:**
|
||||
1. Deuda técnica invisible: artefactos producidos por IA que nadie puede auditar después
|
||||
2. Aprendizaje no se acumula: descubrimientos individuales no se transfieren
|
||||
3. Calidad inconsistente y no atribuible
|
||||
4. Imposibilidad de mejorar sistemáticamente
|
||||
|
||||
**Cómo detectarlo:** preguntá al equipo "¿cómo hicieron este artefacto con IA?" — si las respuestas son distintas y vagas, estás acá.
|
||||
|
||||
**Cómo evitarlo:** este método entero está diseñado para evitarlo. Específicamente: empezar con **Principios 1 y 4**.
|
||||
|
||||
---
|
||||
|
||||
## AP.6 — Brief grande pegado en chat
|
||||
|
||||
**Comportamiento:** pegar un brief de 800+ líneas en el chat del agente como mensaje inicial.
|
||||
|
||||
**Daño concreto:**
|
||||
- Consume contexto innecesariamente
|
||||
- No es auditable después
|
||||
- Se pierde si la sesión se corta
|
||||
- Cuesta tokens repetirlo
|
||||
|
||||
Caso real (InQ ROI Sprint 1.5): el primer intento fue pegar el BRIEF.md completo en chat. Se reemplazó por la estructura `sprints/sprint-N/` con archivos versionados, mensaje inicial corto referenciando paths.
|
||||
|
||||
**Cómo detectarlo:** si tu mensaje inicial al agente tiene más de 30 líneas, probablemente estás en este anti-patrón.
|
||||
|
||||
**Cómo evitarlo:** aplicar **Patrón B.1** (prompt como artefacto del repo) y **Patrón B.2** (mensaje inicial corto).
|
||||
|
||||
---
|
||||
|
||||
## AP.7 — Bypass total de permisos en desarrollo local
|
||||
|
||||
**Comportamiento:** usar `--dangerously-skip-permissions` (o equivalente) rutinariamente en máquinas de desarrollo donde corren proyectos reales. El motivo declarado suele ser "evitar la fricción de aprobar cada comando".
|
||||
|
||||
**Daño concreto:**
|
||||
|
||||
1. **Pérdida de auditoría en tiempo real:** cuando un agente ejecuta una acción no intencionada (borrar archivos, modificar git history, instalar dependencias rotas), el humano lo descubre por consecuencia, no por la pausa de aprobación.
|
||||
|
||||
2. **Datos perdidos en repos en evolución:** un `rm -rf` accidental sobre el directorio equivocado puede borrar trabajo no comiteado, configuraciones locales, o assets generados que no están en git.
|
||||
|
||||
3. **Confianza falsa en el agente:** el dev se acostumbra a "todo funciona" y deja de auditar. Cuando aparece un comando raro, el reflejo de pausar ya no existe.
|
||||
|
||||
4. **Imposibilidad de saber cuándo empezó un problema:** sin puntos de control, debuggear "¿cuándo pasó X?" requiere reconstruir desde logs en vez de tener una traza de aprobaciones.
|
||||
|
||||
**Caso real (no en este equipo, observado en otros proyectos):** un desarrollador con bypass total dejó al agente refactorizar carpetas con `mv` masivos. Una iteración movió archivos a una ruta equivocada por un typo del agente en la ruta destino. Sin permisos, los `mv` se ejecutaron en cadena. Recuperar tomó 4 horas porque los archivos no estaban en stash ni en commits.
|
||||
|
||||
**Cómo detectarlo:**
|
||||
- El dev menciona "skipeo todos los permisos para ir más rápido"
|
||||
- No existe `.claude/settings.json` ni equivalente en el repo
|
||||
- El dev no sabe explicar qué hace `--dangerously-skip-permissions`
|
||||
|
||||
**Cómo evitarlo:**
|
||||
- Aplicar **Patrón D.4** (permisos versionados en `.claude/`)
|
||||
- Configurar `allow` para comandos rutinarios (que es lo que daba fricción)
|
||||
- Configurar `deny` para comandos críticos (`rm -rf` con paths absolutos, `git push --force`, `npm publish`, `git reset --hard`)
|
||||
- **Aceptar la fricción ocasional como precio de la gobernanza**
|
||||
|
||||
**Cuándo SÍ es aceptable usar bypass total:**
|
||||
- Sandboxes efímeros (contenedores Docker que se destruyen post-ejecución)
|
||||
- CI/CD donde no hay datos del dev
|
||||
- Máquinas virtuales dedicadas para experimentación
|
||||
|
||||
**Cuándo NO es aceptable:**
|
||||
- Máquina personal del dev con repos reales
|
||||
- Cualquier entorno donde haya datos no replicables
|
||||
- Cualquier entorno con credenciales o secretos
|
||||
|
||||
---
|
||||
|
||||
## Cómo agregar un anti-patrón
|
||||
|
||||
Solo se agregan anti-patrones con daño concreto observado, no especulativo. Estructura obligatoria:
|
||||
|
||||
1. **Nombre breve** (forma de "AP.N — Descripción")
|
||||
2. **Comportamiento:** descripción de la acción problemática
|
||||
3. **Daño concreto:** qué pasa cuando ocurre, con caso real citado
|
||||
4. **Cómo detectarlo:** señal observable
|
||||
5. **Cómo evitarlo:** principio o patrón que lo previene
|
||||
|
||||
No agregar anti-patrones de "esto sería malo si pasara" — solo de cosas que **pasaron en proyectos reales** del equipo.
|
||||
|
||||
---
|
||||
|
||||
## AP.8 — Adelanto silencioso de deuda técnica
|
||||
|
||||
**Comportamiento:** el agente adelanta ítems explícitos de `TECH_DEBT.md`, `BACKLOG.md`, u otros archivos de planificación sin autorización del director, argumentando "mejora de legibilidad", "consistencia con etapa anterior", "resolver bug menor", o similar.
|
||||
|
||||
El cambio puede ser técnicamente correcto y producir un output mejor, pero **desestabiliza la planificación** del sprint y la priorización del director.
|
||||
|
||||
**Daño concreto:**
|
||||
|
||||
1. **Erosión de la planificación:** ítems planificados para sprints futuros se ejecutan en el sprint actual sin que el director lo decida. La capacidad estimada del sprint se desborda silenciosamente.
|
||||
|
||||
2. **Pérdida de control de scope:** el sprint termina con más cambios que los autorizados. La trazabilidad de "qué se hizo y cuándo" se rompe.
|
||||
|
||||
3. **Mensaje al agente:** "los Nivel 3 mal clasificados a veces se aprueban retroactivamente, podés seguir intentando". La política de iniciativa proactiva se debilita por aceptación implícita.
|
||||
|
||||
4. **Decisiones arquitectónicas no tomadas:** el cambio puede tener implicaciones que el agente no anticipó pero el director hubiera detectado. Por ejemplo: cambiar Σ a "SUM" se ve bien aisladamente, pero puede entrar en conflicto con políticas de tipografía financiera o internacionalización.
|
||||
|
||||
**Caso real (InQ ROI Sprint 1.5):**
|
||||
|
||||
- **Etapa 7:** Claude Code reescribió fórmulas matemáticas de la nota metodológica del PDF ROI a ASCII puro (Σ → "SUM", × → "x", − → "-") sin reportar el cambio en el reporte de la etapa. El bug Unicode estaba documentado en `TECH_DEBT.md` con plan "embebido de fuente Unicode en Sprint 2". El cambio era Nivel 3 (afecta contenido del entregable, choca con plan de TECH_DEBT) pero pasó sin reporte.
|
||||
|
||||
- **Etapa 8:** Claude Code extendió el cambio a los PDFs Actual y Automatizado, esta vez clasificándolo como Nivel 1 con el argumento "mismo criterio que ROI Etapa 7". Detectado en el reporte y discutido. El director decidió aceptar retroactivamente la solución porque resultó ser correcta para el dominio (terminología financiera/Excel), pero el patrón quedó documentado como anti-patrón.
|
||||
|
||||
**Cómo detectarlo:**
|
||||
|
||||
- El agente reporta un cambio justificado como "mejora", "consistencia", "resuelve bug", "limpia código" o similar.
|
||||
- Verificá si ese cambio resuelve un ítem listado en `TECH_DEBT.md`, `BACKLOG.md`, u observaciones diferidas. **Si está listado, era Nivel 3 sin importar el tamaño técnico.**
|
||||
- El agente invoca precedente ("se hizo lo mismo en etapa anterior") como justificación. Verificá si el precedente fue autorizado o si también es un adelanto silencioso.
|
||||
|
||||
**Cómo evitarlo:**
|
||||
|
||||
- Aplicar **Patrón C.6 refinado** (ver `methodology/PATTERNS.md`): cualquier cambio que resuelve ítem listado en archivos de planificación es Nivel 3, consulta previa.
|
||||
- En `CLAUDE.md` del proyecto, agregar regla: "Antes de aplicar un cambio fuera del scope del prompt actual, verificar si está listado en TECH_DEBT.md, BACKLOG.md, o equivalentes. Si está listado, consultar antes de proceder."
|
||||
- Auditoría adversarial al reporte (Patrón C.3): cuando el agente reporta cambios "consistentes" o "menores", verificar contra archivos de planificación, no solo contra el prompt de la etapa.
|
||||
|
||||
**Distinción importante:**
|
||||
|
||||
AP.8 NO es lo mismo que AP.3 (Racionalización de output anómalo). AP.3 es minimizar un problema observado. AP.8 es ejecutar trabajo no autorizado argumentando que es mejora.
|
||||
|
||||
AP.8 también difiere del Patrón C.6 mal aplicado. C.6 mal aplicado es clasificar mal el nivel de impacto. AP.8 es específicamente sobre adelantar deuda técnica planificada — un subconjunto que merece nombre propio.
|
||||
|
||||
---
|
||||
|
||||
## AP.9 — Resolución unilateral de restricciones contradictorias
|
||||
|
||||
**Comportamiento:** el agente enfrenta dos requisitos del prompt que colisionan (ej: "no tocar archivo X" + "build limpio sin errores TypeScript") y resuelve la contradicción actuando mínimamente en lugar de detenerse y reportar el conflicto.
|
||||
|
||||
**Daño concreto:** el agente produce código funcionalmente correcto en el área autorizada pero roto en el área restringida, y describe el resultado como "comportamiento esperado" minimizando la severidad real. Caso real (InQ ROI Sprint 2, Etapa 1): el prompt restringía `ActivityPanel.tsx` explícitamente (`❌`), pero también exigía build sin errores TypeScript. Cambiar `types.ts` inevitablemente generaba errores TypeScript en `ActivityPanel.tsx`. El agente lo corrigió mínimamente (clasificó como Nivel 2) en lugar de detenerse. Resultado: slider roto que requirió toda la Etapa 2 para corregir.
|
||||
|
||||
**Cómo detectarlo:** el reporte declara build limpio pero hay un archivo restringido en el diff. O el agente menciona "ajuste mínimo para compatibilidad de tipos" en un archivo que debería estar intacto.
|
||||
|
||||
**Cómo evitarlo:**
|
||||
1. En el prompt: cuando haya restricción `❌` sobre un archivo, agregar explícitamente: *"Si este cambio produce errores TypeScript en archivos restringidos, DETENTE, no los corrijas, reportá el conflicto al director antes de continuar."*
|
||||
2. Control preventivo: `git diff --name-only HEAD` al final — si aparece un archivo restringido, revertir y reportar.
|
||||
3. No incluir simultáneamente restricciones absolutas (`❌ no tocar X`) y requisitos que inevitablemente afectan X (cambios de tipo transitivos). Si el conflicto es inevitable, resolverlo en el prompt antes de dárselo al agente.
|
||||
|
||||
**Relación con otros patrones:**
|
||||
- Requiere el control preventivo de **C.7** (git diff como control de scope) para detección temprana.
|
||||
- Distinto de **AP.5** (improvisación sin gobernanza): AP.9 ocurre dentro de un prompt estructurado, no por ausencia de método.
|
||||
|
||||
---
|
||||
|
||||
## AP.10 — Propagación silenciosa de inconsistencia entre fuentes
|
||||
**Comportamiento:** un prompt contiene una referencia (código, ruta, esquema) que el agente
|
||||
podría verificar contra otra fuente del repo. En lugar de hacerlo, el agente ejecuta
|
||||
literalmente y traslada la responsabilidad al director con frases como "la validación
|
||||
semántica la hace el director" o "los textos son los solicitados literalmente".
|
||||
**Daño:** las inconsistencias se propagan a producción con apariencia de precisión técnica.
|
||||
*Caso real:* en Etapa 2D del proyecto Datos País, 11 de 12 subtítulos del dashboard
|
||||
quedaron con códigos de pregunta discrepantes del seed real. El dashboard mostraba
|
||||
métricas mal etiquetadas: el KPI "Ausencia promedio · 3.1 · días por mes" referenciaba
|
||||
una pregunta que en el seed real era *gasto en cuidado*, no ausencias. Detectado en la
|
||||
auditoría adversarial posterior, no antes. Si hubieran cargado datos reales, RRHH habría
|
||||
visto guaraníes presentados como días.
|
||||
**Detectar:** el reporte del agente declara "los textos son los solicitados literalmente";
|
||||
hay códigos, rutas o nombres que el agente pudo verificar contra otra fuente y no lo hizo;
|
||||
el agente delega la verificación semántica al humano sin haber verificado lo verificable.
|
||||
**Evitar:** Patrón C.8 + cláusula explícita en el prompt: *"si detectás una discrepancia
|
||||
entre el prompt y la fuente única, DETENÉ la ejecución y reportá antes de avanzar"*.
|
||||
**Distinción:** ≠ AP.3 — AP.3 minimiza un problema observado; AP.10 evita verificar lo
|
||||
verificable. ≠ AP.9 — AP.9 resuelve restricciones contradictorias del propio prompt;
|
||||
AP.10 propaga inconsistencias entre el prompt y otra fuente.
|
||||
|
||||
## AP.11 — Contrato desactualizado por código
|
||||
**Comportamiento:** una etapa modifica el comportamiento real del sistema (seed, esquema,
|
||||
RLS, RPCs) pero no actualiza el documento que sirve como contrato o fuente única
|
||||
(`SEED_SPEC.md`, `ARCHITECTURE.md`, ADRs). El documento queda mintiendo por silencio.
|
||||
**Daño:** el próximo agente o autor de prompts que lo lea va a tomar decisiones basadas
|
||||
en información falsa. *Caso real:* en Etapa 1D del proyecto Datos País se modificó
|
||||
`seed.sql` para que A1/A2/A3 fueran widgets geo (Departamento/Ciudad/Barrio en lugar de
|
||||
texto libre País/Ciudad/Sucursal), pero no se actualizó `SEED_SPEC.md`. Dos sprints
|
||||
después, Etapa 2E se escribió tomando SEED_SPEC como verdad y proponiendo agregar una
|
||||
pregunta A1b que duplicaba la A1 ya existente. El conflicto se detectó antes de ejecutar
|
||||
porque el agente aplicó el Patrón C.8; sin C.8, se habría creado una pregunta duplicada
|
||||
en producción.
|
||||
**Detectar:** cuando una etapa cambia comportamiento del sistema, preguntar explícitamente
|
||||
"¿qué documento de contrato describe esto, y lo actualicé?". Si la respuesta es "no sé"
|
||||
o "después lo veo", AP.11 latente. También: cuando una etapa posterior asume contenido
|
||||
del contrato que ya no coincide con la realidad.
|
||||
**Evitar:** todo prompt que modifique un sistema documentado en un contrato debe incluir,
|
||||
como parte del DoD, "actualizar el documento de contrato correspondiente con los cambios".
|
||||
Es Principio 4 aplicado al mantenimiento, no solo a la creación.
|
||||
**Distinción:** ≠ AP.4 — AP.4 es decisión no registrada; AP.11 es decisión registrada en
|
||||
código pero NO actualizada en el contrato escrito.
|
||||
513
method/PATTERNS.md
Normal file
513
method/PATTERNS.md
Normal file
@@ -0,0 +1,513 @@
|
||||
# Patrones operativos
|
||||
|
||||
> Aplicaciones concretas de los 5 principios. Estructurados por categoría operativa.
|
||||
>
|
||||
> Estado de cada patrón: **propuesto** (validado en 1 proyecto) o **estable** (validado en 2+ proyectos).
|
||||
>
|
||||
> A la fecha, todos los patrones aquí son propuestos. Caso de validación: InQ ROI Sprint 1.5.
|
||||
|
||||
---
|
||||
|
||||
## Categoría A — Disciplina antes de codear
|
||||
|
||||
### A.1 Brief versionado antes que código
|
||||
**Estado:** propuesto · **Deriva de:** Principio 1
|
||||
|
||||
Todo sprint o cambio mayor arranca con un brief en `sprints/sprint-N/BRIEF.md` antes de pedirle algo a un agente. El brief define alcance, decisiones tomadas, criterios de éxito y lo que NO entra.
|
||||
|
||||
**Indicador de aplicación correcta:** podés iniciar una sesión nueva con Claude Code pasándole solo el path del brief y CLAUDE.md, sin contexto adicional, y empieza a trabajar correctamente.
|
||||
|
||||
**Indicador de violación:** el agente te hace preguntas que ya están respondidas en otro lugar (o peor, no las hace y asume).
|
||||
|
||||
---
|
||||
|
||||
### A.2 Mockup como contrato visual
|
||||
**Estado:** propuesto · **Deriva de:** Principio 1
|
||||
|
||||
Si el cambio es visual, antes de implementarlo se construye un mockup interactivo (HTML, Figma, Visualizer) que el humano aprueba. El mockup es el contrato: el agente lo replica, no lo interpreta libremente.
|
||||
|
||||
**Indicador de aplicación correcta:** cuando ves el output del agente, podés compararlo lado a lado con el mockup y la diferencia es marginal.
|
||||
|
||||
**Indicador de violación:** llegás a Etapa N de un rediseño visual y descubrís que el output "no tiene nada que ver" con lo que esperabas.
|
||||
|
||||
---
|
||||
|
||||
### A.3 Roadmap explícito antes de arrancar
|
||||
**Estado:** propuesto · **Deriva de:** Principios 1 y 4
|
||||
|
||||
Todo sprint con más de 3 etapas tiene un roadmap explícito desde el inicio. Cada etapa tiene **expectativa calibrada** de qué cambia y qué NO cambia respecto a la anterior.
|
||||
|
||||
**Indicador de aplicación correcta:** después de cada checkpoint, el resultado coincide con lo que esperabas ver, no te sorprende ni para bien ni para mal.
|
||||
|
||||
**Indicador de violación:** te sorprende negativamente un output intermedio porque pensabas que ya tenía X feature que en realidad está agendado 3 etapas después.
|
||||
|
||||
---
|
||||
|
||||
## Categoría B — Operación con agentes
|
||||
|
||||
### B.1 Prompt como artefacto del repo
|
||||
**Estado:** propuesto · **Deriva de:** Principios 4 y 5
|
||||
|
||||
Cada etapa de trabajo agéntico tiene su prompt en `sprints/sprint-N/ETAPA_X_PROMPT.md`, versionado. El chat con el agente es **puente** entre el prompt y el reporte, no contenedor de la decisión.
|
||||
|
||||
**Por qué:** los prompts en archivo son auditables, editables antes de ejecutar, replicables, y no se pierden al cerrar la sesión.
|
||||
|
||||
**Estructura mínima de un prompt de etapa:**
|
||||
1. Objetivo
|
||||
2. Pre-requisitos / contexto previo
|
||||
3. Decisiones ya tomadas (no consultar de nuevo)
|
||||
4. Tareas concretas
|
||||
5. Entregables (DoD checklist)
|
||||
6. Qué reportar al final
|
||||
7. Qué NO hacer en esta etapa
|
||||
|
||||
---
|
||||
|
||||
### B.2 Mensaje inicial corto, contexto en archivos
|
||||
**Estado:** propuesto · **Deriva de:** Principio 5
|
||||
|
||||
El mensaje al agente al arrancar una sesión es de 3-5 párrafos, NO el prompt entero pegado. Estructura:
|
||||
1. "Procedé con etapa X"
|
||||
2. Archivos a leer en orden
|
||||
3. Confirmaciones que esperás antes de que empiece
|
||||
4. Restricción de alcance ("no avances a Y sin OK")
|
||||
|
||||
**Por qué:** el contexto en archivos sobrevive a la sesión, queda versionado, y el agente lo consulta cuando lo necesita en vez de tenerlo permanentemente en ventana.
|
||||
|
||||
---
|
||||
|
||||
### B.3 Decisiones tomadas como sección explícita
|
||||
**Estado:** propuesto · **Deriva de:** Principio 4
|
||||
|
||||
En cada prompt de etapa, incluir sección "DECISIONES YA TOMADAS — no consultar de nuevo" con las definiciones cerradas. Esto impide que el agente vuelva a preguntar lo ya resuelto.
|
||||
|
||||
**Ejemplo:**
|
||||
```
|
||||
DECISIONES YA TOMADAS (no consultar de nuevo)
|
||||
- Payback negativo → "Sin payback (costo aumenta)"
|
||||
- Caracteres compactos: "<" y "~"
|
||||
- Pluralización: siempre plural ("1.0 meses")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### B.4 Reglas no negociables en CLAUDE.md
|
||||
**Estado:** propuesto · **Deriva de:** Principio 4
|
||||
|
||||
Las reglas que aplican a TODO el proyecto van en `CLAUDE.md`, no solo en briefs específicos. El agente carga CLAUDE.md en cada sesión nueva por defecto.
|
||||
|
||||
**Ejemplo de qué va ahí:**
|
||||
- Reglas de marca (cómo se escribe el nombre, qué colores)
|
||||
- Restricciones de comportamiento (qué no decir, qué no asumir)
|
||||
- Verificaciones obligatorias pre-entrega
|
||||
|
||||
---
|
||||
|
||||
## Categoría C — Validación de output
|
||||
|
||||
### C.1 Validación visual mandatoria del artefacto final
|
||||
**Estado:** propuesto · **Deriva de:** Principio 2
|
||||
|
||||
Si el output de la etapa es un artefacto visible (PDF, screenshot, página renderizada, documento), el humano lo abre con sus ojos antes del OK. Tests verdes no es suficiente.
|
||||
|
||||
**Tooling sugerido:**
|
||||
- PDFs/imágenes: Preview en Mac, visor PDF en Linux
|
||||
- Páginas web: dev server en navegador
|
||||
- Documentos: editor que renderice (no solo source)
|
||||
|
||||
**Cuándo evolucionar este patrón:** ver methodology/AUTOMATION_STRATEGY.md.
|
||||
La validación visual humana es Fase 1. Puede complementarse con agentes
|
||||
auditores (Fase 3) pero NO se reemplaza completamente hasta que el
|
||||
equipo desarrolle criterio adversarial maduro.
|
||||
|
||||
*Cuando el output incluye textos que referencian otras fuentes del repo (códigos, IDs, rutas), aplicar también C.8.*
|
||||
|
||||
---
|
||||
|
||||
### C.2 Validación programática antes de declarar completado (agente)
|
||||
**Estado:** propuesto · **Deriva de:** Principios 2 y 3
|
||||
|
||||
Antes de declarar una etapa completada, el agente debe ejecutar validaciones programáticas, no solo confiar en tests unitarios. Para outputs visuales:
|
||||
- Análisis de píxeles (¿están los colores esperados?)
|
||||
- Text extraction (¿están las palabras clave?)
|
||||
- Tamaño de archivo (¿es razonable?)
|
||||
- Cantidad de páginas (¿coincide con el diseño?)
|
||||
|
||||
**Por qué:** un test unitario puede pasar mientras el output final está roto. La validación programática del artefacto cierra ese gap.
|
||||
|
||||
---
|
||||
|
||||
### C.3 Auditoría adversarial al reporte
|
||||
**Estado:** propuesto · **Deriva de:** Principio 3
|
||||
|
||||
Cuando llega un reporte de agente "todo OK", aplicar 3 preguntas:
|
||||
1. ¿Los números reportados cuadran si recalculo o pido dump?
|
||||
2. ¿El artefacto es el que pedí?
|
||||
3. ¿Lo que no menciona podría ser relevante?
|
||||
|
||||
**Casos típicos de detección:**
|
||||
- PDF de 36 KB con 4 páginas (es muy poco → algo no se renderizó)
|
||||
- KPI con valor "0.0 meses" (numéricamente correcto pero confuso)
|
||||
- Reporte que dice "PDF generado" sin abrir el PDF generado
|
||||
|
||||
---
|
||||
|
||||
### C.4 Reporte con datos concretos, no descripciones (agente)
|
||||
**Estado:** propuesto · **Deriva de:** Principio 3
|
||||
|
||||
Los reportes del agente deben incluir métricas concretas, no descripciones genéricas.
|
||||
|
||||
**Mal reporte:** "PDF generado correctamente, contiene todo lo esperado."
|
||||
|
||||
**Buen reporte:**
|
||||
```
|
||||
PDF generado: 78 KB, 4 páginas
|
||||
- Página 1: KPI Hero + 4 cards secundarios + top 3 ahorros
|
||||
- Página 2: tabla con 11 filas + footer
|
||||
- Página 3: 3 secciones (composición, trayectoria, transparencia)
|
||||
- Página 4: nota metodológica
|
||||
Análisis de píxeles del heatmap: verde 36.329px, ámbar 3.889px, rojo 3.713px
|
||||
Text extraction confirma palabras clave: "InQ ROI", "Powered by InQuality", "Payback"
|
||||
```
|
||||
|
||||
### C.5 Validaciones programáticas explícitas en el prompt
|
||||
**Estado:** propuesto · **Deriva de:** Principios 2 y 5
|
||||
|
||||
Cada prompt de etapa incluye una sección **"Validaciones automatizadas mandatorias"** con criterios objetivos verificables por código que el agente debe ejecutar antes de declarar terminada la etapa o usar `present_files`.
|
||||
|
||||
**Por qué:** documenta explícitamente qué se va a auto-validar, hace transferible el conocimiento operativo al equipo, captura el aprendizaje sobre qué problemas son recurrentes y vale la pena chequear automáticamente.
|
||||
|
||||
**Cómo se aplica:**
|
||||
|
||||
```markdown
|
||||
## Validaciones automatizadas mandatorias
|
||||
|
||||
Antes de present_files, ejecutá y confirmá:
|
||||
|
||||
PDFs:
|
||||
- pdf-parse confirma palabras clave esperadas: [lista]
|
||||
- pdf-parse rechaza términos prohibidos: [lista]
|
||||
- Cada página entre [N-M] KB
|
||||
- Cantidad de páginas == [N exacto]
|
||||
|
||||
Tests:
|
||||
- Tests previos verdes (>= [N])
|
||||
- Tests nuevos verdes
|
||||
|
||||
Build:
|
||||
- npm run build limpio
|
||||
- Sin warnings TS ni lint nuevos
|
||||
```
|
||||
|
||||
**Indicador de aplicación correcta:** los problemas que ya cazaste manualmente en sprints anteriores aparecen como validación automatizada en sprints siguientes. Aprendizaje acumulativo.
|
||||
|
||||
**Indicador de violación:** el agente declara etapa terminada y vos cazás el mismo tipo de problema que ya cazaste 3 etapas atrás. Significa que el aprendizaje no se está capturando como validación.
|
||||
|
||||
**Relación con otros patrones:**
|
||||
- Extiende **C.2** (validación programática) haciéndola explícita en el prompt
|
||||
- No reemplaza **C.1** (validación visual humana) — son complementarias
|
||||
- Es Fase 2 del roadmap de `AUTOMATION_STRATEGY.md`
|
||||
|
||||
### C.6 Iniciativa agéntica con consulta clasificada por impacto
|
||||
**Estado:** propuesto · **Deriva de:** Principios 3, 4 y 5
|
||||
|
||||
Cuando el agente detecta una oportunidad de mejora **fuera del scope del prompt**, debe clasificar el impacto del cambio antes de proceder y actuar según la política definida en `PRINCIPLES.md` sección "Política de iniciativa agéntica".
|
||||
|
||||
**Niveles de impacto:**
|
||||
|
||||
1. **Trivial** (cambio interno, no visible al usuario, reversible sin costo)
|
||||
→ ejecutar y reportar al final como "decisión menor tomada"
|
||||
|
||||
2. **Visible reversible** (cambio visible al usuario pero sin afectar marca, datos, ni arquitectura)
|
||||
→ ejecutar, **mencionar explícitamente en el reporte final** como "decisión proactiva, validar/revertir"
|
||||
|
||||
3. **De marca, arquitectura o datos** (cambio que afecta identidad, estructura, o información del usuario)
|
||||
→ **NO ejecutar**. Consultar antes de proceder.
|
||||
|
||||
**Por qué:** los agentes producen mejor cuando tienen libertad para optimizar lo local. Pero esa libertad sin límites lleva a cambios silenciosos que el humano descubre por casualidad. Una clasificación explícita preserva la velocidad agéntica sin perder gobernanza.
|
||||
|
||||
**Caso real:** InQ ROI Sprint 1.5 Etapa 3. Claude Code agregó "Hecho desde Paraguay 🇵🇾" al footer del web app sin estar en el brief. El cambio era razonable, alineado con identidad de marca, pero **el humano lo descubrió revisando screenshots**, no porque se reportara. Decisión retroactiva: mantener, pero documentar el patrón para que en el futuro este tipo de cambios (impacto nivel 3, afecta marca) sean consulta previa, no decisión unilateral.
|
||||
|
||||
**Indicador de aplicación correcta:**
|
||||
- El reporte final del agente tiene una sección explícita "Decisiones tomadas por iniciativa" listando cambios fuera del scope
|
||||
- Los cambios nivel 3 no se ejecutan sin consulta previa documentada
|
||||
- El humano puede aprobar o revertir cambios nivel 2 sin tener que reconstruir contexto
|
||||
|
||||
**Indicador de violación:**
|
||||
- Descubrir cambios en el output que no están ni en el prompt ni en el reporte
|
||||
- Cambios de marca, arquitectura o datos ejecutados sin consulta previa
|
||||
- "Pequeñas mejoras de UX" agregadas en silencio que cambian el comportamiento del producto
|
||||
|
||||
**Relación con otros patrones:**
|
||||
- Complementa **E.2** (Hallazgos no esperados) — pero E.2 es para descubrimientos pasivos (cosas que el agente notó), C.6 es para decisiones activas (cosas que el agente hizo).
|
||||
- Refuerza **C.3** (Auditoría adversarial) — provee al humano una sección concreta donde buscar cambios silenciosos.
|
||||
|
||||
## Refinamiento — criterios explícitos de clasificación (aprendizajes de Sprint 1.5)
|
||||
|
||||
Durante Sprint 1.5 InQ ROI se detectaron dos casos de mala clasificación de impacto. A partir de esos casos, se refinaron los criterios para distinguir entre Niveles 1, 2 y 3:
|
||||
|
||||
### Reglas de clasificación (en orden de evaluación)
|
||||
|
||||
Evaluar en este orden estricto. El primer criterio que aplique determina el nivel — no seguir evaluando los siguientes.
|
||||
|
||||
**1. ¿El cambio resuelve un ítem listado en `TECH_DEBT.md`, `BACKLOG.md`, o archivos de planificación?**
|
||||
→ Nivel 3 (consulta previa). Sin importar el tamaño técnico.
|
||||
|
||||
**Justificación:** ítems en archivos de planificación son decisiones del director sobre cuándo trabajarlos. El agente no puede promover ítems al sprint en curso por iniciativa.
|
||||
|
||||
**2. ¿El cambio choca con una restricción explícita del prompt de la etapa?**
|
||||
→ Nivel 3 (consulta previa). Sin excepciones.
|
||||
|
||||
Ejemplo de restricciones explícitas:
|
||||
- "NO entra: X" en sección de alcance
|
||||
- "NO tocar Y" en reglas operativas
|
||||
- "X está agendado para Etapa Z" en decisiones tomadas
|
||||
|
||||
**3. ¿El cambio modifica paginación, layout, cantidad de archivos generados, o estructura de salida?**
|
||||
→ Nivel 2 mínimo. Reportar.
|
||||
|
||||
**4. ¿El cambio es visible al usuario final (texto en interfaz, color, posición de elemento, contenido de PDF)?**
|
||||
→ Nivel 2 mínimo. Reportar.
|
||||
|
||||
**5. ¿El cambio afecta marca, identidad, dependencias, o configuración del producto?**
|
||||
→ Nivel 3 (consulta previa).
|
||||
|
||||
**6. Si nada de lo anterior aplica → Nivel 1**
|
||||
|
||||
Ejemplos válidos de Nivel 1:
|
||||
- Renombrar variable interna por claridad
|
||||
- Reordenar imports
|
||||
- Mejorar comentario de código
|
||||
- Extraer función helper usada en múltiples lugares
|
||||
- Eliminar código muerto evidente (sin riesgo de uso futuro)
|
||||
- Limpiar artefactos accidentales del refactor (variables `void` no usadas)
|
||||
|
||||
### Argumentos que NO son justificación válida para Nivel 1
|
||||
|
||||
El agente puede invocar argumentos que parecen razonables pero violan los criterios de arriba:
|
||||
|
||||
- **"Es mejora de legibilidad"** → si afecta contenido visible, es Nivel 2 mínimo
|
||||
- **"Es consistencia con etapa anterior"** → verificar si la etapa anterior fue autorizada o si también es adelanto silencioso (ver AP.8)
|
||||
- **"Es trivial de revertir"** → la reversibilidad NO es criterio de clasificación. Cambios reversibles también pueden ser Nivel 3.
|
||||
- **"Resuelve un bug menor"** → si el bug está documentado en TECH_DEBT.md, es Nivel 3
|
||||
- **"Es lo que el usuario esperaría"** → eso es asumir. La política existe para no asumir.
|
||||
|
||||
### Casos reales documentados (Sprint 1.5 InQ ROI)
|
||||
|
||||
**Caso 1 (Etapa 5):** compactación de páginas reportada como Nivel 1.
|
||||
- Realidad: Nivel 2 mínimo (modificó paginación visible)
|
||||
- Resultado: nota metodológica truncada, descubierto en validación visual
|
||||
- Lección: paginación es estructura de salida, criterio 3 aplica
|
||||
|
||||
**Caso 2 (Etapa 7-8):** reescritura de fórmulas Σ → "SUM" reportada inicialmente sin clasificación, después como Nivel 1.
|
||||
- Realidad: Nivel 3 (resolvía ítem de TECH_DEBT.md sobre Unicode roto)
|
||||
- Resultado: aceptado retroactivamente porque resultó ser solución correcta para el dominio, pero el patrón quedó como anti-patrón AP.8
|
||||
- Lección: ítem en TECH_DEBT.md activa criterio 1 directo, sin importar el tamaño
|
||||
|
||||
### Implicancia para CLAUDE.md del proyecto
|
||||
|
||||
En el `CLAUDE.md` de cada proyecto, agregar a la sección "Política de iniciativa proactiva":
|
||||
|
||||
```
|
||||
Antes de aplicar un cambio fuera del scope explícito del prompt actual:
|
||||
|
||||
1. Verificá si el cambio está listado en TECH_DEBT.md, BACKLOG.md, o
|
||||
archivos de planificación equivalentes. Si está, es Nivel 3 — consultá.
|
||||
|
||||
2. Verificá si el cambio choca con una restricción explícita del prompt
|
||||
("NO entra", "NO tocar", "está agendado para Etapa X"). Si choca,
|
||||
es Nivel 3 — consultá.
|
||||
|
||||
3. Verificá si el cambio modifica paginación, layout, estructura de
|
||||
salida, contenido visible. Si lo hace, es Nivel 2 mínimo — reportá.
|
||||
|
||||
4. Solo si nada de lo anterior aplica, ejecutá como Nivel 1.
|
||||
|
||||
Argumentos NO válidos para evitar la consulta:
|
||||
- "Es mejora de legibilidad"
|
||||
- "Es consistencia con etapa anterior" (verificar si esa etapa fue
|
||||
autorizada o también fue adelanto silencioso)
|
||||
- "Es trivial de revertir"
|
||||
- "Resuelve un bug menor"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Categoría D — Estructura del repo
|
||||
|
||||
### D.1 Carpeta `sprints/` para activos de planificación
|
||||
**Estado:** propuesto · **Deriva de:** Principio 4
|
||||
|
||||
Cada sprint tiene su carpeta `sprints/sprint-N/` con todos sus artefactos:
|
||||
- `BRIEF.md`
|
||||
- `ETAPA_X_PROMPT.md` (uno por etapa)
|
||||
- `ETAPA_X_REPORT.md` (opcional, reporte del agente capturado)
|
||||
- `etapa-X-audit.md` (cuando aplica)
|
||||
- `screenshots/` con evidencia visual
|
||||
|
||||
**Por qué:** en 6 meses, abrir esa carpeta te da el historial completo de cómo se construyó el sprint. Sin esto, el conocimiento queda en commits + memoria, que es frágil.
|
||||
|
||||
---
|
||||
|
||||
### D.2 Carpeta `methodology/` para el método en sí
|
||||
**Estado:** propuesto · **Deriva de:** Principio 4
|
||||
|
||||
El método de trabajo vive en `methodology/`, separado del producto. Esto permite que el método evolucione independiente de cualquier producto específico.
|
||||
|
||||
---
|
||||
|
||||
### D.3 ADRs para decisiones arquitectónicas
|
||||
**Estado:** propuesto · **Deriva de:** Principio 4
|
||||
|
||||
Las decisiones arquitectónicas importantes se registran como ADR (Architectural Decision Record) en `ARCHITECTURE.md` o `docs/adr/`. Estructura mínima:
|
||||
- Contexto (qué problema teníamos)
|
||||
- Decisión (qué elegimos)
|
||||
- Alternativas consideradas
|
||||
- Consecuencias (qué implica)
|
||||
|
||||
---
|
||||
|
||||
### D.4 Permisos del agente versionados en `.claude/`
|
||||
**Estado:** propuesto · **Deriva de:** Principios 4 y 5
|
||||
|
||||
Las decisiones sobre qué comandos puede ejecutar el agente sin consultar al humano se versionan en archivos de configuración del repo. Esto evita dos extremos: la fricción de aprobar cada comando manualmente, y el bypass total que abre la puerta a acciones no intencionadas.
|
||||
|
||||
**Estructura recomendada:**
|
||||
|
||||
```
|
||||
repo/
|
||||
└── .claude/
|
||||
├── settings.json ← commitable: deny patterns críticos
|
||||
└── settings.local.json ← gitignored: allow personales del dev
|
||||
```
|
||||
|
||||
**Reglas:**
|
||||
|
||||
1. **`settings.json` es del proyecto** — contiene patrones `deny` (comandos peligrosos prohibidos para todos: `rm -rf /`, `git push --force`, `npm publish`, etc.). Se comitea al repo.
|
||||
|
||||
2. **`settings.local.json` es del desarrollador** — contiene patrones `allow` personales (cada uno decide qué comandos pre-aprueba). Va en `.gitignore`. Cuando un dev nuevo arranca, **debe construir su propio** archivo siguiendo el template, no copiar el de otro.
|
||||
|
||||
3. **El equipo hereda las protecciones, no las preferencias** — los `deny` son compartidos, los `allow` son individuales.
|
||||
|
||||
**Por qué:**
|
||||
- La fricción de aprobar cada `npm install` destruye el flow de trabajo
|
||||
- Pero el bypass total elimina los puntos de control donde se detectan acciones no intencionadas
|
||||
- La gobernanza está en clasificar comandos por riesgo, no en aprobar todo o aprobar nada
|
||||
|
||||
**Indicador de aplicación correcta:**
|
||||
- En cada sesión de Claude Code, los comandos rutinarios pasan sin pedir aprobación
|
||||
- Los comandos no rutinarios siguen pidiendo aprobación (es bueno — ahí prestás atención)
|
||||
- Si un comando peligroso aparece, está bloqueado por `deny` o pide aprobación explícita
|
||||
|
||||
**Indicador de violación:**
|
||||
- El dev usa `--dangerously-skip-permissions` rutinariamente
|
||||
- El archivo `settings.json` contiene `allow` que deberían ser personales (paga la complejidad de mantenerlo sincronizado entre devs)
|
||||
- No hay archivo de configuración y cada dev aprueba todo manualmente cada vez (fricción crónica, señal de que falta este patrón)
|
||||
|
||||
**Template:** ver `methodology/TEMPLATES/CLAUDE_PERMISSIONS_TEMPLATE.md`.
|
||||
|
||||
**Relación con otros patrones:**
|
||||
- Complementa **B.4** (reglas no negociables en CLAUDE.md) — pero B.4 es sobre comportamiento del agente, D.4 es sobre permisos del sistema operativo
|
||||
- Refuerza **Principio 5** (humano director) — sin permisos configurados, el dev se vuelve operador de aprobación
|
||||
|
||||
**Casos reales que valida:**
|
||||
- InQ ROI: el director identificó fatiga de aprobación después de ~30 etapas. Configurar permisos eliminó la fricción sin perder gobernanza.
|
||||
|
||||
## Categoría E — Comunicación humano ↔ agente
|
||||
|
||||
### E.1 Separar lo validado de lo asumido
|
||||
**Estado:** propuesto · **Deriva de:** Principios 2 y 3
|
||||
|
||||
En los reportes del agente, marcar explícitamente qué fue verificado vs qué se asume funcionando.
|
||||
|
||||
**Ejemplo:**
|
||||
```
|
||||
Verificado:
|
||||
- Tests verdes (455/455)
|
||||
- PDF generado y abierto con pdf-parse
|
||||
- Análisis de píxeles confirma heatmap completo
|
||||
|
||||
Asumido:
|
||||
- El componente se renderiza igual en Firefox (solo testeado en Chromium)
|
||||
- La fuente Roboto está disponible en el sistema cliente (no validado)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### E.2 Hallazgos no esperados como sección explícita
|
||||
**Estado:** propuesto · **Deriva de:** Principios 3 y 5
|
||||
|
||||
Al final de cada etapa, el agente reporta "hallazgos no esperados" — cosas que descubrió durante la implementación que merecen decisión arquitectónica, aunque no formen parte del DoD.
|
||||
|
||||
**Ejemplo:** "El cambio de `--primary` propagó a focus rings de inputs (no estaba en el plan). Decisión necesaria: ¿se mantiene o se vuelve a slate?"
|
||||
|
||||
---
|
||||
|
||||
### E.3 Anti-racionalización explícita
|
||||
**Estado:** propuesto · **Deriva de:** Principio 3
|
||||
|
||||
Cuando un test revela algo raro, un cálculo no cuadra, o un output difiere de lo esperado: **investigar causa raíz, no minimizar**.
|
||||
|
||||
Patrón típico de violación: el agente dice "el cálculo es numéricamente correcto" cuando lo que está reportando es un problema de presentación (ej. "0.0 meses" es matemáticamente correcto pero un bug de UX).
|
||||
|
||||
---
|
||||
|
||||
### C.7 Control preventivo de scope — `git diff` antes de reportar
|
||||
**Estado:** propuesto · **Deriva de:** Principios 3 y 4 · **Origen:** Sprint 2 Etapa 1
|
||||
|
||||
Incluir en todo prompt de Claude Code la siguiente verificación como paso obligatorio antes de reportar:
|
||||
|
||||
```bash
|
||||
git diff --name-only HEAD
|
||||
```
|
||||
|
||||
El resultado debe contener únicamente los archivos listados como autorizados en el prompt. Si aparece un archivo no autorizado:
|
||||
1. Revertirlo: `git checkout HEAD -- <archivo>`
|
||||
2. Listarlo bajo "Conflictos de scope" en el reporte
|
||||
3. No incluirlo en el entregable de la etapa
|
||||
|
||||
**Por qué es preventivo, no detectivo:** los controles detectivos (grep de tipos, build, tests) verifican que el código resultante funciona, pero no detectan archivos modificados fuera del scope. Un agente puede tocar `ActivityPanel.tsx` y los tests seguir verdes si el cambio no rompe nada. El `git diff` captura el cambio antes de que se declare la etapa terminada.
|
||||
|
||||
**Indicador de aplicación correcta:** el reporte del agente incluye explícitamente la salida de `git diff --name-only HEAD` y todos los archivos listados son los autorizados.
|
||||
|
||||
**Indicador de violación:** el reporte no incluye la salida del git diff, o el diff contiene archivos no listados sin explicación.
|
||||
|
||||
**Relación con otros patrones:**
|
||||
- Es respuesta directa al daño documentado en **AP.9** (resolución unilateral de restricciones contradictorias).
|
||||
- Complementa **C.3** (auditoría adversarial) dándole al humano una checklist concreta de qué verificar.
|
||||
|
||||
---
|
||||
|
||||
### C.8 — Verificación de códigos contra fuente única · *deriva de P3, P4 · origen: 2do proyecto*
|
||||
Cuando un prompt menciona códigos de un instrumento, esquema o contrato (preguntas, columnas,
|
||||
campos), el agente verifica cada código contra la fuente única (`SEED_SPEC.md`, `schema.sql`,
|
||||
ADRs) **antes** de ejecutar. Si detecta una discrepancia entre el código nombrado en el
|
||||
prompt y su definición en la fuente única, **DETIENE la ejecución y reporta el conflicto**.
|
||||
Ejecutar "literalmente" y delegar la verificación semántica al director es AP.10.
|
||||
- ✅ **Correcto:** el reporte del agente incluye una sección "Verificación C.8" con cada
|
||||
código del prompt mapeado a su fila en la fuente única, antes de la ejecución. Si hay
|
||||
discrepancia, la ejecución no arranca hasta que el director resuelva.
|
||||
- ❌ **Violación:** el agente ejecuta el prompt al pie de la letra y reporta "los textos son
|
||||
los solicitados literalmente; la validación semántica la hace el director" — exporta el
|
||||
trabajo de verificar al humano y propaga la inconsistencia.
|
||||
|
||||
**Cómo se incorpora a un prompt:** agregar un paso "Verificación C.8" como primera tarea
|
||||
del DoD, listando los códigos a verificar y la ruta exacta de la fuente única.
|
||||
**Casos reales en Datos País:** Etapa 2D (no se aplicó — daño AP.10), Etapa 2E (se aplicó —
|
||||
detectó conflicto A1/A1b antes de ejecutar), Etapa 2F (se aplicó — sin discrepancias).
|
||||
|
||||
---
|
||||
|
||||
## Cómo agregar un patrón nuevo
|
||||
|
||||
1. Identificar el patrón durante un proyecto real (no inventarlo en abstracto).
|
||||
2. Documentarlo con: nombre, categoría, principio del que deriva, descripción, indicador de aplicación correcta, indicador de violación.
|
||||
3. Marcarlo como **propuesto** con cita del proyecto donde apareció.
|
||||
4. Después de aplicarlo en un 2do proyecto distinto, promoverlo a **estable**.
|
||||
|
||||
## Cómo retirar un patrón
|
||||
|
||||
Si un patrón no se sostiene en práctica o tiene falsos positivos frecuentes, se retira con un PR que documente:
|
||||
- Por qué no funcionó
|
||||
- Qué alternativa probamos
|
||||
- Si hay un patrón nuevo que lo reemplaza
|
||||
178
migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md
Normal file
178
migrate-claude/ESTADO_PROYECTO_JUNIO_2026.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# ESTADO DEL PROYECTO — Datos País sobre el Cuidado
|
||||
## Documento de continuidad — junio 2026
|
||||
|
||||
> Generado al cierre de una sesión extendida de dirección técnica.
|
||||
> Propósito: minimizar pérdida de contexto al abrir una nueva conversación.
|
||||
> Director: Marcos (Roy) · Metodología: Maria Mersan · Contacto Fundación: Valeria Arce
|
||||
|
||||
---
|
||||
|
||||
## 1. Qué es el proyecto
|
||||
|
||||
Plataforma soberana de encuestas sobre economía del cuidado en Paraguay.
|
||||
- **Producto:** formulario anónimo + dashboard analítico con k-anonimato.
|
||||
- **Marca:** RE (Responsabilidad y Empatía Empresarial) · Fundación Solidaridad · InQuality.
|
||||
- **URL producción:** https://economia-cuidado.inqualityhq.com
|
||||
- **Survey demo:** https://economia-cuidado.inqualityhq.com/?s=30000000-0000-0000-0000-000000000001
|
||||
|
||||
---
|
||||
|
||||
## 2. Infraestructura actual — OPERATIVA ✅
|
||||
|
||||
### Stack
|
||||
- **App:** Next.js 15.3.3 + TypeScript + Tailwind CSS
|
||||
- **DB:** Supabase self-hosted en Dokploy (stack oficial clonado)
|
||||
- **Deploy:** Dokploy v0.29.4 en VPS inq-mart-2 (IP VPN: 172.20.5.11)
|
||||
- **Repos:**
|
||||
- App: `git.inquality.com.py/marcos/motor-de-encuestas` (migrado desde GitHub InqGit)
|
||||
- Supabase stack: `git.inquality.com.py/marcos/supabase-motor-fomularios`
|
||||
- **Dominio API Supabase:** `db.inqualityhq.com` (Kong expuesto con SSL via Traefik/Let's Encrypt)
|
||||
|
||||
### Supabase self-hosted — detalles técnicos
|
||||
- Stack corriendo desde repo Gitea (modo Git, NO docker raw — docker raw no tiene ./volumes/db/)
|
||||
- Kong conectado a `dokploy-network` (persistente en docker-compose.yml del repo)
|
||||
- Fix aplicado: `LUA_AUTH_EXPR` en `kong-entrypoint.sh` con prefijo `Bearer ` para PostgREST JWT propagation
|
||||
- Variables de entorno en Dokploy: ANON_KEY, SERVICE_ROLE_KEY, SUPABASE_ANON_KEY, SUPABASE_SERVICE_KEY (nombres que usa kong.yml)
|
||||
- SMTP_PORT=587 requerido aunque SMTP esté desactivado (GoTrue lo exige como int)
|
||||
- GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false (GoTrue lo exige como bool explícito)
|
||||
- Backups: script GFS en /usr/local/bin/supabase-backup.sh, cron 3AM root, retención 3 días diarios + domingos 14 días
|
||||
- Studio: solo accesible via SSH tunnel `ssh -L 3001:localhost:3001 administrator@172.20.5.11`
|
||||
|
||||
### Variables de entorno de la app Next.js (Build Arguments en Dokploy)
|
||||
- NEXT_PUBLIC_SUPABASE_URL=https://db.inqualityhq.com
|
||||
- NEXT_PUBLIC_SUPABASE_ANON_KEY=<JWT anon>
|
||||
- SUPABASE_SERVICE_ROLE_KEY=<JWT service_role> (Environment Setting, no Build Arg)
|
||||
|
||||
---
|
||||
|
||||
## 3. Estado del código — Sprint 2 COMPLETADO, Sprint 3 COMPLETADO
|
||||
|
||||
### Migraciones aplicadas (12 en total, en orden)
|
||||
1. 20260531000001_create_tables.sql — 7 tablas
|
||||
2. 20260531000002_enable_rls.sql
|
||||
3. 20260531000003_rls_policies.sql — PARCIAL (ver TECH-DEBT-007)
|
||||
4. 20260531000004_constraints_triggers.sql
|
||||
5. 20260531000005_agg_segment.sql
|
||||
6. 20260531000006_grants.sql
|
||||
7. 20260602111851_revoke_consent_select.sql
|
||||
8. 20260604200932_fix_answers_insert_policy.sql
|
||||
9. 20260605093948_dashboard_rpc.sql
|
||||
10. 20260605093949_rls_deny_sensitive.sql
|
||||
11. 20260605140000_011_truncate_testing_data.sql
|
||||
12. 20260605140001_012_dashboard_rpc_v2.sql — 11 RPCs v2 del dashboard
|
||||
|
||||
### Políticas RLS activas (7)
|
||||
- answers_insert, deny_sensitive_answers
|
||||
- consent_records_insert, consent_versions_select_all
|
||||
- questions_select_live, responses_insert
|
||||
- surveys_select_live_anon
|
||||
|
||||
### Políticas RLS que fallaron (TECH-DEBT-007)
|
||||
- org_select_own, surveys_select_auth, questions_select_auth
|
||||
- Causa: auth.jwt() deprecado en GoTrue v2.189+ — no es bloqueante para el piloto
|
||||
|
||||
### Dashboard — widgets y códigos reales del instrumento
|
||||
| Widget | Códigos reales | Subtítulo |
|
||||
|---|---|---|
|
||||
| K1 Cuidadores activos | 1.6 + 1.13 | % plantilla con responsabilidades de cuidado |
|
||||
| K2 Frecuencia ausentismo | 5.2 | % con 4+ ausencias al año |
|
||||
| K3 Bienestar | 4.3 (rating 1-5) | autorreporte bienestar general |
|
||||
| W1 Prevalencia por depto | A1 (Departamento) | % cuidadores activos por departamento |
|
||||
| W2 Perfil población | 1.1+1.6+1.13 | composición no excluyente |
|
||||
| W3 Madurez organizacional | 6.1(×0.7)+6.5(×0.3) | score 0-100 |
|
||||
| W4 Horas×Ausentismo | 2.2×5.2 | correlación intensidad/ausentismo |
|
||||
| W5 Pirámide demográfica | A5×A6 | etario por género (k≥5) |
|
||||
| W6 Apoyos demandados | 6.2 | top 8 multi-select |
|
||||
| W7 Intensidad vs Bienestar | 2.2×4.3 | bins k≥5 |
|
||||
| W8 Saturación cuidador | 4.3+5.1+5.2 | indicador compuesto |
|
||||
|
||||
---
|
||||
|
||||
## 4. Deuda técnica registrada
|
||||
|
||||
| ID | Descripción | Bloqueante |
|
||||
|---|---|---|
|
||||
| TECH-DEBT-006 | Backup remoto S3 (Backblaze B2 o Wasabi ~$5/mes) | **SÍ — antes del primer dato real** |
|
||||
| TECH-DEBT-007 | auth.jwt() deprecado en GoTrue v2.189+ — current_app_role() y current_org_id() no funcionales | No (service_role bypasa RLS) |
|
||||
| CVE-2025-66478 | Next.js 15.3.3 con vulnerabilidad de seguridad crítica | **SÍ — antes del primer dato real** |
|
||||
|
||||
---
|
||||
|
||||
## 5. Prompts pendientes de ejecutar en Claude Code
|
||||
|
||||
### ETAPA 2G — Cierre Sprint 2 (método)
|
||||
**Archivo:** `sprints/sprint-2/ETAPA_2G_PROMPT.md`
|
||||
**Qué hace:** promueve Patrón C.8 a estable + documenta AP.10 y AP.11 en method/ANTI_PATTERNS.md con casos reales del proyecto.
|
||||
|
||||
### ETAPA 3V — Vista de control /admin/responses
|
||||
**Archivo:** `sprints/sprint-3-infra/ETAPA_3V_PROMPT.md`
|
||||
**Qué hace:** crea ruta Next.js /admin/responses que muestra tabla de todas las respuestas ordenadas por fecha descendente, sin auth, usando service_role.
|
||||
|
||||
---
|
||||
|
||||
## 6. Contexto comercial
|
||||
|
||||
- **2 empresas comprometidas** para piloto (no en producción aún)
|
||||
- **Cronograma deck:** JUN 2026 inicio · JUL-AGO relevamiento · SEP análisis · OCT primer informe + evento
|
||||
- **Meta:** 20 empresas pioneras
|
||||
- **Precios:** Gs. 4.5M (≤50) · 7.5M (≤200) · 12M (≤400)
|
||||
- **GAPs deck vs. producto:**
|
||||
- RE Index no implementado (fórmula: Bienestar×0.35 + Concentración×0.25 + Descanso×0.25 + Apoyo×0.15)
|
||||
- Informe descargable prometido en el deck, no existe todavía
|
||||
- **Contacto Fundación:** Valeria Arce — ddff@fundacionsolidaridad.org.py · +595 991 854333
|
||||
|
||||
---
|
||||
|
||||
## 7. Documentos producidos en estas sesiones
|
||||
|
||||
| Archivo | Descripción |
|
||||
|---|---|
|
||||
| INSTALL_SUPABASE.md | Guía instalación self-hosted (384 líneas) — con tabla de errores conocidos |
|
||||
| BACKUP_STRATEGY.md | Estrategia backup GFS (335 líneas) — con procedimiento de restore |
|
||||
| DOCUMENTACION_PLATAFORMA.md | Doc conceptual con 3 diagramas Mermaid |
|
||||
| DatoPais_DocumentacionConceptual.docx | Word con 3 PNGs de diagramas embebidos |
|
||||
| DatoPais_DimensionamientoEconomico.docx | Word con 5 charts económicos |
|
||||
| DatoPais_ValidacionInstrumento.xlsx | Planilla colaborativa 4 hojas |
|
||||
| sprints/sprint-2/ETAPA_2G_PROMPT.md | Cierre Sprint 2 — PENDIENTE ejecución |
|
||||
| sprints/sprint-3-infra/ETAPA_3V_PROMPT.md | Vista /admin/responses — PENDIENTE ejecución |
|
||||
|
||||
---
|
||||
|
||||
## 8. Decisiones clave tomadas (no reabrir)
|
||||
|
||||
1. **docker raw de Dokploy es incompatible con Supabase** — usar siempre modo Git; el compose oficial necesita ./volumes/db/ que docker raw no tiene.
|
||||
2. **POSTGRES_PASSWORD siempre con openssl rand -hex** — base64 tiene caracteres especiales que rompen el escaping de Docker Compose.
|
||||
3. **kong.yml: una sola entrada por bloque keyauth_credentials** — dos entradas con el mismo valor JWT causa uniqueness violation.
|
||||
4. **SMTP_PORT=587 siempre explícito** aunque SMTP esté desactivado — GoTrue lo parsea como int y falla si está vacío.
|
||||
5. **GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false siempre explícito** — GoTrue lo parsea como bool y falla si está vacío.
|
||||
6. **LUA_AUTH_EXPR con prefijo "Bearer "** en kong-entrypoint.sh rama legacy — sin el prefijo PostgREST recibe JWT vacío.
|
||||
7. **kong.yml usa $ANON_KEY y $SERVICE_ROLE_KEY** — alineado con los nombres de variables en Dokploy; SUPABASE_ANON_KEY y SUPABASE_SERVICE_KEY son aliases adicionales.
|
||||
8. **App Next.js conecta a Supabase por URL pública** (https://db.inqualityhq.com) — las variables NEXT_PUBLIC_* se embeben en el bundle en build time y el navegador no puede resolver nombres Docker internos.
|
||||
9. **NEXT_PUBLIC_SUPABASE_URL va como Build Argument** en Dokploy, no como Environment Setting — se embebe en el bundle de JavaScript durante el build.
|
||||
10. **Repos en Gitea** (migrado desde GitHub InqGit) — VS Code configurado apuntando a git.inquality.com.py.
|
||||
|
||||
---
|
||||
|
||||
## 9. Patrones y anti-patrones del método (pendiente formalización en 2G)
|
||||
|
||||
### C.8 — Verificación de códigos contra fuente única (PROMOVER A ESTABLE)
|
||||
Todos los widget subtitles y RPC mappings deben verificarse contra el instrumento real (SEED_SPEC.md). El drift silencioso entre instrumento e implementación es un failure mode de alto costo.
|
||||
|
||||
### AP.10 — Propagación silenciosa de inconsistencia entre fuentes
|
||||
Caso real: Etapa 2D tenía subtítulos con códigos incorrectos que no se detectaron hasta auditoría explícita. Los errores de mapeo de códigos no generan errores en tiempo de compilación.
|
||||
|
||||
### AP.11 — Contrato desactualizado por código
|
||||
Caso real: Etapa 1D dejó SEED_SPEC.md desactualizado respecto al seed.sql. El documento de referencia divergió del código sin señal visible.
|
||||
|
||||
---
|
||||
|
||||
## 10. Próximos pasos en orden de prioridad
|
||||
|
||||
1. **TECH-DEBT-006** — Backup remoto S3 (bloqueante antes del primer dato real)
|
||||
2. **CVE-2025-66478** — Actualizar Next.js antes del primer dato real
|
||||
3. **ETAPA 2G** — Ejecutar en Claude Code (método, no infraestructura)
|
||||
4. **ETAPA 3V** — Ejecutar en Claude Code (vista /admin/responses)
|
||||
5. **RE Index** — Implementar como widget real (confirmar mapeo G1/G2/G4/G5 → códigos seed con Marcos primero)
|
||||
6. **Informe descargable** — Prometido en deck comercial, no existe
|
||||
7. **Onboarding 2 empresas comprometidas** — cuando validación esté lista
|
||||
8. **Validación instrumento** — Respuestas equipo de salud en DatoPais_ValidacionInstrumento.xlsx (12 preguntas metodológicas abiertas)
|
||||
70
migrate-claude/PROMPT_NUEVA_SESION.md
Normal file
70
migrate-claude/PROMPT_NUEVA_SESION.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# PROMPT DE CONTINUIDAD — nueva sesión de dirección técnica
|
||||
## Datos País sobre el Cuidado
|
||||
|
||||
> Pegá este prompt al inicio de una nueva conversación con Claude.
|
||||
> Actualizado: junio 2026.
|
||||
|
||||
---
|
||||
|
||||
Sos el copiloto técnico del proyecto "Datos País sobre el Cuidado" — una plataforma soberana de encuestas sobre economía del cuidado en Paraguay. Ya tenemos múltiples sesiones de trabajo juntos. Este mensaje te pone al día antes de continuar.
|
||||
|
||||
## Contexto del proyecto
|
||||
|
||||
Plataforma de dos caras: formulario web anónimo + dashboard analítico con k-anonimato. Marca RE, Fundación Solidaridad, powered by InQuality.
|
||||
|
||||
- **Stack:** Next.js 15.3.3 + TypeScript + Tailwind CSS + Supabase self-hosted
|
||||
- **Deploy:** Dokploy v0.29.4 en VPS (IP VPN: 172.20.5.11)
|
||||
- **URL producción:** https://economia-cuidado.inqualityhq.com
|
||||
- **Repos:** app en `git.inquality.com.py/marcos/motor-de-encuestas`, Supabase stack en `git.inquality.com.py/marcos/supabase-motor-fomularios`
|
||||
- **API Supabase:** https://db.inqualityhq.com (Kong con SSL)
|
||||
|
||||
## Estado actual — Sprint 3 COMPLETADO
|
||||
|
||||
El formulario y el dashboard están operativos con Supabase self-hosted. Las 12 migraciones están aplicadas. Backups locales configurados con retención GFS. El formulario carga y permite completar la encuesta end-to-end.
|
||||
|
||||
## Deuda técnica abierta (ver TECH_DEBT.md en el repo)
|
||||
|
||||
- **TECH-DEBT-006** — Backup remoto S3 — BLOQUEANTE antes del primer dato real de empresa
|
||||
- **TECH-DEBT-007** — auth.jwt() deprecado en GoTrue v2.189+ — no bloqueante para el piloto
|
||||
- **CVE-2025-66478** — Next.js 15.3.3 con vulnerabilidad crítica — BLOQUEANTE antes del primer dato real
|
||||
|
||||
## Decisiones tomadas — no reabrir
|
||||
|
||||
1. Supabase en Dokploy siempre via modo Git (docker raw no tiene ./volumes/db/)
|
||||
2. POSTGRES_PASSWORD siempre con openssl rand -hex (evita caracteres problemáticos)
|
||||
3. kong.yml: una sola entrada por bloque keyauth_credentials
|
||||
4. SMTP_PORT=587 siempre explícito aunque SMTP esté desactivado
|
||||
5. GOTRUE_EXTERNAL_ANONYMOUS_USERS_ENABLED=false siempre explícito
|
||||
6. LUA_AUTH_EXPR con prefijo "Bearer " en kong-entrypoint.sh rama legacy
|
||||
7. NEXT_PUBLIC_SUPABASE_URL via Build Argument en Dokploy (no Environment Setting)
|
||||
8. App Next.js conecta a Supabase por URL pública https://db.inqualityhq.com
|
||||
|
||||
## Prompts pendientes de ejecutar en Claude Code
|
||||
|
||||
1. **ETAPA 2G** — `sprints/sprint-2/ETAPA_2G_PROMPT.md` — cierre Sprint 2, formaliza patrones C.8, AP.10, AP.11 en el método
|
||||
2. **ETAPA 3V** — `sprints/sprint-3-infra/ETAPA_3V_PROMPT.md` — vista /admin/responses para control operativo
|
||||
|
||||
## Próximos pasos en orden
|
||||
|
||||
1. TECH-DEBT-006 (S3 backup) — bloqueante
|
||||
2. CVE-2025-66478 (Next.js update) — bloqueante
|
||||
3. Ejecutar ETAPA 2G en Claude Code
|
||||
4. Ejecutar ETAPA 3V en Claude Code
|
||||
5. RE Index — confirmar mapeo G1/G2/G4/G5 → códigos seed antes de implementar
|
||||
6. Informe descargable — prometido en deck comercial, no existe aún
|
||||
|
||||
## Método de trabajo
|
||||
|
||||
Usamos niveles de autonomía explícitos en los prompts (Nivel 1 = reportar antes de hacer, Nivel 3 = ejecutar con validación al final). Los prompts de Claude Code están en `sprints/`. Las decisiones arquitecturales se documentan como ADRs en `docs/adr/`. Las reglas no negociables del proyecto están en `CLAUDE.md` del repo.
|
||||
|
||||
## Preguntas frecuentes de contexto
|
||||
|
||||
**¿Por qué Supabase self-hosted y no Cloud?** Soberanía del dato — datos de cuidadores paraguayos no deben vivir en servidores de terceros fuera de control del proyecto. Decisión tomada antes de ingresar el primer dato real.
|
||||
|
||||
**¿Por qué k-anonimato k≥5?** Regla no negociable del sistema. En empresas chicas, un cruce demográfico de menos de 5 personas puede reidentificar a un individuo aunque el nombre nunca se haya guardado.
|
||||
|
||||
**¿Por qué el dashboard usa service_role?** Porque las políticas de org_admin con JWT custom claims no están implementadas (TECH-DEBT-007). Para el piloto, el dashboard es herramienta interna y service_role es aceptable. Cuando haya org_admin real, hay que resolver TECH-DEBT-007 primero.
|
||||
|
||||
---
|
||||
|
||||
¿Listo para continuar? El próximo paso es [COMPLETAR CON LO QUE VAYAS A HACER].
|
||||
48
migrate-claude/TECH_DEBT_ACTUALIZADO.md
Normal file
48
migrate-claude/TECH_DEBT_ACTUALIZADO.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# TECH_DEBT.md — actualización junio 2026
|
||||
## Para aplicar en el repo del proyecto
|
||||
|
||||
> Reemplazar el contenido existente de TECH_DEBT.md en el repo con este bloque,
|
||||
> o mergear los ítems nuevos si ya existe el archivo con otros ítems previos.
|
||||
|
||||
---
|
||||
|
||||
## Deuda técnica abierta
|
||||
|
||||
### TECH-DEBT-006 — Backup remoto S3
|
||||
**Estado:** ABIERTO — BLOQUEANTE antes del primer dato real de empresa
|
||||
**Descripción:** Los backups locales están configurados y funcionando (GFS, 3AM diario, retención 3 días + domingos 14 días). Pero los backups viven solo en el mismo VPS. Si el VPS muere, se pierden datos Y backups.
|
||||
**Solución:** rsync nocturno a S3-compatible (Backblaze B2 o Wasabi, ~USD 5/mes) usando rclone.
|
||||
**Referencia:** BACKUP_STRATEGY.md sección 5 — TECH-DEBT-006.
|
||||
|
||||
### TECH-DEBT-007 — auth.jwt() deprecado en GoTrue v2.189+
|
||||
**Estado:** ABIERTO — no bloqueante para el piloto
|
||||
**Descripción:** Las funciones helper `current_app_role()` y `current_org_id()` en migration 003 usan `auth.jwt()` que fue eliminado en GoTrue v2.189+. Las políticas que dependen de estas funciones no se crearon:
|
||||
- `org_select_own` en organizations
|
||||
- `surveys_select_auth` en surveys
|
||||
- `questions_select_auth` en questions
|
||||
**Impacto actual:** ninguno — el dashboard usa service_role que bypasa RLS. Los respondentes usan anon que tiene las políticas básicas de INSERT.
|
||||
**Impacto futuro:** org_admin no podrá autenticarse con custom claims (app_role, org_id) hasta resolver esto.
|
||||
**Solución:** migrar a auth.session() o implementar JWT claims via hook de GoTrue v2.
|
||||
**NO resolver hasta tener caso de uso real de org_admin autenticado.**
|
||||
|
||||
### CVE-2025-66478 — Next.js 15.3.3 con vulnerabilidad de seguridad
|
||||
**Estado:** ABIERTO — BLOQUEANTE antes del primer dato real de empresa
|
||||
**Descripción:** Next.js 15.3.3 tiene una vulnerabilidad de seguridad crítica. El build lo reporta explícitamente: "This version has a security vulnerability. Please upgrade to a patched version."
|
||||
**Solución:** actualizar next en package.json a la versión parcheada disponible, correr npm install, hacer redeploy.
|
||||
**Referencia:** https://nextjs.org/blog/CVE-2025-66478
|
||||
|
||||
---
|
||||
|
||||
## Deuda técnica cerrada en Sprint 3
|
||||
|
||||
### TECH-DEBT-002 — KPI NPS eliminado ✅
|
||||
KPI NPS eliminado — instrumento no tiene pregunta NPS.
|
||||
|
||||
### TECH-DEBT-003 — Brecha de políticas ✅
|
||||
Widget W6 redefinido como demanda-only (6.2), sin oferta. Documentado.
|
||||
|
||||
### TECH-DEBT-004 — Intención directa de salida no capturada
|
||||
Intención directa de salida laboral no capturada en el instrumento v2. No resuelto pero documentado como gap conocido.
|
||||
|
||||
### TECH-DEBT-005 — A1 País vs A1b ✅
|
||||
Cerrado: A1 ya es Departamento (18 opciones Paraguay) en el instrumento final.
|
||||
81
package-lock.json
generated
81
package-lock.json
generated
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@supabase/supabase-js": "^2.49.8",
|
||||
"next": "15.3.3",
|
||||
"next": "15.3.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
@@ -895,15 +895,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/env": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz",
|
||||
"integrity": "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==",
|
||||
"version": "15.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.8.tgz",
|
||||
"integrity": "sha512-SAfHg0g91MQVMPioeFeDjE+8UPF3j3BvHjs8ZKJAUz1BG7eMPvfCKOAgNWJ6s1MLNeP6O2InKQRTNblxPWuq+Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@next/swc-darwin-arm64": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz",
|
||||
"integrity": "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.5.tgz",
|
||||
"integrity": "sha512-lM/8tilIsqBq+2nq9kbTW19vfwFve0NR7MxfkuSUbRSgXlMQoJYg+31+++XwKVSXk4uT23G2eF/7BRIKdn8t8w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -917,9 +917,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-darwin-x64": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz",
|
||||
"integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.5.tgz",
|
||||
"integrity": "sha512-WhwegPQJ5IfoUNZUVsI9TRAlKpjGVK0tpJTL6KeiC4cux9774NYE9Wu/iCfIkL/5J8rPAkqZpG7n+EfiAfidXA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -933,9 +933,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz",
|
||||
"integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.5.tgz",
|
||||
"integrity": "sha512-LVD6uMOZ7XePg3KWYdGuzuvVboxujGjbcuP2jsPAN3MnLdLoZUXKRc6ixxfs03RH7qBdEHCZjyLP/jBdCJVRJQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -952,9 +952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-arm64-musl": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz",
|
||||
"integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.5.tgz",
|
||||
"integrity": "sha512-k8aVScYZ++BnS2P69ClK7v4nOu702jcF9AIHKu6llhHEtBSmM2zkPGl9yoqbSU/657IIIb0QHpdxEr0iW9z53A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -971,9 +971,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-gnu": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz",
|
||||
"integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.5.tgz",
|
||||
"integrity": "sha512-2xYU0DI9DGN/bAHzVwADid22ba5d/xrbrQlr2U+/Q5WkFUzeL0TDR963BdrtLS/4bMmKZGptLeg6282H/S2i8A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -990,9 +990,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-linux-x64-musl": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz",
|
||||
"integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.5.tgz",
|
||||
"integrity": "sha512-TRYIqAGf1KCbuAB0gjhdn5Ytd8fV+wJSM2Nh2is/xEqR8PZHxfQuaiNhoF50XfY90sNpaRMaGhF6E+qjV1b9Tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1009,9 +1009,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz",
|
||||
"integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.5.tgz",
|
||||
"integrity": "sha512-h04/7iMEUSMY6fDGCvdanKqlO1qYvzNxntZlCzfE8i5P0uqzVQWQquU1TIhlz0VqGQGXLrFDuTJVONpqGqjGKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1025,9 +1025,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz",
|
||||
"integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==",
|
||||
"version": "15.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.5.tgz",
|
||||
"integrity": "sha512-5fhH6fccXxnX2KhllnGhkYMndhOiLOLEiVGYjP2nizqeGWkN10sA9taATlXwake2E2XMvYZjjz0Uj7T0y+z1yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -2808,13 +2808,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/next": {
|
||||
"version": "15.3.3",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz",
|
||||
"integrity": "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==",
|
||||
"deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/CVE-2025-66478 for more details.",
|
||||
"version": "15.3.8",
|
||||
"resolved": "https://registry.npmjs.org/next/-/next-15.3.8.tgz",
|
||||
"integrity": "sha512-L+4c5Hlr84fuaNADZbB9+ceRX9/CzwxJ+obXIGHupboB/Q1OLbSUapFs4bO8hnS/E6zV/JDX7sG1QpKVR2bguA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@next/env": "15.3.3",
|
||||
"@next/env": "15.3.8",
|
||||
"@swc/counter": "0.1.3",
|
||||
"@swc/helpers": "0.5.15",
|
||||
"busboy": "1.6.0",
|
||||
@@ -2829,14 +2828,14 @@
|
||||
"node": "^18.18.0 || ^19.8.0 || >= 20.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@next/swc-darwin-arm64": "15.3.3",
|
||||
"@next/swc-darwin-x64": "15.3.3",
|
||||
"@next/swc-linux-arm64-gnu": "15.3.3",
|
||||
"@next/swc-linux-arm64-musl": "15.3.3",
|
||||
"@next/swc-linux-x64-gnu": "15.3.3",
|
||||
"@next/swc-linux-x64-musl": "15.3.3",
|
||||
"@next/swc-win32-arm64-msvc": "15.3.3",
|
||||
"@next/swc-win32-x64-msvc": "15.3.3",
|
||||
"@next/swc-darwin-arm64": "15.3.5",
|
||||
"@next/swc-darwin-x64": "15.3.5",
|
||||
"@next/swc-linux-arm64-gnu": "15.3.5",
|
||||
"@next/swc-linux-arm64-musl": "15.3.5",
|
||||
"@next/swc-linux-x64-gnu": "15.3.5",
|
||||
"@next/swc-linux-x64-musl": "15.3.5",
|
||||
"@next/swc-win32-arm64-msvc": "15.3.5",
|
||||
"@next/swc-win32-x64-msvc": "15.3.5",
|
||||
"sharp": "^0.34.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^5.4.0",
|
||||
"@supabase/supabase-js": "^2.49.8",
|
||||
"next": "15.3.3",
|
||||
"next": "15.3.8",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-hook-form": "^7.55.0",
|
||||
|
||||
189
sprints/sprint-3-infra/ETAPA_3C_PROMPT.md
Normal file
189
sprints/sprint-3-infra/ETAPA_3C_PROMPT.md
Normal file
@@ -0,0 +1,189 @@
|
||||
# ETAPA 3C — Texto de consentimiento + dos nuevas encuestas piloto
|
||||
|
||||
## 1. Objetivo
|
||||
Dos cambios independientes:
|
||||
1. Actualizar el texto de la pantalla de consentimiento para usar el nombre de empresa
|
||||
dinámico desde la DB y el nuevo wording aprobado.
|
||||
2. Insertar en el seed dos nuevas organizaciones y dos nuevos surveys (InQuality y
|
||||
Fundación Solidaridad) como encuestas de espacio de pruebas reales.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexto previo
|
||||
- El nombre de empresa se lee de `surveys.brand_config.org_name` (JSONB), no de
|
||||
`organizations.name`. El formulario ya tiene acceso a ese valor via `brand_config`.
|
||||
- El survey actual (`30000000-...-0001`) tiene `brand_config.org_name = "InQuality"`.
|
||||
- El componente que muestra la pantalla de consentimiento es `ConsentScreen`,
|
||||
llamado desde `survey-form.tsx` con `brandConfig={survey.brand_config}`.
|
||||
- Las dos nuevas encuestas son para uso real en espacio de pruebas — los encuestados
|
||||
serán notificados cuando la plataforma madure.
|
||||
|
||||
---
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
|
||||
- Organizaciones separadas para cada encuesta — cada una con su propio `org_id`.
|
||||
- Los surveys nuevos tienen `status='live'` para que sean accesibles por UUID.
|
||||
- El survey demo existente (`30000000-...-0001`) NO se toca — sigue funcionando.
|
||||
- Los nuevos UUIDs siguen el patrón del seed: `10000000-...-0002` para org InQuality
|
||||
real, `10000000-...-0003` para Fundación Solidaridad, y surveys `30000000-...-0002`
|
||||
y `30000000-...-0003` respectivamente.
|
||||
- `brand_config.org_name` es el campo que alimenta el texto dinámico del consentimiento.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cambio 1 — Texto del consentimiento
|
||||
|
||||
### 4.1 Texto actual (aproximado)
|
||||
```
|
||||
Diagnóstico interno de [org_name]: tus respuestas ayudan a la empresa a entender
|
||||
la realidad de las personas que cuidan a familiares con discapacidad o adultos
|
||||
mayores, para diseñar apoyos más adecuados. Solo se trabaja con datos agregados
|
||||
— nunca accede a respuestas individuales.
|
||||
|
||||
Estudio país (opcional): tus respuestas anonimizadas contribuyen a un mapeo nacional
|
||||
sobre la economía del cuidado en Paraguay. Los datos se procesan de forma
|
||||
irreversiblemente desvinculada de cualquier empresa o persona.
|
||||
```
|
||||
|
||||
### 4.2 Texto nuevo
|
||||
```
|
||||
Diagnóstico interno de [ORG_NAME]: Tus respuestas ayudan a la empresa, en alianza
|
||||
con Fundación Solidaridad, a entender la realidad de las personas que cuidan a
|
||||
familiares con discapacidad o adultos mayores, para diseñar apoyos más adecuados.
|
||||
Solo se trabaja con datos agregados — nunca accede a respuestas individuales.
|
||||
|
||||
Estudio país (opcional): Tus respuestas serán anónimas y contribuyen a un mapeo
|
||||
nacional sobre la economía del cuidado en Paraguay. Los datos se procesan de manera
|
||||
que no pueden asociarse a ninguna empresa ni persona.
|
||||
```
|
||||
|
||||
`[ORG_NAME]` = `brand_config.org_name` — ya disponible en el componente.
|
||||
Las palabras en negrita son: el nombre de la empresa, "datos agregados",
|
||||
"anónimas". Mantener el mismo patrón de negritas que el texto actual.
|
||||
|
||||
### 4.3 Archivo a modificar
|
||||
Buscar en `ConsentScreen` o donde esté el JSX que renderiza estos dos párrafos.
|
||||
Solo cambiar el texto — no la estructura ni el estilo.
|
||||
|
||||
---
|
||||
|
||||
## 5. Cambio 2 — Dos nuevas organizaciones y surveys en seed.sql
|
||||
|
||||
### 5.1 Organización InQuality
|
||||
```sql
|
||||
INSERT INTO public.organizations (id, name, sector, size_bucket)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000002',
|
||||
'InQuality',
|
||||
'Consultoría/Tecnología',
|
||||
'1-50'
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
sector = EXCLUDED.sector,
|
||||
size_bucket = EXCLUDED.size_bucket;
|
||||
```
|
||||
|
||||
### 5.2 Organización Fundación Solidaridad
|
||||
```sql
|
||||
INSERT INTO public.organizations (id, name, sector, size_bucket)
|
||||
VALUES (
|
||||
'10000000-0000-0000-0000-000000000003',
|
||||
'Fundación Solidaridad',
|
||||
'Sociedad Civil/ONG',
|
||||
'1-50'
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
sector = EXCLUDED.sector,
|
||||
size_bucket = EXCLUDED.size_bucket;
|
||||
```
|
||||
|
||||
### 5.3 Survey InQuality
|
||||
```sql
|
||||
INSERT INTO public.surveys (
|
||||
id, org_id, title, status, is_anonymous,
|
||||
data_sensitivity, security_class, controller_role,
|
||||
consent_version_id, brand_config, k_threshold
|
||||
) VALUES (
|
||||
'30000000-0000-0000-0000-000000000002',
|
||||
'10000000-0000-0000-0000-000000000002',
|
||||
'Mapeo de Cuidadores — 2026',
|
||||
'live',
|
||||
true,
|
||||
'sensible',
|
||||
'confidencial',
|
||||
'A',
|
||||
'20000000-0000-0000-0000-000000000001',
|
||||
'{"org_name": "InQuality", "branch_name": "Equipo InQuality", "branch_city": "Asunción"}',
|
||||
5
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
brand_config = EXCLUDED.brand_config,
|
||||
status = EXCLUDED.status;
|
||||
```
|
||||
|
||||
### 5.4 Survey Fundación Solidaridad
|
||||
```sql
|
||||
INSERT INTO public.surveys (
|
||||
id, org_id, title, status, is_anonymous,
|
||||
data_sensitivity, security_class, controller_role,
|
||||
consent_version_id, brand_config, k_threshold
|
||||
) VALUES (
|
||||
'30000000-0000-0000-0000-000000000003',
|
||||
'10000000-0000-0000-0000-000000000003',
|
||||
'Mapeo de Cuidadores — 2026',
|
||||
'live',
|
||||
true,
|
||||
'sensible',
|
||||
'confidencial',
|
||||
'A',
|
||||
'20000000-0000-0000-0000-000000000001',
|
||||
'{"org_name": "Fundación Solidaridad", "branch_name": "Equipo Fundación", "branch_city": "Asunción"}',
|
||||
5
|
||||
) ON CONFLICT (id) DO UPDATE SET
|
||||
brand_config = EXCLUDED.brand_config,
|
||||
status = EXCLUDED.status;
|
||||
```
|
||||
|
||||
### 5.5 Notas sobre el seed
|
||||
- Usar `ON CONFLICT (id) DO UPDATE` para que el seed sea idempotente.
|
||||
- El `consent_version_id` referencia el consentimiento existente
|
||||
(`20000000-0000-0000-0000-000000000001`) — mismo texto para ambas.
|
||||
- Las URLs resultantes serán:
|
||||
- InQuality: `?s=30000000-0000-0000-0000-000000000002`
|
||||
- Fundación Solidaridad: `?s=30000000-0000-0000-0000-000000000003`
|
||||
|
||||
---
|
||||
|
||||
## 6. Entregables (DoD)
|
||||
|
||||
- [ ] Texto del consentimiento actualizado en el componente con el nuevo wording.
|
||||
- [ ] `[ORG_NAME]` dinámico desde `brand_config.org_name`.
|
||||
- [ ] Negritas correctas: nombre de empresa, "datos agregados", "anónimas".
|
||||
- [ ] Dos nuevas organizaciones en `seed.sql` con `ON CONFLICT`.
|
||||
- [ ] Dos nuevos surveys en `seed.sql` con `ON CONFLICT`.
|
||||
- [ ] Build exitoso.
|
||||
- [ ] `git diff --name-only HEAD` en scope.
|
||||
|
||||
## 7. Qué reportar
|
||||
```
|
||||
Verificado:
|
||||
- Texto de consentimiento actualizado ✓
|
||||
- org_name dinámico ✓ (valor en demo: "InQuality" o "Empresa Demo")
|
||||
- Seed actualizado con 2 orgs + 2 surveys ✓
|
||||
- ON CONFLICT en todos los nuevos INSERTs ✓
|
||||
- Build exitoso ✓
|
||||
- Diff en scope ✓
|
||||
|
||||
URLs de los nuevos surveys:
|
||||
- InQuality: ?s=30000000-0000-0000-0000-000000000002
|
||||
- Fundación Solidaridad: ?s=30000000-0000-0000-0000-000000000003
|
||||
|
||||
Asumido:
|
||||
- [decisiones de implementación]
|
||||
```
|
||||
|
||||
## 8. Qué NO hacer
|
||||
- ❌ No tocar el survey demo existente (30000000-...-0001).
|
||||
- ❌ No cambiar la estructura de `brand_config` — solo el texto del componente.
|
||||
- ❌ No re-seedear la DB de producción — solo actualizar seed.sql.
|
||||
- ❌ No cambiar estilos ni layout del ConsentScreen.
|
||||
- ❌ No tocar dashboard ni RPCs.
|
||||
174
sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md
Normal file
174
sprints/sprint-3-infra/ETAPA_3I2_PROMPT.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# ETAPA 3I2 — Revisión del instrumento v2.1 (feedback equipo médico)
|
||||
|
||||
## 1. Objetivo
|
||||
Aplicar 10 cambios al instrumento "Mapeo de Cuidadores" resultado del feedback del equipo
|
||||
de salud. Los cambios afectan SEED_SPEC.md, seed.sql, y el código del formulario Next.js.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexto previo
|
||||
- Instrumento actual: v2, 44 preguntas, 8 secciones, en `SEED_SPEC.md`.
|
||||
- La DB tiene al menos 1 respuesta real. Algunos cambios son breaking para datos existentes
|
||||
(opciones renombradas, tipo cambiado) — documentar en OBSERVATIONS.md, no bloquear.
|
||||
- El formulario Next.js usa un sistema de conditional_rules en el JSON de cada pregunta.
|
||||
- Los 3 triggers (1.1, 1.6, 1.13) controlan bloques condicionales enteros.
|
||||
|
||||
---
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
|
||||
- Cambio 4: el salto a 6.4 ocurre cuando las TRES preguntas 1.1, 1.6, 1.13 dan "No"
|
||||
simultáneamente. Si al menos una da "Sí", las secciones 2-5 y 6.1-6.3 se muestran normalmente.
|
||||
- Cambio 4: las secciones 2, 3, 4, 5, 6.1, 6.2, 6.3 se saltean completamente — ir directo a 6.4.
|
||||
- Cambio 5: 2.1 pasa a multiple_choice. Es breaking para datos existentes — documentar, no bloquear.
|
||||
- Cambio 6: 2.4 se elimina del seed. Si hay respuestas con question_id de 2.4, se pierden
|
||||
al re-seedear. Documentar en OBSERVATIONS.md.
|
||||
- Cambio 4: pregunta 6.6 opciones: Sí · No · Tal vez.
|
||||
- Cambio 3: "Tecnología asistiva" va en penúltima posición (antes de "No utilizo"/"No utiliza").
|
||||
- "Prefiero no decir" se elimina de TODAS las preguntas que lo tengan.
|
||||
|
||||
---
|
||||
|
||||
## 4. Los 10 cambios — especificación exacta
|
||||
|
||||
### Cambio 1 — Eliminar "Prefiero no decir" de todo el instrumento
|
||||
Preguntas afectadas: A5, A6, 1.1, 1.5, 1.6, 1.13, 2.5, 3.1, 3.4.
|
||||
En cada una, eliminar la opción "Prefiero no decir" del array de opciones en seed.sql.
|
||||
En SEED_SPEC.md, eliminarla de la columna Opciones de cada pregunta.
|
||||
|
||||
⚠️ Los triggers 1.1, 1.6, 1.13 quedan con solo 2 opciones: Sí · No.
|
||||
La lógica condicional no cambia — solo "Sí" dispara el bloque.
|
||||
|
||||
### Cambio 2 — Subtítulo hint en todas las preguntas multiple_choice
|
||||
En el componente del formulario que renderiza preguntas de tipo `multiple_choice`,
|
||||
agregar un subtítulo/hint debajo del prompt: "Podés marcar más de una opción".
|
||||
Preguntas afectadas: 1.2, 1.3, 1.7, 1.8, 1.9, 1.10, 1.15, 2.3, 3.2, 3.3, 4.2, 6.2.
|
||||
Es un cambio de UI — no afecta seed.sql ni SEED_SPEC.md.
|
||||
|
||||
### Cambio 3 — Agregar "Tecnología asistiva" en 1.3 y 1.10
|
||||
1.3 opciones actuales: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utilizo
|
||||
1.3 opciones nuevas: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utilizo
|
||||
|
||||
1.10 opciones actuales: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · No utiliza
|
||||
1.10 opciones nuevas: Silla de ruedas · Bastón · Andador · Muletas · Prótesis · Tecnología asistiva · No utiliza
|
||||
|
||||
### Cambio 4 — Salto condicional triple + nueva pregunta 6.6
|
||||
|
||||
#### 4a. Lógica de salto
|
||||
Cuando las tres responden "No": 1.1="No" AND 1.6="No" AND 1.13="No",
|
||||
las secciones 2, 3, 4, 5 completas + preguntas 6.1, 6.2, 6.3 se ocultan.
|
||||
El formulario salta directamente a 6.4.
|
||||
|
||||
Implementación sugerida: agregar una regla condicional en las preguntas de las secciones
|
||||
2-5 y 6.1-6.3 que las oculte cuando se cumple esa condición triple. La forma exacta de
|
||||
implementarlo (conditional_rules en cada pregunta vs. lógica en el componente de sección)
|
||||
la decide Claude Code según la arquitectura existente del formulario.
|
||||
|
||||
#### 4b. Nueva pregunta 6.6
|
||||
Agregar al final de la Sección 6 (después de 6.5, antes de Sección 7):
|
||||
- Code: 6.6
|
||||
- Tipo: single_choice
|
||||
- Pregunta: "¿Estarías dispuesto/a a ser voluntario/a en iniciativas y/o redes de apoyo
|
||||
a personas con discapacidad y/o cuidadores?"
|
||||
- Opciones: Sí · No · Tal vez
|
||||
- is_sensitive: false
|
||||
- top-level (siempre visible)
|
||||
|
||||
Actualizar position de 7.1 para que quede después de 6.6.
|
||||
Actualizar el conteo total de preguntas en SEED_SPEC.md (pasa de 44 a 45, o 44 si se
|
||||
compensa con la eliminación de 2.4 — verificar el conteo final).
|
||||
|
||||
### Cambio 5 — 2.1 pasa a multiple_choice
|
||||
Cambiar tipo de `single_choice` a `multiple_choice` en seed.sql y SEED_SPEC.md.
|
||||
Agregar nota en SEED_SPEC.md:
|
||||
"⚠️ TECH-DEBT: múltiple selección sin orden de prioridad. Deuda: implementar ranking
|
||||
(Principal cuidador soy yo > secundario instituto > principal pagado > secundario familiar)."
|
||||
|
||||
### Cambio 6 — Eliminar 2.4
|
||||
Eliminar la pregunta 2.4 ("¿Contás con apoyo de otras personas para el cuidado?")
|
||||
del seed.sql y SEED_SPEC.md.
|
||||
Renumerar positions de las preguntas posteriores si es necesario.
|
||||
Documentar en OBSERVATIONS.md: "2.4 eliminada por feedback equipo médico (similar a 2.1)."
|
||||
|
||||
### Cambio 7 — En 3.1 eliminar "Sin gastos significativos"
|
||||
El cambio 1 ya elimina "Prefiero no decir" de 3.1.
|
||||
Adicionalmente, eliminar "Sin gastos significativos" de las opciones de 3.1.
|
||||
3.1 opciones finales: Menos de Gs. 500.000 · Gs. 500.000–1.500.000 · Gs. 1.500.000–3.000.000
|
||||
· Gs. 3.000.000–6.000.000 · Más de Gs. 6.000.000
|
||||
|
||||
### Cambio 8 — En 3.3 cambiar texto de opción
|
||||
Cambiar: "Es difícil sostenerlo con mis ingresos"
|
||||
Por: "No puedo sostenerlo con mis ingresos"
|
||||
En seed.sql y SEED_SPEC.md.
|
||||
Documentar en OBSERVATIONS.md: "Respuestas existentes con el texto anterior quedan
|
||||
con valor 'Es difícil sostenerlo con mis ingresos' — breaking change en datos existentes."
|
||||
|
||||
### Cambio 9 — En 3.4 reestructurar primera franja salarial
|
||||
Opción actual: "Sueldo mínimo – Gs. 5.000.000"
|
||||
Reemplazar por dos opciones:
|
||||
1. "Sueldo mínimo"
|
||||
2. "Gs. 3.000.000 a 5.000.000"
|
||||
Las demás franjas no cambian.
|
||||
Documentar en OBSERVATIONS.md: "Respuestas existentes con 'Sueldo mínimo – Gs. 5.000.000'
|
||||
quedan con valor anterior — breaking change en datos existentes."
|
||||
|
||||
### Cambio 10 — En 6.1 cambiar texto de última opción
|
||||
Cambiar: "No, nunca se ha hablado del tema"
|
||||
Por: "No, no se puede hablar del tema"
|
||||
En seed.sql y SEED_SPEC.md.
|
||||
Documentar en OBSERVATIONS.md: "Breaking change en datos existentes para esta opción."
|
||||
|
||||
---
|
||||
|
||||
## 5. Archivos a modificar
|
||||
|
||||
| Archivo | Cambios |
|
||||
|---|---|
|
||||
| `SEED_SPEC.md` | Todos los cambios de opciones, tipos, preguntas nuevas/eliminadas |
|
||||
| `supabase/seed.sql` | Todos los cambios — es la fuente de verdad para re-seedear |
|
||||
| Componente formulario (multiple_choice) | Cambio 2: agregar hint |
|
||||
| Lógica condicional del formulario | Cambio 4a: salto triple |
|
||||
| `OBSERVATIONS.md` o `TECH_DEBT.md` | Documentar breaking changes y nueva deuda |
|
||||
|
||||
---
|
||||
|
||||
## 6. Entregables (DoD)
|
||||
|
||||
- [ ] SEED_SPEC.md actualizado con todos los cambios.
|
||||
- [ ] seed.sql actualizado — re-seedeable desde cero produce el instrumento v2.1.
|
||||
- [ ] Componente multiple_choice muestra hint "Podés marcar más de una opción".
|
||||
- [ ] Lógica de salto 1.1+1.6+1.13 todos "No" → salta a 6.4.
|
||||
- [ ] Pregunta 6.6 visible en el formulario.
|
||||
- [ ] "Prefiero no decir" ausente en todas las preguntas.
|
||||
- [ ] 2.4 eliminada del seed.
|
||||
- [ ] Breaking changes documentados en OBSERVATIONS.md.
|
||||
- [ ] Build exitoso.
|
||||
- [ ] `git diff --name-only HEAD` en scope.
|
||||
|
||||
## 7. Qué reportar
|
||||
```
|
||||
Verificado:
|
||||
- SEED_SPEC.md actualizado ✓
|
||||
- seed.sql actualizado ✓
|
||||
- Conteo final de preguntas: [N]
|
||||
- Hint en multiple_choice: ✓
|
||||
- Salto triple implementado: ✓ (describir cómo)
|
||||
- 6.6 agregada ✓
|
||||
- "Prefiero no decir" eliminado de: [lista de preguntas]
|
||||
- 2.4 eliminada ✓
|
||||
- Build exitoso ✓
|
||||
- Diff en scope ✓
|
||||
|
||||
Breaking changes documentados en OBSERVATIONS.md:
|
||||
- [lista]
|
||||
|
||||
Asumido:
|
||||
- [decisiones de implementación]
|
||||
```
|
||||
|
||||
## 8. Qué NO hacer
|
||||
- ❌ No re-seedear la DB de producción — solo actualizar seed.sql.
|
||||
Re-seedear producción requiere aprobación explícita del director.
|
||||
- ❌ No cambiar la estructura de tablas ni agregar columnas — solo opciones y lógica.
|
||||
- ❌ No tocar el dashboard ni las RPCs.
|
||||
- ❌ No resolver ítems de TECH_DEBT que no estén listados acá.
|
||||
- ❌ No cambiar el diseño visual del formulario más allá del hint de multiple_choice.
|
||||
437
sprints/sprint-3-infra/ETAPA_3I_PROMPT.md
Normal file
437
sprints/sprint-3-infra/ETAPA_3I_PROMPT.md
Normal file
@@ -0,0 +1,437 @@
|
||||
# ETAPA 3I — Migración a Supabase self-hosted en Dokploy
|
||||
|
||||
## 1. Objetivo
|
||||
Migrar de Supabase Cloud a Supabase self-hosted en Dokploy, manteniendo Cloud
|
||||
operativo en paralelo durante la transición y como respaldo durante 30 días post-cierre.
|
||||
|
||||
**Esta etapa es operativa, no de código. Nivel 3 extendido por tocar infraestructura.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Modo de ejecución — diferente a etapas anteriores
|
||||
|
||||
Esta etapa **NO se ejecuta de una tirada**. Son 6 pasos atómicos, cada uno con su gate
|
||||
de validación. Después de cada paso, el agente reporta lo verificado y **espera
|
||||
confirmación explícita del director** antes de avanzar al siguiente.
|
||||
|
||||
Si un paso falla, el rollback es el del propio paso (documentado). No avanzar al siguiente
|
||||
hasta que el actual cierre limpio.
|
||||
|
||||
---
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
|
||||
- Supabase self-hosted **completo** (Postgres + Auth + Storage + Realtime + Studio + Kong),
|
||||
no Postgres pelado. Compatibilidad 100% con código actual.
|
||||
- **Partida limpia:** no se migran datos de Cloud. Se aplican las 12 migraciones del repo
|
||||
sobre una DB vacía. No hay nada en Cloud que conservar (testing ya truncado en 2E).
|
||||
- **Cloud y self-hosted en paralelo** durante la transición. Cloud se mantiene 30 días
|
||||
post-validación como respaldo, después se elimina.
|
||||
- **Studio detrás de SSH** (no expuesto a internet sin auth). API por subdominio propio
|
||||
con SSL.
|
||||
- **Backups locales** en el mismo VPS (diarios, retención 30 días, con restore probado).
|
||||
Backup remoto queda como deuda técnica antes del primer dato real.
|
||||
- **VPS objetivo:** el mismo VPS de Dokploy donde corre la app Next.js (17 GB RAM
|
||||
disponibles, según notas del proyecto).
|
||||
- **Dominio nuevo:** `db.inqualityhq.com` o `supabase.inqualityhq.com` para el API
|
||||
(decidir cuál en el paso 4). Studio sin dominio público.
|
||||
|
||||
---
|
||||
|
||||
## 4. PASOS
|
||||
|
||||
### PASO 1 · Levantar Supabase self-hosted en Dokploy
|
||||
|
||||
**Qué hacer:**
|
||||
1. Acceder a Dokploy.
|
||||
2. Crear un nuevo proyecto "supabase-prod" (o nombre equivalente).
|
||||
3. Usar el template oficial de Supabase self-hosted (Dokploy lo tiene como template
|
||||
prefijado, o se carga el `docker-compose.yml` oficial de Supabase desde
|
||||
https://supabase.com/docs/guides/self-hosting/docker).
|
||||
4. Generar los secrets críticos **localmente, no usar los defaults del template:**
|
||||
- `POSTGRES_PASSWORD` — generar con `openssl rand -base64 32`
|
||||
- `JWT_SECRET` — generar con `openssl rand -base64 64`
|
||||
- `ANON_KEY` y `SERVICE_ROLE_KEY` — generar a partir del JWT_SECRET con la
|
||||
herramienta oficial: https://supabase.com/docs/guides/self-hosting/docker#generate-api-keys
|
||||
- `DASHBOARD_USERNAME` y `DASHBOARD_PASSWORD` — generar fuerte (16+ caracteres)
|
||||
5. **NO usar las claves de ejemplo** que vienen en la documentación de Supabase. Esas
|
||||
están publicadas y son inseguras.
|
||||
6. Configurar las variables de entorno del stack en Dokploy.
|
||||
7. Iniciar el stack.
|
||||
8. Esperar que todos los contenedores estén `healthy`.
|
||||
|
||||
**Gate de validación (verificar TODO antes de avanzar al paso 2):**
|
||||
|
||||
```bash
|
||||
# Mostrame el output de estos checks:
|
||||
docker ps --filter "label=com.docker.compose.project=supabase-prod" \
|
||||
--format "table {{.Names}}\t{{.Status}}"
|
||||
# Esperado: ~8 contenedores, todos "Up (healthy)" o "Up"
|
||||
|
||||
# Postgres responde
|
||||
docker exec supabase-db psql -U postgres -c 'SELECT version();'
|
||||
# Esperado: versión de Postgres
|
||||
|
||||
# Kong responde
|
||||
curl -s http://localhost:8000/rest/v1/ -H "apikey: <ANON_KEY>"
|
||||
# Esperado: respuesta JSON (no 404, no 502)
|
||||
|
||||
# Studio responde localmente (todavía no por dominio)
|
||||
curl -s -o /dev/null -w "%{http_code}" http://localhost:3001
|
||||
# Esperado: 200 o 401 (auth requerida)
|
||||
```
|
||||
|
||||
**Qué reportar:**
|
||||
- Confirmación de los 4 checks arriba con outputs reales.
|
||||
- **NUNCA pegar los secrets en el reporte.** Mostrar solo que existen, no su valor.
|
||||
Ejemplo: `JWT_SECRET configurado (longitud: 64 chars) ✓`
|
||||
- Recursos consumidos: `docker stats --no-stream` con los 8 contenedores.
|
||||
|
||||
**Rollback si falla:**
|
||||
- Detener y borrar el stack en Dokploy.
|
||||
- Investigar logs: `docker logs <container>` del que falle.
|
||||
- Volver a empezar el paso 1.
|
||||
|
||||
**DETENERSE. Esperar confirmación del director antes del paso 2.**
|
||||
|
||||
---
|
||||
|
||||
### PASO 2 · Aplicar las 12 migraciones del repo
|
||||
|
||||
**Qué hacer:**
|
||||
1. Conectarse al Postgres self-hosted desde el VPS (no desde fuera):
|
||||
```bash
|
||||
docker exec -it supabase-db psql -U postgres -d postgres
|
||||
```
|
||||
2. Verificar que la DB está vacía:
|
||||
```sql
|
||||
\dt public.*
|
||||
-- Esperado: 0 tablas (o solo las del seed de Supabase, no las del proyecto)
|
||||
```
|
||||
3. Salir del psql.
|
||||
4. Desde el directorio del repo del proyecto (en local del dev, no en el VPS), aplicar
|
||||
las migraciones usando la CLI de Supabase apuntada a la nueva DB:
|
||||
```bash
|
||||
# Configurar conexión a la nueva DB self-hosted
|
||||
# (la DB URL incluye el password generado en paso 1)
|
||||
export SUPABASE_DB_URL="postgresql://postgres:<POSTGRES_PASSWORD>@<VPS_IP>:5432/postgres"
|
||||
|
||||
# O usar supabase CLI con link:
|
||||
supabase link --project-ref <self-hosted-ref> --db-url $SUPABASE_DB_URL
|
||||
|
||||
# Aplicar migraciones
|
||||
supabase db push --db-url $SUPABASE_DB_URL
|
||||
```
|
||||
**Alternativa si la CLI tiene problemas:** copiar el contenido de cada archivo
|
||||
`supabase/migrations/*.sql` y ejecutarlo manualmente con `psql`, en orden de timestamp.
|
||||
|
||||
**Gate de validación:**
|
||||
```sql
|
||||
-- Conteo de objetos creados (esperado: 12 tablas + N funciones)
|
||||
SELECT count(*) FROM pg_tables WHERE schemaname='public';
|
||||
-- Esperado: ≥10 tablas (surveys, responses, answers, consent_records, questions, etc.)
|
||||
|
||||
SELECT count(*) FROM pg_proc p
|
||||
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||
WHERE n.nspname='public' AND p.proname LIKE 'rpc_%';
|
||||
-- Esperado: 9 funciones rpc_* (las del dashboard de Etapa 2E)
|
||||
|
||||
-- Seed cargado
|
||||
SELECT count(*) FROM public.surveys;
|
||||
-- Esperado: 1 (la encuesta piloto)
|
||||
|
||||
SELECT count(*) FROM public.questions;
|
||||
-- Esperado: 44
|
||||
|
||||
SELECT count(*) FROM public.question_options;
|
||||
-- Esperado: ≥200 (las opciones de las preguntas single/multiple choice)
|
||||
|
||||
-- RLS habilitada
|
||||
SELECT tablename FROM pg_tables WHERE schemaname='public'
|
||||
AND rowsecurity = false;
|
||||
-- Esperado: vacío (todas las tablas con RLS habilitada)
|
||||
|
||||
-- Test funcional: una RPC del dashboard devuelve suppressed con N=0
|
||||
SELECT rpc_dashboard_kpis('30000000-0000-0000-0000-000000000001'::uuid, '{}'::jsonb);
|
||||
-- Esperado: status='suppressed'
|
||||
```
|
||||
|
||||
**Qué reportar:**
|
||||
- Outputs de los 6 queries arriba.
|
||||
- Si algún número está fuera de lo esperado: detener, no continuar.
|
||||
|
||||
**Rollback si falla:**
|
||||
- `TRUNCATE` no aplica todavía (no hay nada que conservar).
|
||||
- Borrar la DB y recrearla:
|
||||
```bash
|
||||
docker exec supabase-db psql -U postgres -c 'DROP DATABASE postgres;'
|
||||
# Reiniciar el contenedor para que reaplique el init
|
||||
```
|
||||
- Reaplicar migraciones desde cero.
|
||||
|
||||
**DETENERSE. Esperar confirmación del director antes del paso 3.**
|
||||
|
||||
---
|
||||
|
||||
### PASO 3 · Backups locales con prueba de restore
|
||||
|
||||
**Qué hacer:**
|
||||
1. Crear directorio de backups en el VPS: `mkdir -p /var/backups/supabase-prod && chmod 700 /var/backups/supabase-prod`
|
||||
2. Crear script de backup `/usr/local/bin/supabase-backup.sh`:
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
TS=$(date +%Y%m%d_%H%M%S)
|
||||
OUT=/var/backups/supabase-prod/supabase_$TS.sql.gz
|
||||
docker exec supabase-db pg_dump -U postgres postgres | gzip > "$OUT"
|
||||
chmod 600 "$OUT"
|
||||
# Retención 30 días
|
||||
find /var/backups/supabase-prod -name "supabase_*.sql.gz" -mtime +30 -delete
|
||||
echo "Backup OK: $OUT ($(du -h "$OUT" | cut -f1))"
|
||||
```
|
||||
3. `chmod +x /usr/local/bin/supabase-backup.sh`
|
||||
4. Probar manualmente: ejecutarlo una vez, verificar que el archivo se crea.
|
||||
5. Agregar a crontab: `0 3 * * * /usr/local/bin/supabase-backup.sh >> /var/log/supabase-backup.log 2>&1`
|
||||
6. **Prueba de restore obligatoria:**
|
||||
- Crear una DB temporal de prueba: `docker exec supabase-db createdb -U postgres restore_test`
|
||||
- Restaurar el último backup en esa DB: `gunzip < /var/backups/supabase-prod/supabase_*.sql.gz | docker exec -i supabase-db psql -U postgres restore_test`
|
||||
- Verificar que tiene los mismos objetos que la DB principal:
|
||||
```sql
|
||||
\c restore_test
|
||||
SELECT count(*) FROM public.questions;
|
||||
-- Esperado: 44 (mismo que la principal)
|
||||
```
|
||||
- Eliminar la DB de prueba: `docker exec supabase-db dropdb -U postgres restore_test`
|
||||
|
||||
**Gate de validación:**
|
||||
- Script ejecutable y verificado manualmente.
|
||||
- Backup más reciente: `ls -lh /var/backups/supabase-prod/` muestra al menos un archivo.
|
||||
- Cron line confirmada: `crontab -l | grep supabase-backup`
|
||||
- Restore probado: la DB de prueba mostró el conteo correcto antes de eliminarse.
|
||||
|
||||
**Qué reportar:**
|
||||
- Tamaño del primer backup (informativo: nos da una idea de cuánto va a crecer).
|
||||
- Confirmación de los 4 puntos del gate.
|
||||
|
||||
**Rollback si falla:**
|
||||
- Eliminar el script y el cron.
|
||||
- Re-investigar y volver a configurar.
|
||||
|
||||
**Deuda técnica explícita** (agregar a TECH_DEBT.md como TECH-DEBT-006):
|
||||
> Backup remoto pendiente. Hoy solo local en el mismo VPS. Si el VPS muere, se pierden
|
||||
> los datos. **Bloqueante antes del primer dato real de empresa.** Recomendación:
|
||||
> rsync nocturno a S3-compatible (Backblaze B2 o Wasabi, ~$5/mes).
|
||||
|
||||
**DETENERSE. Esperar confirmación del director antes del paso 4.**
|
||||
|
||||
---
|
||||
|
||||
### PASO 4 · Dominio + SSL para API, Studio detrás de SSH
|
||||
|
||||
**Qué hacer:**
|
||||
1. **Decidir nombre del subdominio para API.** Recomendación: `db.inqualityhq.com`.
|
||||
2. Crear el registro DNS A apuntando al IP del VPS (esto se hace en el panel de DNS
|
||||
del dominio, fuera de Dokploy).
|
||||
3. En Dokploy, configurar el reverse proxy / Traefik:
|
||||
- Ruta `db.inqualityhq.com` → contenedor `supabase-kong:8000`.
|
||||
- SSL automático con Let's Encrypt.
|
||||
4. Esperar a que el certificado SSL se emita (1-5 min normalmente).
|
||||
5. **Bloquear Studio del exterior:**
|
||||
- El contenedor `supabase-studio` (puerto 3001) NO debe tener regla de exposición
|
||||
pública en Dokploy/Traefik.
|
||||
- Verificar con: `curl http://<VPS_IP>:3001` desde fuera del VPS → debe **fallar**
|
||||
(connection refused o timeout).
|
||||
6. **Acceso a Studio vía SSH tunnel** (documentar el comando para el director):
|
||||
```bash
|
||||
# Desde la máquina del dev:
|
||||
ssh -L 3001:localhost:3001 usuario@<VPS_IP>
|
||||
# Después abrir http://localhost:3001 en el navegador
|
||||
```
|
||||
|
||||
**Gate de validación:**
|
||||
```bash
|
||||
# API por dominio con SSL
|
||||
curl -s https://db.inqualityhq.com/rest/v1/ -H "apikey: <ANON_KEY>"
|
||||
# Esperado: respuesta JSON, no error de certificado
|
||||
|
||||
# SSL válido
|
||||
curl -vI https://db.inqualityhq.com/ 2>&1 | grep "SSL certificate verify"
|
||||
# Esperado: "SSL certificate verify ok"
|
||||
|
||||
# Studio NO accesible desde fuera
|
||||
curl -s -o /dev/null -w "%{http_code}" http://<VPS_IP>:3001 --max-time 5
|
||||
# Esperado: timeout o connection refused (no 200)
|
||||
|
||||
# Studio sí accesible vía túnel SSH (documentar instrucciones para el director)
|
||||
```
|
||||
|
||||
**Qué reportar:**
|
||||
- URL del API funcionando con SSL.
|
||||
- Confirmación de que Studio no está expuesto.
|
||||
- Comando exacto para que el director acceda a Studio por SSH tunnel.
|
||||
|
||||
**Rollback si falla:**
|
||||
- Quitar el registro DNS.
|
||||
- Quitar la regla de Traefik.
|
||||
- Volver a usar IP+puerto temporalmente.
|
||||
|
||||
**DETENERSE. Esperar confirmación del director antes del paso 5.**
|
||||
|
||||
---
|
||||
|
||||
### PASO 5 · Cambiar variables de entorno de la app Next.js + deploy
|
||||
|
||||
**ESTE ES EL PASO MÁS CRÍTICO. El error más fácil de cometer es apuntar la app a la
|
||||
DB nueva pero dejar alguna variable apuntando a Cloud — comportamiento mixto, datos
|
||||
inconsistentes.**
|
||||
|
||||
**Qué hacer:**
|
||||
1. Listar TODAS las variables de entorno de la app actual relacionadas con Supabase:
|
||||
```
|
||||
NEXT_PUBLIC_SUPABASE_URL
|
||||
NEXT_PUBLIC_SUPABASE_ANON_KEY
|
||||
SUPABASE_SERVICE_ROLE_KEY (server-only)
|
||||
IP_HASH_SALT (independiente, no tocar)
|
||||
```
|
||||
Verificar que **no hay otras** referencias a Cloud en el código:
|
||||
```bash
|
||||
grep -rE "supabase\.co|<cloud-project-ref>" --include="*.ts" --include="*.tsx" \
|
||||
--include="*.env*" .
|
||||
# Esperado: vacío o solo en comentarios
|
||||
```
|
||||
2. Generar un snapshot de las variables ACTUALES (sin pegarlas en el reporte):
|
||||
```bash
|
||||
# En el dashboard de Dokploy de la app Next.js, exportar las env vars actuales
|
||||
# Guardarlas en un archivo local seguro como backup
|
||||
```
|
||||
3. Actualizar en Dokploy las 3 variables:
|
||||
- `NEXT_PUBLIC_SUPABASE_URL` → `https://db.inqualityhq.com`
|
||||
- `NEXT_PUBLIC_SUPABASE_ANON_KEY` → la nueva (de paso 1)
|
||||
- `SUPABASE_SERVICE_ROLE_KEY` → la nueva (de paso 1)
|
||||
4. Trigger redeploy de la app Next.js.
|
||||
5. Esperar que termine el build y arranque.
|
||||
|
||||
**Gate de validación:**
|
||||
- App Next.js arranca sin errores (`docker logs` del contenedor de la app).
|
||||
- Abrir `https://economia-cuidado.inqualityhq.com` — el consent screen carga.
|
||||
- En la consola del navegador, verificar que las requests van a `db.inqualityhq.com`,
|
||||
NO a `*.supabase.co`.
|
||||
- Verificar que NO hay errores 401/403 en el Network tab del navegador.
|
||||
|
||||
**Qué reportar:**
|
||||
- Confirmación de que la app arranca.
|
||||
- Captura de Network tab mostrando requests a `db.inqualityhq.com`.
|
||||
- Confirmación de que no hay errores en consola del navegador.
|
||||
|
||||
**Rollback si falla:**
|
||||
- Volver las 3 variables a los valores anteriores (los del archivo backup del punto 2).
|
||||
- Trigger redeploy.
|
||||
- La app vuelve a apuntar a Cloud. **Cloud sigue vivo, así que rollback es completo.**
|
||||
|
||||
**DETENERSE. Esperar confirmación del director antes del paso 6.**
|
||||
|
||||
---
|
||||
|
||||
### PASO 6 · Validación funcional end-to-end + ventana de observación
|
||||
|
||||
**Qué hacer:**
|
||||
1. Director ejecuta manualmente el flujo completo:
|
||||
- Abre `https://economia-cuidado.inqualityhq.com?s=30000000-0000-0000-0000-000000000001`
|
||||
- Acepta los 2 consentimientos.
|
||||
- Completa el formulario hasta el final.
|
||||
- Envía.
|
||||
- Verifica que llegó al "gracias por participar" o equivalente.
|
||||
2. Agente verifica en la nueva DB que se persistió:
|
||||
```sql
|
||||
-- Conectado a la self-hosted DB
|
||||
SELECT count(*) FROM responses WHERE survey_id = '30000000-0000-0000-0000-000000000001';
|
||||
-- Esperado: ≥1
|
||||
|
||||
SELECT count(*) FROM answers a
|
||||
JOIN responses r ON a.response_id = r.id
|
||||
WHERE r.survey_id = '30000000-0000-0000-0000-000000000001';
|
||||
-- Esperado: ≥1 (varias filas)
|
||||
|
||||
SELECT count(*) FROM consent_records;
|
||||
-- Esperado: ≥1
|
||||
|
||||
-- Verificar qi_zone se popula (Etapa 2E)
|
||||
SELECT qi_zone, count(*) FROM responses GROUP BY qi_zone;
|
||||
-- Esperado: al menos una fila con qi_zone NO NULL
|
||||
```
|
||||
3. Abrir `/dashboard?survey_id=...` y verificar que los widgets muestran suppressed
|
||||
(N<5 todavía).
|
||||
4. Cargar 4 respuestas más para alcanzar N=5 y verificar que al menos un widget pasa
|
||||
a estado `ok` (validación de que el k-anonimato libera datos al cruzar el umbral).
|
||||
5. **Ventana de observación de 24-48h:**
|
||||
- Sin tráfico real (todavía no hay piloto), durante este período el director
|
||||
y/o el agente monitorean los logs por errores no esperados.
|
||||
6. Truncar las respuestas de prueba (igual que en Etapa 2E):
|
||||
```sql
|
||||
TRUNCATE answers, responses, consent_records RESTART IDENTITY CASCADE;
|
||||
```
|
||||
|
||||
**Gate de validación final:**
|
||||
- Flujo end-to-end funcionó.
|
||||
- Datos llegaron a la DB correcta (self-hosted).
|
||||
- qi_zone se populó (Etapa 2E está funcionando).
|
||||
- Dashboard responde con datos reales (sale de suppressed al cruzar k=5).
|
||||
- 24-48h sin errores en logs.
|
||||
- Tablas truncadas, queda solo el seed.
|
||||
|
||||
**Qué reportar:**
|
||||
- Capturas del flujo end-to-end completado.
|
||||
- Outputs de los queries de verificación.
|
||||
- Confirmación del período de observación sin errores.
|
||||
- Confirmación de que las tablas se truncaron al final.
|
||||
|
||||
**DETENERSE. Esperar confirmación del director.**
|
||||
|
||||
**Recién con esta confirmación, Cloud queda como respaldo congelado por 30 días.**
|
||||
|
||||
---
|
||||
|
||||
## 5. DoD de la etapa completa
|
||||
|
||||
- [ ] Los 6 pasos completados con sus gates de validación.
|
||||
- [ ] Director confirmó cada paso antes del siguiente.
|
||||
- [ ] Supabase self-hosted operativo en Dokploy.
|
||||
- [ ] App Next.js apuntando a la nueva DB.
|
||||
- [ ] Backups locales diarios configurados, restore probado.
|
||||
- [ ] Studio no expuesto a internet (acceso solo por SSH tunnel).
|
||||
- [ ] API por subdominio con SSL.
|
||||
- [ ] TECH-DEBT-006 agregado (backup remoto pendiente antes de dato real).
|
||||
- [ ] Cloud sigue operativa pero NO recibe tráfico. Marcada para eliminación a 30 días.
|
||||
|
||||
## 6. Qué reportar al final de la etapa
|
||||
```
|
||||
Migración Supabase Cloud → self-hosted completada el [fecha].
|
||||
|
||||
Self-hosted operativo en:
|
||||
- API: https://db.inqualityhq.com
|
||||
- Studio: solo vía SSH tunnel
|
||||
- Backups: /var/backups/supabase-prod (diarios, retención 30d, restore probado)
|
||||
|
||||
App Next.js:
|
||||
- Apuntando a self-hosted ✓
|
||||
- Flujo end-to-end validado ✓
|
||||
- Sin errores en ventana de observación de [X] horas ✓
|
||||
|
||||
Cloud:
|
||||
- Sigue activa, sin tráfico.
|
||||
- Marcada para eliminación: [fecha + 30 días].
|
||||
|
||||
Pendiente / deuda técnica:
|
||||
- TECH-DEBT-006: backup remoto antes del primer dato real.
|
||||
|
||||
Decisiones proactivas (Nivel 2):
|
||||
- [si las hubo]
|
||||
```
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ Avanzar al siguiente paso sin confirmación del director.
|
||||
- ❌ Pegar secrets (passwords, keys) en reportes — solo confirmar que existen.
|
||||
- ❌ Eliminar la cuenta de Supabase Cloud antes de los 30 días post-validación.
|
||||
- ❌ Tocar código de la aplicación (esta etapa es 100% infraestructura).
|
||||
- ❌ Usar los secrets de ejemplo de la documentación de Supabase — todos generados localmente.
|
||||
- ❌ Exponer Studio a internet sin auth.
|
||||
- ❌ Saltarse la prueba de restore (un backup sin restore probado no es un backup).
|
||||
175
sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md
Normal file
175
sprints/sprint-3-infra/ETAPA_3V_B_PROMPT.md
Normal file
@@ -0,0 +1,175 @@
|
||||
# ETAPA 3V-B — Vista de control: tabla plana de respuestas individuales
|
||||
|
||||
## 1. Objetivo
|
||||
Reemplazar la vista `/admin/responses` actual (una fila por submission) por una tabla plana
|
||||
con **una fila por respuesta individual** (una fila por cada pregunta respondida), mostrando
|
||||
todos los campos explícitamente incluyendo nulos marcados visualmente.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexto previo
|
||||
- La Etapa 3V-A implementó la vista de metadatos de submissions (una fila por response).
|
||||
- El director quiere ver las respuestas individuales a las preguntas, no solo los metadatos.
|
||||
- El esquema relevante: `responses` → `answers` (via response_id) → `questions` (via question_id).
|
||||
- `answers.value` es JSONB. Los tipos de preguntas existentes en el instrumento son:
|
||||
- `single_choice` → value es string: `"Asunción (Capital)"`
|
||||
- `multiple_choice` → value es array: `["opción1","opción2"]`
|
||||
- `rating` → value es número: `3`
|
||||
- `text_short` → value es string corto
|
||||
- `text_long` → **NUNCA mostrar** — regla no negociable (RLS + privacidad)
|
||||
|
||||
---
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
|
||||
- Ruta: `/admin/responses` — reemplaza la página anterior completamente.
|
||||
- Sin auth (herramienta interna de control).
|
||||
- Sin paginación por ahora — máximo 500 filas (limit en la query).
|
||||
- Ordenado por `responses.submitted_at DESC`, luego por `questions.position ASC` dentro de cada submission.
|
||||
- Valores nulos mostrados con badge visual "sin dato" — fondo `--color-re-light` (#E7E6E5), texto `--color-re-dark` (#494F4F) opacidad 0.6.
|
||||
- text_long excluido completamente — ni la columna ni el valor aparecen.
|
||||
- Convención CSS: `.admin-*` en `app/globals.css`, igual que la etapa anterior.
|
||||
- Cliente service-role inline en el Server Component, igual que en la versión anterior.
|
||||
|
||||
---
|
||||
|
||||
## 4. Estructura de la tabla
|
||||
|
||||
### 4.1 Encabezado de página
|
||||
- Título: "Control de respuestas"
|
||||
- Subtítulo: total de submissions + total de respuestas individuales + fecha/hora actualización.
|
||||
- Botón "Actualizar" (`<a href="/admin/responses">`).
|
||||
|
||||
### 4.2 Columnas de la tabla (una fila por answer)
|
||||
|
||||
| Columna | Fuente | Notas |
|
||||
|---|---|---|
|
||||
| # submission | índice de la submission (1, 2, 3...) | mismo número para todas las filas de esa submission |
|
||||
| Fecha | `responses.submitted_at` | DD/MM/YYYY HH:mm — solo en la primera fila de cada submission |
|
||||
| Código | `questions.code` | A1, A2, 1.1, 1.6, etc. |
|
||||
| Pregunta | `questions.prompt` | texto de la pregunta, truncado a 60 chars con "..." |
|
||||
| Tipo | `questions.type` | badge con el tipo |
|
||||
| Valor | `answers.value` | renderizado según tipo (ver 4.3) |
|
||||
| Departamento | `responses.qi_zone` | solo en primera fila de cada submission |
|
||||
| Sector | `responses.qi_sector` | solo en primera fila de cada submission |
|
||||
|
||||
### 4.3 Renderizado del valor según tipo
|
||||
|
||||
```typescript
|
||||
function renderValue(value: unknown, type: string): string {
|
||||
if (value === null || value === undefined) return null // → badge "sin dato"
|
||||
|
||||
switch(type) {
|
||||
case 'single_choice':
|
||||
case 'text_short':
|
||||
return String(value).replace(/^"|"$/g, '') // quitar comillas del JSONB string
|
||||
|
||||
case 'multiple_choice':
|
||||
if (Array.isArray(value)) return value.join(', ')
|
||||
return String(value)
|
||||
|
||||
case 'rating':
|
||||
return String(value) // número, opcionalmente con ★ repetidas
|
||||
|
||||
case 'text_long':
|
||||
return null // NUNCA mostrar — excluir de la query directamente
|
||||
|
||||
default:
|
||||
return String(value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 Agrupación visual de submissions
|
||||
Las filas de una misma submission deben estar visualmente agrupadas:
|
||||
- Separador visual entre submissions (border-top más pronunciado o fondo alternado por submission).
|
||||
- Fecha, Departamento y Sector solo se muestran en la primera fila de cada submission.
|
||||
- Las filas siguientes de la misma submission muestran esas celdas vacías (o con colspan si es HTML puro).
|
||||
|
||||
---
|
||||
|
||||
## 5. Query recomendada
|
||||
|
||||
```typescript
|
||||
const { data } = await supabase
|
||||
.from('responses')
|
||||
.select(`
|
||||
id,
|
||||
submitted_at,
|
||||
qi_zone,
|
||||
qi_sector,
|
||||
qi_age_bucket,
|
||||
ip_hash,
|
||||
surveys ( title ),
|
||||
answers (
|
||||
value,
|
||||
questions (
|
||||
code,
|
||||
prompt,
|
||||
type,
|
||||
position,
|
||||
is_sensitive
|
||||
)
|
||||
)
|
||||
`)
|
||||
.order('submitted_at', { ascending: false })
|
||||
.limit(500)
|
||||
```
|
||||
|
||||
Después en el componente, aplanar el resultado:
|
||||
```typescript
|
||||
// Para cada response, para cada answer (excluir text_long e is_sensitive):
|
||||
// generar una fila con los datos de la response + los datos de la answer/question
|
||||
const rows = responses.flatMap((response, responseIdx) =>
|
||||
response.answers
|
||||
.filter(a => a.questions?.type !== 'text_long' && !a.questions?.is_sensitive)
|
||||
.sort((a, b) => (a.questions?.position ?? 0) - (b.questions?.position ?? 0))
|
||||
.map((answer, answerIdx) => ({
|
||||
responseIdx: responseIdx + 1,
|
||||
isFirstInGroup: answerIdx === 0,
|
||||
submitted_at: response.submitted_at,
|
||||
qi_zone: response.qi_zone,
|
||||
qi_sector: response.qi_sector,
|
||||
ip_hash: response.ip_hash,
|
||||
survey_title: response.surveys?.title,
|
||||
code: answer.questions?.code,
|
||||
prompt: answer.questions?.prompt,
|
||||
type: answer.questions?.type,
|
||||
value: answer.value,
|
||||
}))
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Entregables (DoD)
|
||||
|
||||
- [ ] Ruta `/admin/responses` renderiza con tabla plana (HTTP 200).
|
||||
- [ ] Una fila por answer, agrupadas visualmente por submission.
|
||||
- [ ] Columnas: # submission, Fecha, Código, Pregunta (truncada), Tipo, Valor, Departamento, Sector.
|
||||
- [ ] `text_long` e `is_sensitive=true` excluidos.
|
||||
- [ ] Valores nulos muestran badge "sin dato" con estilo tenue.
|
||||
- [ ] Fecha, Departamento, Sector solo en primera fila de cada grupo.
|
||||
- [ ] Build exitoso.
|
||||
- [ ] `git diff --name-only HEAD` solo en `app/admin/` y `app/globals.css`.
|
||||
|
||||
## 7. Qué reportar
|
||||
```
|
||||
Verificado:
|
||||
- Ruta /admin/responses accesible ✓
|
||||
- Total submissions: [N] · Total filas en tabla: [N]
|
||||
- Tipos de preguntas visibles: [lista]
|
||||
- text_long excluido ✓ / is_sensitive excluido ✓
|
||||
- Badge "sin dato" visible en campos nulos ✓ / no hay nulos en los datos actuales
|
||||
- Build exitoso ✓
|
||||
- Diff en scope ✓
|
||||
|
||||
Asumido:
|
||||
- [decisiones cosméticas]
|
||||
```
|
||||
|
||||
## 8. Qué NO hacer
|
||||
- ❌ No mostrar text_long ni is_sensitive bajo ningún concepto.
|
||||
- ❌ No exponer SUPABASE_SERVICE_ROLE_KEY al cliente.
|
||||
- ❌ No tocar el formulario ni el dashboard.
|
||||
- ❌ No agregar dependencias npm.
|
||||
- ❌ No paginar todavía — limit 500 es suficiente para el piloto.
|
||||
133
sprints/sprint-3-infra/ETAPA_3V_PROMPT.md
Normal file
133
sprints/sprint-3-infra/ETAPA_3V_PROMPT.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# ETAPA 3V — Vista de control: listado de respuestas
|
||||
|
||||
## 1. Objetivo
|
||||
Crear una página de control operativo en `/admin/responses` que muestre todas las
|
||||
respuestas recibidas, ordenadas por fecha descendente (más recientes primero).
|
||||
Es una herramienta interna de diagnóstico — sin auth, sin PII, sin datos sensibles.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexto previo
|
||||
- La migración está en Supabase self-hosted.
|
||||
- Las tablas relevantes son `responses`, `answers`, `questions`, `surveys`.
|
||||
- `answers.value` es JSONB — contiene el valor de la respuesta.
|
||||
- Campos sensibles protegidos por RLS: `answers` donde `question.is_sensitive=true`
|
||||
y los tipos `text_long`. No mostrar esos campos aunque lleguen.
|
||||
- La página usa el cliente server-side (`SUPABASE_SERVICE_ROLE_KEY`) porque bypasa RLS
|
||||
y permite ver todos los datos. No usar el cliente anon.
|
||||
|
||||
---
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
|
||||
- Ruta: `/admin/responses` — Server Component de Next.js (no client component).
|
||||
- Sin auth (es herramienta interna de control).
|
||||
- Sin paginación por ahora — mostrar las últimas 200 respuestas máximo.
|
||||
- Ordenado por `submitted_at DESC`.
|
||||
- HTML semántico limpio, paleta RE existente (`#43BBC8`, `#494F4F`, `#F8AD13`).
|
||||
- No usar componentes del dashboard existente — página standalone.
|
||||
|
||||
---
|
||||
|
||||
## 4. Qué mostrar
|
||||
|
||||
### 4.1 Encabezado de página
|
||||
- Título: "Control de respuestas"
|
||||
- Subtítulo: conteo total de respuestas en la base + fecha/hora de última actualización.
|
||||
- Botón "Actualizar" que recarga la página (simple `<a href="/admin/responses">`).
|
||||
|
||||
### 4.2 Tabla de respuestas (una fila por `response`)
|
||||
|
||||
Columnas:
|
||||
|
||||
| Columna | Fuente | Notas |
|
||||
|---|---|---|
|
||||
| # | índice visual (1, 2, 3...) | más reciente = 1 |
|
||||
| Fecha | `responses.submitted_at` | formato: DD/MM/YYYY HH:mm |
|
||||
| Encuesta | `surveys.title` | JOIN con surveys |
|
||||
| Departamento | `responses.qi_zone` | puede ser null → mostrar "—" |
|
||||
| Sector | `responses.qi_sector` | puede ser null → mostrar "—" |
|
||||
| Rango etario | `responses.qi_age_bucket` | puede ser null → mostrar "—" |
|
||||
| Respuestas | count de `answers` para esa response | número simple |
|
||||
| IP hash | `responses.ip_hash` | primeros 8 chars + "..." — solo para dedup |
|
||||
|
||||
### 4.3 Detalle expandible (opcional, Nivel 2 — reportar si se implementa)
|
||||
Si el tiempo lo permite: al hacer click en una fila, mostrar un panel con las
|
||||
respuestas individuales (código de pregunta + valor), excluyendo:
|
||||
- Preguntas con `is_sensitive = true`
|
||||
- Preguntas de tipo `text_long`
|
||||
|
||||
Si no se implementa el detalle, la tabla sola es suficiente para cerrar la etapa.
|
||||
|
||||
---
|
||||
|
||||
## 5. Implementación
|
||||
|
||||
### 5.1 Query recomendada
|
||||
```typescript
|
||||
// En un Server Component — usar el cliente con service_role
|
||||
const { data: responses } = await supabase
|
||||
.from('responses')
|
||||
.select(`
|
||||
id,
|
||||
submitted_at,
|
||||
qi_zone,
|
||||
qi_sector,
|
||||
qi_age_bucket,
|
||||
ip_hash,
|
||||
surveys ( title ),
|
||||
answers ( count )
|
||||
`)
|
||||
.order('submitted_at', { ascending: false })
|
||||
.limit(200)
|
||||
```
|
||||
|
||||
### 5.2 Cliente server-side
|
||||
Usar `createClient` con `SUPABASE_SERVICE_ROLE_KEY` (server-side only, no exponer
|
||||
al cliente). Si existe `lib/supabase-server.ts` o similar, usarlo. Si no, crear
|
||||
el cliente inline en el Server Component con la service role key.
|
||||
|
||||
### 5.3 Estilos
|
||||
- Paleta RE: teal `#43BBC8`, dark `#494F4F`, orange `#F8AD13`, light `#E7E6E5`.
|
||||
- Tabla con filas alternadas (zebra), hover sutil.
|
||||
- Header de tabla sticky si la lista es larga.
|
||||
- Responsive — en mobile, las columnas menos importantes se ocultan.
|
||||
- Fuente: la que ya usa el proyecto (IBM Plex Sans o la que esté configurada).
|
||||
- NO usar Tailwind si el proyecto no lo usa — verificar primero con
|
||||
`grep "tailwind" package.json`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Entregables (DoD)
|
||||
|
||||
- [ ] Ruta `/admin/responses` accesible y renderiza.
|
||||
- [ ] Tabla muestra respuestas ordenadas por fecha descendente.
|
||||
- [ ] Conteo total en el encabezado.
|
||||
- [ ] Columnas: #, Fecha, Encuesta, Departamento, Sector, Rango etario, Respuestas, IP hash (truncado).
|
||||
- [ ] Campos nulos muestran "—".
|
||||
- [ ] IP hash truncada (primeros 8 chars).
|
||||
- [ ] Build sin errores (`next build`).
|
||||
- [ ] `git diff --name-only HEAD` solo en `app/admin/` y posiblemente `lib/`.
|
||||
|
||||
## 7. Qué reportar
|
||||
```
|
||||
Verificado:
|
||||
- Ruta /admin/responses accesible ✓
|
||||
- Columnas presentes: [lista]
|
||||
- Conteo total: [número]
|
||||
- Build exitoso ✓
|
||||
- Diff en scope ✓
|
||||
|
||||
Asumido:
|
||||
- [decisiones cosméticas o de implementación]
|
||||
|
||||
Decisiones proactivas (Nivel 2):
|
||||
- [si implementó el detalle expandible o no]
|
||||
```
|
||||
|
||||
## 8. Qué NO hacer
|
||||
- ❌ No agregar auth — es herramienta interna, la URL es suficiente como "seguridad por oscuridad" por ahora.
|
||||
- ❌ No mostrar campos text_long ni is_sensitive=true.
|
||||
- ❌ No exponer SUPABASE_SERVICE_ROLE_KEY al cliente (solo server-side).
|
||||
- ❌ No tocar el formulario ni el dashboard existente.
|
||||
- ❌ No agregar dependencias npm.
|
||||
- ❌ No resolver ítems del backlog que no sean los listados acá.
|
||||
124
sprints/sprint-3-instrumento/ETAPA_3ALL_PROMPT.md
Normal file
124
sprints/sprint-3-instrumento/ETAPA_3ALL_PROMPT.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# ETAPA 3ALL — Flag p_all en el dashboard (vista agregada de todos los surveys)
|
||||
|
||||
## 1. Objetivo
|
||||
Permitir que el dashboard muestre datos de TODOS los surveys mezclados, usando un
|
||||
UUID especial como flag. Cambio mínimo — solo toca `_dash_responses` y el frontend.
|
||||
Las 11 RPCs no se modifican.
|
||||
|
||||
---
|
||||
|
||||
## 2. Contexto previo
|
||||
- `_dash_responses(p_survey_id uuid, ...)` es la función base que usan las 11 RPCs.
|
||||
- Todas las RPCs pasan `p_survey_id` a `_dash_responses`.
|
||||
- El dashboard lee `?survey_id=UUID` de la URL y lo pasa a `/api/dashboard/[widget]`.
|
||||
- `route.ts` requiere `survey_id` en la query string (400 si falta).
|
||||
|
||||
## 3. DECISIONES YA TOMADAS — no consultar de nuevo
|
||||
- UUID especial para "todos": `00000000-0000-0000-0000-000000000000` (UUID cero).
|
||||
No puede existir como survey real. Es el flag `p_all`.
|
||||
- Implementación en `_dash_responses`: una sola línea cambia.
|
||||
- Las 11 RPCs NO se tocan — el cambio es transparente para ellas.
|
||||
- El cambio va en una NUEVA migración, no modifica las existentes.
|
||||
- El k-anonimato sigue aplicando — el pool más grande solo beneficia la protección.
|
||||
|
||||
---
|
||||
|
||||
## 4. Cambios requeridos
|
||||
|
||||
### 4.1 Nueva migración SQL
|
||||
Crear `supabase/migrations/20260611000001_dash_responses_all.sql`:
|
||||
|
||||
```sql
|
||||
-- Migración: agregar flag p_all a _dash_responses
|
||||
-- UUID especial '00000000-0000-0000-0000-000000000000' = todos los surveys
|
||||
CREATE OR REPLACE FUNCTION public._dash_responses(
|
||||
p_survey_id uuid,
|
||||
p_depto text DEFAULT NULL,
|
||||
p_genero text[] DEFAULT NULL,
|
||||
p_etario text[] DEFAULT NULL,
|
||||
p_modalidad text[] DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE(response_id uuid)
|
||||
LANGUAGE sql STABLE SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT r.id
|
||||
FROM public.responses r
|
||||
WHERE (p_survey_id = '00000000-0000-0000-0000-000000000000'
|
||||
OR r.survey_id = p_survey_id)
|
||||
AND (p_depto IS NULL OR r.qi_zone = p_depto)
|
||||
AND (p_genero IS NULL OR cardinality(p_genero) = 0 OR EXISTS (
|
||||
SELECT 1 FROM public.answers a
|
||||
JOIN public.questions q ON q.id = a.question_id
|
||||
WHERE a.response_id = r.id AND q.code = 'A6'
|
||||
AND a.value #>> '{}' = ANY(p_genero)
|
||||
))
|
||||
AND (p_etario IS NULL OR cardinality(p_etario) = 0
|
||||
OR r.qi_age_bucket = ANY(p_etario))
|
||||
AND (p_modalidad IS NULL OR cardinality(p_modalidad) = 0 OR EXISTS (
|
||||
SELECT 1 FROM public.answers a
|
||||
JOIN public.questions q ON q.id = a.question_id
|
||||
WHERE a.response_id = r.id AND q.code = 'A7'
|
||||
AND a.value #>> '{}' = ANY(p_modalidad)
|
||||
));
|
||||
$$;
|
||||
```
|
||||
|
||||
### 4.2 route.ts — permitir el UUID especial
|
||||
En `app/api/dashboard/[widget]/route.ts`, donde se valida `survey_id`:
|
||||
El UUID cero debe pasar la validación existente — verificar si hay algún check
|
||||
adicional que lo rechazaría (ej. validación de formato UUID o lookup en DB).
|
||||
Si `route.ts` solo verifica presencia del parámetro (400 si falta), no hay
|
||||
nada que cambiar. Reportar cuál es el caso.
|
||||
|
||||
### 4.3 dashboard/page.tsx — agregar link/selector "Todos"
|
||||
Agregar en el header del dashboard un selector o links de navegación mínimo:
|
||||
|
||||
```
|
||||
[ Todos ] [ InQuality ] [ Fundación Solidaridad ] [ Demo ]
|
||||
```
|
||||
|
||||
Implementación sugerida: links `<a>` simples que navegan a:
|
||||
- Todos: `/dashboard?survey_id=00000000-0000-0000-0000-000000000000`
|
||||
- InQuality: `/dashboard?survey_id=30000000-0000-0000-0000-000000000002`
|
||||
- Fundación Solidaridad: `/dashboard?survey_id=30000000-0000-0000-0000-000000000003`
|
||||
- Demo: `/dashboard?survey_id=30000000-0000-0000-0000-000000000001`
|
||||
|
||||
El survey activo se destaca visualmente (comparar `surveyId` del searchParam
|
||||
con el UUID de cada link). Estilo `.admin-*` no aplica acá — usar clases
|
||||
`.db-*` o tokens CSS existentes del dashboard.
|
||||
|
||||
El label de cada opción puede ser hardcodeado por ahora — no hacer fetch
|
||||
a `surveys` para obtener los nombres (Nivel 3, fuera de scope).
|
||||
|
||||
---
|
||||
|
||||
## 5. Entregables (DoD)
|
||||
|
||||
- [ ] Nueva migración `20260611000001_dash_responses_all.sql` creada.
|
||||
- [ ] `_dash_responses` actualizada con el flag de UUID cero.
|
||||
- [ ] `route.ts` permite el UUID especial (verificar y reportar).
|
||||
- [ ] Dashboard muestra selector de empresa con "Todos" + las 3 empresas.
|
||||
- [ ] Build exitoso.
|
||||
- [ ] `git diff --name-only HEAD` en scope.
|
||||
|
||||
## 6. Qué reportar
|
||||
```
|
||||
Verificado:
|
||||
- Migración creada ✓
|
||||
- route.ts: [necesitó cambio / no necesitó cambio — explicar]
|
||||
- Selector visible en dashboard ✓
|
||||
- Build exitoso ✓
|
||||
- Diff en scope ✓
|
||||
|
||||
URL de validación:
|
||||
- Todos: /dashboard?survey_id=00000000-0000-0000-0000-000000000000
|
||||
- InQuality: /dashboard?survey_id=30000000-0000-0000-0000-000000000002
|
||||
```
|
||||
|
||||
## 7. Qué NO hacer
|
||||
- ❌ No modificar las migraciones existentes — solo agregar una nueva.
|
||||
- ❌ No tocar las 11 RPCs directamente.
|
||||
- ❌ No hacer fetch a `surveys` para obtener nombres de empresa.
|
||||
- ❌ No agregar dependencias npm.
|
||||
- ❌ No tocar el formulario ni `/admin/responses`.
|
||||
33
supabase/migrations/20260611000001_dash_responses_all.sql
Normal file
33
supabase/migrations/20260611000001_dash_responses_all.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
-- Migración: agregar flag p_all a _dash_responses
|
||||
-- UUID especial '00000000-0000-0000-0000-000000000000' = todos los surveys
|
||||
CREATE OR REPLACE FUNCTION public._dash_responses(
|
||||
p_survey_id uuid,
|
||||
p_depto text DEFAULT NULL,
|
||||
p_genero text[] DEFAULT NULL,
|
||||
p_etario text[] DEFAULT NULL,
|
||||
p_modalidad text[] DEFAULT NULL
|
||||
)
|
||||
RETURNS TABLE(response_id uuid)
|
||||
LANGUAGE sql STABLE SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
SELECT r.id
|
||||
FROM public.responses r
|
||||
WHERE (p_survey_id = '00000000-0000-0000-0000-000000000000'
|
||||
OR r.survey_id = p_survey_id)
|
||||
AND (p_depto IS NULL OR r.qi_zone = p_depto)
|
||||
AND (p_genero IS NULL OR cardinality(p_genero) = 0 OR EXISTS (
|
||||
SELECT 1 FROM public.answers a
|
||||
JOIN public.questions q ON q.id = a.question_id
|
||||
WHERE a.response_id = r.id AND q.code = 'A6'
|
||||
AND a.value #>> '{}' = ANY(p_genero)
|
||||
))
|
||||
AND (p_etario IS NULL OR cardinality(p_etario) = 0
|
||||
OR r.qi_age_bucket = ANY(p_etario))
|
||||
AND (p_modalidad IS NULL OR cardinality(p_modalidad) = 0 OR EXISTS (
|
||||
SELECT 1 FROM public.answers a
|
||||
JOIN public.questions q ON q.id = a.question_id
|
||||
WHERE a.response_id = r.id AND q.code = 'A7'
|
||||
AND a.value #>> '{}' = ANY(p_modalidad)
|
||||
));
|
||||
$$;
|
||||
1408
supabase/seed.sql
1408
supabase/seed.sql
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user