From 352d9f1e28a90b6b8d68f30ddf668b0f868fca80 Mon Sep 17 00:00:00 2001 From: borja Date: Thu, 16 Oct 2025 16:52:29 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1ade=20semilla=20de=20desarrollo?= =?UTF-8?q?=20enriquecida=20y=20docs=20de=20ejecuci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5) --- apps/web/src/lib/server/dev-seed.ts | 198 ++++++++++++++++++++-------- docs/operations.md | 21 +++ 2 files changed, 166 insertions(+), 53 deletions(-) diff --git a/apps/web/src/lib/server/dev-seed.ts b/apps/web/src/lib/server/dev-seed.ts index 8c03d92..43965a9 100644 --- a/apps/web/src/lib/server/dev-seed.ts +++ b/apps/web/src/lib/server/dev-seed.ts @@ -1,6 +1,7 @@ /** - * Seed mínimo de datos de demo para desarrollo. - * Inserta usuarios, grupos permitidos, membresías, preferencias y tareas variadas. + * Semilla enriquecida de datos de demo para desarrollo. + * Inserta usuarios, grupos (allowed/pending), membresías, preferencias y un set amplio de tareas. + * Idempotente: solo ejecuta si la tabla tasks está vacía. */ function toIsoYmd(d: Date): string { const yyyy = d.getUTCFullYear(); @@ -11,6 +12,9 @@ function toIsoYmd(d: Date): string { function addDays(base: Date, days: number): Date { return new Date(base.getTime() + days * 24 * 3600 * 1000); } +function isoSql(dt: Date): string { + return dt.toISOString().replace('T', ' ').replace('Z', ''); +} export async function seedDev(db: any, defaultUser: string): Promise { try { db.exec(`PRAGMA foreign_keys = ON;`); } catch {} @@ -26,83 +30,171 @@ export async function seedDev(db: any, defaultUser: string): Promise { const nextWeek = toIsoYmd(addDays(now, 7)); const overdue = toIsoYmd(addDays(now, -1)); const inTwoDays = toIsoYmd(addDays(now, 2)); + const noDate = null; - const otherUser = '5550001111'; - // Usuarios - db.prepare(` - INSERT OR IGNORE INTO users (id, first_seen, last_seen) - VALUES (?, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now')) - `).run(defaultUser); - db.prepare(` + // Usuarios (incluye el de desarrollo aunque no sea numérico) + const DEV = (defaultUser || '').trim(); + const U1 = DEV; // usuario de desarrollo (puede no ser numérico; lo insertamos igualmente) + const U2 = '34600123456'; + const U3 = '5550001111'; + const U4 = '600123456'; + const U5 = '34987654321'; + const users = [U1, U2, U3, U4, U5].filter(Boolean); + + const insertUser = db.prepare(` INSERT OR IGNORE INTO users (id, first_seen, last_seen) VALUES (?, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now')) - `).run(otherUser); + `); + for (const u of users) insertUser.run(u); - // Grupos - db.prepare(` - INSERT OR IGNORE INTO groups (id, community_id, name, active, last_verified) - VALUES (?, 'comm-1', ?, 1, strftime('%Y-%m-%d %H:%M:%f','now')) - `).run('g-1@g.us', 'Grupo Alpha'); - db.prepare(` - INSERT OR IGNORE INTO groups (id, community_id, name, active, last_verified) - VALUES (?, 'comm-1', ?, 1, strftime('%Y-%m-%d %H:%M:%f','now')) - `).run('g-2@g.us', 'Grupo Beta'); + // Grupos (IDs tipo JID) y estados + const G_FAM = 'g-familia@g.us'; + const G_TRA = 'g-trabajo@g.us'; + const G_VOL = 'g-voluntariado@g.us'; + const G_COM = 'g-compras@g.us'; + const G_VAR = 'g-varios@g.us'; + const groups: Array<{ id: string; name: string; allowed: 'allowed' | 'pending' | 'blocked' }> = [ + { id: G_FAM, name: 'Familia', allowed: 'allowed' }, + { id: G_TRA, name: 'Trabajo', allowed: 'allowed' }, + { id: G_VOL, name: 'Voluntariado', allowed: 'allowed' }, + { id: G_COM, name: 'Compras', allowed: 'allowed' }, // allowed pero sin membresía del usuario DEV + { id: G_VAR, name: 'Varios', allowed: 'pending' } // pendiente/bloqueado para validar gating + ]; - // Allowed groups - try { - db.prepare(` - INSERT OR IGNORE INTO allowed_groups (group_id, label, status, discovered_at, updated_at, discovered_by) - VALUES (?, 'Alpha', 'allowed', strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'), NULL) - `).run('g-1@g.us'); - db.prepare(` - INSERT OR IGNORE INTO allowed_groups (group_id, label, status, discovered_at, updated_at, discovered_by) - VALUES (?, 'Beta', 'allowed', strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'), NULL) - `).run('g-2@g.us'); - } catch {} + const insertGroup = db.prepare(` + INSERT OR IGNORE INTO groups (id, community_id, name, active, last_verified) + VALUES (?, 'comm-dev', ?, 1, strftime('%Y-%m-%d %H:%M:%f','now')) + `); + const insertAllowed = db.prepare(` + INSERT OR IGNORE INTO allowed_groups (group_id, label, status, discovered_at, updated_at, discovered_by) + VALUES (?, ?, ?, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now'), NULL) + `); + for (const g of groups) { + insertGroup.run(g.id, g.name); + try { insertAllowed.run(g.id, g.name, g.allowed); } catch {} + } - // Membresías (usuario por defecto activo en ambos grupos) - db.prepare(` - INSERT OR REPLACE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) - VALUES (?, ?, 0, 1, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now')) - `).run('g-1@g.us', defaultUser); - db.prepare(` + // Membresías activas: el usuario DEV en Familia, Trabajo, Voluntariado; otros usuarios repartidos + const insertMember = db.prepare(` INSERT OR REPLACE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) VALUES (?, ?, 0, 1, strftime('%Y-%m-%d %H:%M:%f','now'), strftime('%Y-%m-%d %H:%M:%f','now')) - `).run('g-2@g.us', defaultUser); + `); + for (const gid of [G_FAM, G_TRA, G_VOL]) insertMember.run(gid, U1); + // Otros usuarios en distintos grupos para facilitar múltiples responsables + insertMember.run(G_FAM, U2); + insertMember.run(G_FAM, U3); + insertMember.run(G_TRA, U3); + insertMember.run(G_TRA, U4); + insertMember.run(G_VOL, U5); + // Compras: allowed pero sin membresía del usuario DEV (solo U2), para validar gating + insertMember.run(G_COM, U2); - // Preferencias del usuario + // Preferencias del usuario DEV try { db.prepare(` INSERT OR REPLACE INTO user_preferences (user_id, reminder_freq, reminder_time, last_reminded_on, updated_at) VALUES (?, 'daily', '08:30', NULL, strftime('%Y-%m-%d %H:%M:%f','now')) - `).run(defaultUser); + `).run(U1); } catch {} - // Crear tareas diversas + // Insertadores const insertTask = db.prepare(` - INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at) - VALUES (?, ?, ?, ?, COALESCE(?, 0), ?) + INSERT INTO tasks (description, due_date, group_id, created_by, completed, completed_at, completed_by) + VALUES (?, ?, ?, ?, COALESCE(?, 0), ?, ?) `); const assignStmt = db.prepare(` INSERT OR IGNORE INTO task_assignments (task_id, user_id, assigned_by) VALUES (?, ?, ?) `); - const tasksToCreate = [ - { desc: 'Tarea sin responsable (hoy) - Grupo Alpha', due: today, group: 'g-1@g.us', assignTo: null, completed: 0, completedAt: null }, - { desc: 'Tarea sin responsable (mañana) - Grupo Alpha', due: tomorrow, group: 'g-1@g.us', assignTo: null, completed: 0, completedAt: null }, - { desc: 'Tarea sin responsable (próx. semana) - Grupo Beta', due: nextWeek, group: 'g-2@g.us', assignTo: null, completed: 0, completedAt: null }, - { desc: 'Mi tarea personal (en 2 días)', due: inTwoDays, group: null, assignTo: defaultUser, completed: 0, completedAt: null }, - { desc: 'Mi tarea (vencida ayer) - Grupo Alpha', due: overdue, group: 'g-1@g.us', assignTo: defaultUser, completed: 0, completedAt: null }, - { desc: 'Tarea completada reciente', due: today, group: null, assignTo: defaultUser, completed: 1, completedAt: new Date().toISOString().replace('T',' ').replace('Z','') } + // Helpers para completadas + const completedRecent = isoSql(addDays(now, 0)); // ahora + const completed2hAgo = isoSql(new Date(Date.now() - 2 * 3600 * 1000)); + const completed12hAgo = isoSql(new Date(Date.now() - 12 * 3600 * 1000)); + const completed48hAgo = isoSql(new Date(Date.now() - 48 * 3600 * 1000)); + const completed72hAgo = isoSql(new Date(Date.now() - 72 * 3600 * 1000)); + + type Spec = { + desc: string; + due: string | null; + group: string | null; + createdBy: string; + completed?: 0 | 1; + completedAt?: string | null; + completedBy?: string | null; + assignees?: string[]; // responsables + }; + + const specs: Spec[] = [ + // Familia (mezcla: sin responsables, 1 responsable, múltiples, completadas reciente/antigua) + { desc: 'Compra semanal para la casa', due: today, group: G_FAM, createdBy: U1, assignees: [] }, + { desc: 'Llevar coche al taller', due: tomorrow, group: G_FAM, createdBy: U1, assignees: [U1] }, + { desc: 'Organizar cumpleaños (lista de invitados y tarta)', due: nextWeek, group: G_FAM, createdBy: U2, assignees: [U1, U2] }, + { desc: 'Revisar facturas de luz y gas', due: overdue, group: G_FAM, createdBy: U1, assignees: [], completed: 0 }, + { desc: 'Pedir cita pediatra', due: inTwoDays, group: G_FAM, createdBy: U1, assignees: [U3] }, + { desc: 'Sacar basura orgánica', due: noDate, group: G_FAM, createdBy: U3, assignees: [], completed: 1, completedAt: completed2hAgo, completedBy: U1 }, + { desc: 'Regar plantas del balcón (abundante agua)', due: today, group: G_FAM, createdBy: U1, assignees: [U1, U3] }, + + // Trabajo + { desc: 'Preparar informe trimestral de ventas', due: nextWeek, group: G_TRA, createdBy: U3, assignees: [U1] }, + { desc: 'Reunión con proveedor clave', due: tomorrow, group: G_TRA, createdBy: U4, assignees: [], completed: 1, completedAt: completed12hAgo, completedBy: U1 }, + { desc: 'Actualizar tablero de tareas del sprint', due: today, group: G_TRA, createdBy: U1, assignees: [U1, U4] }, + { desc: 'Revisión de PRs acumuladas', due: overdue, group: G_TRA, createdBy: U1, assignees: [U3] }, + { desc: 'Definir OKRs del próximo trimestre', due: noDate, group: G_TRA, createdBy: U1, assignees: [] }, + { desc: 'Publicar release menor (v1.0.1)', due: inTwoDays, group: G_TRA, createdBy: U2, assignees: [U1], completed: 1, completedAt: completed48hAgo, completedBy: U2 }, + + // Voluntariado + { desc: 'Clasificar donaciones de ropa', due: today, group: G_VOL, createdBy: U5, assignees: [] }, + { desc: 'Coordinar recogida de alimentos', due: tomorrow, group: G_VOL, createdBy: U1, assignees: [U1, U5] }, + { desc: 'Llamar a nuevos voluntarios (lista A–M)', due: nextWeek, group: G_VOL, createdBy: U1, assignees: [U1] }, + { desc: 'Actualizar listado de familias beneficiarias', due: overdue, group: G_VOL, createdBy: U5, assignees: [], completed: 1, completedAt: completed72hAgo, completedBy: U5 }, + { desc: 'Solicitar permiso para evento solidario', due: noDate, group: G_VOL, createdBy: U1, assignees: [] }, + + // Compras (allowed pero sin membresía del usuario DEV) + { desc: 'Comprar detergente y suavizante', due: today, group: G_COM, createdBy: U2, assignees: [U2] }, + { desc: 'Reponer comida para mascotas', due: tomorrow, group: G_COM, createdBy: U2, assignees: [] }, + { desc: 'Comparar precios de frutas y verduras', due: nextWeek, group: G_COM, createdBy: U2, assignees: [U2], completed: 1, completedAt: completed2hAgo, completedBy: U2 }, + { desc: 'Planificar compra mensual a granel', due: noDate, group: G_COM, createdBy: U2, assignees: [] }, + + // Personales (group_id NULL) + { desc: 'Pagar recibo del móvil', due: overdue, group: null, createdBy: U1, assignees: [U1] }, + { desc: 'Hacer copia de seguridad del portátil', due: today, group: null, createdBy: U1, assignees: [], completed: 1, completedAt: completed2hAgo, completedBy: U1 }, + { desc: 'Llamar al banco para aclarar comisión', due: tomorrow, group: null, createdBy: U1, assignees: [U1] }, + { desc: 'Leer artículo técnico pendiente', due: noDate, group: null, createdBy: U1, assignees: [] }, + { desc: 'Renovar DNI (pedir cita)', due: inTwoDays, group: null, createdBy: U1, assignees: [U1] }, + { desc: 'Terminar curso online de accesibilidad', due: nextWeek, group: null, createdBy: U1, assignees: [U1, U3] }, + { desc: 'Ordenar fotos antiguas en la nube (muchas carpetas)', due: noDate, group: null, createdBy: U1, assignees: [], completed: 1, completedAt: completed48hAgo, completedBy: U1 }, + + // Más casos para densidad y mezcla (≈ 30–35 total) + { desc: 'Preparar lista de la compra grande', due: tomorrow, group: G_FAM, createdBy: U1, assignees: [U1, U2] }, + { desc: 'Pintar habitación pequeña', due: nextWeek, group: G_FAM, createdBy: U2, assignees: [] }, + { desc: 'Plan de pruebas regresivas', due: today, group: G_TRA, createdBy: U1, assignees: [U4] }, + { desc: 'Reunión semanal con equipo', due: today, group: G_TRA, createdBy: U3, assignees: [U1, U3] }, + { desc: 'Retirar material donado del almacén', due: tomorrow, group: G_VOL, createdBy: U5, assignees: [] }, + { desc: 'Preparar carteles del evento solidario (texto largo y revisión)', due: noDate, group: G_VOL, createdBy: U1, assignees: [U1] } ]; + // Transacción para insertar todo db.transaction(() => { - for (const t of tasksToCreate) { - const res = insertTask.run(t.desc, t.due, t.group, defaultUser, t.completed, t.completedAt); + for (const t of specs) { + const res = insertTask.run( + t.desc, + t.due ?? null, + t.group ?? null, + t.createdBy, + t.completed ?? 0, + t.completed ? (t.completedAt ?? completedRecent) : null, + t.completed ? (t.completedBy ?? t.createdBy) : null + ); const id = Number((res as any)?.lastInsertRowid ?? 0); - if (t.assignTo && id > 0) { - assignStmt.run(id, t.assignTo, defaultUser); + if (id > 0 && Array.isArray(t.assignees)) { + const seen = new Set(); + for (const uid of t.assignees) { + if (uid && !seen.has(uid)) { + seen.add(uid); + assignStmt.run(id, uid, t.createdBy); + } + } } } })(); diff --git a/docs/operations.md b/docs/operations.md index af403e4..5b33b93 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -26,6 +26,8 @@ Variables de entorno (principales) - DB_PATH: ruta absoluta o relativa al archivo SQLite; si se define, tiene prioridad sobre DATA_DIR. Ej.: DB_PATH='./data/tasks.db' - MIGRATIONS_LOG_LEVEL: 'silent' para silenciar logs del migrador (en test se silencian automáticamente). - WEB_BASE_URL: base pública de la interfaz web para construir enlaces absolutos (p. ej., /login?token=...). Obligatoria para /t web. Ej.: WEB_BASE_URL='https://wtask.org' +- DEV_AUTOSEED_DB: 'true'/'false' para sembrar automáticamente la BD en desarrollo cuando está vacía (apps/web). Ej.: DEV_AUTOSEED_DB='true' +- DEV_DEFAULT_USER: ID de usuario por defecto en desarrollo (bypass y semilla). Idealmente numérico (solo dígitos). Ej.: DEV_DEFAULT_USER='34600123456' Endpoints operativos - GET /metrics @@ -81,6 +83,25 @@ Datos y backups - DB_PATH permite aislar BD por rama/entorno sin tocar DATA_DIR; útil para pruebas en CapRover. - En Docker/CapRover, el volumen por defecto es /app/data. Para persistencia, usa rutas de DB_PATH dentro de ese directorio (p. ej., /app/data/tasks-next.db). +Semilla de desarrollo (apps/web) +- Activación: establecer DEV_AUTOSEED_DB='true'. La semilla solo se ejecuta en desarrollo cuando la tabla tasks está vacía. +- Usuario por defecto: definir DEV_DEFAULT_USER con un ID numérico (p. ej., 34600123456). Se crea como usuario, se hace miembro activo de varios grupos y se usa para asignaciones. +- Ruta del archivo en dev (apps/web): por defecto tmp/tasks.db (véase apps/web/src/lib/server/env.ts). En producción, la web usa /app/data por defecto. +- Regenerar la BD de dev: detener el servidor web, borrar el archivo de BD y reiniciar con DEV_AUTOSEED_DB='true'. + - Ejemplo: rm -f tmp/tasks.db +- Datos que se crean: + - Usuarios: 3–5 (incluido el usuario por defecto). + - Grupos: “Familia”, “Trabajo”, “Voluntariado”, “Compras” (allowed) y “Varios” (pending). + - Allowed groups: allowed para los grupos principales; “Compras” allowed sin membresía del usuario por defecto (sirve para validar gating); “Varios” en pending. + - Membresías: el usuario por defecto activo en Familia, Trabajo y Voluntariado; otros usuarios repartidos para soportar múltiples responsables. + - Preferencias: recordatorios diarios a las 08:30 para el usuario por defecto. + - Tareas: ~30–35 con mezcla rica: + - Personales (sin grupo) y de grupo. + - due_date en pasado, hoy, futuro y NULL. + - Sin responsables, con 1 responsable y con múltiples responsables (incluye “tú”). + - Completadas recientemente (≤24h) y antiguas (>48h), con completed_by coherente. +- Idempotencia: si ya existen tareas no vuelve a sembrar. Para resembrar, borra el archivo de BD o define un DB_PATH nuevo. + Métricas de referencia - sync_runs_total, identity_alias_resolved_total, contadores/gauges específicos de colas y limpieza. - Control de acceso por grupos (multicomunidades):