fester/cockpit/fester-module/targets.js

170 lines
5.5 KiB
JavaScript

// cockpit/fester-module/targets.js
//
// Target registry + WebSocket subscription.
// Exposes window.FesterTargets with onUpdate() / update() / reset()
// so that live_dag.html can subscribe to a stable API even when
// loaded outside of Cockpit (e.g. via the mock dev server).
(function () {
'use strict';
// ---------- State ----------
const state = {
targets: [],
nodes: [], // DAG nodes
edges: [], // DAG edges
byNodeId: {},
};
const subs = new Set();
function emit() {
subs.forEach(cb => {
try { cb(state); } catch (e) { console.error('targets subscriber error', e); }
});
}
// ---------- Public API ----------
const FesterTargets = {
// Subscribe to snapshot updates.
// cb(snapshot) — snapshot = { targets, nodes, edges, byNodeId }
// Returns an unsubscribe function.
onUpdate(cb) {
subs.add(cb);
try { cb(state); } catch (e) { console.error(e); }
return () => subs.delete(cb);
},
// Patch the snapshot.
// patch = { targets?, nodes?, edges? }
// nodes are upserted by id; edges are deduped by from+to.
update(patch) {
if (patch.targets) {
state.targets = patch.targets;
}
if (patch.nodes) {
patch.nodes.forEach(n => {
const existing = state.byNodeId[n.id];
state.byNodeId[n.id] = existing ? Object.assign(existing, n) : n;
});
state.nodes = Object.values(state.byNodeId);
}
if (patch.edges) {
const seen = new Set(state.edges.map(e => e.from + '→' + e.to));
patch.edges.forEach(e => {
const k = e.from + '→' + e.to;
if (!seen.has(k)) { state.edges.push(e); seen.add(k); }
});
}
emit();
},
reset() {
state.nodes = [];
state.edges = [];
state.byNodeId = {};
emit();
},
get() { return state; },
};
window.FesterTargets = FesterTargets;
// ---------- Target list (REST) ----------
async function fetchTargets() {
try {
const res = await fetch('/api/targets');
const targets = await res.json();
FesterTargets.update({ targets });
renderTargets();
} catch (err) {
console.error('Failed to fetch targets:', err);
}
}
function renderTargets() {
let sidebar = document.getElementById('sidebar');
if (!sidebar) return;
let panel = document.getElementById('targets-panel');
if (!panel) {
panel = document.createElement('div');
panel.id = 'targets-panel';
panel.className = 'panel';
sidebar.appendChild(panel);
}
panel.innerHTML = '<h2>Targets</h2>';
state.targets.forEach(target => {
const container = document.createElement('div');
container.style.marginBottom = '5px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.id = `target-${target.name}`;
checkbox.checked = !!target.enabled;
checkbox.addEventListener('change', () => toggleTarget(target.name, checkbox.checked));
const label = document.createElement('label');
label.htmlFor = checkbox.id;
label.innerText = target.name;
container.appendChild(checkbox);
container.appendChild(label);
panel.appendChild(container);
});
}
async function toggleTarget(targetName, enabled) {
try {
const res = await fetch(`/api/targets/${targetName}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
});
if (!res.ok) {
console.error('Failed to update target', targetName);
} else {
console.log(`Target ${targetName} set to ${enabled}`);
}
} catch (err) {
console.error('Error toggling target:', err);
}
}
// ---------- WebSocket for live target updates ----------
function connectWs() {
const url = (location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host + '/ws-targets';
try {
const ws = new WebSocket(url);
ws.onmessage = (msg) => {
try {
const event = JSON.parse(msg.data);
if (event.type === 'target-update') {
const t = state.targets.find(t => t.name === event.data.name);
if (t) {
t.enabled = event.data.enabled;
FesterTargets.update({ targets: state.targets });
renderTargets();
}
}
} catch (e) {
console.error('bad ws-targets payload', e);
}
};
ws.onclose = () => {
// simple reconnect
setTimeout(connectWs, 2000);
};
} catch (e) {
console.warn('ws-targets unavailable', e);
}
}
// ---------- Init ----------
fetchTargets();
connectWs();
})();