Files
reflector-dashboard/lastheard_view.js
T

108 lines
4.4 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';
let __lhInflight = false;
async function refreshLastheard(){
if (__lhInflight) return;
__lhInflight = true;
try {
const r = await apiFetch('/svxportal/api/lastheard?limit=200'); // lib/api.js
if(!r.ok || !r.json){
renderLastheard({events:[], count:0, _err:`http ${r.status}`});
return;
}
const j = r.json;
renderLastheard(j);
} catch(e){
renderLastheard({events:[], count:0, _err:String(e)});
} finally {
__lhInflight = false;
}
}
function renderLastheard(payload){
const tbody = document.getElementById('lastheardRows');
const cnt = document.getElementById('lastheardCount');
if(!tbody) return;
const evs = (payload?.events || []).map(e => {
if (typeof e === 'string'){ try{ return JSON.parse(e); }catch{ return null; } }
return e;
}).filter(Boolean);
if (cnt) cnt.textContent = evs.length ? `${evs.length} event${evs.length===1?'':'s'} / last 24h` :
(payload?._err ? `error: ${payload._err}` : 'no events yet');
if (evs.length === 0){
tbody.innerHTML = `<tr><td colspan="8" class="text-secondary">No keyups recorded in last 24h.</td></tr>`;
return;
}
// Network bridge directory (crawled from every reflector): so a bridge that
// arrives bare over a trunk (hub reflector: node/system empty) renders IDENTICALLY to
// where it is a local client (bridge box: astmeta filled node/system). Same code,
// same result on both boxes.
const bn = (window.__svxportal_lastStatus && window.__svxportal_lastStatus.bridge_nodes) || {};
const now = Math.floor(Date.now()/1000);
tbody.innerHTML = evs.map(e => {
const ts = Number(e.ts || 0);
const age = now - ts;
const ageStr = formatAge(age);
const absTs = new Date(ts*1000).toLocaleString();
const dur = Number(e.duration_s || 0);
const durStr = dur >= 60 ? `${Math.floor(dur/60)}m${dur%60}s` : `${dur}s`;
const sig = (e.siglev !== undefined && e.siglev !== null && e.siglev !== 0)
? ` <span class="text-secondary mono">${Number(e.siglev).toFixed(0)}</span>` : '';
const rx = e.rx_name ? `${escHtml(e.rx_name)}${sig}` : '<span class="text-secondary">-</span>';
const bi = bn[String(e.callsign || '').toUpperCase()];
const eSystem = e.system || (bi && bi.system) || '';
// Backend = HOW the talker enters (AllStar / SVXLink / trunk / DMR / PBX …),
// the ingress path — same concept the DMR side calls "Backend" (there "Mode"
// means the DV protocol, a different fact). Carried in the network field.
const eBackend = e.network || (bi && bi.network) || '';
// Callsign column = the callsign field as-is (e.g. the trunk callsign
// KT4ROY-ASL). The gateway/reflector descriptor ("East Coast Reflector via
// 45225 / W2ECR") lives in the Location column. Uniform on every box: an
// AllStar row (bridge box) and the same talker arriving over the trunk (hub)
// render identically — the backend already fills callsign + location, the
// web just shows them, it never re-derives the name.
const csCell = `<b>${escHtml(e.callsign || eSystem || '')}</b>`;
return `<tr>
<td class="mono nowrap" title="${escHtml(absTs)}">${ageStr}</td>
<td class="mono">${csCell}</td>
<td class="wrap">${escHtml(eBackend)}</td>
<td class="mono nowrap">${Number(e.tg||0)}</td>
<td class="wrap">${escHtml(e.location || '')}</td>
<td class="wrap">${rx}</td>
<td class="mono nowrap">${durStr}</td>
</tr>`;
}).join('');
}
function formatAge(sec){
if (sec < 0) sec = 0;
if (sec < 60) return `${sec}s ago`;
if (sec < 3600) return `${Math.floor(sec/60)}m ago`;
if (sec < 86400) return `${Math.floor(sec/3600)}h ago`;
const d = Math.floor(sec/86400);
return `${d}d ago`;
}
function escHtml(s){
return String(s).replace(/[&<>"']/g, c => ({
'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'
}[c]));
}
window.refreshLastheard = refreshLastheard;
window.renderLastheard = renderLastheard;