arcadiasuite/server/xos/scheduler.ts

120 lines
4.1 KiB
TypeScript

/**
* XOS Scheduler
* - Every 5 min: check SLA breaches and emit events
* - Every 30 sec: broadcast live supervisor stats via Socket.IO
*/
import { db } from "../db";
import { sql } from "drizzle-orm";
import { broadcastXos } from "../socket-io";
const SLA_CHECK_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
const STATS_BROADCAST_INTERVAL_MS = 30 * 1000; // 30 seconds
/** Mark overdue protocols as SLA-breached and emit event per tenant */
async function checkSlaBreaches() {
try {
// Mark protocols where sla_deadline has passed and still open/unbreached
const breached = await db.execute(sql`
UPDATE xos_protocols
SET sla_breach = true, updated_at = NOW()
WHERE status = 'open'
AND sla_deadline IS NOT NULL
AND sla_deadline < NOW()
AND sla_breach = false
RETURNING id, tenant_id, protocol_number, contact_id, queue_id, assigned_to
`);
const rows = (breached as any).rows ?? [];
for (const row of rows) {
broadcastXos("sla.breach", {
protocolId: row.id,
tenantId: row.tenant_id,
protocolNumber: row.protocol_number,
contactId: row.contact_id,
queueId: row.queue_id,
assignedTo: row.assigned_to,
breachedAt: new Date().toISOString(),
});
// Forward to automation engine (non-blocking)
try {
const engineHost = process.env.AUTOMATION_ENGINE_HOST || "localhost";
const enginePort = process.env.AUTOMATION_ENGINE_PORT || "8005";
await fetch(`http://${engineHost}:${enginePort}/xos/trigger`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
event_type: "crm.sla.breached",
payload: { protocol_id: row.id, protocol_number: row.protocol_number },
tenant_id: row.tenant_id,
}),
signal: AbortSignal.timeout(5000),
});
} catch {
// Non-blocking — engine may be offline
}
}
if (rows.length > 0) {
console.log(`[xos:scheduler] SLA check: ${rows.length} breach(es) detected`);
}
} catch (err) {
console.error("[xos:scheduler] SLA check error:", err);
}
}
/** Broadcast aggregate supervisor stats to all connected clients */
async function broadcastSupervisorStats() {
try {
const [convsResult, ticketsResult, agentsResult] = await Promise.all([
db.execute(sql`
SELECT
COUNT(*) FILTER (WHERE status = 'open') AS open_conversations,
COUNT(*) FILTER (WHERE status = 'resolved' AND updated_at > NOW() - INTERVAL '24h') AS resolved_today
FROM xos_conversations
`),
db.execute(sql`
SELECT
COUNT(*) FILTER (WHERE status IN ('open','in_progress') AND priority = 'urgent') AS urgent_open
FROM xos_tickets
`),
db.execute(sql`
SELECT COUNT(*) FILTER (WHERE is_online = true) AS agents_online
FROM xos_agents
`),
]);
const c = ((convsResult as any).rows ?? [])[0] ?? {};
const t = ((ticketsResult as any).rows ?? [])[0] ?? {};
const a = ((agentsResult as any).rows ?? [])[0] ?? {};
broadcastXos("supervisor.stats", {
openConversations: Number(c.open_conversations ?? 0),
resolvedToday: Number(c.resolved_today ?? 0),
urgentTickets: Number(t.urgent_open ?? 0),
agentsOnline: Number(a.agents_online ?? 0),
timestamp: new Date().toISOString(),
});
} catch (err) {
// Suppress — tables may not exist in dev
if (process.env.NODE_ENV !== "production") {
console.debug("[xos:scheduler] stats broadcast skipped:", (err as any)?.message);
}
}
}
export function startXosScheduler() {
// Stagger initial runs slightly to avoid startup congestion
setTimeout(() => {
checkSlaBreaches();
setInterval(checkSlaBreaches, SLA_CHECK_INTERVAL_MS);
}, 15_000); // first SLA check 15s after boot
setTimeout(() => {
broadcastSupervisorStats();
setInterval(broadcastSupervisorStats, STATS_BROADCAST_INTERVAL_MS);
}, 5_000); // first broadcast 5s after boot
console.log("[xos:scheduler] Started — SLA checks every 5min, stats every 30s");
}