Files

165 lines
6.8 KiB
JavaScript

// 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 =
'<td class="mono">' + tglEsc(r.tg) + '</td>' +
'<td>' + tglEsc(r.name) + '</td>' +
'<td>' + tglEsc(r.system) + '</td>' +
'<td class="mono">' + tglEsc(r.allstar) + '</td>' +
'<td>' + tglEsc(r.mode) + '</td>' +
'<td class="mono">' + tglEsc(r.owner) + '</td>' +
'<td class="text-end" style="white-space:nowrap;">' +
(canEdit ? '<button class="btn btn-outline-secondary btn-sm" data-tgl-edit="' + tglEsc(r.tg) + '" style="font-size:10px;padding:1px 6px;">edit</button>' : '') +
(tglCanAdmin ? ' <button class="btn btn-outline-danger btn-sm" data-tgl-del="' + tglEsc(r.tg) + '" style="font-size:10px;padding:1px 6px;">del</button>' : '') +
'</td>';
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' ? '&#9650; asc' : '&#9660; 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'));
});
});