Files
reflector-dashboard/lastheard_view.js
T
bjorneandClaude Fable 5.1 25b481e2ae Public web-only release: SvxPortal front-end + auth backend, install templates and INSTALL.md
Stripped from the internal repo: per-box deploy trees, Go services (nodelist/astmeta/proxy/svxdr),
deploy/sync scripts and all secrets. Config templates for nginx, PHP-FPM, Redis and tmpfiles
live in install/ with CHANGEME placeholders.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-26 23:07:05 +02:00

99 lines
3.9 KiB
JavaScript

'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 (speed: node/system empty) renders IDENTICALLY to
// where it is a local client (aldmr: 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 (aldmr) and the same talker arriving over the trunk (speed)
// 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;