From 3de5e68e6808d41f917e89528def953738dadbcc Mon Sep 17 00:00:00 2001 From: ribs Date: Sat, 28 Feb 2026 14:11:04 -0800 Subject: [PATCH 1/5] fix: improve pager message display and encryption classification Three issues caused POCSAG messages to be incorrectly hidden or misclassified in the Device Intelligence panel: 1. detectEncryption used a narrow character class ([a-zA-Z0-9\s.,!?-]) to measure "printable ratio". Messages containing common printable ASCII characters like : = / + @ fell below the 0.8 threshold and returned null ("Unknown") instead of false ("Plaintext"). Simplified to check all printable ASCII (\x20-\x7E) which correctly classifies base64, structured data, and punctuation-heavy content. 2. The default hideToneOnly filter was true, hiding all address-only (Tone) pager messages. When RF conditions cause multimon-ng to decode the address but not the message content, the resulting Tone card was silently filtered. Changed default to false so users see all traffic and can opt-in to filtering. 3. The multimon-ng output parser only recognized "Alpha" and "Numeric" content type labels. Added a catch-all pattern to capture any additional content type labels that future multimon-ng versions or forks might emit, rather than dropping them to raw output. Co-Authored-By: Claude Opus 4.6 --- routes/pager.py | 14 ++++++++++++++ templates/index.html | 23 ++++++++++++----------- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/routes/pager.py b/routes/pager.py index 7777ced..3c53d7a 100644 --- a/routes/pager.py +++ b/routes/pager.py @@ -55,6 +55,20 @@ def parse_multimon_output(line: str) -> dict[str, str] | None: 'message': pocsag_match.group(5).strip() or '[No Message]' } + # POCSAG parsing - other content types (catch-all for non-Alpha/Numeric labels) + pocsag_other_match = re.match( + r'(POCSAG\d+):\s*Address:\s*(\d+)\s+Function:\s*(\d+)\s+(\w+):\s*(.*)', + line + ) + if pocsag_other_match: + return { + 'protocol': pocsag_other_match.group(1), + 'address': pocsag_other_match.group(2), + 'function': pocsag_other_match.group(3), + 'msg_type': pocsag_other_match.group(4), + 'message': pocsag_other_match.group(5).strip() or '[No Message]' + } + # POCSAG parsing - address only (no message content) pocsag_addr_match = re.match( r'(POCSAG\d+):\s*Address:\s*(\d+)\s+Function:\s*(\d+)\s*$', diff --git a/templates/index.html b/templates/index.html index 9d5cbf8..f4493df 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3611,7 +3611,7 @@ // Pager message filter settings let pagerFilters = { - hideToneOnly: true, + hideToneOnly: false, keywords: [] }; @@ -6911,20 +6911,21 @@ return null; // Can't determine } - // Check for high entropy (random-looking data) - const printableRatio = (message.match(/[a-zA-Z0-9\s.,!?-]/g) || []).length / message.length; - - // Check for common encrypted patterns (hex strings, base64-like) - const hexPattern = /^[0-9A-Fa-f\s]+$/; + // Check for non-printable characters (outside printable ASCII range) const hasNonPrintable = /[^\x20-\x7E]/.test(message); - if (printableRatio > 0.8 && !hasNonPrintable) { - return false; // Likely plaintext - } else if (hexPattern.test(message.replace(/\s/g, '')) || hasNonPrintable) { - return true; // Likely encrypted or encoded + // Check for common encrypted patterns (hex strings) + const hexPattern = /^[0-9A-Fa-f\s]+$/; + + if (hasNonPrintable) { + return true; // Contains non-printable chars — likely encrypted or encoded + } + if (hexPattern.test(message.replace(/\s/g, ''))) { + return true; // Pure hex data — likely encoded } - return null; // Unknown + // All printable ASCII (covers base64, structured data, punctuation, etc.) + return false; // Likely plaintext } // Generate device fingerprint From d3326409bf73b7d0b430594b1e3d2cb6a93f15b0 Mon Sep 17 00:00:00 2001 From: ribs Date: Sat, 28 Feb 2026 14:15:39 -0800 Subject: [PATCH 2/5] test: add unit tests for pager multimon-ng output parser Cover all parse_multimon_output code paths: - Alpha and Numeric content types across POCSAG baud rates - Empty content and special characters (base64, punctuation) - Catch-all pattern for non-standard content type labels - Address-only (Tone) messages with trailing whitespace - FLEX simple format and unrecognized input lines Co-Authored-By: Claude Opus 4.6 --- tests/test_pager_parser.py | 123 +++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 tests/test_pager_parser.py diff --git a/tests/test_pager_parser.py b/tests/test_pager_parser.py new file mode 100644 index 0000000..4437bee --- /dev/null +++ b/tests/test_pager_parser.py @@ -0,0 +1,123 @@ +"""Tests for pager multimon-ng output parser.""" + +from __future__ import annotations + +from routes.pager import parse_multimon_output + + +class TestPocsagAlphaNumeric: + """Standard POCSAG messages with Alpha or Numeric content.""" + + def test_alpha_message(self): + line = "POCSAG1200: Address: 1337 Function: 3 Alpha: Hello World" + result = parse_multimon_output(line) + assert result is not None + assert result["protocol"] == "POCSAG1200" + assert result["address"] == "1337" + assert result["function"] == "3" + assert result["msg_type"] == "Alpha" + assert result["message"] == "Hello World" + + def test_numeric_message(self): + line = "POCSAG1200: Address: 500 Function: 2 Numeric: 55512345" + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Numeric" + assert result["message"] == "55512345" + + def test_alpha_empty_content(self): + line = "POCSAG1200: Address: 200 Function: 3 Alpha: " + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Alpha" + assert result["message"] == "[No Message]" + + def test_pocsag512_baud(self): + line = "POCSAG512: Address: 12345 Function: 0 Alpha: test" + result = parse_multimon_output(line) + assert result is not None + assert result["protocol"] == "POCSAG512" + assert result["message"] == "test" + + def test_pocsag2400_baud(self): + line = "POCSAG2400: Address: 9999 Function: 1 Numeric: 0" + result = parse_multimon_output(line) + assert result is not None + assert result["protocol"] == "POCSAG2400" + + def test_alpha_with_special_characters(self): + """Base64, colons, equals signs, and other punctuation should parse.""" + line = "POCSAG1200: Address: 1337 Function: 3 Alpha: 0:U0tZLQ==" + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Alpha" + assert result["message"] == "0:U0tZLQ==" + + +class TestPocsagCatchAll: + """Catch-all pattern for non-standard content type labels.""" + + def test_unknown_content_label(self): + """Future multimon-ng versions might emit new type labels.""" + line = "POCSAG1200: Address: 1337 Function: 3 Skyper: some data" + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Skyper" + assert result["message"] == "some data" + + def test_char_content_label(self): + line = "POCSAG1200: Address: 1337 Function: 2 Char: ABCDEF" + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Char" + assert result["message"] == "ABCDEF" + + def test_catchall_empty_content(self): + line = "POCSAG1200: Address: 1337 Function: 2 Raw: " + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Raw" + assert result["message"] == "[No Message]" + + def test_alpha_still_matches_first(self): + """Alpha/Numeric pattern should take priority over catch-all.""" + line = "POCSAG1200: Address: 100 Function: 3 Alpha: priority" + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Alpha" + assert result["message"] == "priority" + + +class TestPocsagToneOnly: + """Address-only lines with no message content.""" + + def test_tone_only(self): + line = "POCSAG1200: Address: 1977540 Function: 2" + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Tone" + assert result["message"] == "[Tone Only]" + assert result["address"] == "1977540" + + def test_tone_only_with_trailing_spaces(self): + line = "POCSAG1200: Address: 1337 Function: 1 " + result = parse_multimon_output(line) + assert result is not None + assert result["msg_type"] == "Tone" + + +class TestFlexParsing: + """FLEX protocol output parsing.""" + + def test_simple_flex(self): + line = "FLEX: Some flex message here" + result = parse_multimon_output(line) + assert result is not None + assert result["protocol"] == "FLEX" + assert result["message"] == "Some flex message here" + + def test_no_match(self): + """Unrecognized lines should return None.""" + assert parse_multimon_output("multimon-ng 1.2.0") is None + assert parse_multimon_output("") is None + assert parse_multimon_output("Enabled decoders: POCSAG512") is None From cf022ed1c025bda1c518c512dacefa535d57e1fa Mon Sep 17 00:00:00 2001 From: ribs Date: Sat, 28 Feb 2026 14:24:03 -0800 Subject: [PATCH 3/5] fix: add .gitattributes to enforce LF line endings for shell scripts Docker containers crash on startup when shell scripts have CRLF line endings (from Windows git checkout with core.autocrlf=true). The start.sh gunicorn entrypoint fails with "$'\r': command not found". Add .gitattributes forcing eol=lf for *.sh and Dockerfile so Docker builds work regardless of the developer's git line ending config. Also normalizes two scripts that were committed with CRLF. Co-Authored-By: Claude Opus 4.6 --- .gitattributes | 3 + build-multiarch.sh | 278 ++++++++++++++++---------------- download-weather-sat-samples.sh | 60 +++---- 3 files changed, 172 insertions(+), 169 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..05a6dec --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Force LF line endings for files that must run on Linux (Docker) +*.sh text eol=lf +Dockerfile text eol=lf diff --git a/build-multiarch.sh b/build-multiarch.sh index 4798be8..faa72bf 100755 --- a/build-multiarch.sh +++ b/build-multiarch.sh @@ -1,139 +1,139 @@ -#!/bin/bash -# INTERCEPT - Multi-architecture Docker image builder -# -# Builds for both linux/amd64 and linux/arm64 using Docker buildx. -# Run this on your x64 machine to cross-compile the arm64 image -# instead of building natively on the RPi5. -# -# Prerequisites (one-time setup): -# docker run --privileged --rm tonistiigi/binfmt --install all -# docker buildx create --name intercept-builder --use --bootstrap -# -# Usage: -# ./build-multiarch.sh # Build both platforms, load locally -# ./build-multiarch.sh --push # Build and push to registry -# ./build-multiarch.sh --arm64-only # Build arm64 only (for RPi) -# REGISTRY=ghcr.io/user ./build-multiarch.sh --push -# -# Environment variables: -# REGISTRY - Container registry (default: docker.io/library) -# IMAGE_NAME - Image name (default: intercept) -# IMAGE_TAG - Image tag (default: latest) - -set -euo pipefail - -# Configuration -REGISTRY="${REGISTRY:-}" -IMAGE_NAME="${IMAGE_NAME:-intercept}" -IMAGE_TAG="${IMAGE_TAG:-latest}" -BUILDER_NAME="intercept-builder" -PLATFORMS="linux/amd64,linux/arm64" - -# Parse arguments -PUSH=false -LOAD=false -ARM64_ONLY=false - -for arg in "$@"; do - case $arg in - --push) PUSH=true ;; - --load) LOAD=true ;; - --arm64-only) - ARM64_ONLY=true - PLATFORMS="linux/arm64" - ;; - --amd64-only) - PLATFORMS="linux/amd64" - ;; - --help|-h) - echo "Usage: $0 [--push] [--load] [--arm64-only] [--amd64-only]" - echo "" - echo "Options:" - echo " --push Push to container registry" - echo " --load Load into local Docker (single platform only)" - echo " --arm64-only Build arm64 only (for RPi5 deployment)" - echo " --amd64-only Build amd64 only" - echo "" - echo "Environment variables:" - echo " REGISTRY Container registry (e.g. ghcr.io/username)" - echo " IMAGE_NAME Image name (default: intercept)" - echo " IMAGE_TAG Image tag (default: latest)" - echo "" - echo "Examples:" - echo " $0 --push # Build both, push" - echo " REGISTRY=ghcr.io/myuser $0 --push # Push to GHCR" - echo " $0 --arm64-only --load # Build arm64, load locally" - echo " $0 --arm64-only --push && ssh rpi docker pull # Build + deploy to RPi" - exit 0 - ;; - *) - echo "Unknown option: $arg" - exit 1 - ;; - esac -done - -# Build full image reference -if [ -n "$REGISTRY" ]; then - FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" -else - FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" -fi - -echo "============================================" -echo " INTERCEPT Multi-Architecture Builder" -echo "============================================" -echo " Image: ${FULL_IMAGE}" -echo " Platforms: ${PLATFORMS}" -echo " Push: ${PUSH}" -echo "============================================" -echo "" - -# Check if buildx builder exists, create if not -if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then - echo "Creating buildx builder: ${BUILDER_NAME}" - docker buildx create --name "$BUILDER_NAME" --use --bootstrap - - # Check for QEMU support - if ! docker run --rm --privileged tonistiigi/binfmt --install all >/dev/null 2>&1; then - echo "WARNING: QEMU binfmt setup may have failed." - echo "Run: docker run --privileged --rm tonistiigi/binfmt --install all" - fi -else - docker buildx use "$BUILDER_NAME" -fi - -# Build command -BUILD_CMD="docker buildx build --platform ${PLATFORMS} --tag ${FULL_IMAGE}" - -if [ "$PUSH" = true ]; then - BUILD_CMD="${BUILD_CMD} --push" - echo "Will push to: ${FULL_IMAGE}" -elif [ "$LOAD" = true ]; then - # --load only works with single platform - if echo "$PLATFORMS" | grep -q ","; then - echo "ERROR: --load only works with a single platform." - echo "Use --arm64-only or --amd64-only with --load." - exit 1 - fi - BUILD_CMD="${BUILD_CMD} --load" - echo "Will load into local Docker" -fi - -echo "" -echo "Building..." -echo "Command: ${BUILD_CMD} ." -echo "" - -$BUILD_CMD . - -echo "" -echo "============================================" -echo " Build complete!" -if [ "$PUSH" = true ]; then - echo " Image pushed to: ${FULL_IMAGE}" - echo "" - echo " Pull on RPi5:" - echo " docker pull ${FULL_IMAGE}" -fi -echo "============================================" +#!/bin/bash +# INTERCEPT - Multi-architecture Docker image builder +# +# Builds for both linux/amd64 and linux/arm64 using Docker buildx. +# Run this on your x64 machine to cross-compile the arm64 image +# instead of building natively on the RPi5. +# +# Prerequisites (one-time setup): +# docker run --privileged --rm tonistiigi/binfmt --install all +# docker buildx create --name intercept-builder --use --bootstrap +# +# Usage: +# ./build-multiarch.sh # Build both platforms, load locally +# ./build-multiarch.sh --push # Build and push to registry +# ./build-multiarch.sh --arm64-only # Build arm64 only (for RPi) +# REGISTRY=ghcr.io/user ./build-multiarch.sh --push +# +# Environment variables: +# REGISTRY - Container registry (default: docker.io/library) +# IMAGE_NAME - Image name (default: intercept) +# IMAGE_TAG - Image tag (default: latest) + +set -euo pipefail + +# Configuration +REGISTRY="${REGISTRY:-}" +IMAGE_NAME="${IMAGE_NAME:-intercept}" +IMAGE_TAG="${IMAGE_TAG:-latest}" +BUILDER_NAME="intercept-builder" +PLATFORMS="linux/amd64,linux/arm64" + +# Parse arguments +PUSH=false +LOAD=false +ARM64_ONLY=false + +for arg in "$@"; do + case $arg in + --push) PUSH=true ;; + --load) LOAD=true ;; + --arm64-only) + ARM64_ONLY=true + PLATFORMS="linux/arm64" + ;; + --amd64-only) + PLATFORMS="linux/amd64" + ;; + --help|-h) + echo "Usage: $0 [--push] [--load] [--arm64-only] [--amd64-only]" + echo "" + echo "Options:" + echo " --push Push to container registry" + echo " --load Load into local Docker (single platform only)" + echo " --arm64-only Build arm64 only (for RPi5 deployment)" + echo " --amd64-only Build amd64 only" + echo "" + echo "Environment variables:" + echo " REGISTRY Container registry (e.g. ghcr.io/username)" + echo " IMAGE_NAME Image name (default: intercept)" + echo " IMAGE_TAG Image tag (default: latest)" + echo "" + echo "Examples:" + echo " $0 --push # Build both, push" + echo " REGISTRY=ghcr.io/myuser $0 --push # Push to GHCR" + echo " $0 --arm64-only --load # Build arm64, load locally" + echo " $0 --arm64-only --push && ssh rpi docker pull # Build + deploy to RPi" + exit 0 + ;; + *) + echo "Unknown option: $arg" + exit 1 + ;; + esac +done + +# Build full image reference +if [ -n "$REGISTRY" ]; then + FULL_IMAGE="${REGISTRY}/${IMAGE_NAME}:${IMAGE_TAG}" +else + FULL_IMAGE="${IMAGE_NAME}:${IMAGE_TAG}" +fi + +echo "============================================" +echo " INTERCEPT Multi-Architecture Builder" +echo "============================================" +echo " Image: ${FULL_IMAGE}" +echo " Platforms: ${PLATFORMS}" +echo " Push: ${PUSH}" +echo "============================================" +echo "" + +# Check if buildx builder exists, create if not +if ! docker buildx inspect "$BUILDER_NAME" >/dev/null 2>&1; then + echo "Creating buildx builder: ${BUILDER_NAME}" + docker buildx create --name "$BUILDER_NAME" --use --bootstrap + + # Check for QEMU support + if ! docker run --rm --privileged tonistiigi/binfmt --install all >/dev/null 2>&1; then + echo "WARNING: QEMU binfmt setup may have failed." + echo "Run: docker run --privileged --rm tonistiigi/binfmt --install all" + fi +else + docker buildx use "$BUILDER_NAME" +fi + +# Build command +BUILD_CMD="docker buildx build --platform ${PLATFORMS} --tag ${FULL_IMAGE}" + +if [ "$PUSH" = true ]; then + BUILD_CMD="${BUILD_CMD} --push" + echo "Will push to: ${FULL_IMAGE}" +elif [ "$LOAD" = true ]; then + # --load only works with single platform + if echo "$PLATFORMS" | grep -q ","; then + echo "ERROR: --load only works with a single platform." + echo "Use --arm64-only or --amd64-only with --load." + exit 1 + fi + BUILD_CMD="${BUILD_CMD} --load" + echo "Will load into local Docker" +fi + +echo "" +echo "Building..." +echo "Command: ${BUILD_CMD} ." +echo "" + +$BUILD_CMD . + +echo "" +echo "============================================" +echo " Build complete!" +if [ "$PUSH" = true ]; then + echo " Image pushed to: ${FULL_IMAGE}" + echo "" + echo " Pull on RPi5:" + echo " docker pull ${FULL_IMAGE}" +fi +echo "============================================" diff --git a/download-weather-sat-samples.sh b/download-weather-sat-samples.sh index ce13900..60237c3 100755 --- a/download-weather-sat-samples.sh +++ b/download-weather-sat-samples.sh @@ -1,30 +1,30 @@ -#!/usr/bin/env bash -# Download sample NOAA APT recordings for testing the weather satellite -# test-decode feature. These are FM-demodulated audio WAV files. -# -# Usage: -# ./download-weather-sat-samples.sh -# docker exec intercept /app/download-weather-sat-samples.sh - -set -euo pipefail - -SAMPLE_DIR="$(dirname "$0")/data/weather_sat/samples" -mkdir -p "$SAMPLE_DIR" - -echo "Downloading NOAA APT sample files to $SAMPLE_DIR ..." - -# Full satellite pass recorded over Argentina (NOAA, 11025 Hz mono WAV) -# Source: https://github.com/martinber/noaa-apt -if [ ! -f "$SAMPLE_DIR/noaa_apt_argentina.wav" ]; then - echo " -> noaa_apt_argentina.wav (18 MB) ..." - curl -fSL -o "$SAMPLE_DIR/noaa_apt_argentina.wav" \ - "https://noaa-apt.mbernardi.com.ar/examples/argentina.wav" -else - echo " -> noaa_apt_argentina.wav (already exists)" -fi - -echo "" -echo "Done. Test decode with:" -echo " Satellite: NOAA-18" -echo " File path: data/weather_sat/samples/noaa_apt_argentina.wav" -echo " Sample rate: 11025 Hz" +#!/usr/bin/env bash +# Download sample NOAA APT recordings for testing the weather satellite +# test-decode feature. These are FM-demodulated audio WAV files. +# +# Usage: +# ./download-weather-sat-samples.sh +# docker exec intercept /app/download-weather-sat-samples.sh + +set -euo pipefail + +SAMPLE_DIR="$(dirname "$0")/data/weather_sat/samples" +mkdir -p "$SAMPLE_DIR" + +echo "Downloading NOAA APT sample files to $SAMPLE_DIR ..." + +# Full satellite pass recorded over Argentina (NOAA, 11025 Hz mono WAV) +# Source: https://github.com/martinber/noaa-apt +if [ ! -f "$SAMPLE_DIR/noaa_apt_argentina.wav" ]; then + echo " -> noaa_apt_argentina.wav (18 MB) ..." + curl -fSL -o "$SAMPLE_DIR/noaa_apt_argentina.wav" \ + "https://noaa-apt.mbernardi.com.ar/examples/argentina.wav" +else + echo " -> noaa_apt_argentina.wav (already exists)" +fi + +echo "" +echo "Done. Test decode with:" +echo " Satellite: NOAA-18" +echo " File path: data/weather_sat/samples/noaa_apt_argentina.wav" +echo " Sample rate: 11025 Hz" From 302b150c36909271d44683ab4747b01d013032e0 Mon Sep 17 00:00:00 2001 From: ribs Date: Sat, 28 Feb 2026 14:25:31 -0800 Subject: [PATCH 4/5] fix: strip CRLF from shell scripts during Docker build Safety net for Windows developers whose git config (core.autocrlf=true) converts LF to CRLF on checkout. Even with .gitattributes forcing eol=lf, some git configurations can still produce CRLF working copies. The sed pass after COPY ensures start.sh and other scripts always have Unix line endings inside the container. Co-Authored-By: Claude Opus 4.6 --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 77fc60c..3776650 100644 --- a/Dockerfile +++ b/Dockerfile @@ -256,6 +256,9 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy application code COPY . . +# Strip Windows CRLF from shell scripts (git autocrlf can re-introduce them) +RUN find . -name '*.sh' -exec sed -i 's/\r$//' {} + + # Create data directory for persistence RUN mkdir -p /app/data /app/data/weather_sat /app/data/radiosonde/logs From ec62cd9083b8bcc5733d11d4c1399d92746adb7a Mon Sep 17 00:00:00 2001 From: ribs Date: Tue, 3 Mar 2026 15:59:42 -0800 Subject: [PATCH 5/5] fix: prevent silent muting from hiding pager messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Mute" button on pager cards persists muted addresses to localStorage with no visible indicator, making it easy to accidentally hide an address and forget about it. This caused flag fragment messages on RIC 1337 to silently disappear. - Add "X muted source(s) — Unmute All" indicator to sidebar - Stop persisting hideToneOnly filter across sessions so the default (show all) always applies on page load - Remove default checked state from Tone Only filter checkbox Co-Authored-By: Claude Opus 4.6 --- static/js/components/signal-cards.js | 27 +++++++++++++++++++++++++++ templates/index.html | 7 ++++++- templates/partials/modes/pager.html | 10 +++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/static/js/components/signal-cards.js b/static/js/components/signal-cards.js index 07e81f8..43575d6 100644 --- a/static/js/components/signal-cards.js +++ b/static/js/components/signal-cards.js @@ -1492,6 +1492,7 @@ const SignalCards = (function() { muted.push(address); localStorage.setItem('mutedAddresses', JSON.stringify(muted)); showToast(`Source ${address} hidden from view`); + updateMutedIndicator(); // Hide existing cards with this address document.querySelectorAll(`.signal-card[data-address="${address}"], .signal-card[data-callsign="${address}"], .signal-card[data-sensor-id="${address}"]`).forEach(card => { @@ -1510,6 +1511,30 @@ const SignalCards = (function() { return muted.includes(address); } + /** + * Unmute all addresses and refresh display + */ + function unmuteAll() { + localStorage.setItem('mutedAddresses', '[]'); + updateMutedIndicator(); + showToast('All sources unmuted'); + // Reload to re-display previously muted messages + location.reload(); + } + + /** + * Update the muted address count indicator in the sidebar + */ + function updateMutedIndicator() { + const muted = JSON.parse(localStorage.getItem('mutedAddresses') || '[]'); + const info = document.getElementById('mutedAddressInfo'); + const count = document.getElementById('mutedAddressCount'); + if (info && count) { + count.textContent = muted.length; + info.style.display = muted.length > 0 ? 'block' : 'none'; + } + } + /** * Show location on map (for APRS) */ @@ -2262,6 +2287,8 @@ const SignalCards = (function() { copyMessage, muteAddress, isAddressMuted, + unmuteAll, + updateMutedIndicator, showOnMap, showStationRawData, showSignalDetails, diff --git a/templates/index.html b/templates/index.html index f4493df..eb49f7e 100644 --- a/templates/index.html +++ b/templates/index.html @@ -3663,7 +3663,11 @@ const saved = localStorage.getItem('pagerFilters'); if (saved) { try { - pagerFilters = JSON.parse(saved); + const parsed = JSON.parse(saved); + // Only persist keywords across sessions. + // hideToneOnly defaults to false every session so users + // always see the full traffic stream unless they opt-in. + if (Array.isArray(parsed.keywords)) pagerFilters.keywords = parsed.keywords; } catch (e) { console.warn('Failed to load pager filters:', e); } @@ -3964,6 +3968,7 @@ // Load pager message filters loadPagerFilters(); + if (typeof SignalCards !== 'undefined') SignalCards.updateMutedIndicator(); // Initialize dropdown nav active state updateDropdownActiveState(); diff --git a/templates/partials/modes/pager.html b/templates/partials/modes/pager.html index 7d75119..dba62a4 100644 --- a/templates/partials/modes/pager.html +++ b/templates/partials/modes/pager.html @@ -62,7 +62,7 @@

Message Filters

@@ -73,6 +73,14 @@
Messages matching these keywords will be hidden from display but still logged.
+