Files

709 lines
33 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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 &amp; 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 &#9662;</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">&#9656; All</button>
<button id="ctcssCollapseAll" class="btn btn-outline-secondary btn-sm" title="Hide all tone details">&#9662; 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&rarr;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. &nbsp;&nbsp;•&nbsp;&nbsp; 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> &mdash; Reflector Activity Dashboard And Reporting</div>
&copy; 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>