// SvxPortal — web front-end for SvxReflector
// Copyright (C) 2026 SA7AUX (Björn)
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU Affero General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option) any
// later version. See the LICENSE file for details.
'use strict';
// TG Monitor — vilka klienter finns på varje TG, per mode + sida (SVX/DMR).
// Symmetrisk motsvarighet till DMR-webbens TG Monitor. Läser status-noderna
// (network.reflectors[].nodes + network.trunks[].nodes): reflektor-/lokala noder
// = SVX-sidan, trunk-noder = DMR-sidan (kom in via satellite-trunken). Dedupar
// per (tg, side, callsign) så en callsign kan finnas på flera TG och båda sidor
// utan att kollapsa (den globala callsign-keyingen i nodelist gör annars det).
// Bara TG med >=1 nod. Klick på en TG öppnar en egen detaljvy. Uppdateras live
// av status-refreshen (render() anropar renderTgMon när panelen är synlig).
(function () {
let _sel = null; // vald TG (detaljvy) eller null (lista)
let _page = 1; // sida i TG-listan
let _tgNames = {}; // tg -> name, from the canonical /tglist.php
function tgName(tg) { return _tgNames[tg] || ''; }
// "TG 3149050 — East Coast Reflector" when the list names the TG, else "TG n".
function tgLabel(tg) { const n = tgName(tg); return 'TG ' + tg + (n ? ' — ' + escHtml(n) : ''); }
// Rader/sida anpassas till FAKTISKT utrymme under tabellen (mät var den börjar)
// så den får plats utan att scrolla; ta i lite (footer-pager + rubrik).
// En bryggnod (chan_svx-asterisk, DMR/trunk-brygga …) är ingen riktig klient —
// den kopplar bara ihop system och gör listan missvisande. Känns igen på sw
// (chan_svx) eller nod-namnets brygg-suffix (-AC/-DMR/-T). Gäller ALLA sådana,
// både lokala och fjärran (t.ex. KT4ROY:s asterisk) — vi tillskriver dem bara
// aldrig fel ägare.
const BRIDGE_SUFFIX = /-(AC|DMR|T)$/i;
function isBridge(n) {
if (!n) return true;
const sw = String(n.sw || '').toLowerCase();
// chan_svx_sat = a real AllStar node over the satellite trunk → KEEP it.
if (sw.indexOf('chan_svx_sat') !== -1) return false;
if (sw.indexOf('chan_svx') !== -1) return true; // plain chan_svx = reflector bridge
const nm = String(n.name || n.node || n.id || '');
if (nm && BRIDGE_SUFFIX.test(nm)) return true;
return false;
}
// The real mode/type of a node, derived from its software (svx nodes have no
// "mode" field like the dmr side does). SVX / AllStar / DMR / YSF / …
function modeOf(n) {
const sw = String((n && n.sw) || '').toLowerCase();
if (sw.indexOf('chan_svx_sat') !== -1) return 'AllStar';
if (sw.indexOf('chan_svx') !== -1) return 'AllStar';
if (sw.indexOf('svxlink') !== -1) return 'SVX';
if (sw.indexOf('mmdvm') !== -1 || sw.indexOf('brandmeister') !== -1 || sw.indexOf('dmr') !== -1) return 'DMR';
if (sw.indexOf('ysf') !== -1) return 'YSF';
if (sw.indexOf('nxdn') !== -1) return 'NXDN';
if (sw.indexOf('m17') !== -1) return 'M17';
return (n && n.sw) ? String(n.sw) : 'SVX';
}
// Samla alla noder → {tg, callsign, mode, side, location}. En nod läggs under
// sin nuvarande TG OCH varje monitored TG (så "lyssnar på TG x" räknas med).
// Bryggnoder hoppas över (bara riktiga radioklienter).
function collect() {
const st = window.__svxportal_lastStatus || {};
const net = st.network || {};
const out = [];
const push = (n) => {
if (!n || isBridge(n)) return;
const cs = n.callsign || '';
if (!cs) return;
const m = modeOf(n); // real type from sw: SVX / AllStar / DMR / …
const tgs = new Set();
if (Number(n.tg) > 0) tgs.add(Number(n.tg));
if (Array.isArray(n.monitoredTGs)) n.monitoredTGs.forEach(t => { if (Number(t) > 0) tgs.add(Number(t)); });
tgs.forEach(tg => out.push({ tg, callsign: cs, mode: m, side: m, location: n.nodeLocation || n.location || '' }));
};
(Array.isArray(net.reflectors) ? net.reflectors : []).forEach(r => {
if (!r || !Array.isArray(r.nodes)) return;
if (r.type === 'local' || (r.type === 'reflector' && r.online && !r.firewalled))
r.nodes.forEach(n => push(n));
});
(Array.isArray(net.trunks) ? net.trunks : []).forEach(t => {
if (!t || !Array.isArray(t.nodes)) return;
t.nodes.forEach(n => push(n));
});
return out;
}
// Gruppera per TG, dedupa per (tg, side, callsign). Bara TG med noder.
function byTg() {
const map = new Map();
for (const n of collect()) {
if (!map.has(n.tg)) map.set(n.tg, new Map());
const key = n.side + '|' + n.callsign;
if (!map.get(n.tg).has(key)) map.get(n.tg).set(key, n);
}
const arr = [...map.entries()].map(([tg, m]) => ({ tg, nodes: [...m.values()] }))
.filter(e => e.nodes.length > 0);
arr.sort((a, b) => a.tg - b.tg);
return arr;
}
function sideBadge(m) {
const map = { SVX:'bg-info text-dark', AllStar:'bg-warning text-dark', DMR:'bg-secondary', YSF:'bg-success', NXDN:'bg-primary', M17:'bg-danger' };
return `${escHtml(m)}`;
}
function renderTgMon() {
const body = document.getElementById('tgmonBody');
if (!body) return;
const entries = byTg();
if (_sel != null) {
const t = entries.find(e => e.tg === _sel);
const nodes = t ? t.nodes : [];
let inner;
if (!nodes.length) {
inner = '