709 lines
33 KiB
HTML
709 lines
33 KiB
HTML
<!doctype html>
|
||
<!-- Reflector Dashboard — Copyright (C) 2026 SA7AUX (Björn) — SPDX-License-Identifier: AGPL-3.0-or-later — see LICENSE -->
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<!-- Always revalidate the HTML so bumped ?v= asset versions are picked up (no Cache-Control header on the server yet). -->
|
||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||
<meta http-equiv="Pragma" content="no-cache" />
|
||
<meta http-equiv="Expires" content="0" />
|
||
<title>R.A.D.A.R. — Reflector Dashboard | SvxLink Nodes, Lastheard, CTCSS & Map</title>
|
||
<meta name="description" content="Reflector Dashboard — live dashboard for the SA7AUX SvxLink reflector: connected nodes, reflector clients, talkgroup monitor, lastheard, CTCSS tone table and node map for amateur radio / ham radio." />
|
||
<link rel="canonical" href="https://svxportal.sa7aux.se/" />
|
||
<meta property="og:type" content="website" />
|
||
<meta property="og:url" content="https://svxportal.sa7aux.se/" />
|
||
<meta property="og:title" content="Reflector Dashboard — SvxLink Reflector" />
|
||
<meta property="og:description" content="Live nodes, lastheard, talkgroup monitor, CTCSS table and map for the SA7AUX SvxLink reflector." />
|
||
<script type="application/ld+json">
|
||
{
|
||
"@context": "https://schema.org",
|
||
"@type": "WebSite",
|
||
"name": "Reflector Dashboard",
|
||
"url": "https://svxportal.sa7aux.se/",
|
||
"description": "Live dashboard for the SA7AUX SvxLink amateur radio reflector",
|
||
"publisher": { "@type": "Organization", "name": "SA7AUX", "url": "https://sa7aux.se/" }
|
||
}
|
||
</script>
|
||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||
|
||
<!-- Theme engine (Dark/Light/Color). External + in <head> (NOT deferred) so it
|
||
runs before first paint AND satisfies the site CSP (script-src 'self' — inline
|
||
scripts are blocked). Anonymous visitors are remembered in a cookie; signed-in
|
||
accounts also sync to the profile. -->
|
||
<script src="lib/theme.js?v=20260922a"></script>
|
||
|
||
<style>
|
||
/* Theme system — Dark (default) / Light / Color (user-defined).
|
||
All the cyan hardcodes below are driven off these vars: solid accents use
|
||
var(--fg)/var(--accent), translucent ones use rgba(var(--accent-rgb),a),
|
||
so switching a theme only swaps the tokens here — dark stays byte-identical. */
|
||
:root{
|
||
color-scheme: dark;
|
||
--accent-rgb: 0,255,255;
|
||
--bg: #000;
|
||
--panel: #111;
|
||
--accent: #00ffff;
|
||
--fg: #00ffff;
|
||
--fg2: rgba(var(--accent-rgb),.75);
|
||
--line: rgba(var(--accent-rgb),.35);
|
||
}
|
||
:root[data-theme="light"]{
|
||
color-scheme: light;
|
||
--accent-rgb: 10,102,194;
|
||
--bg: #f6f8fa;
|
||
--panel: #ffffff;
|
||
--accent: #0a66c2;
|
||
--fg: #14202b;
|
||
--fg2: #5a6b7b;
|
||
--line: rgba(20,32,43,.18);
|
||
}
|
||
:root[data-theme="color"]{
|
||
color-scheme: dark;
|
||
--accent-rgb: var(--uc-accent-rgb, 255,93,177);
|
||
--bg: var(--uc-bg, #000);
|
||
--panel: var(--uc-panel, #111);
|
||
--accent: var(--uc-accent, #ff5db1);
|
||
--fg: var(--uc-fg, #ff5db1);
|
||
--fg2: var(--uc-fg2, rgba(var(--accent-rgb),.78));
|
||
--line: var(--uc-line, rgba(var(--accent-rgb),.35));
|
||
}
|
||
|
||
html, body{ height:100%; background:var(--bg); color:var(--fg); margin:0; }
|
||
body{ display:flex; flex-direction:column; min-height:100vh; }
|
||
.navbar{ position:sticky; top:0; z-index:1030; }
|
||
main, .container-fluid.py-3{ flex:1; }
|
||
footer{ flex-shrink:0; position:sticky; bottom:0; background:var(--bg); }
|
||
body{ margin:0; }
|
||
|
||
/* Global "black + cyan" */
|
||
*{ color:var(--fg); }
|
||
*::selection{ background: rgba(var(--accent-rgb),.2); }
|
||
.border, .border-bottom, .border-top, .border-start, .border-end, .rounded { border-color: var(--line) !important; }
|
||
.navbar{ background: var(--bg) !important; }
|
||
/* Federation-banner: rullande (marquee) + tydlig accent-färg som sticker ut mot cyan/svart */
|
||
#federationBanner{ --fed: #ffb000; overflow:hidden; white-space:nowrap; box-sizing:border-box;
|
||
color:var(--fed) !important; font-weight:600; letter-spacing:.03em;
|
||
text-shadow:0 0 6px rgba(255,176,0,.35); }
|
||
#federationBanner .fed-track{ display:inline-block; padding-left:100%; will-change:transform;
|
||
animation: fedscroll 26s linear infinite; }
|
||
#federationBanner:hover .fed-track{ animation-play-state: paused; }
|
||
#federationBanner .fed-track span{ color:var(--fed) !important; }
|
||
@keyframes fedscroll{ 0%{ transform:translateX(0); } 100%{ transform:translateX(-100%); } }
|
||
@media (prefers-reduced-motion: reduce){ #federationBanner .fed-track{ animation:none; padding-left:0; text-align:center; display:block; } }
|
||
a, .nav-link{ color:var(--fg) !important; text-decoration:none; }
|
||
.nav-link.active{ text-decoration: underline; text-underline-offset: 3px; }
|
||
.text-muted, .text-secondary{ color: var(--fg2) !important; }
|
||
|
||
.btn.btn-outline-secondary{
|
||
background: var(--bg) !important;
|
||
color: var(--fg) !important;
|
||
border-color: rgba(var(--accent-rgb),.7) !important;
|
||
}
|
||
|
||
select, option, .form-select{
|
||
background: var(--bg) !important;
|
||
color: var(--fg) !important;
|
||
border-color: rgba(var(--accent-rgb),.7) !important;
|
||
}
|
||
|
||
/* Panels */
|
||
.panel{
|
||
border: 1px solid var(--line);
|
||
border-radius: 10px;
|
||
padding: 10px;
|
||
}
|
||
|
||
/* Talking now — compact strip on the menu row (left of the sign-in buttons) */
|
||
#talkerPopup{
|
||
display: none; /* toggled to inline-flex by JS via style.display */
|
||
align-items: baseline;
|
||
gap: 8px;
|
||
padding: 2px 10px;
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
max-width: min(55vw, 720px);
|
||
overflow: hidden;
|
||
white-space: nowrap;
|
||
}
|
||
#talkerPopup #talkerBody{ overflow: hidden; text-overflow: ellipsis; }
|
||
#talkerPopup #talkerBody > div{ display: inline; margin-right: 12px; }
|
||
@media (max-width: 992px){ #talkerPopup{ display: none !important; } } /* hide on narrow screens where the row has no room */
|
||
|
||
/* Lists */
|
||
.lineRow{
|
||
display:flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding: 6px 0;
|
||
border-bottom: 1px solid rgba(var(--accent-rgb),.18);
|
||
flex-wrap: wrap; /* mobile-safe */
|
||
}
|
||
.lineRow:last-child{ border-bottom: 0; }
|
||
|
||
.mono{ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; }
|
||
.nowrap{ white-space: nowrap; }
|
||
.wrap{ overflow-wrap: anywhere; }
|
||
|
||
/* TG groups (All active) */
|
||
.tgGroup{
|
||
border: 1px solid rgba(var(--accent-rgb),.25);
|
||
border-radius: 10px;
|
||
padding: 8px 10px;
|
||
margin: 8px 0;
|
||
}
|
||
.tgHeader{
|
||
display:flex;
|
||
align-items: baseline;
|
||
justify-content: space-between;
|
||
gap: 10px;
|
||
padding-bottom: 6px;
|
||
border-bottom: 1px solid rgba(var(--accent-rgb),.18);
|
||
margin-bottom: 6px;
|
||
}
|
||
.csGrid{
|
||
display:grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 4px 10px;
|
||
}
|
||
.csItem{
|
||
padding: 2px 0;
|
||
}
|
||
|
||
/* Table */
|
||
.table, .table > :not(caption) > * > * { background: var(--bg) !important; color: var(--fg) !important; }
|
||
|
||
/* Borderless, uniform-bg tables for Lastheard + CTCSS + Monitor */
|
||
#lastheardPanel .table, #ctcssPanel .table, #monitorPanel .table, #clientsPanel .table, #trunksPanel .table, #topologyPanel .table {
|
||
--bs-table-bg: var(--bg);
|
||
--bs-table-color: var(--fg);
|
||
--bs-table-striped-bg: var(--bg);
|
||
--bs-table-striped-color: var(--fg);
|
||
--bs-table-hover-bg: var(--bg);
|
||
--bs-table-hover-color: var(--fg);
|
||
--bs-table-active-bg: var(--bg);
|
||
--bs-table-active-color: var(--fg);
|
||
--bs-table-border-color: transparent;
|
||
}
|
||
#lastheardPanel .table > :not(caption) > * > *,
|
||
#ctcssPanel .table > :not(caption) > * > *,
|
||
#monitorPanel .table > :not(caption) > * > *,
|
||
#clientsPanel .table > :not(caption) > * > *,
|
||
#trunksPanel .table > :not(caption) > * > *,
|
||
#topologyPanel .table > :not(caption) > * > * {
|
||
border-top-width: 0 !important;
|
||
border-bottom-width: 0 !important;
|
||
box-shadow: none !important;
|
||
background: var(--bg) !important;
|
||
color: var(--fg) !important;
|
||
}
|
||
#lastheardPanel .table-responsive, #ctcssPanel .table-responsive, #monitorPanel .table-responsive, #clientsPanel .table-responsive, #trunksPanel .table-responsive, #topologyPanel .table-responsive,
|
||
#lastheardPanel .border, #ctcssPanel .border, #trunksPanel .border, #topologyPanel .border,
|
||
#lastheardPanel .border.rounded, #ctcssPanel .border.rounded, #trunksPanel .border.rounded, #topologyPanel .border.rounded { border-color: transparent !important; }
|
||
#lastheardPanel thead tr, #ctcssPanel thead tr, #clientsPanel thead tr, #topologyPanel thead tr { border-bottom: 1px solid var(--line) !important; }
|
||
/* Monitor → Trunks: no line under the column header either (no lines at all on this page). */
|
||
#trunksPanel thead tr { border-bottom: 0 !important; }
|
||
/* Monitor → Nodes + TG Monitor: completely borderless — NO lines between anything. */
|
||
#monitorPanel .table, #monitorPanel .table th, #monitorPanel .table td, #monitorPanel thead tr, #monitorPanel tbody tr,
|
||
#tgmonPanel .table, #tgmonPanel .table th, #tgmonPanel .table td, #tgmonPanel thead tr, #tgmonPanel tbody tr { border: 0 !important; box-shadow: none !important; }
|
||
/* ...except the section-name rows (Local / each trunk) get a line UNDER the name so the sections read apart. */
|
||
#monitorPanel tr.mon-head td { border-bottom: 2px solid var(--line) !important; }
|
||
#tgmonPanel .table { --bs-table-bg: var(--bg); --bs-table-color: var(--fg); --bs-table-border-color: transparent; }
|
||
|
||
/* CTCSS expandable rows */
|
||
#ctcssPanel tr.ctcssMain { cursor: pointer; }
|
||
#ctcssPanel tr.ctcssMain:hover { background: rgba(var(--accent-rgb),.05) !important; }
|
||
#ctcssPanel tr.ctcssMain td.ctcssChevron { width: 1.5em; text-align: center; user-select: none; color: var(--fg2); }
|
||
#ctcssPanel tr.ctcssMain td.ctcssCb { width: 1.5em; }
|
||
#ctcssPanel tr.ctcssDetail > td { padding: 8px 10px !important; }
|
||
#ctcssPanel .ctcssSub {
|
||
width: 100%;
|
||
border-collapse: collapse;
|
||
background: var(--bg);
|
||
}
|
||
#ctcssPanel .ctcssSub th, #ctcssPanel .ctcssSub td {
|
||
padding: 3px 8px;
|
||
border: none !important;
|
||
color: var(--fg);
|
||
background: var(--bg);
|
||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||
}
|
||
#ctcssPanel .ctcssSub th { color: var(--fg2); font-weight: normal; text-align: left; border-bottom: 1px solid rgba(var(--accent-rgb),.18) !important; }
|
||
#ctcssPanel .ctcssEmpty { color: var(--fg2); font-style: italic; padding: 4px 0; }
|
||
|
||
/* Mobile responsive */
|
||
@media (max-width: 768px) {
|
||
.container, .container-fluid { padding-left: 10px; padding-right: 10px; }
|
||
table.data-table { font-size: .75rem; }
|
||
table.data-table th, table.data-table td { padding: 4px 6px; }
|
||
.node-grid { grid-template-columns: 1fr; gap: 1px 0; }
|
||
.node-grid dt { margin-top: 6px; }
|
||
.panel { padding: 12px; }
|
||
.d-flex.gap-2 { gap: 4px !important; }
|
||
.btn-sm { font-size: .75rem; padding: 3px 8px; }
|
||
.mono { font-size: .8rem; word-break: break-all; }
|
||
.navbar-text { font-size: .75rem; }
|
||
#paginationWrap .btn { padding: 2px 6px !important; font-size: .7rem; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
table.data-table th:nth-child(3), table.data-table td:nth-child(3),
|
||
table.data-table th:nth-child(5), table.data-table td:nth-child(5),
|
||
table.data-table th:nth-child(6), table.data-table td:nth-child(6) { display: none; }
|
||
.profile-tabs { gap: 2px; }
|
||
.profile-tab { padding: 6px 8px; font-size: .8rem; }
|
||
}
|
||
</style>
|
||
|
||
<style>
|
||
.dropdown-menu{
|
||
background: var(--panel) !important;
|
||
border-color: rgba(var(--accent-rgb),.35) !important;
|
||
}
|
||
.dropdown-item{
|
||
color: var(--fg) !important;
|
||
}
|
||
.dropdown-item:hover{
|
||
background: rgba(var(--accent-rgb),.1) !important;
|
||
}
|
||
.dropdown-divider{
|
||
border-color: rgba(var(--accent-rgb),.35) !important;
|
||
}
|
||
#adminSubMenu{
|
||
position: absolute;
|
||
right: 100%;
|
||
left: auto;
|
||
top: 0;
|
||
min-width: 160px;
|
||
background: var(--panel);
|
||
border: 1px solid rgba(var(--accent-rgb),.35);
|
||
border-radius: 6px;
|
||
padding: 4px 0;
|
||
list-style: none;
|
||
margin: 0;
|
||
z-index: 2000;
|
||
}
|
||
#adminMenuItem{ position: relative; }
|
||
#adminSubMenu .dropdown-item{ padding: 6px 16px; white-space: nowrap; }
|
||
|
||
#monitorMenuItem{ position: relative; }
|
||
#monitorSubMenu{
|
||
position: absolute;
|
||
left: 0;
|
||
top: 100%;
|
||
min-width: 140px;
|
||
background: var(--panel);
|
||
border: 1px solid rgba(var(--accent-rgb),.35);
|
||
border-radius: 6px;
|
||
padding: 4px 0;
|
||
list-style: none;
|
||
margin: 0;
|
||
z-index: 2000;
|
||
}
|
||
#monitorSubMenu .dropdown-item{ padding: 6px 16px; white-space: nowrap; }
|
||
|
||
/* Mobile responsive */
|
||
@media (max-width: 768px) {
|
||
.container, .container-fluid { padding-left: 10px; padding-right: 10px; }
|
||
table.data-table { font-size: .75rem; }
|
||
table.data-table th, table.data-table td { padding: 4px 6px; }
|
||
.node-grid { grid-template-columns: 1fr; gap: 1px 0; }
|
||
.node-grid dt { margin-top: 6px; }
|
||
.panel { padding: 12px; }
|
||
.d-flex.gap-2 { gap: 4px !important; }
|
||
.btn-sm { font-size: .75rem; padding: 3px 8px; }
|
||
.mono { font-size: .8rem; word-break: break-all; }
|
||
.navbar-text { font-size: .75rem; }
|
||
#paginationWrap .btn { padding: 2px 6px !important; font-size: .7rem; }
|
||
}
|
||
@media (max-width: 480px) {
|
||
table.data-table th:nth-child(3), table.data-table td:nth-child(3),
|
||
table.data-table th:nth-child(5), table.data-table td:nth-child(5),
|
||
table.data-table th:nth-child(6), table.data-table td:nth-child(6) { display: none; }
|
||
.profile-tabs { gap: 2px; }
|
||
.profile-tab { padding: 6px 8px; font-size: .8rem; }
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<nav class="navbar navbar-expand border-bottom">
|
||
<div class="container-fluid">
|
||
<ul class="navbar-nav">
|
||
<li class="nav-item"><a class="nav-link active" href="#" id="navClients">Reflector clients</a></li>
|
||
<li class="nav-item" id="monitorMenuItem">
|
||
<a class="nav-link" href="#" id="navMonitor">Monitor ▾</a>
|
||
<ul id="monitorSubMenu" style="display:none;">
|
||
<li><a class="dropdown-item" href="#" id="navMonitorNodes">Nodes</a></li>
|
||
<li><a class="dropdown-item" href="#" id="navMonitorTrunks">Trunks</a></li>
|
||
<li><a class="dropdown-item" href="#" id="navMonitorTopology">Topology</a></li>
|
||
</ul>
|
||
</li>
|
||
<li class="nav-item"><a class="nav-link" href="#" id="navTgmon">TG Monitor</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="#" id="navLastheard">Lastheard</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="#" id="navCTCSS">CTCSS Table</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="#" id="navMap">Map</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="#" id="navChangelog">Changelog</a></li>
|
||
<li class="nav-item"><a class="nav-link" href="/help.html">Help</a></li>
|
||
</ul>
|
||
|
||
<!-- Talking now: lives on the menu row, to the left (after the links) -->
|
||
<div id="talkerPopup" class="ms-3" style="display:none;">
|
||
<span class="fw-bold" id="talkerTitle">Talking now</span>
|
||
<span id="talkerBody" class="small"></span>
|
||
</div>
|
||
|
||
<div style="margin-left:auto;"></div>
|
||
<select id="svxThemeSel" class="form-select form-select-sm me-2" style="width:auto;" title="Theme" aria-label="Theme" autocomplete="off">
|
||
<option value="dark">Dark</option>
|
||
<option value="light">Light</option>
|
||
<option value="color">Color</option>
|
||
</select>
|
||
<button class="btn btn-outline-secondary btn-sm me-2" id="btnColorEdit" type="button" title="Edit the Color theme" aria-label="Edit colors">🎨</button>
|
||
<button class="btn btn-outline-secondary btn-sm me-2" id="btnRegister" type="button">Register</button>
|
||
<button class="btn btn-outline-secondary btn-sm" id="btnSignIn" type="button">Sign in</button>
|
||
</div>
|
||
</nav>
|
||
|
||
<main class="container-fluid py-3">
|
||
<h1 class="h4 mb-3">Nodes</h1>
|
||
|
||
<div class="d-flex flex-wrap gap-3 small text-secondary mb-3">
|
||
<div><strong id="nodeCount">-</strong></div>
|
||
<div>Date <span id="dateTxt">-</span></div>
|
||
<div>Time <span id="timeTxt">-</span></div>
|
||
<div>Age <span id="ageTxt">-</span></div>
|
||
<div id="sseTxt">SSE: -</div>
|
||
</div>
|
||
|
||
<!-- Reflector clients -->
|
||
<div id="clientsPanel" class="panel mb-3">
|
||
<div class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="fw-bold me-auto">Reflector clients</div>
|
||
<input id="clientsSearch" type="search" class="form-control form-control-sm" style="width:auto;max-width:220px;" placeholder="Filter callsign or TG…" autocomplete="off">
|
||
</div>
|
||
<div id="clientsBody" class="small"></div>
|
||
</div>
|
||
|
||
<!-- Monitor / Nodes -->
|
||
<div id="monitorPanel" class="panel mb-3" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="fw-bold me-auto">Monitor — Nodes</div>
|
||
<input id="monitorSearch" type="search" class="form-control form-control-sm" style="width:auto;max-width:240px;" placeholder="Filter callsign / reflector (regex)…" autocomplete="off">
|
||
<select id="monitorMode" class="form-select form-select-sm" style="width:auto;">
|
||
<option value="all">All nodes</option>
|
||
<option value="active">All active</option>
|
||
</select>
|
||
</div>
|
||
<div id="monitorBody" class="small"></div>
|
||
</div>
|
||
|
||
<!-- Monitor / Trunks -->
|
||
<div id="trunksPanel" class="panel mb-3" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="fw-bold me-auto">Monitor — Trunks</div>
|
||
<input id="trunksSearch" type="search" class="form-control form-control-sm" style="width:auto;max-width:240px;" placeholder="Filter trunk / callsign / TG (regex)…" autocomplete="off">
|
||
</div>
|
||
<div id="trunksBody" class="small"></div>
|
||
</div>
|
||
|
||
<!-- Monitor / Topology -->
|
||
<div id="topologyPanel" class="panel mb-3" style="display:none;">
|
||
<div class="fw-bold mb-2">Monitor — Topology</div>
|
||
<div id="topologyBody" class="small"></div>
|
||
</div>
|
||
|
||
<!-- Lastheard -->
|
||
<div id="lastheardPanel" class="panel mb-3" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="fw-bold me-auto">Last Heard (24h)</div>
|
||
<span id="lastheardCount" class="small text-secondary"></span>
|
||
</div>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm mb-0 data-table">
|
||
<thead>
|
||
<tr>
|
||
<th class="nowrap">Time</th>
|
||
<th>Callsign</th>
|
||
<th class="nowrap">Backend</th>
|
||
<th class="nowrap">TG</th>
|
||
<th>Location</th>
|
||
<th>RX</th>
|
||
<th class="nowrap">Dur</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="lastheardRows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CTCSS Table -->
|
||
<div id="ctcssPanel" class="panel mb-3" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="fw-bold me-auto">CTCSS Table</div>
|
||
<button id="ctcssSelectAll" class="btn btn-outline-secondary btn-sm">Select all</button>
|
||
<button id="ctcssSelectNone" class="btn btn-outline-secondary btn-sm">None</button>
|
||
<button id="ctcssExpandAll" class="btn btn-outline-secondary btn-sm" title="Show tones for all nodes">▸ All</button>
|
||
<button id="ctcssCollapseAll" class="btn btn-outline-secondary btn-sm" title="Hide all tone details">▾ None</button>
|
||
<select id="ctcssFormat" class="form-select form-select-sm" style="width:auto;">
|
||
<option value="anytone878">AnyTone 878</option>
|
||
<option value="anytone890">AnyTone 890</option>
|
||
<option value="ftm400">FTM-400</option>
|
||
<option value="ic5100">IC 5100</option>
|
||
<option value="ic7100">IC 7100</option>
|
||
</select>
|
||
<button id="ctcssExport" class="btn btn-primary btn-sm">Export</button>
|
||
</div>
|
||
<div id="ctcssNote" class="small text-secondary mb-2"></div>
|
||
<div id="ctcssAnyToneDefaults" class="border rounded p-2 mb-2" style="display:none;"></div>
|
||
<div class="table-responsive border rounded">
|
||
<table class="table table-sm mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th></th>
|
||
<th></th>
|
||
<th>Callsign</th>
|
||
<th>Location</th>
|
||
<th class="nowrap">RX</th>
|
||
<th class="nowrap">TX</th>
|
||
<th class="nowrap">CTCSS→TG</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="ctcssRows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Changelog: recent updates, fed by changelog.json, paginated client-side. -->
|
||
<div id="changelogPanel" class="panel mb-3" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 mb-2">
|
||
<div class="fw-bold me-auto">Recent updates</div>
|
||
<span id="chFetchAt" class="small text-secondary"></span>
|
||
</div>
|
||
<div id="chBody" class="small"><div class="text-secondary">Loading…</div></div>
|
||
<div id="chPager" class="d-flex justify-content-center align-items-center gap-1 mt-2 flex-wrap"></div>
|
||
</div>
|
||
|
||
<div id="tgmonPanel" class="panel mb-3" style="display:none;">
|
||
<div class="d-flex align-items-center gap-2 mb-1">
|
||
<div class="fw-bold me-auto">TG Monitor <span class="text-secondary small">— clients per talkgroup, per mode</span></div>
|
||
<input id="tgmonSearch" type="search" class="form-control form-control-sm" style="width:auto;max-width:260px;" placeholder="Search TG or callsign/node…" autocomplete="off">
|
||
</div>
|
||
<div class="small text-secondary mb-2">Only talkgroups with at least one client. Both sides shown together. Search a TG number to see who's on it, or a callsign/node to see which TGs it's on. Click a talkgroup to expand.</div>
|
||
<div id="tgmonBody"><div class="text-secondary">Loading…</div></div>
|
||
</div>
|
||
|
||
<!-- List (legacy, hidden — Reflector clients is now the default landing view) -->
|
||
<div id="listPanel" style="display:none;">
|
||
<div class="table-responsive border rounded">
|
||
<table class="table table-sm mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Callsign</th>
|
||
<th class="nowrap">TG#</th>
|
||
<th>Location</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<script src="lib/api.js?v=20260922a" defer></script>
|
||
<script src="lib/ui_msg.js?v=20260922a" defer></script>
|
||
<script src="lib/pager.js?v=20260922a" defer></script>
|
||
<script src="lib/changelog.js?v=20260922a" defer></script>
|
||
<script src="lib/auth_ui.js?v=20260922a" defer></script>
|
||
<script src="lib/fmt.js?v=20260922a" defer></script>
|
||
<script src="lib/ui_list.js?v=20260922a" defer></script>
|
||
<script src="lib/ui_field.js?v=20260922a" defer></script>
|
||
<script src="common.js?v=20260702a" defer></script>
|
||
<script src="hoverinfo.js?v=20260704a" defer></script>
|
||
<script src="list_view.js?v=20260702a" defer></script>
|
||
<script src="clients_view.js?v=20260727a" defer></script>
|
||
<script src="monitor_view.js?v=20260806e" defer></script>
|
||
<script src="trunks_view.js?v=20260806c" defer></script>
|
||
<script src="topology_view.js?v=20260806b" defer></script>
|
||
<script src="lastheard_view.js?v=20260803a" defer></script>
|
||
<script src="talker_view.js?v=20260720a" defer></script>
|
||
<script src="ctcss_view.js?v=20260704e" defer></script>
|
||
<script src="tgmon_view.js?v=20260826a" defer></script>
|
||
<script src="auth_ui.js?v=20260717a" defer></script>
|
||
<script src="app.js?v=20260728a" defer></script>
|
||
|
||
<!-- Color edit — pick each part of the "Color" theme with a colour swatch.
|
||
Applies live; remembered in a cookie (and on the account when signed in). -->
|
||
<div class="modal fade" id="colorEditModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border rounded" style="background: var(--bg);">
|
||
<div class="modal-header border-bottom">
|
||
<h5 class="modal-title">Color edit</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="filter: invert(1);"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="small text-secondary mb-2">Pick each colour for the <b>Color</b> theme. Changes apply live.</div>
|
||
<div id="colorPickers" class="d-flex flex-column gap-2"></div>
|
||
<div class="small text-secondary mt-3 mb-1">Accent quick-picks</div>
|
||
<div id="colorPresets" class="d-flex flex-wrap gap-2"></div>
|
||
<div class="small text-secondary mt-3 mb-1">Preview</div>
|
||
<div class="panel d-flex gap-2 flex-wrap align-items-center">
|
||
<button class="btn btn-sm btn-primary" type="button">Primary</button>
|
||
<button class="btn btn-sm btn-outline-secondary" type="button">Outline</button>
|
||
<span class="mono">mono text</span>
|
||
<span class="text-secondary">secondary</span>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer border-top">
|
||
<button class="btn btn-outline-secondary" id="btnColorReset" type="button">Reset to defaults</button>
|
||
<button class="btn btn-primary" data-bs-dismiss="modal" type="button">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Cookie consent -->
|
||
<div class="modal fade" id="cookieModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border rounded" style="background: var(--bg);">
|
||
<div class="modal-header border-bottom">
|
||
<h5 class="modal-title">Cookies</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
||
style="filter: invert(1);"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="small text-secondary">
|
||
Vi använder nödvändiga cookies för inloggning (session). Valfria cookies används bara om du accepterar alla.
|
||
</div>
|
||
<div class="small mt-2">
|
||
<a href="/privacy" class="text-decoration-underline">Cookie/Privacy policy</a>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer border-top">
|
||
<button class="btn btn-outline-secondary" id="btnCookiesNecessary" type="button">Endast nödvändiga</button>
|
||
<button class="btn btn-primary" id="btnCookiesAll" type="button">Acceptera alla</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sign in modal -->
|
||
<div class="modal fade" id="signinModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border rounded" style="background: var(--bg);">
|
||
<div class="modal-header border-bottom">
|
||
<h5 class="modal-title">Sign in</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"
|
||
style="filter: invert(1);"></button>
|
||
</div>
|
||
<div class="modal-body"><form id="signinForm" autocomplete="on">
|
||
<div id="signinErr" class="small mb-2" style="display:none; color:#ff8080;"></div>
|
||
|
||
<label class="form-label small text-secondary">Callsign or email</label>
|
||
<input id="signinUser" class="form-control mb-2" autocomplete="username" placeholder="SA7AUX or you@example.com" />
|
||
|
||
<label class="form-label small text-secondary">Password</label>
|
||
<input id="signinPass" type="password" class="form-control mb-3" autocomplete="current-password" />
|
||
|
||
<div class="d-flex gap-2 align-items-center">
|
||
<button id="signinSubmit" class="btn btn-primary" type="submit">Login</button>
|
||
<div class="ms-auto small">
|
||
<a href="#" id="linkSignup" class="text-decoration-underline">Register</a>
|
||
<span class="text-secondary">·</span>
|
||
<a href="#" id="linkForgot" class="text-decoration-underline">Forgot password?</a>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="small text-secondary mt-3">
|
||
Om du har 2FA (Authenticator) aktiverat kan du bli ombedd om en 6‑siffrig kod efter lösenordet.
|
||
</div>
|
||
</form></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Forgot password modal -->
|
||
<div class="modal fade" id="forgotModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border rounded" style="background: var(--bg);">
|
||
<div class="modal-header border-bottom">
|
||
<h5 class="modal-title">Forgot Password</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="filter: invert(1);"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="forgotForm">
|
||
<div id="forgotMsg" class="small mb-2" style="display:none;"></div>
|
||
<label class="form-label small text-secondary">Callsign</label>
|
||
<input id="forgotCallsign" class="form-control form-control-sm mb-2" placeholder="SA7AUX" autocomplete="off" />
|
||
<label class="form-label small text-secondary">Email</label>
|
||
<input id="forgotEmail" type="email" class="form-control form-control-sm mb-3" autocomplete="email" />
|
||
<button id="forgotSubmit" class="btn btn-primary btn-sm" type="submit">Send Reset Link</button>
|
||
<p class="small text-secondary mt-2 mb-0">A password reset link will be sent to your email address.</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Register modal -->
|
||
<div class="modal fade" id="registerModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered">
|
||
<div class="modal-content border rounded" style="background: var(--bg);">
|
||
<div class="modal-header border-bottom">
|
||
<h5 class="modal-title">Create Account</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" style="filter: invert(1);"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form id="registerForm" autocomplete="on">
|
||
<div id="regMsg" class="small mb-2" style="display:none;"></div>
|
||
<label class="form-label small text-secondary">Callsign <span style="color:#ff4040;">*</span></label>
|
||
<input id="regCallsign" class="form-control form-control-sm mb-2" placeholder="SA7AUX" autocomplete="off" />
|
||
<div class="row">
|
||
<div class="col-6">
|
||
<label class="form-label small text-secondary">First name <span style="color:#ff4040;">*</span></label>
|
||
<input id="regFirstname" class="form-control form-control-sm mb-2" autocomplete="given-name" />
|
||
</div>
|
||
<div class="col-6">
|
||
<label class="form-label small text-secondary">Last name</label>
|
||
<input id="regLastname" class="form-control form-control-sm mb-2" autocomplete="family-name" />
|
||
</div>
|
||
</div>
|
||
<label class="form-label small text-secondary">Email <span style="color:#ff4040;">*</span></label>
|
||
<input id="regEmail" type="email" class="form-control form-control-sm mb-2" autocomplete="email" />
|
||
<label class="form-label small text-secondary">Password <span style="color:#ff4040;">*</span></label>
|
||
<input id="regPass1" type="password" class="form-control form-control-sm mb-2" autocomplete="new-password" />
|
||
<label class="form-label small text-secondary">Confirm password <span style="color:#ff4040;">*</span></label>
|
||
<input id="regPass2" type="password" class="form-control form-control-sm mb-2" autocomplete="new-password" />
|
||
<div class="row">
|
||
<div class="col-4">
|
||
<label class="form-label small text-secondary">City</label>
|
||
<input id="regCity" class="form-control form-control-sm mb-2" autocomplete="address-level2" />
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small text-secondary">State</label>
|
||
<input id="regState" class="form-control form-control-sm mb-2" autocomplete="address-level1" />
|
||
</div>
|
||
<div class="col-4">
|
||
<label class="form-label small text-secondary">Country</label>
|
||
<input id="regCountry" class="form-control form-control-sm mb-2" autocomplete="country-name" />
|
||
</div>
|
||
</div>
|
||
<button id="regSubmit" class="btn btn-primary btn-sm mt-2" type="submit">Create Account</button>
|
||
<p class="small text-secondary mt-2 mb-0">After email verification, an administrator will activate your account.</p>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Federation banner — moved from the top to just above the copyright. -->
|
||
<div id="federationBanner" class="py-1" style="font-size:1.02rem;" title="Hover to pause">
|
||
<span class="fed-track"><span>Part of the reflector-network.com federation — a growing mesh of independent networks. • Part of the reflector-network.com federation — a growing mesh of independent networks.</span></span>
|
||
</div>
|
||
<footer style="text-align:center; padding:16px; color:rgba(var(--accent-rgb),.4); font-size:.75rem; border-top:1px solid rgba(var(--accent-rgb),.15); ">
|
||
<div class="mb-1"><strong>R.A.D.A.R.</strong> — Reflector Activity Dashboard And Reporting</div>
|
||
© 2026 SA7AUX. All rights reserved.
|
||
</footer>
|
||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||
</body>
|
||
</html> |