Alpha v0.1.0 — KAPPA Hub inicial

- Auth con KAPPA (login + token Bearer)
- Cliente HTTP para 10 endpoints (proyectos, HUs, bitácoras, planeaciones)
- Dashboard multi-proyecto con concepto médico Teloprax
- Calendario colombiano con 19 feriados (Ley Emiliani + Pascua)
- Scheduler tipo cron con Dexie (reglas recurrentes, toasts, log)
- Diseño marca Teloprax: Inter, Space Grotesk, #1A1A2E, rojo #E63946
- Stack: Vue 3 + TypeScript + Pinia + Vite + Bun
This commit is contained in:
2026-05-22 20:18:54 -05:00
commit 66fd4e175a
26 changed files with 2227 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
/**
* 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<ScheduleRule, 'id'>
logs: EntityTable<ExecutionLog, 'id'>
}
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<typeof setInterval> | 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<string> {
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<ScheduleRule, 'id' | 'createdAt' | 'lastRun'>[] = [
{
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,
},
]