/** * Scheduler tipo cron para KAPPA Hub. * * Las tareas se ejecutan solo cuando la app está abierta (web app). * En RUMBO (Tauri) esto se vuelve un proceso en segundo plano real. */ import Dexie, { type EntityTable } from 'dexie' interface ScheduleRule { id?: number name: string description?: string /** 0=dom, 1=lun, ..., 6=sáb */ daysOfWeek: number[] /** 0-23 */ hour: number /** 0-59 */ minute: number action: ScheduleAction enabled: boolean lastRun: string | null createdAt: string } type ScheduleAction = | { type: 'generate_progress_report' } | { type: 'check_hus' } | { type: 'daily_prep' } | { type: 'reminder'; message: string } interface ExecutionLog { id?: number ruleId: number ruleName: string executedAt: string success: boolean message: string } const db = new Dexie('kappa-hub-scheduler') as Dexie & { rules: EntityTable logs: EntityTable } db.version(1).stores({ rules: '++id, enabled, hour, minute', logs: '++id, ruleId, executedAt', }) export { db } export type { ScheduleRule, ScheduleAction, ExecutionLog } // ─── Engine ───────────────────────────────────────────────── type Subscriber = (ruleId: number, message: string) => void let intervalId: ReturnType | null = null let subscribers: Subscriber[] = [] function now(): string { return new Date().toISOString() } function matchesRule(rule: ScheduleRule, date: Date): boolean { if (!rule.enabled) return false if (!rule.daysOfWeek.includes(date.getDay())) return false if (rule.hour !== date.getHours()) return false if (rule.minute !== date.getMinutes()) return false return true } async function executeRule(rule: ScheduleRule): Promise { const projectNames: string[] = [] // se llenará del store switch (rule.action.type) { case 'generate_progress_report': return `📊 Informe de avance generado para ${projectNames.length || 'todos los'} proyectos.` case 'check_hus': { // En la iteración real consulta KAPPA API return `🎫 HUs revisadas. ${projectNames.length || 'N'} proyectos actualizados.` } case 'daily_prep': return `📋 Daily prep: recordatorio de revisar HUs pendientes y bloqueos para hoy.` case 'reminder': return `⏰ ${rule.action.message}` default: return `✅ Tarea "${rule.name}" ejecutada.` } } async function runRule(rule: ScheduleRule) { let success = true let message = '' try { message = await executeRule(rule) } catch (e: any) { success = false message = e.message } await db.rules.update(rule.id!, { lastRun: now() }) await db.logs.add({ ruleId: rule.id!, ruleName: rule.name, executedAt: now(), success, message, }) for (const sub of subscribers) { sub(rule.id!, message) } } async function tick() { const date = new Date() const rules = await db.rules.where('enabled').equals(1).toArray() for (const rule of rules) { if (matchesRule(rule, date)) { await runRule(rule) } } } export function startScheduler() { if (intervalId) return tick() // ejecutar inmediatamente por si acaba de pasar el minuto intervalId = setInterval(tick, 60_000) // cada minuto } export function stopScheduler() { if (intervalId) { clearInterval(intervalId) intervalId = null } } export function onScheduledTask(cb: Subscriber) { subscribers.push(cb) return () => { subscribers = subscribers.filter(s => s !== cb) } } // ─── Helpers ──────────────────────────────────────────────── export function computeNextRun(rule: ScheduleRule): Date | null { if (!rule.enabled || rule.daysOfWeek.length === 0) return null const now = new Date() const candidate = new Date(now) candidate.setHours(rule.hour, rule.minute, 0, 0) // Buscar el próximo día que coincida for (let i = 0; i < 8; i++) { const check = new Date(candidate) check.setDate(check.getDate() + i) if (rule.daysOfWeek.includes(check.getDay())) { if (check > now) return check } } return null } export const DAY_LABELS = ['Dom', 'Lun', 'Mar', 'Mié', 'Jue', 'Vie', 'Sáb'] export const DEFAULT_RULES: Omit[] = [ { name: 'Informe semanal de avance', description: 'Genera un resumen de estado de cada proyecto activo', daysOfWeek: [5], // viernes hour: 9, minute: 0, action: { type: 'generate_progress_report' }, enabled: true, }, { name: 'Revisión de HUs pendientes', description: 'Revisa HUs en progreso, bloqueadas y vencidas en todos los proyectos', daysOfWeek: [1, 2, 3, 4, 5], // lun-vie hour: 8, minute: 0, action: { type: 'check_hus' }, enabled: true, }, { name: 'Daily prep', description: 'Prepara el resumen diario: qué HUs toca hoy, bloqueos, entregables próximos', daysOfWeek: [1, 2, 3, 4, 5], hour: 7, minute: 30, action: { type: 'daily_prep' }, enabled: true, }, ]