// 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'; // TG List editor — canonical TG -> name/system list (served publicly by // /tglist.php). Mirrors the Global Bans admin pattern. Add/delete are admin-only; // an owner (matching callsign) can edit their own TG. The server enforces this; // the UI only reflects it. var $ = function (id) { return document.getElementById(id); }; var tglData = [], tglCanAdmin = false, tglMyCalls = [], tglEditingTg = null; function tglEsc(s) { var d = document.createElement('div'); d.textContent = s == null ? '' : s; return d.innerHTML; } function tglMsg(t, err) { var e = $('tglMsg'); if (!e) return; if (!t) { e.style.display = 'none'; return; } e.style.display = 'block'; e.textContent = t; e.style.color = err ? '#ff8080' : '#00ff80'; } async function tapi(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; }); } function tglCanEdit(row) { if (tglCanAdmin) return true; var o = (row.owner || '').toUpperCase(); return o !== '' && tglMyCalls.indexOf(o) >= 0; } // Filter predicate from the search box: try regex, fall back to a literal // substring if the pattern is invalid — it never throws. function tglMatcher(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); }; } function tglRender() { var body = $('tglBody'); if (!body) return; var match = tglMatcher($('tglSearch').value); var key = $('tglSortKey').value; var dir = $('tglSortDir').getAttribute('data-dir') === 'desc' ? -1 : 1; var rows = tglData.filter(function (r) { return match([r.tg, r.name, r.system, r.allstar, r.mode, r.owner].join(' ')); }); 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; }); body.innerHTML = ''; rows.forEach(function (r) { var tr = document.createElement('tr'); var canEdit = tglCanEdit(r); tr.innerHTML = '' + tglEsc(r.tg) + '' + '' + tglEsc(r.name) + '' + '' + tglEsc(r.system) + '' + '' + tglEsc(r.allstar) + '' + '' + tglEsc(r.mode) + '' + '' + tglEsc(r.owner) + '' + '' + (canEdit ? '' : '') + (tglCanAdmin ? ' ' : '') + ''; body.appendChild(tr); }); $('tglCount').textContent = rows.length + ' / ' + tglData.length + ' TG'; } function tglOpenForm(row) { var isNew = !row; tglEditingTg = isNew ? null : row.tg; $('tglfTg').value = isNew ? '' : row.tg; $('tglfName').value = isNew ? '' : row.name; $('tglfSystem').value = isNew ? '' : row.system; $('tglfAllstar').value = isNew ? '' : row.allstar; $('tglfMode').value = isNew ? '' : row.mode; $('tglfOwner').value = isNew ? '' : row.owner; $('tglfTg').disabled = !isNew; // TG fixed on edit, set only when adding $('tglfOwner').disabled = true; // owner is display-only (set via request approval) $('tglForm').style.display = 'block'; $('tglfName').focus(); } function tglCloseForm() { $('tglForm').style.display = 'none'; tglEditingTg = null; } async function tglSave() { var tg = parseInt($('tglfTg').value, 10); if (!tg || tg <= 0) { tglMsg('TG must be a positive number', true); return; } var body = { action: 'save', tg: tg, name: $('tglfName').value, system: $('tglfSystem').value, allstar: $('tglfAllstar').value.trim(), mode: $('tglfMode').value }; var res = await tapi(body); if (res && res.ok) { tglMsg('Saved TG ' + tg, false); tglCloseForm(); await tglLoad(); } else tglMsg((res && res.error) || 'Failed', true); } async function tglDelete(tg) { if (!confirm('Delete TG ' + tg + '?')) return; var res = await tapi({ action: 'delete', tg: parseInt(tg, 10) }); if (res && res.ok) { tglMsg('Deleted TG ' + tg, false); await tglLoad(); } else tglMsg((res && res.error) || 'Failed', true); } async function tglLoad() { var j = await tapi({ action: 'list' }); if (!j || !j.ok) return; tglData = j.tgs || []; tglCanAdmin = !!j.can_admin; tglMyCalls = (j.my_calls || []).map(function (s) { return (s || '').toUpperCase(); }); $('btnTglAdd').style.display = tglCanAdmin ? '' : 'none'; var ro = $('tglRoBadge'); if (ro) ro.style.display = (tglCanAdmin || tglMyCalls.length) ? 'none' : ''; tglRender(); } document.addEventListener('DOMContentLoaded', function () { fetch('/auth/me.php', { credentials: 'same-origin' }) .then(function (r) { return r.json(); }) .then(function (j) { if (j && j.csrf_token) window._csrfToken = j.csrf_token; }) .catch(function () {}) .finally(tglLoad); $('btnTglRefresh').addEventListener('click', tglLoad); $('btnTglAdd').addEventListener('click', function () { tglOpenForm(null); }); $('tglfSave').addEventListener('click', tglSave); $('tglfCancel').addEventListener('click', tglCloseForm); $('tglSearch').addEventListener('input', tglRender); $('tglSortKey').addEventListener('change', tglRender); $('tglSortDir').addEventListener('click', function () { var b = $('tglSortDir'); var d = b.getAttribute('data-dir') === 'asc' ? 'desc' : 'asc'; b.setAttribute('data-dir', d); b.innerHTML = d === 'asc' ? '▲ asc' : '▼ desc'; tglRender(); }); $('tglBody').addEventListener('click', function (e) { var ed = e.target.closest('button[data-tgl-edit]'); if (ed) { var tg = parseInt(ed.getAttribute('data-tgl-edit'), 10); var row = tglData.filter(function (r) { return r.tg === tg; })[0]; if (row) tglOpenForm(row); return; } var de = e.target.closest('button[data-tgl-del]'); if (de) tglDelete(de.getAttribute('data-tgl-del')); }); });