170 lines
5.5 KiB
JavaScript
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();
|
|
})();
|