120 lines
4.1 KiB
TypeScript
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");
|
|
}
|