From 6abb437fa499a21bbb079dd2eb5e799a9705eb0d Mon Sep 17 00:00:00 2001 From: Cort Buffington Date: Mon, 6 Jul 2026 08:31:58 -0500 Subject: [PATCH] dashboard: add Last Heard table and collapsible peer lists - Last Heard: most-recent unique callers derived from the call log (User / Type / Source / TS / Talkgroup / When), collapsible with per-browser state; configurable via LAST_HEARD / LAST_HEARD_COUNT. - Systems: a Show/Hide Peers button collapses each system's peer table; the system header and TS1/TS2 activity pills stay visible so live traffic is always in view. Default via SYSTEM_PEERS. - Docs updated (README.md, INSTALL.md). Co-Authored-By: Claude Opus 4.8 --- INSTALL.md | 4 ++ README.md | 3 +- dashboard/config_sample.py | 8 +++ dashboard/server.py | 17 +++++- dashboard/static/dashboard.html | 103 +++++++++++++++++++++++++++++++- 5 files changed, 131 insertions(+), 4 deletions(-) diff --git a/INSTALL.md b/INSTALL.md index bd4c793..40284d0 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -92,6 +92,10 @@ Edit `dashboard/config.py`: - `DMRLINK_IP` / `DMRLINK_PORT` — point at dmrlink3's `REPORT_PORT`. - `WEB_PORT` — port the dashboard web server listens on (default `8080`). - `PATH` — directory containing alias files (`peer_ids.json`, etc.). +- `LAST_HEARD` / `LAST_HEARD_COUNT` — default state (`'open'`/`'closed'`/`'off'`) + and length of the Last Heard table. +- `SYSTEM_PEERS` — default state (`'open'`/`'closed'`) of each system's + collapsible peer list; `'closed'` gives a compact view for many-system installs. --- diff --git a/README.md b/README.md index c5afdfa..155b2ad 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,8 @@ No dmrlink3 connection is required. This is useful for understanding what the da looks like under normal operating conditions before any traffic has been seen. **Dashboard features:** -- Live IPSC system status: peer table with radio IDs, addresses, and connection state +- **Last Heard** — most-recent unique callers (user, call type, source peer, timeslot, talkgroup, and when), derived live from the call log; collapsible, with its open/closed state remembered per-browser (`LAST_HEARD` / `LAST_HEARD_COUNT`) +- Live IPSC system status: peer table with radio IDs, addresses, and connection state. Each system's peer list is collapsible with the **Show/Hide Peers** button — the system header and TS1/TS2 activity pills stay visible, so live traffic is always in view even when collapsed. Default state is set by `SYSTEM_PEERS` (`'open'`/`'closed'`) and per-system choices are remembered per-browser - Per-system **TS1/TS2 activity pills** showing real-time slot state: - **RX** (green) — receiving a call from a peer, with source radio, peer ID, TGID, and elapsed time - **TX** (orange) — forwarding a bridged call, with originating radio, source peer, TGID, and elapsed time diff --git a/dashboard/config_sample.py b/dashboard/config_sample.py index d4fdf7a..c021444 100644 --- a/dashboard/config_sample.py +++ b/dashboard/config_sample.py @@ -24,6 +24,14 @@ WEB_PORT = 8080 # Port (must be > 1024 if not root) # Call log LOG_LINES = 300 # Number of recent call-log entries to retain +# Last Heard table — most-recent unique callers, shown at the top of the dashboard. +LAST_HEARD = 'open' # 'open' | 'closed' | 'off' (default state; 'off' hides it entirely) +LAST_HEARD_COUNT = 10 # Number of recent callers to list + +# Systems display — default state for each system's collapsible peer/repeater list. +# Users can toggle each system open/closed in their own browser (remembered locally). +SYSTEM_PEERS = 'open' # 'open' | 'closed' ('closed' gives a compact view for many-system installs) + # Alias files — the dashboard owns downloading, storing, and refreshing these. # Files live in the dashboard's own subdirectory (PATH = './') by default. # Set TRY_DOWNLOAD = True to fetch fresh files from radioid.net on startup and diff --git a/dashboard/server.py b/dashboard/server.py index ea529de..ed4db9a 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -62,6 +62,17 @@ try: except ImportError: FILTER_COUNTRIES = None +try: + from config import LAST_HEARD, LAST_HEARD_COUNT +except ImportError: + LAST_HEARD = 'open' + LAST_HEARD_COUNT = 10 + +try: + from config import SYSTEM_PEERS +except ImportError: + SYSTEM_PEERS = 'open' + logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s') logger = logging.getLogger('dmrdash') @@ -301,7 +312,11 @@ app = FastAPI(lifespan=lifespan) async def index(): with open(os.path.join(STATIC, 'dashboard.html'), encoding='utf-8') as f: html = f.read() - return html.replace('{{REPORT_NAME}}', REPORT_NAME).replace('{{LOGO_HTML}}', LOGO_HTML) + return (html.replace('{{REPORT_NAME}}', REPORT_NAME) + .replace('{{LOGO_HTML}}', LOGO_HTML) + .replace('{{LAST_HEARD}}', str(LAST_HEARD)) + .replace('{{LAST_HEARD_COUNT}}', str(LAST_HEARD_COUNT)) + .replace('{{SYSTEM_PEERS}}', str(SYSTEM_PEERS))) @app.get('/logo') async def serve_logo(): diff --git a/dashboard/static/dashboard.html b/dashboard/static/dashboard.html index 8fd5903..4e2ecc1 100644 --- a/dashboard/static/dashboard.html +++ b/dashboard/static/dashboard.html @@ -64,6 +64,13 @@ .sys-table th:nth-child(1),.sys-table td:nth-child(1){width:22%} .sys-table th:nth-child(2),.sys-table td:nth-child(2){width:42%} .sys-table th:nth-child(3),.sys-table td:nth-child(3){width:36%} + .lh-head{display:flex;align-items:center;gap:10px} + .toggle-btn{cursor:pointer;user-select:none;display:inline-flex;align-items:center;gap:6px;font-size:12px;font-weight:600;color:var(--accent);background:var(--panel2);border:1px solid var(--border);border-radius:6px;padding:3px 10px;text-transform:none;letter-spacing:0} + .toggle-btn:hover{border-color:var(--accent)} + .toggle-btn .caret{font-size:11px;line-height:1} + #lastHeardBody.hidden{display:none} + .peers-hdr .toggle-btn .sub{font-weight:400} + .sys-block.collapsed .sys-table,.sys-block.collapsed .empty{display:none} @@ -76,6 +83,7 @@
DMRlink3: connecting…
+

Last Heard ▾Hide

Systems

Conference Bridges

Call Log

@@ -84,6 +92,9 @@