2025-12-10 10:42:29 -05:00

141 lines
4.2 KiB
JavaScript

const socket = io();
const dashboard = document.getElementById('dashboard');
// Identify as a monitor
socket.emit('join_monitor');
socket.on('monitor_update', ({ streams, scores }) => {
updateDashboard(streams, scores);
});
function updateDashboard(streams, scores) {
const activeStreamNames = Object.keys(streams);
// 1. Remove streams that no longer exist
const existingContainers = document.querySelectorAll('.stream-container');
existingContainers.forEach(container => {
const name = container.getAttribute('data-stream');
if (!streams[name]) {
container.remove();
}
});
// 2. Add or Update streams
if (activeStreamNames.length === 0) {
// If empty, show message (only if not already showing it)
if (!dashboard.innerText.includes("No Active Streams")) {
dashboard.innerHTML = "<div style='text-align:center; color:#555; margin-top:50px;'>No Active Streams</div>";
}
return;
} else {
// FIX: Check if we are still showing a text message (Connecting, No Active Streams, etc)
// If we have streams but no stream containers yet, clear the text.
if (!dashboard.querySelector('.stream-container')) {
dashboard.innerHTML = "";
}
}
activeStreamNames.forEach(name => {
let container = document.getElementById(`stream-${name}`);
const chain = streams[name];
// Create Container if it doesn't exist
if (!container) {
container = document.createElement('div');
container.id = `stream-${name}`;
container.className = 'stream-container';
container.setAttribute('data-stream', name);
// Structure
const title = document.createElement('div');
title.className = 'stream-title';
const wrapper = document.createElement('div');
wrapper.className = 'chain-wrapper';
const visual = document.createElement('div');
visual.className = 'chain-visual';
wrapper.appendChild(visual);
container.appendChild(title);
container.appendChild(wrapper);
dashboard.appendChild(container);
}
// Update Title
const titleEl = container.querySelector('.stream-title');
titleEl.innerText = `Stream: ${name} (${chain.length} nodes)`;
// Update Nodes
updateChainVisual(container.querySelector('.chain-visual'), chain, scores);
});
}
function updateChainVisual(visualContainer, chain, scores) {
const existingNodes = Array.from(visualContainer.querySelectorAll('.node-wrapper'));
const nodeMap = {};
existingNodes.forEach(el => nodeMap[el.getAttribute('data-id')] = el);
const processedIds = new Set();
chain.forEach((socketId, index) => {
processedIds.add(socketId);
const score = scores[socketId] !== undefined ? scores[socketId] : '??';
let healthClass = 'meh';
if (score >= 80) healthClass = 'healthy';
if (score < 50) healthClass = 'weak';
if (index === 0) healthClass = 'healthy';
const role = index === 0 ? "SOURCE" : (index === chain.length - 1 ? "VIEWER" : "RELAY");
const shortId = socketId.substring(0, 4);
let nodeWrapper = nodeMap[socketId];
// --- CREATE ---
if (!nodeWrapper) {
nodeWrapper = document.createElement('div');
nodeWrapper.className = 'node-wrapper';
nodeWrapper.setAttribute('data-id', socketId);
nodeWrapper.style.display = 'flex';
nodeWrapper.style.alignItems = 'center';
nodeWrapper.innerHTML = `
<div class="arrow">➔</div>
<div class="node">
<div class="node-role"></div>
<div class="node-id"></div>
<div class="node-score"></div>
</div>
`;
visualContainer.appendChild(nodeWrapper);
}
// --- UPDATE ---
// 1. Order (Preserve Scroll)
if (visualContainer.children[index] !== nodeWrapper) {
visualContainer.insertBefore(nodeWrapper, visualContainer.children[index]);
}
// 2. Arrow Visibility
const arrow = nodeWrapper.querySelector('.arrow');
arrow.style.opacity = index === 0 ? "0" : "1";
// 3. Data
const nodeEl = nodeWrapper.querySelector('.node');
nodeEl.className = `node ${healthClass}`;
nodeWrapper.querySelector('.node-role').innerText = role;
nodeWrapper.querySelector('.node-id').innerText = shortId;
nodeWrapper.querySelector('.node-score').innerText = score;
});
// --- REMOVE ---
existingNodes.forEach(el => {
const id = el.getAttribute('data-id');
if (!processedIds.has(id)) {
el.remove();
}
});
}