feat: añade semilla de desarrollo enriquecida y docs de ejecución

Co-authored-by: aider (openrouter/openai/gpt-5) <aider@aider.chat>
webui
borja 2 weeks ago
parent 4d9ea0ca8a
commit 352d9f1e28

@ -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<void> {
try { db.exec(`PRAGMA foreign_keys = ON;`); } catch {}
@ -26,83 +30,171 @@ export async function seedDev(db: any, defaultUser: string): Promise<void> {
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 AM)', 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 (≈ 3035 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<string>();
for (const uid of t.assignees) {
if (uid && !seen.has(uid)) {
seen.add(uid);
assignStmt.run(id, uid, t.createdBy);
}
}
}
}
})();

@ -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: 35 (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: ~3035 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):

Loading…
Cancel
Save