// Reflector Dashboard — 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';
// Admin TG editor (List B) — mirrors the DMR sysconfig SVX-satellite editor:
// list (data-table) -> per-row Edit -> grouped-field detail form -> an
// "Edit admins (N)" button that drills into an admins editor (owner shown
// first as always-admin, then co-admins with Add/Delete + Save/Back), exactly
// like renderSvxSatelliteEdit_ + renderAdminsEditor_. Backed by tglist_api.php.
var $ = function (id) { return document.getElementById(id); };
var tgaData = [], tgaPending = [], tgaSearch = '', tgaSortKey = 'tg', tgaSortDir = 'asc';
var tgaSelected = new Set(); // selected TG numbers for bulk actions
function esc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : String(s); return d.innerHTML; }
function tgaMsg(t, err) {
var e = $('tgadminMsg'); if (!e) return;
if (!t) { e.style.display = 'none'; return; }
e.style.display = 'block'; e.textContent = t; e.style.color = err ? '#ff8080' : '#0f0';
}
async function tgaApi(body) {
var r = await fetch('/auth/tglist_api.php', {
method: 'POST', credentials: 'same-origin',
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window._csrfToken || '' },
body: JSON.stringify(body)
});
return r.json().catch(function () { return null; });
}
// regex filter with literal fallback (never throws)
function tgaMatcher(q) {
q = (q || '').trim();
if (!q) return function () { return true; };
var re; try { re = new RegExp(q, 'i'); }
catch (e) { var lit = q.toLowerCase(); return function (s) { return s.toLowerCase().indexOf(lit) >= 0; }; }
return function (s) { return re.test(s); };
}
async function tgaLoad() {
var j = await tgaApi({ action: 'list' });
if (!j || !j.ok) { tgaMsg((j && j.error) || 'Load failed', true); return; }
tgaData = j.tgs || [];
var p = await tgaApi({ action: 'pending_list' });
tgaPending = (p && p.ok && p.pending) ? p.pending : [];
renderList();
}
function pendingHtml() {
if (!tgaPending.length) return '';
var h = '
Pending requests (' + tgaPending.length + ')
' +
'
TG
Name
System
AllStar
Mode
Requester
Actions
';
tgaPending.forEach(function (p) {
h += '
' +
'
' + esc(p.tg) + '
' + esc(p.name) + '
' + esc(p.system) + '
' +
'
' + esc(p.allstar) + '
' + esc(p.mode) + '
' + esc(p.callsign) + '
' +
'
' +
' ' +
'' +
'
';
});
return h + '
';
}
async function tgaApprove(uuid) {
var res = await tgaApi({ action: 'approve', uuid: uuid });
if (res && res.ok) { tgaMsg('Approved — created TG ' + res.tg, false); await tgaLoad(); }
else tgaMsg((res && res.error) || 'Failed', true);
}
async function tgaDeny(uuid) {
if (!confirm('Deny this TG request?')) return;
var res = await tgaApi({ action: 'deny', uuid: uuid });
if (res && res.ok) { tgaMsg('Denied.', false); await tgaLoad(); }
else tgaMsg((res && res.error) || 'Failed', true);
}
function rowByTg(tg) { var n = parseInt(tg, 10); return tgaData.filter(function (r) { return r.tg === n; })[0]; }
// ── list view (data-table, like renderArrList) ───────────────────────────
function renderList() {
var app = $('tgadminApp'); if (!app) return;
var match = tgaMatcher(tgaSearch);
var rows = tgaData.filter(function (r) { return match([r.tg, r.name, r.system, r.allstar, r.mode, r.owner].join(' ')); });
var key = tgaSortKey, dir = tgaSortDir === 'desc' ? -1 : 1;
rows.sort(function (a, b) {
var x = a[key], y = b[key];
if (key === 'tg') return (x - y) * dir;
x = (x || '').toLowerCase(); y = (y || '').toLowerCase();
return (x < y ? -1 : x > y ? 1 : 0) * dir;
});
var h = pendingHtml() +
'