From a0149c2af715ce2ca53bda2c73e1fc2e78f85625 Mon Sep 17 00:00:00 2001 From: deacon Date: Fri, 26 Sep 2025 17:45:20 -0400 Subject: [PATCH 1/4] complete --- src/components/agents/AgentChartStatus.vue | 97 +++--- src/components/agents/DetailsModal.vue | 332 +++++++++++---------- src/stores/agentStore.js | 161 +++++----- src/utils/agentUtil.js | 24 +- src/utils/utils.js | 223 ++++++-------- src/views/AgentsView.vue | 207 +++++++------ 6 files changed, 548 insertions(+), 496 deletions(-) diff --git a/src/components/agents/AgentChartStatus.vue b/src/components/agents/AgentChartStatus.vue index 4f2678b..6e11031 100644 --- a/src/components/agents/AgentChartStatus.vue +++ b/src/components/agents/AgentChartStatus.vue @@ -10,21 +10,47 @@ const $api = inject("$api"); const agentStore = useAgentStore(); const { agents } = storeToRefs(agentStore); + const agentChartStatus = ref(null); const chart = ref(null); +const poll = ref(null); -onMounted(() => { - initChart(); +onMounted(async () => { window.addEventListener("resize", resizeChart); + + const [cfg, ag] = await Promise.allSettled([ + agentStore.getAgentConfig($api), + agentStore.getAgents($api), + ]); + if (cfg.status === "rejected") console.warn("[cmp] getAgentConfig failed:", cfg.reason); + if (ag.status === "rejected") console.error("[cmp] getAgents failed:", ag.reason); + + await initChart(); + + poll.value = setInterval(() => { + agentStore.getAgents($api).catch(err => console.warn("[poll] getAgents failed:", err)); + }, 10_000); }); onBeforeUnmount(() => { window.removeEventListener("resize", resizeChart); + if (poll.value) clearInterval(poll.value); + if (chart.value) { + chart.value.dispose(); // avoid ECharts memory leaks + chart.value = null; + } }); -watch(agents, () => { - setChartOption(); -}); +watch( + agents, + (newVal, oldVal) => { + if (chart.value) { + setChartOption(); + } else { + console.log("[watch] chart not ready yet, skipping setOption"); + } + } +); async function initChart() { chart.value = echarts.init(agentChartStatus.value); @@ -34,27 +60,25 @@ async function initChart() { }); resizeChart(); - await agentStore.getAgents($api); - setChartOption(); - chart.value.hideLoading(); + try { + await agentStore.getAgents($api); // initial fetch + } finally { + setChartOption(); + chart.value.hideLoading(); + } } function setChartOption() { - chart.value.setOption({ + const option = { title: { - text: `${agents.value.length} Agent${agents.value.length == 1 ? "" : "s"}`, - textStyle: { - color: "white", - }, - }, - tooltip: { - trigger: "item", - }, - legend: { - show: false, + text: `${agents.value.length} Agent${agents.value.length === 1 ? "" : "s"}`, + textStyle: { color: "white" }, }, + tooltip: { trigger: "item" }, + legend: { show: false }, series: [ { + id: "agent-status", name: "Agent Status", type: "pie", radius: ["40%", "70%"], @@ -64,47 +88,39 @@ function setChartOption() { borderColor: "hsl(0deg, 0%, 14%)", borderWidth: 2, }, - label: { - show: false, - position: "center", - }, - labelLine: { - show: false, - }, + label: { show: false, position: "center" }, + labelLine: { show: false }, data: getChartData(), }, ], - }); + }; + chart.value.setOption(option, { notMerge: true, replaceMerge: ["series"], lazyUpdate: true }); } function getChartData() { if (!agents.value.length) return []; + const nowMs = agentStore.serverNowMs ?? Date.now(); + const cfg = agentStore.agentConfig; + return [ { name: "Alive (trusted)", - value: agents.value.filter( - (agent) => getAgentStatus(agent) === "alive" && agent.trusted - ).length, + value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "alive" && a.trusted).length, itemStyle: { color: "#4a9" }, }, { name: "Alive (untrusted)", - value: agents.value.filter( - (agent) => getAgentStatus(agent) === "alive" && !agent.trusted - ).length, + value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "alive" && !a.trusted).length, itemStyle: { color: "#F7DB89" }, }, { name: "Pending kill", - value: agents.value.filter( - (agent) => getAgentStatus(agent) === "pending kill" - ).length, + value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "pending kill").length, itemStyle: { color: "hsl(207deg, 61%, 53%)" }, }, { name: "Dead", - value: agents.value.filter((agent) => getAgentStatus(agent) === "dead") - .length, + value: agents.value.filter(a => getAgentStatus(a, nowMs, cfg) === "dead").length, itemStyle: { color: "#c31" }, }, ]; @@ -121,8 +137,5 @@ function resizeChart() { diff --git a/src/components/agents/DetailsModal.vue b/src/components/agents/DetailsModal.vue index 1797d66..3bfc091 100644 --- a/src/components/agents/DetailsModal.vue +++ b/src/components/agents/DetailsModal.vue @@ -1,178 +1,202 @@ + From 77adc28bbc164a035c6eab99e77036a78d3136d3 Mon Sep 17 00:00:00 2001 From: deacon Date: Fri, 26 Sep 2025 18:49:34 -0400 Subject: [PATCH 2/4] fixed banner metrics to match table agents status --- src/components/agents/AgentChartStatus.vue | 4 +- src/stores/agentStore.js | 50 +++++++++++++++++----- src/utils/agentUtil.js | 14 +++++- src/views/AgentsView.vue | 27 +++++++++--- 4 files changed, 73 insertions(+), 22 deletions(-) diff --git a/src/components/agents/AgentChartStatus.vue b/src/components/agents/AgentChartStatus.vue index 6e11031..20bd94a 100644 --- a/src/components/agents/AgentChartStatus.vue +++ b/src/components/agents/AgentChartStatus.vue @@ -41,9 +41,7 @@ onBeforeUnmount(() => { } }); -watch( - agents, - (newVal, oldVal) => { +watch(() => { if (chart.value) { setChartOption(); } else { diff --git a/src/stores/agentStore.js b/src/stores/agentStore.js index 13ac9cd..f418949 100644 --- a/src/stores/agentStore.js +++ b/src/stores/agentStore.js @@ -8,8 +8,9 @@ export const useAgentStore = defineStore("agentStore", { agentConfig: {}, selectedAgent: {}, agentGroups: [], - // new: server clock from API response headers + // server clock from API response headers serverNowMs: undefined, + pendingKill: new Set(), }), actions: { @@ -23,12 +24,21 @@ export const useAgentStore = defineStore("agentStore", { // capture server time (kills client/server clock skew) this.serverNowMs = Date.parse(res.headers?.date) || Date.now(); - // normalize last_seen -> _lastSeenMs and sort by it (desc) + // remember which agents were pending kill + const pendingMap = new Map( + (this.agents || []) + .filter(a => a._pendingKill) + .map(a => [a.paw, true]) + ); + + // normalize + re-apply pendingKill this.agents = (res.data || []) - .map(a => ({ ...a, _lastSeenMs: toMs(a.last_seen) })) - .sort((a, b) => b._lastSeenMs - a._lastSeenMs); - - const first = this.agents[0] + .map(a => ({ + ...a, + _lastSeenMs: toMs(a.last_seen), + _pendingKill: pendingMap.has(a.paw) || a._pendingKill === true, + })) + .sort((a, b) => b._lastSeenMs - a._lastSeenMs); this.updateAgentGroups(); } catch (error) { throw error; @@ -72,6 +82,8 @@ export const useAgentStore = defineStore("agentStore", { try { const response = await $api.delete(`/api/v2/agents/${agentPaw}`); this.agents.splice(index, 1); + // clean up the flag + this.pendingKill.delete(agentPaw); return response.data; } catch (error) { throw error; @@ -80,11 +92,27 @@ export const useAgentStore = defineStore("agentStore", { async killAgent($api, agentPaw) { try { - - const idx = this.agents.findIndex(a => a.paw === agentPaw); - if (idx !== -1) { - // Flag agent as pending kill immediately - this.agents[idx]._pendingKill = true; + const reqBody = { pending_kill: true }; + const idx = this.agents.findIndex(a => a.paw === agentPaw); + if (idx !== -1) { + // WTF is this API voodoo? Setting watchdog to 1 and sleep to 3 forces an agent to check in almost immediately and then die because of pending_kill flag + // This is a workaround until we have a proper "kill" command that an agent can execute immediately + const reqBody = { + watchdog: 1, + sleep_min: 3, + sleep_max: 3 + }; + + this.pendingKill.add(agentPaw); + const response = await $api.patch(`/api/v2/agents/${agentPaw}`, reqBody); + this.agents = this.agents.map(a => + a.paw === agentPaw + ? { ...a, _pendingKill: true } + : a + ); + // Flag agent as pending kill immediately + this.agents[idx]._pendingKill = true; + return response.data; } } catch (error) { diff --git a/src/utils/agentUtil.js b/src/utils/agentUtil.js index 62a4c13..67f7e77 100644 --- a/src/utils/agentUtil.js +++ b/src/utils/agentUtil.js @@ -1,6 +1,18 @@ // src/utils/agentUtil.js export function getAgentStatus(agent, nowMs = Date.now(), cfg = {}) { - if (agent._pendingKill) return "pending kill"; // <-- simple check + + if (agent._pendingKill) { + // Optional: distinguish final killed vs still pending + const lastSeen = agent._lastSeenMs || (agent.last_seen ? new Date(agent.last_seen).getTime() : null); + if (lastSeen) { + const sleepMaxSec = Number(agent.sleep_max) || Number(cfg.sleep_max) || 60; + const bufferMs = Math.min(180_000, Math.max(15_000, Math.floor(sleepMaxSec * 500))); + if (nowMs - lastSeen > sleepMaxSec * 1000 + bufferMs) { + return "killed"; // final state + } + } + return "pending kill"; + } if (!agent.last_seen) return "dead"; const lastMs = new Date(agent.last_seen).getTime(); diff --git a/src/views/AgentsView.vue b/src/views/AgentsView.vue index b275252..d0f4a2d 100644 --- a/src/views/AgentsView.vue +++ b/src/views/AgentsView.vue @@ -1,3 +1,9 @@ + +