// 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 = '

Targets

'; 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(); })();