JuanCS-Dev's picture
feat: O2b risk queue live binding + SW v2
0000d94 verified
Raw History Blame Contribute Delete
6.11 kB
import {
decideActionQueueItem,
getActionQueue,
getOpsPlaybooks,
triggerOpsPlaybook,
} from "./api.js";
import {
getActionQueueQuery,
getOpsPlaybookTriggerPayload,
renderActionQueuePayload,
renderOpsPlaybooksPayload,
setActionQueueBusy,
showActionQueueFeedback,
showOpsPlaybooksFeedback,
} from "./view.js";
const ACTION_QUEUE_INTERVAL_MS = 12000;
export function createActionQueueController({
aqEls,
fetchAndRenderObservability,
getErrorMessage,
}) {
let isPolling = false;
let timerId = 0;
let selectedChannel = "default";
async function refreshActionQueue({ showFeedback = true } = {}) {
if (!aqEls || isPolling) return;
isPolling = true;
setActionQueueBusy(aqEls, true);
if (showFeedback) {
showActionQueueFeedback(aqEls, "Loading risk queue...", "warn");
showOpsPlaybooksFeedback(
aqEls,
"Loading operational playbooks...",
"warn",
);
}
try {
const query = getActionQueueQuery(aqEls);
const [queueResult, opsResult] = await Promise.allSettled([
getActionQueue(query),
getOpsPlaybooks(selectedChannel),
]);
if (queueResult.status === "fulfilled") {
renderActionQueuePayload(queueResult.value, aqEls, decideQueueItem);
if (showFeedback) {
showActionQueueFeedback(
aqEls,
`Risk queue refreshed (${queueResult.value?.summary?.total || 0} items).`,
"ok",
);
}
} else {
throw queueResult.reason;
}
if (opsResult.status === "fulfilled") {
renderOpsPlaybooksPayload(opsResult.value, aqEls);
if (showFeedback) {
const awaitingCount =
opsResult.value?.summary?.awaiting_decision || 0;
showOpsPlaybooksFeedback(
aqEls,
`Playbooks refreshed (${awaitingCount} awaiting decision).`,
"ok",
);
}
} else {
showOpsPlaybooksFeedback(
aqEls,
`Error: ${getErrorMessage(
opsResult.reason,
"Failed to load operational playbooks.",
)}`,
"error",
);
}
} catch (error) {
console.error("Action queue refresh error", error);
showActionQueueFeedback(
aqEls,
`Error: ${getErrorMessage(error, "Failed to load queue.")}`,
"error",
);
} finally {
isPolling = false;
setActionQueueBusy(aqEls, false);
}
}
function scheduleActionQueuePolling() {
if (timerId) {
window.clearTimeout(timerId);
}
timerId = window.setTimeout(async () => {
await refreshActionQueue({ showFeedback: false });
scheduleActionQueuePolling();
}, ACTION_QUEUE_INTERVAL_MS);
}
async function decideQueueItem(actionId, decision, note = "") {
if (!aqEls) return;
setActionQueueBusy(aqEls, true);
showActionQueueFeedback(
aqEls,
`Applying ${decision} decision on ${actionId}...`,
"warn",
);
try {
await decideActionQueueItem(actionId, decision, note);
showActionQueueFeedback(aqEls, "Decision recorded.", "ok");
await Promise.all([
refreshActionQueue({ showFeedback: false }),
fetchAndRenderObservability(),
]);
} catch (error) {
console.error("Action queue decision error", error);
showActionQueueFeedback(
aqEls,
`Error: ${getErrorMessage(error, "Failed to record decision.")}`,
"error",
);
} finally {
setActionQueueBusy(aqEls, false);
}
}
async function triggerSelectedPlaybook() {
if (!aqEls) return;
const triggerPayload = getOpsPlaybookTriggerPayload(aqEls, selectedChannel);
if (!triggerPayload.playbookId) {
showOpsPlaybooksFeedback(
aqEls,
"Select a playbook before triggering.",
"warn",
);
return;
}
setActionQueueBusy(aqEls, true);
showOpsPlaybooksFeedback(
aqEls,
`Triggering playbook ${triggerPayload.playbookId}...`,
"warn",
);
try {
const payload = await triggerOpsPlaybook(triggerPayload);
renderOpsPlaybooksPayload(payload, aqEls);
showOpsPlaybooksFeedback(aqEls, "Playbook triggered successfully.", "ok");
await Promise.all([
refreshActionQueue({ showFeedback: false }),
fetchAndRenderObservability(),
]);
} catch (error) {
console.error("Ops playbook trigger error", error);
showOpsPlaybooksFeedback(
aqEls,
`Error: ${getErrorMessage(error, "Failed to trigger playbook.")}`,
"error",
);
} finally {
setActionQueueBusy(aqEls, false);
}
}
function bindActionQueueEvents() {
if (!aqEls) return;
if (aqEls.refreshBtn) {
aqEls.refreshBtn.addEventListener("click", () => {
refreshActionQueue({ showFeedback: true });
});
}
if (aqEls.statusFilter) {
aqEls.statusFilter.addEventListener("change", () => {
refreshActionQueue({ showFeedback: true });
});
}
if (aqEls.limitInput) {
aqEls.limitInput.addEventListener("change", () => {
refreshActionQueue({ showFeedback: true });
});
}
if (aqEls.opsRefreshBtn) {
aqEls.opsRefreshBtn.addEventListener("click", () => {
refreshActionQueue({ showFeedback: true });
});
}
if (aqEls.opsTriggerBtn) {
aqEls.opsTriggerBtn.addEventListener("click", () => {
triggerSelectedPlaybook();
});
}
}
return {
bindActionQueueEvents,
refreshActionQueue,
scheduleActionQueuePolling,
// O2b: render the queue from the live ops snapshot (/api/ops/stream) instead
// of a fetch — snap.queue is byte-for-byte the /api/action-queue shape.
renderQueueFromSnapshot(queueSection) {
if (aqEls && queueSection) {
renderActionQueuePayload(queueSection, aqEls, decideQueueItem);
}
},
setSelectedChannel(channelId) {
selectedChannel =
String(channelId || "")
.trim()
.toLowerCase() || "default";
},
};
}