165 lines
6.8 KiB
JavaScript
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' ? '▲ 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'));
|
|
});
|
|
});
|