// 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'; var $ = function(id) { return document.getElementById(id); }; function setMsg(el, txt, err) { if (!el) return; if (!txt) { el.style.display='none'; return; } el.style.display='block'; el.textContent=txt; el.style.color=err?'#ff8080':'#00ff80'; } function esc(s) { var d=document.createElement('div'); d.textContent=s; return d.innerHTML; } async function rapi(body) { var r = await fetch('/auth/admin_reflector_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; }); } var configOriginal = ''; async function loadConfig() { var j = await rapi({action:'config_read'}); if (!j || !j.ok) { setMsg($('cfgMsg'), (j&&j.error)||'Failed', true); return; } configOriginal = j.raw || ''; $('cfgView').textContent = configOriginal; // The backend decides who may edit: LVL2/LVL3 → true (LVL2 sees secrets masked // and they're restored on save), LVL1 → false (read-only). $('btnEdit').style.display = j.can_edit ? '' : 'none'; showView(); } function showView() { $('viewMode').style.display=''; $('editMode').style.display='none'; } function showEdit() { $('cfgEditor').value=configOriginal; $('viewMode').style.display='none'; $('editMode').style.display=''; setMsg($('cfgMsg'),null); } async function saveConfig() { var raw = $('cfgEditor').value; if (!raw.trim()) { setMsg($('cfgMsg'),'Config is empty',true); return; } var j = await rapi({action:'config_save', raw:raw}); if (j && j.ok) { setMsg($('cfgMsg'),'Config saved. Reload service to apply.',false); configOriginal=raw; loadConfig(); } else setMsg($('cfgMsg'),(j&&j.error)||'Failed',true); } 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(){}); loadConfig(); $('btnView').addEventListener('click', showView); $('btnEdit').addEventListener('click', showEdit); $('btnSave').addEventListener('click', saveConfig); $('btnSaveReload').addEventListener('click', async function() { var raw = $('cfgEditor').value; if (!raw.trim()) { setMsg($('cfgMsg'),'Config is empty',true); return; } var j = await rapi({action:'config_save', raw:raw}); if (!j || !j.ok) { setMsg($('cfgMsg'),(j&&j.error)||'Save failed',true); return; } configOriginal = raw; var r = await rapi({action:'service_reload'}); if (r && r.ok) { setMsg($('cfgMsg'),'Config saved and service reloaded.',false); loadConfig(); } else { setMsg($('cfgMsg'),'Saved but reload failed: '+((r&&r.error)||''),true); } }); $('btnDiscard').addEventListener('click', function() { $('cfgEditor').value=configOriginal; showView(); setMsg($('cfgMsg'),'Discarded',false); }); });