Files
reflector-dashboard/app.js
T
bjorneandClaude Fable 5.1 25b481e2ae Public web-only release: SvxPortal front-end + auth backend, install templates and INSTALL.md
Stripped from the internal repo: per-box deploy trees, Go services (nodelist/astmeta/proxy/svxdr),
deploy/sync scripts and all secrets. Config templates for nginx, PHP-FPM, Redis and tmpfiles
live in install/ with CHANGEME placeholders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 23:07:05 +02:00

234 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
let lastNodes = {};
let lastUpdated = 0;
let lastStatus = {};
function setNavActive(which){
const ids = ["navList","navClients","navMonitor","navTgmon","navLastheard","navCTCSS","navMap","navChangelog"];
for(const id of ids){
const el = document.getElementById(id);
if(!el) continue;
el.classList.toggle("active", id === which);
}
}
function setActive(panel){
const listP = document.getElementById('listPanel');
const cliP = document.getElementById('clientsPanel');
const monP = document.getElementById('monitorPanel');
const trkP = document.getElementById('trunksPanel');
const topP = document.getElementById('topologyPanel');
const lhP = document.getElementById('lastheardPanel');
const ctcP = document.getElementById('ctcssPanel');
const chgP = document.getElementById('changelogPanel');
const tgmP = document.getElementById('tgmonPanel');
if(listP) listP.style.display = (panel === 'list') ? 'block' : 'none';
if(cliP) cliP.style.display = (panel === 'clients') ? 'block' : 'none';
if(monP) monP.style.display = (panel === 'monitor') ? 'block' : 'none';
if(trkP) trkP.style.display = (panel === 'trunks') ? 'block' : 'none';
if(topP) topP.style.display = (panel === 'topology') ? 'block' : 'none';
if(lhP) lhP.style.display = (panel === 'lastheard') ? 'block' : 'none';
if(ctcP) ctcP.style.display = (panel === 'ctcss') ? 'block' : 'none';
if(chgP) chgP.style.display = (panel === 'changelog') ? 'block' : 'none';
if(tgmP) tgmP.style.display = (panel === 'tgmon') ? 'block' : 'none';
}
function render(status){
const nodes = status.nodes || {};
lastNodes = nodes;
lastStatus = status;
window.__svxportal_lastStatus = status;
window.__svxportal_lastNodes = nodes;
updateTalkerPopup(nodes, status.talker_meta);
// Header date/time/age
const now = new Date();
document.getElementById('dateTxt').textContent =
now.toLocaleDateString('en-GB', {weekday:'short', month:'short', day:'numeric'});
document.getElementById('timeTxt').textContent =
fmt2(now.getHours()) + ":" + fmt2(now.getMinutes()) + ":" + fmt2(now.getSeconds());
const age = lastUpdated ? Math.max(0, Math.floor((Date.now()-lastUpdated)/1000)) : 0;
document.getElementById('ageTxt').textContent = lastUpdated ? (age + "s") : "-";
// Render visible panels
if (document.getElementById('listPanel')?.style.display !== 'none') renderList(nodes);
if (document.getElementById('clientsPanel')?.style.display !== 'none') renderClients(nodes);
if (document.getElementById('monitorPanel')?.style.display !== 'none') renderMonitor(nodes, status);
if (document.getElementById('trunksPanel')?.style.display !== 'none') window.renderTrunks?.(lastStatus);
if (document.getElementById('topologyPanel')?.style.display !== 'none') window.renderTopology?.(lastStatus);
if (document.getElementById('lastheardPanel')?.style.display !== 'none') window.refreshLastheard?.();
if (document.getElementById('ctcssPanel')?.style.display !== 'none') window.renderCTCSS?.(lastStatus);
if (document.getElementById('tgmonPanel')?.style.display !== 'none') window.renderTgMon?.();
}
async function refresh(){
const j = await apiGet('/svxportal/api/status'); // lib/api.js
if(!j) return;
lastUpdated = Date.now();
render(j);
applyFederationExtra(j);
}
// On the AlabamaLink box (aldmr) fold the AlabamaLink line INTO the existing
// federation marquee so both scroll together in the one bar — same style, no
// second banner. Every other box keeps just the federation line. Gated on the
// local reflector identity from the status (KT4ROY / alabama.reflector-network.com),
// so it's a no-op everywhere else.
let _fedExtraDone = false;
function applyFederationExtra(status){
if(_fedExtraDone) return;
const track = document.querySelector('#federationBanner .fed-track');
if(!track) return;
const refs = (status && status.network && Array.isArray(status.network.reflectors)) ? status.network.reflectors : [];
const isAlabama = refs.some(r => r && r.self && (
String(r.name || '').toUpperCase() === 'KT4ROY' ||
/alabama\.reflector-network\.com/i.test(String(r.hostname || '') + ' ' + String(r.name || ''))
));
if(!isAlabama) return;
_fedExtraDone = true;
const fed = 'Part of the reflector-network.com federation — a growing mesh of independent networks.';
const ala = 'Powered by Executor — a multi-mode digital voice server, part of the AlabamaLink network.';
const sep = '  •  ';
const unit = fed + sep + ala;
const inner = document.createElement('span');
inner.textContent = unit + sep + unit; // doubled so the loop stays seamless
track.replaceChildren(inner);
}
/* ===== Monitor submenu ===== */
function closeMonitorSubMenu(){
const m = document.getElementById('monitorSubMenu');
if(m) m.style.display = 'none';
}
function toggleMonitorSubMenu(){
const m = document.getElementById('monitorSubMenu');
if(!m) return;
m.style.display = (m.style.display === 'none' || !m.style.display) ? 'block' : 'none';
}
document.addEventListener('click', (e) => {
const item = document.getElementById('monitorMenuItem');
if(item && !item.contains(e.target)) closeMonitorSubMenu();
});
/* ===== Wire UI ===== */
document.getElementById('navList')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navList');
setActive('list');
render({nodes:lastNodes});
});
document.getElementById('navClients')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navClients');
setActive('clients');
renderClients(lastNodes);
});
document.getElementById('navMonitor')?.addEventListener('click', (e) => {
e.preventDefault();
toggleMonitorSubMenu();
});
document.getElementById('navMonitorNodes')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navMonitor');
setActive('monitor');
renderMonitor(lastNodes, lastStatus);
});
document.getElementById('navMonitorTrunks')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navMonitor');
setActive('trunks');
window.renderTrunks?.(lastStatus);
});
document.getElementById('navMonitorTopology')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navMonitor');
setActive('topology');
window.renderTopology?.(lastStatus);
});
document.getElementById('navLastheard')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navLastheard');
setActive('lastheard');
window.refreshLastheard?.();
});
document.getElementById('navCTCSS')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navCTCSS');
setActive('ctcss');
window.renderCTCSS?.(lastStatus);
});
document.getElementById('navChangelog')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navChangelog');
setActive('changelog');
chLoad();
});
document.getElementById('navTgmon')?.addEventListener('click', (e) => {
e.preventDefault();
closeMonitorSubMenu();
setNavActive('navTgmon');
setActive('tgmon');
window.renderTgMon?.();
});
/* ===== Remember the last view across reloads (Ctrl+Shift+R) ===== */
const RESTORE_NAVS = ['navList','navClients','navMonitorNodes','navMonitorTrunks','navMonitorTopology','navTgmon','navLastheard','navCTCSS','navChangelog'];
for (const id of RESTORE_NAVS) {
document.getElementById(id)?.addEventListener('click', () => { try { localStorage.setItem('svxLastNav', id); } catch (_) {} });
}
(function restoreLastNav(){
let id = null; try { id = localStorage.getItem('svxLastNav'); } catch (_) {}
if (id && RESTORE_NAVS.includes(id)) { const el = document.getElementById(id); if (el) el.click(); }
})();
// Changelog: lib/changelog.js, fed by changelog.json.
const _chViewer = changelogViewer({ body: 'chBody', pager: 'chPager', fetchAt: 'chFetchAt' });
async function chLoad() { return _chViewer.load(); }
document.getElementById('monitorMode')?.addEventListener('change', () => {
renderMonitor(lastNodes, lastStatus);
});
/* ===== Start + SSE ===== */
refresh();
setInterval(() => { // keep Age ticking even without updates
const ageEl = document.getElementById('ageTxt');
if(lastUpdated) ageEl.textContent = Math.floor((Date.now()-lastUpdated)/1000) + "s";
}, 1000);
const sseTxt = document.getElementById('sseTxt');
function setSSE(state){
if(!sseTxt) return;
sseTxt.textContent = "SSE: " + state;
}
const es = new EventSource('/svxportal/api/stream');
es.addEventListener('update', () => refresh());
es.addEventListener('ping', () => {});
es.onopen = () => setSSE('ok');
es.onerror = () => setSSE('down');
setSSE('connecting');