Professional Emergency Alert System
Emergency alert system for Putnam County, OH
Build Information
What's New in 2.75.0
Latest Release
- **Changelog / Version page was unreadable on every dark theme** — the page predated the project's theme tokens and was authored against a never-implemented "design system" naming scheme (`--color-surface`, `--color-text-primary`, `--color-text-secondary`, `--color-border`, `--color-border-light`, `--color-text-muted`, `--color-neutral-100`, `--color-success-light`). With those variables undefined, `var()` fell through to comma fallbacks for **backgrounds** (white) but had no fallback for **text colors**, so every text rule inherited the body's theme-driven `--text-color`. On all dark themes (midnight, obsidian, charcoal, slate, etc.) that produced white-ish text on a white card and the Changelog / Features tabs were effectively blank. Rewrote `templates/version.html` to use the variables that actually exist on every theme (`--surface-color`, `--text-color`, `--border-color`) and pinned explicit dark foregrounds (`#0d1b2a`, `#212529`, `#495057`) on the white-surfaced cards plus dedicated `.tab-content` typography rules so headings, paragraphs, change-list bullets, and feature names contrast on every theme.
- **Live waterfall on Radio Receiver Diagnostics** — the "Show Waterfall" control used to fire a single capture (≤5 s) and render one static spectrogram, which neither matched what operators expect from a waterfall nor refreshed without re-clicking. Replaced with a continuously-updating waterfall that polls the existing `/api/radio/spectrum/<id>` endpoint (already published to Redis by the SDR hardware service at ~10 Hz and used as the data source for the main `/admin/radio` spectrum view), scrolls one FFT row onto a canvas every ~500 ms with the same blue→green→yellow→red dBFS colour ramp as the one-shot view, and exposes Start/Stop controls plus a row counter and status line. The one-shot capture-and-verdict workflow (which actually computes the clipping verdict from raw IQ) is preserved under a new "Snapshot" button so the existing peak/RMS/clipping diagnostic is not lost.
Release History
- **Changelog / Version page was unreadable on every dark theme** — the page predated the project's theme tokens and was authored against a never-implemented "design system" naming scheme (`--color-surface`, `--color-text-primary`, `--color-text-secondary`, `--color-border`, `--color-border-light`, `--color-text-muted`, `--color-neutral-100`, `--color-success-light`). With those variables undefined, `var()` fell through to comma fallbacks for **backgrounds** (white) but had no fallback for **text colors**, so every text rule inherited the body's theme-driven `--text-color`. On all dark themes (midnight, obsidian, charcoal, slate, etc.) that produced white-ish text on a white card and the Changelog / Features tabs were effectively blank. Rewrote `templates/version.html` to use the variables that actually exist on every theme (`--surface-color`, `--text-color`, `--border-color`) and pinned explicit dark foregrounds (`#0d1b2a`, `#212529`, `#495057`) on the white-surfaced cards plus dedicated `.tab-content` typography rules so headings, paragraphs, change-list bullets, and feature names contrast on every theme.
- **Live waterfall on Radio Receiver Diagnostics** — the "Show Waterfall" control used to fire a single capture (≤5 s) and render one static spectrogram, which neither matched what operators expect from a waterfall nor refreshed without re-clicking. Replaced with a continuously-updating waterfall that polls the existing `/api/radio/spectrum/<id>` endpoint (already published to Redis by the SDR hardware service at ~10 Hz and used as the data source for the main `/admin/radio` spectrum view), scrolls one FFT row onto a canvas every ~500 ms with the same blue→green→yellow→red dBFS colour ramp as the one-shot view, and exposes Start/Stop controls plus a row counter and status line. The one-shot capture-and-verdict workflow (which actually computes the clipping verdict from raw IQ) is preserved under a new "Snapshot" button so the existing peak/RMS/clipping diagnostic is not lost.
- **Tamper-evident audit log.** Extended the existing `audit_logs` table (added by `20251105_add_rbac_and_mfa`) with three new nullable columns — `prev_hash`, `entry_hash`, `signature` — and wired chain construction and Ed25519 signing into `AuditLogger.log()`. Every newly-recorded audit row now carries the SHA-256 of its predecessor's `entry_hash`, an SHA-256 over its own canonical-JSON content (including the `prev_hash` linkage), and an Ed25519 signature over that hash. Verification is exposed via `AuditLogger.verify_chain(limit=None)`, which walks the table, checks every signed row's prev-link, recomputes its content hash, and verifies the signature against the configured public key — returning `{ok, checked, unsigned, ephemeral_key, first_bad_id, reason}`. The signing key is loaded from `AUDIT_SIGNING_KEY_PATH` (production) → `${REPO_ROOT}/secrets/audit_signing.key` (development) → an in-process ephemeral key (with a loud warning) so the chain never silently breaks startup. New SQLAlchemy `after_insert` listeners on `CAPAlert`, `EASMessage`, and `ManualEASActivation` (registered from the app factory via `app_core.auth.audit_listeners.register_audit_listeners`) automatically capture every alert-lifecycle insert without touching the 5+ existing creation sites. **Deliberately reused the existing `audit_logs` table and `/admin/audit-logs` viewer rather than creating a parallel "audit ledger" table or admin page**, so the menu picks up tamper-evidence with zero new navigation entries. Migration `20260515_add_chain_columns_to_audit_logs.py` is fully additive (nullable columns, idempotent up/down, no data backfill). Tests in `tests/test_audit_chain.py` cover happy-path chain construction, verifier acceptance, tamper detection in `details`, tamper at `prev_hash`, signature forgery, mid-chain row deletion, and the ephemeral-key bootstrap fallback. Pinned `cryptography>=46.0.5` in `requirements.txt` (avoids the GHSA on SECT-curve subgroup validation; Ed25519 itself is unaffected, but the same library exposes the affected curves).
- **Attribution for the new `cryptography` dependency.** Added a footer badge in `templates/partials/tech_stack_badges.html`, a top-level shield in `README.md`, a "Security / auth / notifications" attributions table row in the README's `## 📚 Attributions & Open-Source Credits` section, an Authentication & Notifications stack card on `templates/about.html`, and a System and Utilities bullet in `docs/reference/ABOUT.md` — each explaining that the library underpins the Ed25519 signing and SHA-256 hashing for the tamper-evident `audit_logs` chain. The drift-guard `tests/test_tech_stack_badges.py` continues to pass.
- **Single source of truth for tech-stack shields, with what-each-library-does explanations everywhere.** The footer's "Built With Modern Technologies" badge strip was previously inlined into `templates/base.html` while a *second*, larger copy lived in the orphan `templates/partials/footer.html` (not included by any template — silently drifting). The badge list is now consolidated into a new partial **`templates/partials/tech_stack_badges.html`** which `base.html` `{% include %}`s; the orphan `partials/footer.html` was deleted. The canonical set was expanded from ~13 to ~35 shields to credit every major dependency that genuinely powers a user-visible feature: Werkzeug, Jinja2, SciPy, Numba, lxml, Pillow, pydub, FFmpeg, eSpeak NG, PyOTP, Twilio, chrony, gpsd, Docker, and Alembic were missing from the on-page footer; they're now attributed. Every footer badge now carries a `title="..."` hover tooltip that explains, in 1–2 sentences, exactly what that library does *for EAS Station specifically* (not a generic upstream blurb) — e.g. Numba reads "JIT-compiles the SAME DLL and RBDS workers (~6× faster real-time demod on a Pi)", chrony reads "NTP daemon. Consumes the GPS NMEA fix + PPS edge as a kernel refclock and serves stratum-1 NTP". The README's top badge block was rewritten to match the same curated set, and a new **`## 📚 Attributions & Open-Source Credits`** section near the bottom of `README.md` lists every Python dependency from `requirements.txt`, every system package (PostgreSQL/PostGIS, Redis, Nginx, Icecast, FFmpeg, eSpeak NG, chrony, gpsd, systemd, Let's Encrypt/Certbot, Docker), and every vendored/CDN front-end asset (Bootstrap, Font Awesome, Leaflet, Chart.js, Socket.IO client) in nine grouped tables. Each row has a **Purpose in EAS Station** column with the long-form explanation alongside the upstream license identifier and project URL — the proper home for the long tail of credits (Flask-WTF, Flask-Limiter, requests, httpx, pyserial, zigpy, pyshp, pyproj, ...) that don't warrant a top-level shield.
- **Drift guard `tests/test_tech_stack_badges.py`.** Asserts (a) `base.html` actually `{% include %}`s the badge partial, (b) the deleted `partials/footer.html` is not resurrected, (c) for a curated subset of versioned Python libraries (Flask, Werkzeug, Jinja2, Socket.IO, SQLAlchemy, Alembic, Gunicorn, NumPy, SciPy, lxml, Pillow, pydub, PyOTP) the version pinned in `requirements.txt` appears verbatim in **both** the README badge block and the footer partial, (d) system-level dependencies (Nginx, Icecast, SoapySDR, FFmpeg, eSpeak NG, Raspberry Pi, Docker, Systemd, Redis, chrony, gpsd, Twilio, Numba, gevent) remain attributed in both surfaces, and (e) every `<a class="tech-badge">` in the footer partial carries a non-trivial (≥25-char, sentence-shaped) `title="..."` tooltip. Bumping a dependency in `requirements.txt` without bumping the matching shield now fails CI. `docs/process/CONTRIBUTING.md` was updated with a "Keep tech-stack attributions in sync" rule pointing contributors at the canonical files.
- **IQ capture-to-file from Radio Diagnostics page (and via SDR-service command).** Operators can now grab a raw complex64 IQ recording from any active receiver without SSHing into the host — useful as input to `scripts/rbds_diagnose.py`, `inspectrum`, GNU Radio, or any other offline analyser. A new "Capture IQ" button next to each receiver on `/admin/radio/diagnostics` triggers a one-second capture; the browser then streams the resulting `.npy` file via a single-use download URL (file deleted after the download completes). The page also gains an **"About IQ Captures"** collapsible help card that explains, in operator-friendly terms, what an IQ capture is, the typical reasons to grab one (RBDS/RDS decode investigation, SAME/EAS replay, signal-quality analysis, bug-report evidence), and step-by-step instructions for what to do with the downloaded `.npy` file (load in Python, feed to `rbds_diagnose.py`, open in inspectrum / GNU Radio). Two new HTTP endpoints back the button: `POST /api/radio/diagnostics/capture/<receiver_id>` (returns `capture_id`, filename, size, and a `download_url`) and `GET /api/radio/diagnostics/capture/<capture_id>/download` (streams as attachment, then cleans up). A new `capture_iq` action in `sdr_hardware_service.py` does the actual `numpy.save()` to `RADIO_CAPTURE_DIR` (default `/var/log/eas-station/captures`, override via env), capped at `RADIO_CAPTURE_MAX_SAMPLES` (8M samples ≈ 64 MB) to bound memory and disk. Sample count is bounded by the SDR ring buffer (~2 s) and the web layer's `RADIO_CAPTURE_MAX_DURATION_SEC = 5`. Path-traversal is blocked on both sides: capture IDs are hex UUIDs and the on-disk path is verified to resolve under the allow-list directory before the file is served. Regression coverage in `tests/test_radio_diagnostics_capture.py` (happy path, traversal rejection on tampered Redis value, invalid capture-id format, missing/expired capture, timeout from the SDR service).
- **GPS & Time Dashboard** at `/admin/gps-dashboard` — a dense, single-page status page modelled visually on [W0CHP's chrogps-dash](https://w0chp.radio/chrogps-dash/) and reachable from the Admin → Hardware tab. Surfaces, all in one place: a header banner with the station hostname, a live "GPS LOCKED / ACQUIRING / NO GPS" pill, a fix-mode pill (2D/3D), and a colour-graded "STRATUM N" pill; a **System Tracking** card that renders every field of `chronyc -c tracking` (Reference ID, Stratum, Ref time, System / Last / RMS offsets, Frequency, Residual freq, Skew, Root delay/dispersion, Update interval, Leap status) plus a logarithmic Sync-Health bar that maps |offset| → 0-100 % so a sub-microsecond stratum-1 lock visibly pegs the meter; a **Satellite Skyview** card with a polar sky plot (cardinal cross-hair, 30°/60° elevation rings, dots coloured by constellation, alpha by SNR, glowing outer rings on used-in-fix sats), a Signal-Integrity bar (avg-SNR-scaled), GPS position (lat/lon/alt), DOP block (HDOP/VDOP/PDOP), serial port + baud, and Constellation Breakdown chips (e.g. *GPS 9/9, GLONASS 4/5, Galileo 7/7, BeiDou 17/21*); an **Individual Signal Levels** per-PRN bar chart sorted by constellation then PRN; a **Chrony Sources** table parsing `chronyc -c sources` (mode/state glyph, source name, stratum, poll, octal reach, last-sample age, current offset with sign-coloured cells); a **Satellite Data** table (PRN coloured by constellation, EL, AZ, SNR with the existing 6-stop colour scale, ACTIVE/VIEW status) with click-to-filter constellation chips. The page polls a single new endpoint, `/admin/api/gps-dashboard/data`, on a configurable 3 / 5 / 10 / 30 s interval (or paused). All styling uses the existing theme CSS variables so it adapts to every dark and light theme — no fixed palette.
- New backend endpoints in `webapp/admin/hardware.py`: `gps_dashboard_page` (HTML route, gated by `system.configure`) and `gps_dashboard_data` (JSON aggregation route). The JSON route composes the live GPS fix from the hardware service's `/api/hardware/gps/status` (tolerant of an unreachable hardware service so the chrony half still renders during a service restart) with locally-collected `chronyc -c tracking` and `chronyc -c sources` output. The CSV parsers themselves live in a new pure module, `app_utils/chrony_parser.py` (`parse_chronyc_tracking_csv`, `parse_chronyc_sources_csv`) — split out of the route file so they're testable without the Flask stack and reusable from any future timing view. New "GPS & Time Dashboard" tile in `templates/admin.html` Hardware tab links to the page. Regression coverage in `tests/test_gps_dashboard_chrony_parser.py` (8 cases covering full records, blank fields, truncated CSVs, sign-preserving offsets, mixed mode/state rows, short-row skipping, empty input, and non-numeric "-"/"?" sentinels in numeric columns).
- Time-series performance graphs from the chrogps-dash reference (PPS Drift, Clock Stability, Frequency Steering & Skew, Root Dispersion, NTP Measurements, Satellite Visibility History, GPS SNR Trend, DOP History, Satellite SNR by Constellation) are intentionally deferred: they require a historical time-series store this codebase does not yet maintain. The dashboard's footer calls this out.
- **Constellation colour-coding restored on the GPS sky plot and satellite tables (Hardware Settings + System Health) when running in gpsd mode.** The direct-NMEA path emits each satellite with a `"constellation"` key (the 2-letter NMEA talker — `GP`, `GL`, `GA`, `GB`, `GQ`, `GI`); the gpsd path was emitting the same value but under the key `"talker"`. The frontend (`templates/admin/hardware_settings.html` `constellationInfo()` and `templates/system_health.html` `gpsConstInfo()`) only reads `sat.constellation`, so in gpsd mode every dot, badge, and legend chip silently fell back to neutral grey. Renamed the gpsd-side key to `"constellation"` in `app_core/gps/gps_manager.py::_handle_gpsd_sky` so both ingest paths produce identically-shaped satellite records and the UI colours by constellation again. Crucially this is the *recommended* mode for stratum-1 timing (gpsd → chrony refclock), so the regression was hitting exactly the deployments that should look the best.
- **Stratum 1 GPS time server documented as a first-class feature.** The repository previously documented the GPS HAT only as a hardware-setup procedure; the resulting *capability* — a true stratum 1 NTP server, GPS-disciplined via PPS, with a battery-backed RTC and one-click admin-UI setup — was not surfaced where new users decide whether to deploy the platform. Updated the marketing/feature surfaces to call this out: a new "🛰️ Built-In Stratum 1 NTP Time Source" section in `README.md` (between Hardware Integration and the Modern Web Dashboard), a new "Stratum 1 GPS-Disciplined Time" section card on the public About page (`templates/about.html`) with six feature cards (multi-GNSS receiver, hardware PPS edge, true stratum 1 NTP, battery-backed RTC, air-gap friendly, one-click setup), an additional "Stratum 1 Time" hero chip and "Stratum 1 GPS Time Source" Key Features bullet, and a new sentence in the reference build description in `docs/reference/ABOUT.md`. No code changes; existing GPS HAT setup guide at `docs/hardware/GPS_HAT_SETUP.md` remains the authoritative procedural reference.
- Backfill completed for recent release metadata; new changes should be documented here going forward.
- **MDC1200 selective-calling signal profile** — Motorola 1200-baud FFSK selective-calling (mark = 1200 Hz, space = 1800 Hz) is now available as a pre/post-alert signal. Each packet carries a 16-bit Unit ID and an op-code (PTT-ID Pre / Post, Emergency, Request to Talk, Remote Monitor, or operator-supplied raw bytes). When both pre and post signals are MDC1200 and the preset is `ptt_id_pre` (default), the post side automatically substitutes `ptt_id_post` so receiving Motorola subscribers see a complete bookend pair. The driving use case is **forwarding EAS audio over an existing two-way LMR system**: subscribers display the calling unit ID, optionally selectively unmute, log the call, and close the call cleanly on the post-ID. All byte/word fields (`mdc1200_unit_id`, `mdc1200_op_code_raw`, `mdc1200_arg_raw`) accept either decimal or `0x..` hex notation including the `A`–`F` digits, matching Motorola CPS conventions. New encoder lives at `app_utils/mdc1200.py`; full technical reference (frame format, CRC-16, K=7 R=1/2 FEC, 16×7 interleaver, differential modulation, op-code table) at [docs/reference/protocols/MDC1200.md](../reference/protocols/MDC1200.md). Configured under **Admin → EAS Broadcast Settings → Pre/Post-Alert Signaling**, persisted in `eas_settings` (Alembic migration `20260505_add_mdc1200_to_eas_settings`). Verify generated packets with `multimon-ng -a MDC -t wav <file>.wav`.
- **Protocol technical reference docs** — new `docs/reference/protocols/` directory with engineering-level specifications for both [SAME](../reference/protocols/SAME.md) (FCC §11.31 / NRSC-4-B §4 — modulation, burst structure, header field grammar, attention tone, EOM, composite audio assembly, encode/decode pipelines) and [MDC1200](../reference/protocols/MDC1200.md). The index at [docs/reference/protocols/README.md](../reference/protocols/README.md) describes how the two protocols compose in a single broadcast.
- **Pre/post-alert signals** — system-level configurable attention signals played before each SAME header (pre-alert) and/or after the EOM (post-alert). Supported profiles: `none`, `bell` (decaying 880 Hz), `beep` (1 kHz tone), `three_tone` (440 / 880 / 1320 Hz), `qc2` (Motorola Quick Call II two-tone with configurable Tone A / Tone B), and `dtmf` (configurable digit sequence using ITU-T Q.23 / Q.24 100 ms / 50 ms timing). Applies to auto-forwarded CAP/IPAWS, OTA relay, and manual broadcasts; signals sit outside the SAME signalling so they never affect decoder behaviour. Configured under **Admin → EAS Broadcast Settings → Pre/Post-Alert Signaling**, persisted in `eas_settings` (Alembic migration `20260501_add_alert_chime_to_eas_settings`). See [docs/guides/ALERT_SIGNALS.md](../guides/ALERT_SIGNALS.md).
- **Statistics dashboard** — new charts (severity-mix-over-time, cumulative alerts, top 5 events trend, hour-of-day × severity, alert duration histogram, year-over-year overlay, EAS forwarding funnel), a one-click **PDF report export** of the dashboard, and a print stylesheet so the browser "Print to PDF" route also produces a clean report. Filtered alerts CSV / dashboard PDF / summary metrics JSON are now grouped under a single **Export ▾** dropdown in the filter panel.
- **Added (frontend, vendored under `static/vendor/jspdf/`):** `jspdf` 4.2.1 and `html2canvas` 1.4.1 (both MIT) — used by the Statistics dashboard for client-side PDF report generation. No new server-side dependencies; PDF generation runs entirely in the browser.
- **Phantom whitespace below the page footer on every page (PR #2040 follow-up)**:
- **`update.sh` no longer silently corrupts the database when Alembic fails.** When `alembic upgrade head` exited non-zero (or when `alembic` was not found), `update.sh` would silently fall back to `db.create_all()`. For migrations that *move* data — most recently `20260506_split_location_settings` — this fallback was actively destructive: it created the new `alert_filter_settings` table empty, added an empty `hardware_settings.led_default_lines` column, left the old `location_settings.fips_codes` / `zone_codes` / `storage_zone_codes` / `area_terms` / `led_default_lines` columns in place, and never advanced `alembic_version`. The visible symptom was the FIPS / broadcast-zone / storage-zone / area-term lists appearing empty in the admin UI even though the operator had not cleared them. `update.sh` now: (1) prints both `alembic current` and `alembic heads` *before and after* the upgrade attempt so the operator can see exactly which revision is pending and whether it advanced, (2) refuses to run `db.create_all()` as a fallback (a clean Alembic failure is preferable to a half-migrated database), and (3) emits a clear retry command and a pointer to the new recovery script if the upgrade fails.
- **Recovery script for already-broken databases:** new `scripts/database/recover_split_location_settings.py` finishes a half-applied `20260506_split_location_settings`. It detects the half-migrated state (new tables exist but old columns still present on `location_settings` and/or `alembic_version` not advanced), copies any non-empty `fips_codes` / `zone_codes` / `storage_zone_codes` / `area_terms` from `location_settings` into `alert_filter_settings` (only when the destination row is empty/default — never overwrites populated user data), copies `led_default_lines` into `hardware_settings`, drops the orphaned columns, and stamps `alembic_version` to `20260506_split_location_settings`. The script is idempotent and a no-op on a healthy database; it runs automatically at the end of `update.sh`'s migration step but can also be invoked by hand: `sudo -u eas-station /opt/eas-station/venv/bin/python /opt/eas-station/scripts/database/recover_split_location_settings.py [--dry-run] [--quiet]`.
- **GPS live panel now matches the active theme** — replaced all hardcoded GitHub-dark palette values (`#0d1117`, `#161b22`, `#30363d`, `#c9d1d9`, `#8b949e`, etc.) in the GPS section of Hardware Settings with CSS custom properties (`var(--bg-color)`, `var(--surface-color)`, `var(--border-color)`, `var(--text-color)`, `var(--text-muted)`, `var(--warning-color)`, `var(--danger-color)`, `var(--accent-color)`). The sky-plot canvas now reads theme colors at paint time via `getComputedStyle`, so the polar grid, labels, and cardinal marks adapt correctly in all 11 built-in themes (both light and dark).
- **GPIO control panel and pin-map pages returning 500 errors** — `url_for('gpio_statistics_page')` in `templates/gpio_control.html` and `templates/gpio_pin_map.html` raised a `BuildError` at render time because the endpoint lives in the `dashboard` Blueprint and must be referenced as `dashboard.gpio_statistics_page`. Both templates now use the correct qualified name.
- **RBDS detail rendering regression** in `static/js/rbds_visualization.js`. PR #1975
- **EAS Station fingerprint trill changed to `0xA9`**: The post-burst
- **`.env` deprecation cleanup in user-facing docs**: Several docs still instructed users to edit `.env` for settings that have been migrated to the database (configured via the admin UI). Updated to reflect that polling, EAS broadcast, notifications, and application-logging settings now live in dedicated DB tables (per the `migrated_vars` list in `webapp/admin/environment.py`); only boot-time infrastructure (`SECRET_KEY`, `DATABASE_URL`, hostnames, paths) belongs in `.env`. Files updated: `docs/troubleshooting/POLLING_NOT_WORKING.md` (no longer tells users to grep / edit `.env` for `POLL_INTERVAL_SEC` / `IPAWS_CAP_FEED_URLS` / `NOAA_USER_AGENT` — points at Settings → Poller and the `poller_settings` table); `docs/guides/ipaws_feed_integration.md` (removed three `.env`-snippet config examples and the Quick Start "edit `.env`" instructions, replaced with admin-UI guidance); `docs/architecture/THEORY_OF_OPERATION.md` (sequence-diagram loop labels and prose no longer reference `POLL_INTERVAL_SEC` as an env var; "Configuration is read from `.env`" sentence corrected to distinguish runtime settings (DB) from boot-time infrastructure); `docs/architecture/DATA_FLOW_SEQUENCES.md` (originator substitution no longer claims `EAS_ORIGINATOR` env var as an alternative — it's database-only via the Broadcast admin tab); `docs/guides/SETUP_INSTRUCTIONS.md` (Station ID validation note points at the Broadcast admin tab / `eas_settings.station_id` instead of an `EAS_STATION_ID` env var). No code changes.
- **Admin UI Reorganization — Broadcast tab promoted to top-level**: The EAS encoder settings (originator, station ID, sample rate, attention tone, pre/post-alert signals incl. MDC1200 / QC-II / DTMF, auto-forward event filter, authorized event codes) previously lived under **System Settings → Alert Filtering** as an appended section, where they had no logical relationship to alert filtering. They are now in their own top-level **Broadcast** admin tab (between *System* and *Services*). The Broadcast tab also consolidates links to every broadcast-related tool in one place: EAS Workflow (`/eas/`), RWT Schedule (`/rwt-schedule`), EAS Compliance (`/admin/compliance`), Alert Verification (`/admin/alert-verification`), EAS Decoder Monitor (`/admin/eas_decoder_monitor`), Text-to-Speech (`/admin/tts`), and Audio Sources (`/admin/audio-sources`). The System Settings tab now contains only Location, Alert Filtering, and Alert Management subtabs — none of them broadcast-related. No backend, route, schema, or API changes; the `easSettingsForm` and its `/admin/eas_settings` endpoint are unchanged. Documentation updated: `docs/guides/ALERT_SIGNALS.md` now points to the Broadcast tab.
- **Database Schema Reorganization**: Split `location_settings` table into three specialized tables for better separation of concerns:
- `location_settings`: Retains geographic identity (county, state, timezone, map coordinates)
- `alert_filter_settings`: New table for alert filtering criteria (FIPS codes, zone codes, storage zone codes, area terms)
- `hardware_settings`: Now includes `led_default_lines` (moved from location_settings)
- **Admin UI Reorganization**: The admin **Location** subtab has been split into two subtabs: **Location** (county / state / timezone / map defaults) and **Alert Filtering** (FIPS codes, broadcast zones, storage zones, zone lookup, location reference card). The two forms now save independently to `/admin/location_settings` (PUT) and `/admin/alert_filtering` (POST) respectively.
- **API Changes**: Added new `/admin/alert_filtering` endpoint; existing `/admin/location_settings` endpoint remains backwards compatible
- The `get_location_settings()` function maintains backwards compatibility by returning a merged dictionary with the same shape as before
- ...2 more
- **Release metadata CI workflow** — Added `.github/workflows/release-metadata.yml` to run `tests/test_release_metadata.py` on pull requests and pushes to `main`/`develop`. This enforces the existing contributor requirement to keep `VERSION` and `docs/reference/CHANGELOG.md` aligned for behavioral changes.
- **Missed version/changelog updates no longer slip through review** — The repository previously had no workflow running release-governance checks, so instructions in `docs/development/AGENTS.md` / `docs/process/CONTRIBUTING.md` were advisory only. CI now blocks regressions when release metadata is not updated.
- **Per-source EAS decoder-tap streaming** — new endpoint
- **WebSocket `alerts_update` event** — `app_core/websocket_push.py` now
- **RBDS Radio Text reassembly** in `app_core/radio/demodulation.py` (PR #1953).
- **FM stereo (L–R) decoding** now produces real channel separation (PR #1953).
- **`templates/about.html`** — Replaced the generic ham-radio icon in the
- **`README.md`** — Complete rewrite from ~800 lines of technical
- **`scripts/rbds_diagnose.py`** — Updated to introspect the live
- **`app_core/radio/demodulation.py`** — `RBDSWorker` previously built its
- **`scripts/rbds_diagnose.py`** *(new)* — standalone offline diagnostic
- **`app_core/radio/demodulation.py`** — Three compounding RBDS bugs fixed
- **Costas/M&M processing order** — M&M timing recovery was running before
- **Costas loop bandwidth** — previous `alpha=0.026 / beta=0.00035` gave
- **Bandpass filter gain** — `_design_fir_bandpass` normalised by
- **Stale docstrings** — `_process_rbds` and `_costas_pysdr` docstrings
- **Temporary debug capture removed** — the `# TEMPORARY CAPTURE — remove
- **`app_core/radio/demodulation.py`** — `RBDSWorker` presync state machine
- **`app_core/radio/demodulation.py`** — Fragile synced-mode handling caused
- **`app_core/radio/demodulation.py`** — Presync spacing tolerance widened
- **`app_core/radio/demodulation.py`** — `_mm_timing_pysdr` was silently
- **`app_core/radio/demodulation.py`** — Presync spacing check relaxed from
- **`app_core/radio/demodulation.py`** — Per-chunk `x[::decim]` decimation
- **`templates/about.html`** — Complete visual redesign (PR #1912). Added a
- ~400 lines of scoped CSS (inside `{% block extra_css %}`) using CSS custom
- **`templates/audio_monitoring.html`** — Real-time RF signal-strength
- **`app_core/radio/demodulation.py`** — `DemodulatorStatus` gains a
- **`app_core/audio/sources.py`** — Extracts `signal_strength` from the
- RBDS decode now uses an adaptive sliding window: a 3-second window during
- UI converts the linear magnitude to dBFS via `20 * log10(value)` and maps
- **`app_core/websocket_push.py`** — `_emit_analytics_update()` now uses
- **`scripts/screen_manager.py`** — `_has_active_alerts()` filters active
- **`app_core/auth/audit.py`** — `cleanup_old_logs()` switched to the
- `app_core/gps/gps_manager.py` and `tools/download_nws_gis_data.py` move
- All datetime comparisons in the touched modules are now timezone-aware,
- **`app_utils/image_export.py`** — Fills the right-hand info panel of the
- Enables the previously-defined-but-unused `_draw_vtac()` VTEC block.
- **DESCRIPTION** — word-wrapped alert description text.
- **INSTRUCTIONS** — yellow accent bar highlighting safety / action
- **ISSUING OFFICE** — sender name, response type, and category.
- All new sections respect the vertical panel boundary and stop rendering
- **`app_utils/image_export.py`** — The OpenStreetMap background in the
- **`app_utils/image_export.py`** — Re-adds the compass-rose decoration to
- **`app_utils/image_export.py`** — Expands the exported 1200×630 social
- **`app_utils/image_export.py`** *(new)* — Image composition engine built
- OpenStreetMap tile background with the alert polygon overlaid
- Storm-threat card: tornado detection, wind gust, hail size /
- County-coverage percentage with a progress bar and service-boundary
- VTAC decoded labels and raw strings; storm-motion direction / speed.
- Affected-area description wrapped across rows; severity-coloured
- **`webapp/admin/api.py`** — New `/alerts/<id>/export-image.png` route
- **`templates/alert_detail.html`** — "Export Social Image" button added
- Map tiles are fetched live from OSM; a plain dark fallback is rendered
- **`app_utils/eas_fsk.py`, `app_utils/eas_demod.py`, `app_utils/eas.py`**
- **`docs/policies/TRADEMARK_POLICY.md`** *(new)* — Separates trademark
- **`LICENSE-COMMERCIAL`** — Replaced with a full Commercial Software
- **`NOTICE`** — Simplified and clarified to explain dual licensing,
- **`README.md`** — Clarifies AGPL availability, links to
- **`docs/policies/TERMS_OF_USE.md`** and **`templates/terms.html`** —
- Documentation site builds cleanly with `mkdocs build`; repository
- **`webapp/__init__.py` (before-request hook)** — When the 2.71.51
- **`app_utils/eas_fsk.py`** — New `encode_terminator_bits()` helper,
- **`app_utils/eas_demod.py`** — New `ENDEC_MODE_EAS_STATION` constant;
- **`app_utils/eas.py`** — `_generate_station_terminator_samples()`
- **EAS settings** — New "Station Fingerprint" toggle in the broadcast
- `test_eas_decode.py` — Unit test plus DLL integration test for the
- **`webapp/received.py` + `templates/audio_received_detail.html`** — The
- Badge layout updated to stack the numeric code and county name
- **`webapp/__init__.py`** — Registered Python's built-in `min` and `max`
- **`eas_monitor_v3.py`** — The streaming decoder fires the ZCZC callback
- Fix subtracts 1.5 s from the ring position when burst 1 fires, so the
- **`app_utils/eas_demod.py`** — After a burst completed, `synced` stayed
- **`eas_monitor_v3.py`** — The pending alert was unconditionally
- **`app_core/audio/ingest.py` / `eas_monitor_v3.py`** — Headers injected
- **`hardware_service.py`** — `_update_alert_indicators()` was a 2-state
- **`app_utils/eas.py` (TowerLightController)** —
- **`app_utils/eas.py`** — `start_incoming_alert()` was never called from
- Added `test_tower_light_start_incoming_alert_disabled_sends_nothing`
- **EAS settings** — Configurable relay tone duration and relay tone
- Enhanced EOM (`NNNN`) message detection and recognition logic in the
- Low-confidence alerts are no longer silently discarded — they are
- Improved audio tone end-detection so narration timing lines up with
- **`eas_monitor_v3.py` — `_store_received_alert()`** — When the
- Warning / info log messages now describe the degraded state explicitly
- Alert-storage serialisation failures surfaced during test-signal
- Handling of repeated emergency alerts with different event codes — the
- Audio-timing synchronisation so the captured narration starts at the
- Adjusted test-signal injection behaviour so the injected chunks no
- **`eas_monitor_v3.py` / `eas_monitoring_service.py`** — Separated raw
- `_total_alerts_detected` — ZCZC header count (may be 3× per event).
- `_total_alerts_dispatched` — one per EOM-confirmed event.
- `_last_alert_dispatch_time` — Unix timestamp of the most recent
- `get_status()` now exposes:
- `alerts_detected` — EOM-confirmed dispatch count (primary metric).
- `alerts_detected_zczc` — raw ZCZC-burst count (diagnostic metric).
- `last_alert_time` — Unix timestamp of the last dispatch (or `None`).
- `_on_eom_received()` increments `_total_alerts_dispatched` and updates
- Service stop log line now clearly distinguishes "alerts dispatched"
- **`app_core/audio/ingest.py` — `_capture_loop`** — An injected SAME
- Fix gates the live-audio publish to `_eas_broadcast` when
- `test_audio_pipeline_integration.py::TestStreamInjectEASGating`:
- `test_interleaved_live_and_inject_fails_detection` — reproduces the
- `test_gated_inject_detects_eas_signal` — verifies the gated path
- USB tower light (`TowerLightController`) and NeoPixel controller were
- On-air broadcast overlay (global countdown timer popup) could disappear
- `ssl_utils.get_ssl_certificate_info()` incorrectly reported a Let's Encrypt
- `update.sh` nginx config refresh silently reverted a Let's Encrypt certificate back
- `update.sh` showed the "Do you want to continue with the update?" welcome dialog a
- `update.sh` backup whiptail dialog did not call `redraw_screen` on the "No" path,
- `update.sh` migration-error prompt used a plain `read` command whose text was buried
- Alert History table now has server-side sortable columns: clicking any column header
- `GET /eas_messages/<id>/summary` returned HTTP 500 because `EASMessage` has no
- Light theme: table column headers were nearly invisible because the `table-light`
- Removed two orphaned `</div>` closing tags at the end of `alerts.html` that
- Added `flex-shrink: 0` to the footer so it is never compressed by the flex layout,
- **`app_utils/alert_sources.py`** — Two new canonical source-identifier constants:
- **`app_core/models.py`** + migration — `received_eas_alerts` table gains an
- **`eas_monitor.py`** — Resolves the canonical source when an alert is decoded:
- **`templates/audio_received.html`** + detail page — Ingest Path **badge** (RF /
- **`webapp/received.py`** — Wires up the `alert_source` query filter to support the
- **`webapp/admin/coverage.py`** — SAME look-ups store county names as
- **`eas_monitoring_service.py`** — The variable rename from `configured_fips` to
- **Relay audio** — OTA-received alerts that are forwarded now attach the original
- **Live location config reload** — The EAS monitor service re-reads
- **Alert metadata enrichment** — Forwarded alert objects now carry `event_type` and
- SAME header forwarding now preserves statewide wildcard codes (e.g., `039000`)
- FIPS code lists are validated at intake to reject malformed or out-of-range values
- New unit-test coverage for location-code filtering, wildcard preservation, and
- **`eas_monitoring_service.py`** — A confidence threshold of **0.25** is now applied
- **`eas_monitoring_service.py`** — Audio resampling for hardware-controlled sources
- Waveform and spectrogram visualisations in the diagnostics panel are disabled;
- **`eas_monitoring_service.py`** — `UnifiedEASMonitorService` previously shared a
- Ring buffer is updated **before** `process_samples()` so audio is captured in the
- `get_status()` now aggregates `decoder_synced`, `in_message`, and `bytes_decoded`
- The `_current_source_context` mutable field is removed; source identity is carried
- **`app_utils/eas.py`** — `_extract_text_from_payload()`: removed `"headline"` from
- **`app_utils/eas.py`** — Improved punctuation, whitespace, and special-character
- **`app_core/eas_storage.py`** — Added `ensure_eas_settings_columns()` following the
- **`app.py`** — Imports and calls `ensure_eas_settings_columns(logger)` as step 5b in
- **`app_utils/eas_encoding.py`** — When building the SAME header for a forwarded
- **Admin dashboard** — New **Auto-Forward Event Filter** section with grouped
- **`docs/guides/TTS_NORMALIZATION.md`** — New reference guide documenting
- **`tests/test_tts_text_normalization.py`** — 26 tests covering all
- **`app_utils/eas.py`** — `_normalize_text_for_tts()`: added Layer 2 NWS-specific
- Alternate-timezone slash notation (`/5 PM CDT/`) is stripped to plain
- `ST.` abbreviation is expanded to "Saint" (e.g. "ST. JOSEPH" →
- Indiana county-name disambiguation: `IN` is replaced with "Indiana"
- **`app_utils/eas.py`** — Extended `_ACRONYM_MAP` (Layer 3) with:
- `MI` → "Michigan" — NWS county-disambiguation state code; TTS
- `OH` → "Ohio" — NWS county-disambiguation state code; TTS reads bare
- `AFD` → "Air Force Depot" — facility abbreviation used in SAME area
- **`app_utils/eas.py`** — Aligned inline Layer comment numbering (0–3 →
- **`templates/admin/tts_pronunciation.html`** — Info banner now explains
- **`templates/admin/tts.html`** — Pronunciation Preview panel now shows a
- **`templates/help.html`** — New "Text-to-Speech Normalization &
- **`templates/terms.html`** — Replaced the "Jenga tower" fragility callout with three-paragraph
- **`docs/policies/TERMS_OF_USE.md`** — Markdown source updated to match.
- **`templates/terms.html`** — New `alert-danger` callout in Section 4b explaining that EAS was
- **`docs/policies/TERMS_OF_USE.md`** — Mirrored callout added to markdown source.
- **`templates/terms.html`** — Removed **ORC § 2921.13** (Falsification) from the Ohio-specific
- **`docs/policies/TERMS_OF_USE.md`** — Updated markdown source to match.
- **`templates/terms.html`** — Added three additional Ohio-specific statutes to the Section 4a
- **`docs/policies/TERMS_OF_USE.md`** — Updated markdown source to match.
- **`templates/terms.html`** — Added **ORC § 2909.04** (Disrupting Public Services,
- **`docs/policies/TERMS_OF_USE.md`** — Updated markdown source to match.
- **`templates/terms.html`** — Expanded Section 4a "State and local laws" bullet to add an
- **`docs/policies/TERMS_OF_USE.md`** — Updated markdown source to match the above changes.
- **`app_core/models.py`** — `ManualEASActivation` gains two new nullable columns:
- **`webapp/eas/workflow.py` `manual_eas_generate()`** — Captures the client IP
- **`webapp/eas/workflow.py` `manual_eas_send()`** — Same IP capture at broadcast
- **`app_core/migrations/versions/20260327_add_ip_to_manual_eas_activations.py`** —
- **`templates/terms.html`** — Strengthened Section 3 (Disclaimer of Liability & Indemnification)
- **`templates/terms.html`** — Added new Section 4a (Criminal Liability & Federal Law Violations)
- **`docs/policies/TERMS_OF_USE.md`** — Updated markdown source to match all changes above.
- **`webapp/admin/coverage.py`** — Census TIGER fallback for county coverage now
- **`webapp/admin/coverage.py`** — Step 3 Boundary-table fallback (`Boundary.query
- **`webapp/admin/api.py` `_detect_county_wide()`** — `short_with_list` heuristic
- **`webapp/routes_debug.py`** — `.cast("geography")` called directly on a SQLAlchemy
- **`templates/alert_detail.html`** — The debug panel rendered the full errors array with
- **`webapp/routes_debug.py`** — Both `/debug/alert/<id>` and `/debug/boundaries/<id>`
- **`templates/alert_detail.html`** — Debug panel "Boundary Intersection Results" table
- **`webapp/admin/intersections.py`** — `fix_county_intersections` was computing
- **`app_core/alerts.py`** — `_fetch_bulk_intersections` filtered boundaries with
- **`webapp/admin/intersections.py`** — `fix_county_intersections` (the backend for
- **`webapp/admin/intersections.py`** — Wrong import path `from app_core.coverage import
- **`templates/alert_detail.html`** — The `debugBoundaries()` JS function existed but had
- **`templates/components/navbar.html`** — The `/debug/ipaws` IPAWS Poller Debug page
- **`webapp/admin/coverage.py`** — `calculate_coverage_percentages`: Three separate bugs
- **`webapp/admin/api.py`** — `alert_detail`: `is_actually_county_wide` now requires
- **`templates/alert_detail.html`**:
- "COUNTY-WIDE ALERT" banner no longer fires for SAME-estimated coverage.
- "Exact Coverage" label changes to "Estimated Coverage" when `is_estimated=True`,
- Square miles are now displayed next to the coverage percentage in both the
- Coverage badge in the Alert Information header no longer shows the county-wide
- **`webapp/admin/coverage.py`** — `calculate_coverage_percentages`: Added fallback
- **`webapp/admin/api.py`** — `get_boundaries`: When `/api/boundaries?type=county`
- **`poller/cap_poller.py`** — `_update_existing_alert`: No longer clears
- **`webapp/admin/coverage.py`** — `try_build_geometry_from_same_codes`: Added
- **`templates/alert_detail.html`** — Replaced misleading "COVERAGE CALCULATING" /
- **`templates/alert_detail.html`** — `triggerIntersectionFix()`: Added immediate
- **`webapp/admin/intersections.py`** — `calculate_single_alert`: Always calls
- **`webapp/admin/coverage.py`** — `calculate_coverage_percentages`: County coverage
- **`templates/base.html`** — Python badge updated from `3.11` to `3.13` to
- **`templates/partials/footer.html`** — Python badge updated from `3.11.14` to
- **`templates/base.html`** — Updated copyright year 2025 → 2026. Wrapped
- **`templates/partials/footer.html`** — Updated both copyright year references
- **`static/css/styles.css`** — Multiple visual improvements:
- Added the previously missing `page-header-gradient` CSS class (referenced in
- Added animated rainbow bottom accent line (`::after`) to `.navbar`.
- Added `page-header::after` subtle bottom highlight line.
- Enlarged `.footer-logo-mark` icon box (60 → 64 px) with a blue glow shadow.
- Made `.footer-divider` an animated rainbow gradient stripe instead of a plain
- Updated `.footer-column-title::after` underline to teal-to-blue gradient.
- Added `.tech-stack-card` glass-morphism container for the badge row.
- ...4 more
- **`templates/admin/tts_pronunciation.html`** — The JavaScript block was declared as
- **`app_utils/eas_decode.py`** (`_try_multiple_sample_rates`) — Audio file was read and
- **`app_utils/eas_decode.py`** (`_decode_from_samples`) — Extracted the decode body
- **`app_utils/ipaws_enrichment.py`** (`_canonicalize_signed_info`) — `with_comments=False`
- **`poller/cap_poller.py`** (`_convert_cap_alert`) — Alert XML is now serialized using
- **`templates/alert_detail.html`** (`loadCountiesFromSameCodes`) — SAME codes ending in
- **`app_utils/eas.py`** (`_convert_audio_to_samples`) — Added direct `ffmpeg` subprocess
- **`app_core/models.py`** — New `TTSPronunciationRule` model and `TTS_BUILTIN_PRONUNCIATIONS`
- **`app_utils/eas.py`** — `_normalize_text_for_tts()` function: two-layer substitution
- **`app_utils/eas.py`** — `_load_pronunciation_rules()` helper loads enabled rules ordered
- **`webapp/admin/tts_pronunciation.py`** — Full CRUD admin routes under `/admin/tts/pronunciation`
- **`app_core/migrations/versions/20260326_add_tts_pronunciation_rules.py`** — Alembic migration
- **`docs/development/AGENTS.md`** — New "Alembic Migration Rules" section under Database
- **`app_core/migrations/versions/20260326_add_tts_pronunciation_rules.py`** — `down_revision`
- **`app_utils/eas.py`** (`EASBroadcaster.handle_alert`) — `inject_eas_audio()` was called
- **`app_core/audio/eas_stream_injector.py`** (`inject_eas_audio`) — Before publishing EAS
- **`app_core/audio/ingest.py`** (`AudioSourceAdapter`) — Added `_eas_inject_seq` integer
- **`app_core/audio/icecast_output.py`** (`IcecastStreamer._feed_loop`) — Each streamer now
- **`app_core/audio/eas_monitor.py`** (`_store_received_alert`) — If `db.session.commit()`
- **`eas_monitoring_service.py`** (`_ensure_raw_audio_column`) — At startup the service now
- **`app_core/audio/ingest.py`** (`AudioIngestController.inject_eas_test_signal`) — Test
- **`app_core/audio/ingest.py`** (`AudioSourceAdapter`) — Added `_eas_injection_active`
- **`app_core/audio/eas_stream_injector.py`** (`inject_eas_audio`) — Sets
- **`app_core/audio/eas_monitor.py`** (`_store_received_alert`) — `full_alert_data=alert`
- **`app_core/audio/ingest.py`** (`AudioSourceAdapter.schedule_inject`) — New public method
- **`app_core/models.py`** (`ReceivedEASAlert.raw_audio_data`) — New `LargeBinary` column that
- **`app_core/audio/eas_monitor_v3.py`** (`UnifiedEASMonitorService`) — Per-source audio ring
- **`webapp/admin/audio/received.py`** — New `/audio/received/<id>/audio` route that streams
- **`templates/audio_received_detail.html`** — Audio player card showing the raw received OTA
- **`app_core/migrations/versions/20260325_add_raw_audio_to_received_alerts.py`** — Migration
- **`eas_monitoring_service.py`** — `eas_stream_injector.set_controller()` was never called
- **`app_core/audio/ingest.py`** (`inject_eas_test_signal`) — The test signal was injected
- **`webapp/documentation.py`** — `/docs/DIAGRAMS` (and `/docs/CHANGELOG`, `/docs/ABOUT`)
- **`app_core/audio/redis_commands.py`** (`_execute_command` / `source_start`) — The return
- **`app_core/audio/sources.py`** (`StreamSourceAdapter._restart_ffmpeg_process`) — When
- **`eas_service.py`** (`publish_eas_metrics_to_redis`) — When `eas_monitoring_service.py`
- **`webapp/admin/audio_ingest.py`** (`api_delete_audio_source`) — Replace the
- **`webapp/admin/audio_ingest.py`** (`api_delete_audio_source`) — Deleting a
- **`app_core/audio/redis_commands.py`** (`delete_source`) — Added
- **`eas_monitoring_service.py`** (`initialize_audio_controller`) — Wrapped
- **`eas_monitoring_service.py`** (`main`) — Wrapped the
- **`webapp/admin/audio_ingest.py`** (`api_delete_audio_source`) — Delete no
- **`webapp/admin/audio_ingest.py`** (`api_get_audio_sources`) — Sources that
- **`update.sh`** — Added `systemctl reset-failed` for all EAS Station service
- **`systemd/eas-station-audio.service`** — Added `StartLimitBurst=0` to
- **`eas_monitoring_service.py`** (`publish_metrics_to_redis`) — Replaced the
- **`eas_monitoring_service.py`** (main loop) — Reduced metrics publish interval
- **`eas_monitoring_service.py`** (source watchdog) — Watchdog now also
- **`app_core/audio/worker_coordinator_redis.py`** (`read_shared_metrics`) —
- **`app_core/audio/auto_streaming.py`** (`_get_eas_monitor_settings`) —
- **`app_core/audio/auto_streaming.py`** (health-check step) — Dead streamers
- **`app_core/websocket_push.py`** — Reduced the WebSocket push loop from
- **`app_core/audio/auto_streaming.py`** — `_get_eas_monitor_settings()` now
- **`app_core/audio/redis_commands.py`** — `inject_test_signal` handler now
- **`eas_service.py`** — `initialize_eas_monitor()` now wraps the FIPS
- **`eas_monitoring_service.py`** — Added `_redis_publisher_monitor_loop()`
- **`eas_monitor_v3.py`** — `HealthTracker.update_no_audio()` no longer resets
- **`redis_commands.py`** — Added `inject_test_signal` command to
- **`eas_decoder_monitor.py`** — The `/api/admin/eas_decoder_monitor/test_signal`
- `EASMonitor._streaming_decoder` alias, `_restart_count` tracker, `_restart_monitor_thread()`, and `_resample_if_needed()` to support watchdog restarts and stereo audio handling.
- `EASMonitor.get_status()` now includes `restart_count` and computes runtime metrics even when the monitor is stopped.
- `_SoapySDRReceiver._calculate_buffer_size()` dynamically sizes the IQ read buffer based on the configured sample rate.
- Setup wizard now includes a **Core** section (SECRET_KEY and PostgreSQL credentials) that is validated on form submission.
- `_is_valid_partition_code()` in `location_settings.py` — `sanitize_fips_codes()` now accepts SAME partition-digit codes (e.g. `627137`) whose whole-county equivalent is known.
- `tools/download_nws_gis_data.py` — standalone CLI that downloads NWS Public Forecast Zones and NWR Political Subdivisions (partial-county) shapefiles from weather.gov into `assets/`.
- NWS partial-county shapefile `assets/cs16ap26.dbf` (April 2026 vintage) bundled; `_load_county_subdivision_index` now auto-detects the newest `cs*.dbf` in `assets/` and logs a download hint when absent.
- `install.sh` now runs `tools/download_nws_gis_data.py` after database setup to fetch the latest GIS data.
- Removed redundant `import os` inside `_collect_smart_health` that caused `UnboundLocalError` in production.
- `_restart_ffmpeg` in `icecast_output.py` now sleeps for `ICECAST_RESTART_DELAY` seconds before relaunching FFmpeg to prevent rapid restart loops.
- `build_database_url()` now falls back to `POSTGRES_*` environment variables when `DATABASE_URL` is not set.
- SOAPY_SDR error code −7 description now includes "not locked" so the PLL lock hint is surfaced correctly.
- **`broadcast_adapter.py`** — Replaced bare `except:` clause with `except queue.Empty:` so
- **`radio/discovery.py`** — Silent `except Exception: pass` blocks in SoapySDR capability
- **`routes_settings_radio.py`** — Replaced three generic `raise Exception(error)` calls with
- **Migration scripts** — Replaced `print()` calls in five Alembic migration files with
- **Per-source EAS ingest Icecast streams** — The auto-streaming service now creates a
- **EAS decoder monitor respects database settings** — `AutoStreamingService` now reads
- **Test signal injection** — New `POST /api/admin/eas_decoder_monitor/test_signal`
- **Navbar link** — *EAS Decoder Monitor* is now listed under Monitor → Radio Monitoring
- **Updated nginx proxy rule** — The single `/eas-ingest.mp3` location block is replaced
- `AutoStreamingService.__init__` accepts an optional `flask_app` parameter so the
- `AudioIngestController` gains `inject_eas_test_signal(source_name)` method.
- **TTS "No TTS provider configured" for every IPAWS/CAP alert** — `load_eas_config()` was
- **Navbar Tools menu overflow** — The standalone "Tools" dropdown was too long to fit on
- **EAS ingest Icecast stream** (`/eas-ingest.mp3`) — a 3rd Icecast mountpoint that
- **Three working audio pipeline test files** — `tests/test_audio_playout_queue.py` (24
- **Robust test-runner logging** — `routes_audio_tests.py` now scans output from the
- **Listen button** — root cause was `audio.play()` being called inside an async
- **Error messages now actionable** — the error alert distinguishes between "no audio
- **Operator audit trail for manual EAS alerts** — `manual_eas_activations` now stores
- **Application log entries** — `workflow_logger.info` now emits a line such as
- **`generated_by` in SystemLog** — the `admin` code path also records the operator in the
- **Alert self-test log** — `route_logger.info` for `run_alert_self_test` now includes the
- **Database migration** `20260323_add_created_triggered_by_to_activations` adds the two
- **OLED screen previews no longer blank** — the Custom Display Screens management page now
- **Bar graphs visible in previews** — `bar` elements are drawn as filled progress bars on
- **VFD element previews improved** — VFD screens that use the `elements` format now render
- **Legacy `lines`-format OLED screens unaffected** — the previous text-based renderer is
- **ENDEC hardware shown in Alert Verification** — the detected ENDEC type (`endec_mode`)
- **`endec_mode` persisted in stored decode records** — `record_audio_decode_result()` now
- `_deserialize_decode_result` in the alert-verification route now correctly restores
- **ENDEC hardware detection via null/FF terminator bytes** — `detect_endec_mode()` now
- NWS Legacy / EAS.js: 2 × 0x00 → `NWS`
- NWS Broadcast Message Handler: 3 × 0x00 → `NWS_BMH`
- NWS Console Replacement System: 3 × 0x00 with CRS scoring → `NWS_CRS`
- SAGE ANALOG 1822: 1 × 0xFF → `SAGE_ANALOG_1822`
- SAGE DIGITAL 3644: 3 × 0xFF → `SAGE_DIGITAL_3644`
- SAGE DIGITAL 3644 (first burst leading byte): 0x00 before preamble → strong `SAGE_DIGITAL_3644` vote
- DEFAULT / DASDEC / TRILITHIC: identified by inter-burst gap timing (existing logic retained)
- **Post-message terminator capture in `SAMEDemodulatorCore`** — after a SAME message is
- **Leading null byte detection** — a 0x00 byte decoded just before a burst's preamble
- **32-bit PCM WAV files fail to decode** — `_read_audio_samples` only handled 16-bit
- **Goertzel decoder overrides correct DLL result with garbled partial header** — for
- **SAME headers generated with trailing spaces** — `build_same_header` padded the
- **`/admin` returning fallback HTML** — `get_same_lookup()` returns a `MappingProxyType`
- **`/admin/notifications` and `/admin/application` returning fallback HTML** — both pages
- **Setup-mode first-run access** — `before_request` endpoint allowlist for setup mode only
- **`/api/system_status` 500 error** — `_CPU_SAMPLE_INTERVAL_SECONDS` constant was
- **`/logs` page (system_logs.html)** — template used `{% block head %}` which is not
- **Test correctness** — updated `test_admin_dashboard_fixes.py` to reflect the active
- **`admin/notifications/` 500 error** — error-handler in `notifications.py` referenced
- **`admin/poller/` 500 error** — same `admin_page` typo in `poller.py`; corrected.
- **`admin/application-settings/` 500 error** — same `admin_page` typo in
- **`admin/hardware/`, `admin/icecast/`, `admin/tts/`, `admin/certbot/`,
- **Application Settings**, **Alert Poller**, **Text-to-Speech**, **SSL Certificates**, and **Backups**
- New **System** category in the Settings Hub for Backups.
- Certbot (SSL) card added to the **Network** category.
- Notifications card description in the Settings Hub now correctly reads
- **SNMP v2c trap notifications** — EAS Station can now send SNMP traps to NMS targets
- **`pysnmp` added to `requirements.txt`** — previously the SNMP library was an undocumented
- **`test-snmp` endpoint** — `/admin/notifications/test-snmp` (POST) sends a test SNMP trap
- **SNMP fields in `NotificationSettings` model** — `snmp_enabled`, `snmp_targets` (JSONB),
- **Database migration `20260320_add_snmp_to_notifications`** — upgrades existing installs
- **Compliance email alerts now use database SMTP settings** — `system_health.py` was still
- **SNMP health monitor uses database targets** — `system_health.py` now reads SNMP targets
- **Raw SAME Header Parser** on `/admin/alert-verification` — paste any `ZCZC-…` string and
- **Skip baud-rate offset variants when DLL confidence ≥ 0.85** — the Goertzel bit-scan now
- **Early-exit in multi-rate sample-rate selection** — `_try_multiple_sample_rates` stops
- **Vectorized Goertzel filter for tone detection** — `_goertzel_power` in
- **Eliminated double audio load in `detect_eas_from_file`** — tone and narration detection
- **Polyphase audio resampler** — `_resample_with_scipy` now uses `scipy.signal.resample_poly`
- **FIPS lookup singleton** — `get_same_lookup()` returns the module-level `US_FIPS_LOOKUP`
- **DB indexes on alert analytics columns** — added `idx_cap_alerts_sent`,
- **`docs/hardware/ALPHA_*.md` renamed** — removed "Phase X" development numbering from
- **`docs/troubleshooting/AUDIO_STREAMING_SETUP.md`** — rewrote from scratch. Previous
- **`docs/guides/HELP.md`** — fixed Reference Commands table (all entries were Docker
- **`docs/troubleshooting/TTS_TROUBLESHOOTING.md`** — replaced two references to the
- **`docs/guides/MANUAL_EAS_EVENTS.md`** — replaced reference to `debug_tts.py` with
- **`mkdocs.yml`** — removed all nav entries pointing to previously deleted files; updated
- **`docs/INDEX.md`** — added Alpha LED Sign documentation to the Hardware section.
- **`scripts/README.md`** — rewrote to reflect current bare-metal scripts inventory.
- **Missing image beside maintainer bio on About page** – `ham-radio-icon.svg` was a PNG
- **EAS monitor showing false "Disconnected/Unavailable" status** – The
- **Audio System Logs tab always empty** – The `AudioAlert` database model existed and
- **Coverage percentage calculation** – The denominator in `calculate_coverage_percentages`
- **County-wide fallback producing wrong 100 % coverage** – The alert detail view had a
- **"Calculate Coverage Percentage" button failing with missing geometry** – The
- **XML digital signature verification** – Added `_canonicalize_signed_info()` helper in
- **Unauthenticated access to VFD control** – All VFD routes (`/vfd_control`, `/vfd`, and all
- **Unauthenticated access to Displays dashboard** – `/displays` now requires
- **Unauthenticated access to Screen management** – All screen and rotation routes (`/screens`,
- **Unauthenticated access to Alert Verification** – All alert verification routes
- **Unauthenticated access to EAS Compliance dashboard** – All compliance routes
- **Unauthenticated access to LED control** – All LED routes (`/led_control`, `/led`, and all
- **Message history stuck on "Loading message history..."** – `loadMessageHistory()` now updates
- **Live sign preview (canvas simulator) not working** – Fixed a JavaScript bug where a duplicate
- **Search/filter history did nothing** – Implemented the previously empty `displayFilteredHistory()`
- **WYSIWYG LED Sign Simulator** – Live CSS-animated sign panel in the Custom Message tab; all 20 M-Protocol display modes animate in real time (scroll, roll-left/right/up/down, wipe-*, flash, explode, compressed-rotate, auto, clock)
- **Mixed-mode multi-line preview** – each of the 4 lines independently shows its selected effect/color/speed in the simulator panel
- **Layout Preset buttons** – one-click configurations: Static 4, Header+Scroll, Alert, Ticker
- **Per-line effect pills** – colour-coded badges on each line editor card showing the active display mode
- **Speed modifier CSS classes** – speed-1 through speed-5 control animation playback rate
- **Dots / Pixel-Art tab** – 20×7 (up to 160×16) interactive pixel-art canvas; click/drag to paint, shift/invert/fill tools, text-to-dots generator (5×7 bitmap font for A/E/S), five quick patterns (checkerboard, border, diagonal, heart, arrow), live canvas preview; sends via new M-Protocol Picture File (Type I) command
- **RSS Feeds tab** – add/remove RSS feed sources with name, URL, interval, color, effect, max items; per-feed fetch/refresh button; item viewer with click-to-select (up to 4 lines); "Send Selected" and "Send All Enabled Feeds" buttons
- **`send_dots_graphic()` method** on `Alpha9120CController` – encodes a 2-D pixel grid as an M-Protocol Type I (Picture File) frame
- **`LEDRSSFeed` and `LEDRSSItem` database models** with full CRUD API (`/api/led/rss/feeds`, `/api/led/rss/feeds/<id>/fetch`, `/api/led/rss/feeds/<id>/items`, `/api/led/rss/send`)
- **Dots API** (`POST /api/led/dots`) accepts a JSON dot-grid and sends it to the sign
- ...2 more
- **EAS decoding architecture diagram** in `docs/architecture/EAS_DECODING_SUMMARY.md` —
- **Notification delivery flow diagram** in `docs/guides/notifications.md` — Sequence
- **Updated `docs/reference/DIAGRAMS.md`** — Added index entries for 5 previously
- **7 broken Mermaid diagrams** — Fixed parse and lexical errors in
- **Dark theme: invisible text on cards and Bootstrap components** — Bootstrap 5.3
- **`.card` missing explicit text color** — Added `color: var(--text-color)` directly
- **`bg-*-subtle` / `text-*-emphasis` Bootstrap utilities** — Overrode
- **`alert-light` / `alert-secondary` in dark themes** — These alerts previously
- **Severity badge text contrast (`index.html`)** — `.severity-severe` used
- **OTA broadcast silently skipped** — The EAS monitor daemon thread had no
- **`handle_alert()` false-positive success on DB failure** — `same_triggered`
- **EASSettings not loaded from database in CAP poller** — `load_eas_config()`
- **Deprecated `datetime.utcnow()` in `alert_forwarding.py`** — Redis payload
- **OTA auto-forward attempted broadcast for UNKNOWN event codes** — When the
- **`build_files()` exceptions propagated uncaught from `handle_alert()`** —
- **`test_eom_segment_duration_is_reasonable` used wrong lower bound** — The
- **IPAWS alerts with embedded audio fall back to TTS instead of using the pre-recorded
- **`save_ipaws_audio()` skips `derefUri` resources with missing `mimeType`** — The
- **MPEG audio format detection too narrow** — `_convert_audio_to_samples()` checked
- **EAS audio sources stuck in ERROR state after network disruption** — The
- **No automatic recovery of failed audio sources** — Added a source error-recovery
- **"Listen to EAS audio feed" button always fails when EAS monitor has no active
- **Misleading "audio-service may be starting up" error message** — The EAS monitor
- **EAS monitor badge showed no guidance when sources are stopped** — Added a
- **Listen button error showed no actionable guidance** — When the decoder stream
- **Edit Alert modal and Confirmation modal unclickable** — Both Bootstrap modals were
- **"Delete Expired Alerts" button always failed** — The JavaScript `clearExpiredAlerts()`
- **"View Alert" button on Audio Archive** — The button was incorrectly linking to the
- **"Edit Alert" modal not opening on Admin Panel** — The Bootstrap Modal instance for
- **Confirmation modal not opening on Admin Panel** — `window.confirmationModal` was
- **NOAA vs IPAWS polling differentiation** — The CAP poller now writes a separate
- **Per-source error attribution** — Fetch errors (SSL, timeout, request failures) are now
- **`AudioAlert.cleared` AttributeError** — The `audio` log-viewer category referenced a
- **`PollHistory.poll_time` AttributeError** — `websocket_push.py` referenced
- **IPAWS-STAGING endpoints now grouped with IPAWS** — The FEMA TDL staging domain
- **Documentation cleanup** — Removed one-off development artifacts from the docs directory:
- **CSS Variables Migration doc relocated** — Moved `CSS_VARIABLES_MIGRATION.md` from the
- **mkdocs.yml copyright corrected** — Changed "MIT License" to the accurate dual-license
- **mkdocs.yml navigation rebuilt** — Removed 29 navigation entries pointing to files that do
- **Installation** section (7 guides, previously absent from nav)
- **Troubleshooting** section (21 guides, previously entirely absent from nav)
- **Security** section (3 guides, previously absent from nav)
- **Architecture** section expanded from 3 to 11 entries
- **Hardware** section expanded from 4 to 15 entries (including Alpha LED sign docs)
- **Guides** section expanded with all orphaned user guides
- ...2 more
- **RBDS unreliable for stations broadcasting Group 2B (C' blocks)** — When the presync state
- **RBDS polarity not updated at sync achievement** — After presync achieved sync, `_rbds_inverted_polarity`
- **Audio monitor shows "No metrics available from audio-service"** — `_sanitize_value()` in
- **`broadcast_queue` stats never populated** — `collect_metrics()` stored broadcast queue data
- **EAS monitor status stored as string `"None"` in Redis** — when `_eas_monitor.get_status()`
- **`routes_eas_monitor_status.py` "invalid type" error** — the non-dict check now returns
- **Audio monitor VU meter warning hides when sources are running but silent** — the warning
- **EAS Continuous Monitor badge stays "Loading…" on error** — the status badge is now updated
- **Source cards show "STOPPED" for unknown status** — when the audio-service is not running,
- **RBDS crystal-locked carrier phase drift** — `RBDSWorker._pilot_sample_counter` only
- **Stale RBDS unit tests** — Updated `tests/test_rbds_demodulation.py` to test the current
- **Received EAS Alerts log tab** — New "Received EAS" tab on the Logs page shows EAS alerts
- **EAS activity stat cards** — The Statistics dashboard now shows four new metric cards:
- **Urgency and Certainty distribution charts** — New "By Urgency" and "By Certainty" bar/doughnut
- **Received EAS stats in backend** — Stats route now queries `ReceivedEASAlert` and
- **Received EAS category in All Logs** — The "All Logs" view now includes a "Received EAS"
- **Duration chart `avg_hours` field mismatch** — `createDurationChart()` was reading `i.avg_hours`
- **504 Gateway Timeout / Gunicorn worker hung in I2C on Raspberry Pi** — Three
- **Session key inconsistency across Gunicorn workers** (`app.py`). Without
- **Gunicorn workers crashing on startup when PostgreSQL is not yet ready**
- **Web stream stall after extended runtime** (`icecast_output.py`) — The source-timeout restart check was gated on the internal buffer being non-empty (`and buffer`). When the audio source stopped supplying data the buffer drained to zero, causing the check to silently skip and leaving a stalled FFmpeg process running indefinitely. The erroneous guard has been removed so the timeout fires correctly regardless of buffer state.
- **RBDS decoding never locking** (`demodulation.py`) — Two related bugs prevented reliable RBDS carrier lock:
- **Ambient background gradient** — All pages now display a subtle two-orb radial-gradient overlay fixed to the viewport. The gradient is derived from the active theme's `--primary-color` and `--secondary-color` variables, so it automatically adapts across all 20 built-in themes.
- **Admin card headers** — Replaced the flat `var(--bg-color)` fill with a theme-aware gradient tint (`color-mix` at low opacity against `--surface-color`), giving every section card a subtle accent without obscuring form content.
- **Admin header banner** — Replaced hardcoded `#667eea / #764ba2` hex values with `var(--primary-color)` / `var(--secondary-color)` so the banner matches the chosen theme. Added a shimmer highlight overlay and a stronger box-shadow for depth.
- **Admin stat cards** — Replaced hardcoded indigo/purple gradient with theme-aware `var(--primary-color)` → `var(--secondary-color)` gradient. Hover shadow also now uses `color-mix` on the theme primary rather than a hardcoded RGBA.
- **Admin modal headers** — Replaced the hardcoded red gradient with the theme primary→secondary gradient to align with the rest of the UI.
- **Manage-card headers** — Applied the same subtle gradient tint treatment as the main card headers for visual consistency.
- **Form focus glow** — Replaced hardcoded `rgba(102, 126, 234, 0.2)` focus ring with `color-mix(in srgb, var(--primary-color) 20%, transparent)` so the focus state reflects the active theme color.
- **Unified Settings hub page** (`/settings`) — All settings sections (Configuration, Network, Hardware, Security & Access) are now presented as a single card-based overview page, making it much easier to discover and navigate to any setting without hunting through nested dropdown menus.
- **Settings navbar entry simplified** — The Settings dropdown (which previously contained 15+ nested links across four sections) is replaced by a single "Settings" link that navigates directly to the new unified `/settings` hub page, reducing navbar visual complexity.
- **Merged Hardware dropdown into Settings** - The Hardware navigation item has been removed as a standalone top-level dropdown. All hardware-related links (SDR Receivers, Audio Streams, Audio Archives, Hardware Settings, GPIO & Relays, Zigbee) are now organized under a new "Hardware" section within the Settings dropdown, reducing top-level navigation from 7 to 6 items.
- **Moved Audio Health to Monitor** - Audio Health dashboard link moved from Tools > Observability to Monitor > Radio Monitoring, where it logically belongs alongside other audio/radio monitoring links.
- **Removed duplicate Alert Statistics from Tools** - The `/stats` link in Tools > Analytics & Reporting has been removed since Statistics is already accessible from the Monitor dropdown.
- **`POST /api/led/set_time_format` endpoint** (v2.54.0)
- Accepts `time_format` ("TIME_12H" or "TIME_24H"), `color`, and `font` parameters.
- Calls the LED sign controller to apply the selected 12-hour or 24-hour time format, then sends the current time as a two-line message ("CURRENT TIME" / formatted time string) to the sign.
- Records the sent message in the `led_messages` database table.
- Files: `webapp/routes_led.py`
- **`POST /api/led/set_date_format` endpoint** (v2.54.0)
- Accepts `date_format` (one of MMDDYY, DDMMYY, MMDDYYYY, DDMMYYYY, YYMMDD, YYYYMMDD), `color`, and `font` parameters.
- Formats the current date using the requested layout and sends it as a two-line message ("TODAY'S DATE" / formatted date string) to the sign.
- Records the sent message in the `led_messages` database table.
- Files: `webapp/routes_led.py`
- **LED control frontend buttons now fully functional** (v2.54.0)
- Removed the "Time/date display feature coming soon" stub and disabled early-returns from `sendTimeDisplay()` and `sendDateDisplay()` in `templates/led_control.html`.
- `sendDateDisplay()` corrected to call `/api/led/set_date_format` with the `date_format` key instead of the old copy-paste bug that called `/api/led/set_time_format` with `time_format`.
- Files: `templates/led_control.html`
- **CTIA-required opt-out footer in all outgoing EAS alert SMS messages** (v2.53.2)
- `app_core/notifications/sms.py` now appends `Reply STOP to stop msgs` to every alert message body, satisfying CTIA messaging guidelines that Twilio enforces during toll-free number verification. This footer is required for carrier delivery.
- Test SMS messages also include `Reply STOP to stop msgs, HELP for help` so test submissions to Twilio reviewers demonstrate compliance.
- Files: `app_core/notifications/sms.py`
- **Expanded `/sms-compliance` opt-in disclosure page** (v2.53.2)
- Added "Sample Message Format" section with an exact mock-up of what EAS alert messages look like (including the new STOP footer), satisfying Twilio's requirement to show a representative message sample on the opt-in page.
- Added verbatim "Consent Disclosure Language" block (the exact text shown to recipients at opt-in) so Twilio reviewers can verify the opt-in flow.
- Expanded opt-out keyword table to include all Twilio-standard keywords: STOP, STOP ALL, CANCEL, END, QUIT, UNSUBSCRIBE.
- Removed Sprint (now T-Mobile) from the carrier list; list now reflects current major carriers.
- Files: `templates/sms_compliance.html`
- ...11 more
- **AMPR 44.0.0.0/8 Non-Commercial Network Disclaimer** (v2.53.1)
- Added a prominent non-commercial network notice to `templates/about.html` and `templates/terms.html` for deployments accessible via the AMPRNet (44.0.0.0/8) address block.
- Added the same notice as Section 13 to `docs/policies/TERMS_OF_USE.md`.
- Explains FCC Part 97 non-commercial requirements, ARDC allocation policy, and that this service is operated strictly for non-commercial amateur radio research and emergency communications training.
- Files: `templates/about.html`, `templates/terms.html`, `docs/policies/TERMS_OF_USE.md`
- **Consistent visual theming across all pages** (v2.52.0)
- Added the standard `admin-page-header` gradient banner to all 22 admin pages that previously lacked a consistent page header (application_settings, backups, county_boundaries, eas_decoder_monitor, mail_server, notifications, poller, zones, sessions, audio_archives, audio_sdr_fix, audio_sources, radio, radio_diagnostics, certbot, icecast, tailscale, tts, alert_feeds, environment, network, zigbee). Old ad-hoc h1/h2 heading rows removed.
- Migrated `hardware_settings.html` from the non-admin `.page-header` to `.admin-page-header` for consistent admin section styling.
- Fixed `index.html` (dashboard): removed the large inline `<style>` block that overrode the global `.page-header` CSS with conflicting padding, border-radius, and child element structure. Updated dashboard page-header HTML to use the canonical standard pattern (matching alerts.html, etc.).
- Replaced hardcoded hex colors (`#6610f2`, `#6f42c1`) in `.admin-page-header.header-purple` in `static/css/admin.css` with theme-aware CSS variables (`var(--vibrant-indigo)`, `var(--secondary-color)`) so the purple header variant respects the active theme.
- ...73 more
- **Created missing `docs/javascripts/mermaid-init.js`** (v2.53.1)
- `mkdocs.yml` referenced `javascripts/mermaid-init.js` as an extra JavaScript file, but the file and its parent directory did not exist, causing a 404 error when building the MkDocs documentation site.
- Created `docs/javascripts/mermaid-init.js` with proper Mermaid initialization configuration (startOnLoad, theme variables, flowchart and ER diagram options).
- Files: `docs/javascripts/mermaid-init.js`
- **Fixed `.bg-light` text readability in dark and coffee themes** (v2.53.1)
- The `.bg-light` CSS rule hard-coded `color: #212121` (near-black text), which became illegible when the `--light-color` variable resolves to a dark background colour (`#455169` in the dark theme, `#5b4333` in the coffee theme). Added theme-scoped overrides to use `var(--text-color)` and `var(--text-secondary)` for those two dark themes.
- Files: `static/css/styles.css`
- **Updated SMS Messaging Policy date** (v2.53.1)
- Updated the "Last updated" field in `docs/policies/SMS_MESSAGING.md` from a placeholder to the current revision date.
- Files: `docs/policies/SMS_MESSAGING.md`
- ...395 more
- **Display preview styling with type-specific themes** (PR #1670)
- Applied type-specific visual themes to display preview and screens templates
- Files: `templates/displays_preview.html`, `templates/screens.html`
- **Compact SAME codes and geocodes in multi-column grid layout** (PR #1668)
- Alert detail page compresses SAME codes and geocodes into a readable multi-column grid
- Files: `templates/alert_detail.html`
- **Refactored alert detail layout** (PR #1667)
- Moved timing and technical information cards from sidebar into main content flow
- Files: `templates/alert_detail.html`
- **GPIO configuration UI aligned with Hardware Settings** (PR #1653)
- ...34 more
- **CRITICAL: Fix path traversal vulnerability in IPAWS audio serving** (v2.46.4)
- Added filename sanitization using `os.path.basename()` to prevent directory traversal
- Added path validation to ensure resolved path is within output directory
- Changed to use Flask's `send_file()` instead of reading entire file into memory
- File: `webapp/admin/api.py` - `ipaws_original_audio()` endpoint
- **CRITICAL: Fix XSS vulnerability in IPAWS web resource URLs** (v2.46.4)
- Added URL scheme validation to only allow http:// and https:// protocols
- Prevents javascript: URIs and other malicious schemes from being rendered
- File: `webapp/admin/api.py` - `_extract_ipaws_display_data()` function
- **MAJOR: Fix DoS vulnerability in IPAWS audio handling** (v2.46.4)
- Added configurable size limit (10MB default) via `IPAWS_AUDIO_MAX_BYTES` env var
- Validates size hint from resource metadata before decoding
- Estimates decoded size before base64 decode to prevent memory exhaustion
- Uses strict base64 validation to catch malformed payloads
- Verifies actual decoded size before writing to disk
- File: `app_utils/ipaws_enrichment.py` - `save_ipaws_audio()` function
- **CRITICAL: RBDS Buffer Management Fixed** - Changed from buffer-draining to index-based bit processing
- Root cause: `_decode_rbds_groups()` was using `pop(0)` in a `while` loop, consuming ALL bits even during failed presync
- When presync found valid blocks but spacing verification failed, bits were already consumed and lost
- This caused constant `buffer=0` in logs and prevented synchronization from ever being achieved
- Changed to index-based processing (like python-radio reference) that preserves unprocessed bits
- Bits are only removed from buffer after successful processing or when buffer exceeds 6000 bit limit
- Failed presync attempts now preserve bits for retry instead of discarding them
- Added `_rbds_buffer_index` to track position in buffer without destroying data
- Improved logging: spacing mismatches now show which block types caused the mismatch
- Reference: https://github.com/ChrisDev8/python-radio/blob/main/decoder.py (lines 235-280)
- ...28 more
- **Icecast Source Limit Configuration** - Made maximum concurrent sources configurable
- Added `max_sources` field to `IcecastSettings` database model
- Web UI field at `/admin/icecast` to configure max concurrent audio sources
- Supports 0 for unlimited sources, or positive integer for specific limit
- Updates `/etc/icecast2/icecast.xml` `<sources>` limit automatically
- Default behavior: If not set (null), Icecast uses its default of 2 sources
- File: `app_core/models.py`, `webapp/admin/icecast.py`, `templates/admin/icecast.html`
- **RBDS and Stereo Path Verification** - Comprehensive verification tools and documentation
- Added `tools/analyze_rbds_stereo_code.py` - Static code analyzer for RBDS/stereo paths
- Added `tools/trace_rbds_stereo_path.py` - Runtime tracer for signal flow (requires numpy)
- ...53 more
- **CRITICAL: SDR Audio Source Startup Failure** - Fixed `ModuleNotFoundError: No module named 'app_core.radio.rbds'`
- Root cause: `FMDemodulator._init_rbds_state()` was trying to import `RBDSDecoder` from non-existent `.rbds` module
- `RBDSDecoder` class is defined in the same file (`app_core/radio/demodulation.py` line 1662)
- Removed incorrect import statement on line 297
- SDR audio sources now start correctly without module import errors
- Fixes "Audio source is error" message preventing audio monitoring
- File: `app_core/radio/demodulation.py`
- **CRITICAL: Hardware Module Import Errors Fixed** - Fixed `ImportError` crashes in VFD and LED modules
- **VFD**: Removed `VFD_PORT` and `VFD_BAUDRATE` from `app_core/vfd.py` `__all__` exports (not defined as module-level constants)
- **VFD Routes**: Updated `webapp/routes_vfd.py` to use `get_vfd_settings()` from `app_core.hardware_settings` instead of importing constants
- ...227 more
- **EAS Monitor Architecture** - Major architectural improvement: resample BEFORE queueing
- Audio now resampled from source rate (48kHz) to 16kHz BEFORE entering EAS queue
- EAS monitor receives pre-resampled 16kHz audio directly (no conversion needed)
- Eliminates resampling bottleneck that caused packet drops
- Reduces queue memory usage by 3x (16kHz vs 48kHz samples)
- 10000 chunk queue provides ~14 minutes of buffering (same duration at all rates due to resampling)
- At 48kHz: 10000 chunks × 4096 samples = 40.96M samples / 48kHz = 853 seconds
- At 16kHz: 10000 chunks × 1365 samples = 13.65M samples / 16kHz = 853 seconds
- Removed ResamplingBroadcastAdapter dependency - no longer needed
- Each audio source now has two queues: native rate for streaming, 16kHz for EAS
- ...63 more
- **Poller Settings Admin Page** - New database-based poller configuration interface
- Created `/admin/poller` page for managing alert poller settings
- Added `enabled` and `poll_interval_sec` fields to `PollerSettings` model
- Poller now reads configuration from database instead of environment variables
- Dynamic interval updates without service restart (checked each poll cycle)
- Poller can be enabled/disabled via admin UI
- Links to existing `/logs?type=polling&limit=100` for viewing polling logs
- Added navigation link in Settings dropdown menu
- Database migration: `20251218_add_poller_settings.py`
- Replaces `POLL_INTERVAL_SEC` environment variable with database setting
- ...67 more
- **Update Script Password Prompts** - Fixed update.sh asking for eas-station user password
- Added `root ALL=(eas-station) NOPASSWD: ALL` to sudoers configuration
- Allows root to run commands as eas-station user without password prompt
- Update.sh now installs/updates sudoers file early in update process
- Fixed pre-existing sudoers syntax errors (escaped colons in chown commands)
- Addresses: "The update script is asking for eas-stations password"
- **Install/Update Scripts Webroot Directory Ownership** - Fixed webroot directory permissions in install.sh and update.sh
- Changed ownership from www-data:www-data to root:root in both scripts
- Ensures certbot (runs as root) can write challenge files during initial setup
- Previously would fail on first webroot certificate attempt after fresh install
- ...248 more
- **Admin Page Refactoring Phase 2 Complete** - Completed modularization of admin.html JavaScript
- Moved final inline function `sanitizeBoundaryTypeInput` to core.js module
- Removed outdated comments about remaining inline functions
- admin.html reduced from original 7,461 lines to 2,043 lines (73% reduction, exceeding 30% target)
- All JavaScript now modularized into 9 separate files (132KB total) for better maintainability
- Improved browser caching with external modules
- Cleaner separation of concerns between template variables and business logic
- Version bump to 2.38.0 marks completion of Phase 2 refactoring
- **Admin Page Refactoring - Phase 2 (Major Progress)** - Modular JavaScript extraction
- ✅ Moved 449 lines of inline CSS to `/static/css/admin.css`
- ...16 more
- **LED Sign IP Address Configuration** - Added IP address and port fields to admin Hardware tab
- Added `led_ip_address` and `led_port` input fields in admin.html Hardware Integrations tab
- Updated `/api/led/serial_config` endpoint to save IP address and port to both LEDSignStatus and HardwareSettings tables
- JavaScript now loads and saves LED IP/port configuration along with serial settings
- Eliminates confusion about where to configure serial-to-ethernet converter network settings
- Users can now configure all LED sign settings (IP, port, serial mode, baud rate) in one location
- **Admin Role Assignment Fix Script** - Added utility script to fix users without roles
- Created `scripts/fix_admin_roles.py` to assign admin role to users created before roles were initialized
- Script ensures roles/permissions are initialized and assigns admin role to any user without a role
- Run with: `python3 scripts/fix_admin_roles.py`
- ...10 more
- **Hardware Settings Permission Issue** - Fixed "permission denied" error accessing advanced hardware settings
- Changed `/admin/hardware` permission from `'admin'` (superuser only) to `'system.configure'` (regular admins)
- Updated navbar to show Hardware Settings link only to users with `system.configure` permission
- Separated hardware navigation: GPIO/Zigbee for `gpio.view`, Hardware Settings for `system.configure`
- Eliminated confusion caused by two hardware configuration locations
- **Zone Catalog Permission Errors** - Fixed 403 permission_denied on Zone Catalog page
- Changed all zone routes from non-existent `'admin.settings'` to `'system.configure'`
- Zone catalog now accessible to users with system.configure permission
- Fixed: Zone info endpoint, zone management page, zone search, zone upload, zone reload
- **Admin Users Created Without Roles** - Fixed critical issue where admin users show "No Role"
- ...4 more
- **Screen Renderer Connection Error Logging** - Reduced log spam from expected connection failures
- Changed screen_renderer.py to log connection errors at DEBUG level instead of ERROR
- Connection refused errors are expected when web service isn't running (hardware-only mode)
- Prevents log spam while still showing unexpected errors
- **Audio/Icecast Error Logging Fixes** - Resolved excessive error logging and JSON parsing issues
- Fixed JSON parsing error in websocket audio_monitoring_update caused by improper bytes decoding from Redis
- Added proper UTF-8 decoding for Redis hgetall() values (redis-py 7.x returns bytes)
- Added validation to skip empty strings before JSON parsing to prevent "Expecting value" errors
- Reduced Icecast connection error spam by suppressing repetitive "Connection refused" logs during backoff
- Improved audio underrun warning frequency with exponential backoff (10, 50, 100, 200, 500, etc.)
- Added better error handling for invalid heartbeat values in Redis metrics
- **Full Web UI for Certbot Operations** - Complete SSL certificate management through web interface
- Added `/api/certbot/obtain-certificate-execute` endpoint to directly obtain SSL certificates
- Added `/api/certbot/renew-certificate-execute` endpoint to directly renew certificates
- Added `/api/certbot/enable-auto-renewal` endpoint to manage systemd timer
- Users can now obtain, renew, and manage SSL certificates entirely through the web UI
- Supports standalone, nginx plugin, and webroot certificate acquisition methods
- Supports dry-run testing, normal renewal, and forced renewal
- Real-time feedback with certbot output displayed in the UI
- Enable/disable automatic renewal with one click
- Added SSL Certificates link to Settings dropdown in navigation menu
- ...19 more
- **Removed Duplicate Icecast Settings** - Consolidated all Icecast configuration to single location
- Removed entire Icecast settings section from `/settings/audio` page (lines 131-252 HTML)
- Removed all Icecast JavaScript functions from audio.html (300+ lines)
- **All Icecast settings now managed exclusively at `/admin/icecast`**
- Eliminates confusion from having same settings in multiple locations
- Cleaner UI with single source of truth for Icecast configuration
- Addresses new requirement to consolidate settings to one spot
- **Icecast Password Management Improvements** - Transformed password handling to read-only display with regenerate option
- Password fields now read-only to prevent user errors and mismatches with Icecast server
- Added password masking with show/hide toggle buttons for security
- Added copy-to-clipboard functionality for easy password access
- Added informational text explaining passwords are auto-generated during installation
- Added regenerate password functionality that updates database, .env file, AND Icecast server config
- New endpoint `/admin/api/icecast/regenerate-passwords` for secure password regeneration
- **CRITICAL: Now updates Icecast server configuration file** (`/etc/icecast2/icecast.xml`)
- Automatically restarts Icecast service after password regeneration
- Handles default passwords (changeme_admin) by updating server config
- ...97 more
- **Certbot/SSL Certificate Management Security Fix** - Removed sudo calls from web interface
- Removed all `sudo certbot` subprocess calls from web application for security compliance
- Web interface now provides copy-paste commands instead of executing privileged operations
- Added systemd timer status checking for automatic certificate renewal
- Updated UI to display certificate acquisition instructions with multiple methods (standalone, nginx, webroot)
- Added copy-to-clipboard functionality for certificate management commands
- Provides clear guidance on manual certificate operations via command line
- Fixes "no new privileges" flag error when attempting sudo from web app
- Maintains certificate status checking functionality (read-only operations)
- Addresses security concern of web application having elevated privileges
- ...340 more
- **Environment Variables Cleanup** - Removed redundant settings that are now managed via dedicated admin pages
- Removed 'gpio' category from environment variables (now managed via `/admin/hardware`)
- Removed 'icecast' category from environment variables (now managed via `/admin/icecast`)
- Removed duplicate 'notifications' category that contained SDR/audio settings
- GPIO, OLED, LED, VFD, and Icecast settings exclusively managed through database-backed admin UIs
- Cleaner environment configuration focused on core application settings
- VERSION bumped to 2.31.0 (feature enhancement)
- **Documentation Update** - Comprehensive review and updates across all documentation
- Updated all main architecture documents with current timestamps (2025-12-16)
- Updated INDEX.md statistics: 92 total files (was 47), 12 guides (was 6), 18 architecture docs (was 10), 19 troubleshooting guides (was 10)
- ...56 more
- Copy button label on logs page changed from "Copy Logs" to "Copy" for clarity
- CSV export button relabeled to "Excel" to match user terminology
- CSV export icon changed from `fa-file-csv` to `fa-file-excel`
- Update script (`update.sh`) now properly displays VERSION file contents instead of showing "unknown"
- Update script now prioritizes VERSION file over git commit hash for version display
- Updated POLLER_CONFIG_MIGRATION.md to clarify unified poller architecture
- Removed outdated references to separate `ipaws.env` and `noaa.env` files (no longer used in 2.20+)
- Added troubleshooting section for "IPAWS.env not found" error
- **Environment variable consolidation** - Reduced from 93 to 73 variables by consolidating related settings
- `MAIL_URL` replaces 5 mail variables (MAIL_SERVER, MAIL_PORT, MAIL_USERNAME, MAIL_PASSWORD, MAIL_USE_TLS)
- `LOCATION_CONFIG` (JSON) replaces 9 location variables (DEFAULT_TIMEZONE, DEFAULT_COUNTY_NAME, DEFAULT_STATE_CODE, DEFAULT_ZONE_CODES, DEFAULT_FIPS_CODES, DEFAULT_STORAGE_ZONE_CODES, DEFAULT_MAP_CENTER_LAT, DEFAULT_MAP_CENTER_LNG, DEFAULT_MAP_ZOOM)
- `ICECAST_CONFIG` (JSON) replaces 5 Icecast auth variables (ICECAST_SOURCE_PASSWORD, ICECAST_RELAY_PASSWORD, ICECAST_ADMIN_USER, ICECAST_ADMIN_PASSWORD, ICECAST_ADMIN)
- `ICECAST_INTERNAL_URL` and `ICECAST_PUBLIC_URL` replace 4 connection variables (ICECAST_SERVER, ICECAST_PORT, ICECAST_EXTERNAL_PORT, ICECAST_PUBLIC_HOSTNAME)
- `AZURE_OPENAI_CONFIG` (JSON) replaces 5 Azure OpenAI variables (AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_KEY, AZURE_OPENAI_MODEL, AZURE_OPENAI_VOICE, AZURE_OPENAI_SPEED)
- VERSION bumped to 2.21.0
- VERSION bumped to 2.20.2
- **Enhanced PyCharm integration documentation** - Added comprehensive field-by-field setup instructions in `docs/guides/PYCHARM_DEBUGGING.md`
- Added detailed tables for every configuration dialog with all fields explained
- Added example values for each field with explanations
- Added step-by-step Database Tools (DataGrip) configuration
- Added step-by-step Debug Configuration setup for AI coding assistants
- Added validation checklist for complete PyCharm setup
- Added "Quick Start" section with essential settings table for rapid configuration
- **Added comprehensive GitHub Copilot integration section** - Detailed comparison with zencoder.ai
- Added GitHub Copilot setup instructions for PyCharm and VS Code
- Added capability comparison table (Copilot vs zencoder.ai)
- ...12 more
- **Complete whiptail-based setup** - All installation inputs now use professional TUI interface
- **FIPS code checklist interface** - Select multiple counties at once instead of typing
- **Radio button menus** - EAS Originator code selection with predefined options (WXR, EAS, PEP, etc.)
- **Consistent branding** - Copyright and license info on all whiptail dialogs throughout install.sh
- **FIPS management in eas-config** - Configure FIPS codes post-installation with same checklist UI
- Branding footer function for consistent copyright/license display
- Improved Python environment detection for FIPS lookup during installation
- FIPS lookup now gracefully handles case where Python dependencies aren't installed yet
- Better validation for all user inputs with helpful error messages
- Consistent dialog widths and heights for better readability
- **FIPS lookup workflow** - Now shows full county list with checkboxes instead of search-based approach
- EAS Originator input changed from text entry to radio button selection for better validation
- All whiptail dialogs now include branding footer with copyright and license information
- Improved error messaging when Python environment isn't available during installation
- Enhanced user experience with clearer instructions and better dialog sizing
- Added "All Logs" tab as the default view with organized category sections
- Added collapsible accordion sections for each log category (System, Polling, Audio, GPIO, EAS Messages, etc.)
- Added category badges showing log count per category
- Logs now organized by category instead of mixed together chronologically
- Added automatic firewall configuration for Icecast port 8000 during installation
- Added firewall configuration summary in installation completion message
- Added instructions for optionally opening PostgreSQL port with security warnings
- Fixed logs page to display ALL logs instead of only categorized logs
- Fixed Bootstrap modal aria-hidden accessibility warnings when adding/editing audio sources
- Fixed potential focus trap issues preventing interaction with audio source modals
- Fixed missing firewall rules for Icecast streaming (port 8000)
- Default logs view changed from "System" to "All Logs" for better visibility
- "All Logs" view now uses accordion with category grouping for better organization
- Modal elements now properly blur focus before hiding to prevent accessibility issues
- Improved log readability by separating logs into logical categories
- Installation now automatically opens port 8000 for Icecast if enabled
- Firewall status display now shows configured ports
- **PostgreSQL password now displayed during installation** for easy IDE/pgAdmin access
- Added comprehensive database credentials section in installation completion message
- Shows full PostgreSQL connection details (host, port, database, username, password)
- Added instructions for viewing password later: `sudo grep POSTGRES_PASSWORD /opt/eas-station/.env`
- Enhanced installation progress messages with detailed package lists
- Added informative descriptions of what each installation step does
- More aesthetic progress indicators showing estimated time and package counts
- pgAdmin access instructions (if successfully installed) in completion message
- Separate database credentials section with security warnings
- Fixed pgAdmin 4 installation failures by adding better error handling and --allow-downgrades flag
- Fixed pgAdmin installation to gracefully skip if it fails, allowing installation to continue
- Added error detection and informative messages for pgAdmin installation issues
- Installation completion message now includes full database credentials for IDE access
- Made install.sh significantly more informative and user-friendly
- Enhanced progress messages to show what packages are being installed
- Improved visual hierarchy in completion message sections
- pgAdmin configuration skips gracefully if installation failed
- Database password warning emphasizes saving credentials (only shown once)
- Systemd service monitoring for all EAS Station services (web, sdr, audio, eas, hardware, noaa-poller, ipaws-poller)
- Dependency service monitoring (nginx, postgresql, redis-server, icecast2)
- Service status categorization (active, inactive, failed) with visual indicators
- Separate display sections for EAS Station services vs. system dependencies
- Removed Docker/container monitoring from system health in favor of systemd service monitoring
- System health now queries systemd services directly using systemctl for accurate bare metal deployment status
- Updated system health template to display systemd services instead of Docker containers
- Replaced _collect_container_statuses() with _collect_systemd_services() in system.py
- System health data structure now uses "systemd" key instead of "containers"
- Service monitoring now uses native systemctl commands instead of Docker API
- Health dashboard shows systemd service status with active/inactive/failed states
- Reduced excessive whitespace between navbar and page content by decreasing --layout-padding-top from 1.5rem to 0.5rem
- Fixed NOAA_USER_AGENT validation error by adding default value in environment.py configuration
- Fixed environment validation to check default values before reporting "required but not set" errors
- Updated setup wizard configuration persistence notice to remove Docker/container-specific references
- Changed setup wizard text to reflect bare metal deployment with /app-config/.env persistent volume
- Removed Docker-specific terminology from about.html (changed "containers" to "services")
- Removed Docker-specific terminology from admin.html (container references, --network=host flag)
- Updated admin panel text to be deployment-agnostic (removed "inside the app container" references)
- Removed hardcoded version number from NOAA_USER_AGENT default value to prevent version drift
- Changed "System Reinstall" to "Fresh Installation" in setup wizard for clarity
- Updated setup wizard to show accurate configuration persistence behavior for bare metal deployments
- Environment validation now respects default values defined in ENV_CATEGORIES when checking required fields
- About page now uses deployment-agnostic terminology for service architecture
- Admin panel now uses terminology appropriate for both Docker and bare metal deployments
- NOAA_USER_AGENT default value no longer includes version number (simplified to "EAS Station")
- Completely rewrote PyCharm/VS Code debugging guide for bare metal deployment
- Removed all Docker/container references, replaced with systemd service instructions
- Updated all file paths from /home/pi/eas-station to /opt/eas-station
- Added comprehensive section on debugging individual systemd services with debugpy
- Added detailed instructions for using AI coding agents (ZenCoder) with real-time code access
- Updated database configuration section for bare metal PostgreSQL (not containerized)
- Added multiple methods for enabling debugpy: temporary, persistent, and code modification
- Documented debug port assignments for all services (5678-5684)
- Added SSH port forwarding instructions for secure remote debugging
- Updated troubleshooting section with systemd-specific solutions
- ...4 more
- Added security warnings for exposing debugpy ports on all network interfaces
- Documented SSH port forwarding as secure alternative to opening firewall ports
- Improved PostgreSQL remote access documentation with security best practices
- Restricted sudoers examples to specific services and journalctl units only
- Clarified user permissions for AI agent integration with minimal necessary access
- Fixed pgAdmin4 installation to prevent apache2 from being installed as a dependency
- Added python3-typer package installation to resolve "ModuleNotFoundError: No module named 'typer'" in pgAdmin setup
- Added apt preferences to block apache2 packages during pgadmin4 installation
- Added automatic apache2 masking and removal if already installed
- Fixed remote access by configuring UFW firewall to allow ports 80 (HTTP) and 443 (HTTPS)
- Added firewall configuration section in install.sh with proper UFW setup
- Improved installation reliability with better dependency management
- Enhanced install.sh with improved visual design and user experience
- Added colorful banner, progress indicators, and step counters
- Enhanced completion message with detailed component access instructions
- Added comprehensive post-installation checklist with actionable items
- Improved readability with emoji icons, better spacing, and color-coded sections
- Added detailed connection instructions for all components (web UI, pgAdmin, PostgreSQL, Redis)
- Included useful commands for backup, restore, SSL setup, and troubleshooting
- Suppressed verbose output from package installations for cleaner display
- Fixed PostgreSQL authentication configuration in `install.sh` to allow password-based connections
- Added `pg_hba.conf` configuration to enable `scram-sha-256` authentication for `eas_station` user
- Updated `scripts/database/fix_database_permissions.sh` to also configure PostgreSQL authentication
- Resolves "password authentication failed for user eas_station" errors during installation
- Updated architecture documentation to reflect bare-metal systemd deployment
- Replaced "container" terminology with "service" or "process" in architecture docs
- Replaced "Docker" references with "systemd service" or "bare-metal" as appropriate
- Updated mermaid chart labels in `SYSTEM_ARCHITECTURE.md` from container to service
- Updated `HARDWARE_ISOLATION.md` with systemd service terminology and journalctl commands
- Updated `DATA_FLOW_SEQUENCES.md` to reflect systemd service architecture
- Aligned all documentation with ISO_BUILD_READY.md bare-metal migration status
- Added `samples/README.md` documenting EAS test audio files and their purpose
- Added comprehensive `legacy/README.md` explaining Docker-era scripts and their bare-metal replacements
- Added `tests/bug_reproductions/README.md` explaining one-off test files
- Updated `install.sh` to exclude development directories: `bugs/`, `legacy/`, `bare-metal/`, `tests/bug_reproductions/`
- Updated `.gitignore` to exclude `bugs/` and `tests/bug_reproductions/` from version control
- Cleaned samples directory to ~6.2MB (only EAS audio test files remain)
- Added Redis server health check to `/health/dependencies` endpoint
- Created new bare-metal version of `scripts/collect_sdr_diagnostics.sh` using systemd and native tools
- Updated `webapp/routes_monitoring.py` to check Redis instead of Docker daemon
- Updated comment in `webapp/routes_settings_radio.py` to remove Docker architecture reference
- SDR diagnostics now use systemd service status and journalctl for logs instead of Docker commands
- Added **Frontend-First Philosophy** to AI agent guidelines: All system management must be web-accessible
- Added **CLI-Free Operations** requirement: Users should never need SSH or command-line access
- Documented existing web UI features for logs, configuration, services, and troubleshooting
- Updated `docs/installation/INSTALLATION_DETAILS.md` to remove Docker references
- Updated `docs/troubleshooting/AUDIO_SQUEAL_FIX.md` to note it's for legacy Docker deployments
- Updated `scripts/README.md` to remove references to deleted SQL files
- Updated `webapp/routes_ipaws.py` to use systemd commands for service restarts instead of Docker
- Updated `webapp/routes_monitoring.py` to remove docker-compose.yml from configuration checks
- Updated troubleshooting guides to use systemd commands exclusively
- Updated architecture documentation to reflect bare-metal deployment
- Simplified migration guides to focus on bare-metal setup
- Maintenance API uses standard filesystem paths instead of container paths
- Complete installation guide available in `bare-metal/README.md`
- Quick start guide available in `bare-metal/QUICKSTART.md`
- Updated README.md to focus on bare metal deployment via systemd services
- Configuration now uses `/opt/eas-station/.env` as standard location
- Services managed via systemd: `sudo systemctl [start|stop|restart] eas-station.target`
- **EAS Monitor Display Issues**: Fixed decoding rates showing >100% and display bouncing between states
- Root cause 1: Rate calculation `samples_per_second` was sensitive to timing variations and could spike >100%
- Root cause 2: During startup (first 2 seconds), rate calculation reported 0, triggering "no audio" warnings
- Root cause 3: Frontend hysteresis (2 consecutive readings) wasn't enough to prevent flicker at 100ms WebSocket rate
- Fix 1: Added exponential moving average (EMA) smoothing with alpha=0.3 to filter timing noise
- Fix 2: Implemented 2-second minimum sample threshold - report expected rate during warmup instead of 0
- Fix 3: Health percentage grows linearly 0-95% during warmup for smooth visual feedback
- Fix 4: Increased frontend hysteresis from 2 to 5 consecutive readings (500ms stability required)
- Fix 5: Properly clamp health_percentage to [0, 1] range in all code paths
- Result: Rates never exceed 100%, smooth warmup transition, no state bouncing
- **Code Quality**: Extracted magic numbers to named class constants for easier configuration
- `WARMUP_DURATION_SECONDS = 2` - Duration of warmup period
- `WARMUP_MAX_HEALTH_PERCENTAGE = 0.95` - Maximum health shown during warmup
- `RATE_SMOOTHING_ALPHA = 0.3` - EMA smoothing factor (lower=smoother, higher=more responsive)
- `AUDIO_FLOWING_STABILITY_THRESHOLD = 5` - Frontend consecutive readings before state change
- Improves maintainability and makes performance tuning easier
- **CRITICAL: WebSocket Support Broken**: Fixed Flask-SocketIO async_mode mismatch that prevented WebSockets from working
- Root cause: `app.py` used `async_mode='threading'` but gunicorn uses `--worker-class gevent`
- This mismatch caused WebSockets to FAIL SILENTLY and fall back to long-polling
- Fix: Changed `async_mode='threading'` to `async_mode='gevent'` to match gunicorn worker class
- Impact: Enables real-time WebSocket updates at 10Hz (100ms) instead of 1-2 second polling intervals
- This fixes why the entire site was polling despite WebSocket infrastructure being present
- **UI White Space**: Fixed excessive white space at top of pages caused by `flex: 1` on `.page-shell`
- Root cause: Flexbox layout with `flex: 1` caused content to expand and fill all vertical space
- Fix: Removed `flex: 1` from `.page-shell` - footer's `margin-top: auto` handles sticky footer
- Result: Pages now start content immediately after navbar without huge gaps
- ...4 more
- **WebSocket Infrastructure**: Audio monitoring page already uses WebSockets when available
- VU meters, EAS monitor, and broadcast stats all receive real-time updates via WebSocket
- System automatically falls back to polling only if WebSocket connection fails
- With this fix, WebSockets should now work properly and polling fallback won't be needed
- This fixes the root cause of why 10+ previous agent sessions couldn't solve the white space issue
- The white space issue was subtle - `flex: 1` is a common flexbox pattern but caused unwanted expansion
- The WebSocket issue explains why 32 setInterval() polling calls exist throughout the codebase
- Future work: Extend WebSocket push service to broadcast all data types (alerts, system health, etc.) to eliminate remaining polling
- **Application Startup Failure**: Fixed unterminated triple-quoted string literal in `webapp/admin/audio_ingest.py` at line 2237
- Root cause: Docstring for legacy `generate_wav_stream()` function was never closed
- This prevented database migrations from running and caused gunicorn workers to crash on startup
- Fix: Properly closed the docstring and commented out the legacy code inside the function
- **EAS Monitor Runtime Display**: Fixed runtime timer showing "0s" and buffer bar not filling on Audio Monitoring page
- Root cause: API endpoint was not passing `wall_clock_runtime_seconds` from the audio-service metrics
- Fix: Added `wall_clock_runtime_seconds` to the API response in `routes_eas_monitor_status.py`
- **Audio Detail Page Error**: Fixed "Unable to load audio detail at this time" error when viewing IPAWS-generated alerts
- Root cause: Template used `url_for('alert_detail', ...)` but the route is on the `api` blueprint
- Fix: Changed to `url_for('api.alert_detail', ...)` in `audio_detail.html`
- **Layout Spacing**: Reduced global `--layout-padding-top` from `1.5rem` to `0.5rem` to minimize gap between navbar and content
- **Audit Logs UI**: Fixed stat-card styling conflict where global vibrant gradient styles were overriding the audit logs page local styles
- Added more specific CSS selectors (`.stats-row .stat-card`) to ensure local styles take precedence
- Used `!important` flags to override global pseudo-elements that added shimmer/glow effects
- ...1 more
- **Code Quality**: Fixed bare `except:` clauses in multiple files for PEP 8 compliance:
- `scripts/run_radio_manager.py`: Added proper exception logging during cleanup
- `debug_airspy.py`: Changed bare `except:` to `except Exception:` with comments
- **Defensive Coding**: Added None checks for `fetchone()` calls in migration and utility scripts:
- `scripts/apply_source_type_migration.py`: Safe handling when column check returns no result
- `app_core/migrations/versions/20251105_add_rbac_and_mfa.py`: Safe handling when INSERT RETURNING fails
- `app_core/migrations/versions/20251116_populate_oled_example_screens.py`: Safe handling for screen insert
- **Architecture Review**: Reviewed all 17 bugs from ARCHITECTURE_REVIEW_BUGS.md - most critical bugs (1-14) were already fixed in codebase
- Remaining bugs are low-priority design issues or already addressed
- **Dashboard Layout**: Removed duplicate `page-shell` class from dashboard container that caused large gap at top of page
- Root cause: `page-shell` was applied to both `<main>` in base.html and inner container in index.html
- This resulted in double top padding (from both elements)
- Fix: Removed redundant `page-shell` class from inner `<div class="container-fluid">` in index.html
- **BREAKING: Service Renaming - Clean Architecture**
- Renamed `audio_service.py` → `eas_monitoring_service.py` (reflects actual purpose)
- Renamed `sdr_service.py` → `sdr_hardware_service.py` (clarifies exclusive hardware access)
- **Why**: Old names were confusing and led to architectural mistakes
- **No backward compatibility wrappers** - clean break for clarity
- `eas_monitoring_service.py`: New name for EAS monitoring + audio processing service
- `sdr_hardware_service.py`: New name for SDR hardware access service
- `RENAME_SERVICES.md`: Updated to reflect completed rename
- Old files (`audio_service.py`, `sdr_service.py`) removed completely
- **CRITICAL: Complete SDR Hardware Separation**: Removed ALL SDR hardware access from audio-service.py
- **Root Cause**: Both audio-service and sdr-service were fighting for USB access to SDR hardware
- Removed `initialize_radio_receivers()` functionality from audio-service (kept stub for backward compat)
- Removed RadioManager initialization and all `_radio_manager` references
- Removed process_commands() SDR hardware operations (restart, get_spectrum, discover_devices)
- Removed collect_metrics() radio_manager stats collection
- Removed spectrum publishing loop with direct IQ sample access
- **Result**: audio-service.py now ONLY subscribes to Redis channels from sdr-service
- **Impact**: SDR hardware access is now exclusive to sdr-service.py container
- **Why SDR Never Worked**: Both containers tried to open same USB devices → conflict
- ...2 more
- **Code Quality: Removed Bare Except Statements**: Fixed 4 bare `except:` statements that could mask errors
- `app_core/audio/eas_monitor.py`: Database rollback and SAME header parsing now log errors
- `app_core/audio/streaming_same_decoder.py`: Message validation errors now logged at debug level
- `app_core/audio/worker_coordinator_redis.py`: Redis connection close errors now logged
- All exceptions now specify expected types (IndexError, AttributeError, Exception)
- Improves debugging by making error paths visible in logs
- Follows Python best practices for exception handling
- **Impact**: Better error visibility and easier troubleshooting
- **CRITICAL: Multi-Stream EAS Monitoring (LP1, LP2, SP1)**: Implemented per-source EAS monitoring
- **Root Cause**: EAS monitor only listened to ONE audio source at a time (highest priority)
- AudioIngestController.broadcast_pump selected only the highest priority running source
- Main broadcast queue received audio from only ONE source, others were ignored
- Result: LP1, LP2, SP1 web streams ran successfully but only ONE was monitored for EAS
- **Fix**: Changed from single EAS monitor to per-source monitors (one for each stream)
- Each audio source now has its own dedicated EAS monitor instance
- All sources monitored simultaneously for SAME/EAS alerts
- Alerts include source name in metadata for proper attribution
- **Why IPAWS worked**: IPAWS uses internet polling (cap_poller.py), not audio monitoring
- ...5 more
- **Diagnostic Tools**: Created comprehensive audio chain diagnostic utilities
- `diagnose_audio_chain.py` - Full audio chain health check from SDR to EAS monitor
- `fix_audio_source_sync.py` - Manual audio source sync tool with dry-run support
- Both tools check receivers, audio sources, Redis connectivity, and IQ sample flow
- **CRITICAL: Audio Chain for SDR Sources (LP1, LP2, SP1)**: Fixed missing audio pipeline for SDR-based EAS monitoring
- Added automatic audio source synchronization on audio-service startup
- Previously, audio sources for radio receivers weren't created automatically, breaking the audio chain
- In separated architecture, sdr-service publishes IQ samples to Redis, but audio-service needs AudioSourceConfigDB entries
- Without these entries, RedisSDRSourceAdapter instances weren't created, preventing audio from reaching EAS monitor
- New `sync_radio_receiver_audio_sources()` function ensures audio sources exist for all enabled receivers
- Sets critical `managed_by='radio'` flag to trigger Redis adapter creation
- Enhanced logging shows receiver details, subscription channels, and startup status
- Affects LP1, LP2, SP1 and any other SDR receivers with audio_output=True
- **Impact**: Fixes complete loss of EAS monitoring from local/state primary SDR sources
- **Template Consistency**: Fixed deprecated block usage in zigbee.html template, resolving CI failures
- Changed `templates/settings/zigbee.html` from deprecated `{% block extra_js %}` to standard `{% block scripts %}`
- Ensures all templates consistently use the `scripts` block for page-specific JavaScript
- Fixes template consistency check CI workflow that was failing
- Network error displays now show hint and technical details
- Password input now includes real-time validation
- Confirmation dialogs provide more context for destructive actions
- Netmask dropdown now shows common use cases
- DNS server input includes popular server recommendations
- **CRITICAL WiFi BUG**: Fixed WiFi scanning returning no networks even when networks available
- Added nmcli availability check - prevents silent failures when NetworkManager not installed
- Added WiFi interface auto-detection - finds wlan0/wlp* interfaces dynamically instead of hardcoded assumptions
- Fixed network status endpoint - now returns correct data structure with wifi.ssid that frontend expects
- Fixed WiFi scan race condition - replaced arbitrary 2-second sleep with proper completion detection
- Fixed disconnect functionality - backend now auto-detects active connection name instead of requiring frontend to send it
- Fixed empty scan results handling - now properly detects and reports when no networks found vs. scan failure
- Enhanced error handling and logging throughout WiFi operations
- Frontend now properly parses backend network status response structure
- Frontend disconnect sends empty body (backend auto-detects connection)
- ...6 more
- **CRITICAL SEPARATION MISMATCH**: Fixed audio-service startup failing to load SDR sources from database
- audio-service was trying to create SDRSourceAdapter for `source_type='sdr'` but had no radio manager (separated architecture)
- Added detection: if source is radio-managed (`managed_by='radio'`), create RedisSDRSourceAdapter instead
- Now audio-service properly loads SDR sources on startup and subscribes to IQ samples from sdr-service
- ✅ Audio sources persist across audio-service restarts
- ✅ No more "SDR source not available - radio manager missing" errors
- ✅ Separated architecture fully functional at startup
- **CRITICAL AUDIO BUG**: Fixed source_type mismatch preventing audio from playing and Icecast mounts from appearing
- `ensure_sdr_audio_monitor_source` was sending `source_type: 'sdr'` but audio-service expected `'redis_sdr'` for separated architecture
- Result: RedisSDRSourceAdapter was never created, no audio demodulation happened, no Icecast mount appeared
- Changed to `source_type: 'redis_sdr'` so audio-service properly creates Redis IQ subscriber and Icecast output
- ✅ Audio now plays from SDR receivers
- ✅ Icecast mounts now appear (e.g., /receiver.mp3)
- ✅ Complete end-to-end audio pipeline working
- **CRITICAL END-TO-END**: Complete signal chain from detection to audio now works
- **Device Discovery**: Added `discover_devices` command handler in sdr-service
- **Receiver Creation**: Added `reload_receivers` command to sync database changes to sdr-service
- **Auto-Start**: New/updated receivers now automatically loaded by sdr-service
- Webapp now properly communicates with sdr-service for device discovery and receiver management
- _sync_radio_manager_state now tells sdr-service to reload configuration
- Fallback to app-side radio manager if sdr-service unavailable
- Better error handling and logging throughout signal chain
- Device enumeration works in separated architecture
- **CRITICAL AIRSPY BUG**: AirspyReceiver class was completely empty with NO Airspy-specific configuration
- **Airspy Never Worked**: Device would never get warm because no samples were being processed correctly
- Implemented proper `_open_handle()` override with Airspy R2 sample rate validation (2.5 MHz or 10 MHz only)
- Configured linearity gain mode for optimal strong signal handling (FM/NOAA)
- Added Bias-T safety (disabled by default to prevent equipment damage)
- Airspy R2 TCXO provides accurate frequency - no PPM correction needed
- Comprehensive Airspy R2 configuration logging
- Sample rate validation with clear error messages
- Better exception handling for Airspy-specific settings
- **MAJOR FEATURE**: PPM (Parts Per Million) frequency correction support for compensating crystal oscillator drift in SDRs
- Added `frequency_correction_ppm` field to RadioReceiver model and database schema
- Hardware frequency readback verification with mismatch warnings
- Comprehensive frequency tuning diagnostics and logging
- **Frequency Accuracy**: RTL-SDR and other low-cost SDRs now properly compensate for clock drift (typically ±50 PPM)
- **Tuning Verification**: Actual tuned frequency is now logged and verified against requested frequency
- **Diagnostic Logging**: Frequency settings, PPM correction, and readback values now logged for troubleshooting
- Frequency accuracy can now be calibrated using PPM correction (e.g., calibrate with GSM cell tower or known station)
- Mismatch warnings help identify hardware tuning issues (> 1 kHz error triggers warning)
- Better separation: PPM correction in `ReceiverConfig` dataclass, not just database
- **CRITICAL Demodulation Bug**: Added missing `process()` method to FMDemodulator and AMDemodulator classes that was being called by RedisSDRSourceAdapter but didn't exist, causing audio demodulation to fail completely
- Fixed method signature mismatch where redis_sdr_adapter.py called `demodulator.process()` but only `demodulate()` existed, preventing any audio from being generated from IQ samples
- **SDR Core**: Implemented missing `get_ring_buffer_stats()` method in `_SoapySDRReceiver` that was being called by sdr_service.py but didn't exist, causing silent failures in buffer health monitoring
- **SDR Core**: Integrated SDRRingBuffer initialization in receiver startup to enable proper USB jitter absorption and backpressure handling
- **SDR Core**: Ring buffer now properly instantiated when device opens, providing robust sample buffering for reliable 24/7 SDR operation
- **SDR Core**: Capture loop now writes samples to ring buffer for overflow detection and backpressure monitoring
- **SDR Core**: Ring buffer properly shut down when receiver stops, preventing resource leaks
- Enhanced ring buffer statistics reporting with fallback to simple buffer stats when SDRRingBuffer unavailable
- Added comprehensive buffer health metrics (overflow/underflow counts, fill percentage, total samples) to Redis
- Improved separation between app.py and SDR service - all SDR operations completely independent of Flask application
- Ring buffer overflow detection now logs dropped samples when processing can't keep up with USB data rate
- **CRITICAL**: Fixed audio sources not starting when clicking start button - source name mismatch between webapp and audio-service (webapp sends "WIMT", audio-service expected "redis-WIMT")
- Fixed race condition in metrics publishing where audio-service was deleting eas_monitor metrics from Redis causing "No metrics available from audio-service" error
- Audio-service now uses original source names (not prefixed with "redis-") for separated architecture compatibility
- Fixed audio-service container running Flask app.py during migrations by skipping database migrations in standalone service containers (audio-service, sdr-service, eas-service, hardware-service) that should not load the main Flask application
- Fixed AirspyReceiver method override bug where `_open_device()` was defined but parent class uses `_open_handle()`, preventing Airspy-specific configuration (sample rate validation, linearity mode, bias-T settings) from ever executing
- Added `get_ring_buffer_stats()` method to SDR receivers to fix method-not-found errors when SDR service attempts to publish ring buffer statistics to Redis
- Made SDR++ Server the default and recommended SDR option in the Radio Receiver settings UI
- Added prominent "SDR++ Server" quick-add button in the Quick Setup panel
- SDR++ Server now appears as the first option in the device selection dropdown
- Updated documentation (SDR Setup Guide) with comprehensive SDR++ Server setup instructions
- Added SDR++ Server to the hardware comparison table and configuration examples
- Reordered SDR presets to prioritize SDR++ Server (network SDR) over direct USB connections
- Updated capture workflow description to mention SDR++ Server as the recommended approach
- Renamed "Discover Devices" button to "Discover USB Devices" for clarity
- Let OLED alert scrolls run across the full padded buffer before wrapping so alert text cleanly exits and re-enters the screen instead of freezing or overlaying fragments.
- Restored OLED alert scrolling by advancing the seamless scroll window based on elapsed frame time and speed settings so high-priority messages animate smoothly instead of freezing on a single frame.
- Added IPv6 connectivity troubleshooting documentation (`docs/troubleshooting/FIX_IPV6_CONNECTIVITY.md`) so operators can diagnose SSL Labs IPv6 test failures and nginx upstream connection errors.
- Redirected the policy docs URLs to the canonical `/terms` and `/privacy` routes and updated the documentation index to point to those pages so users no longer see divergent copies of the legal notices.
- Redirect permission-denied responses to the dashboard blueprint's admin route so settings pages (including `/settings/alert-feeds`) return a proper 403 flow instead of a 500 BuildError when the non-namespaced endpoint is unavailable.
- Downsampled the continuous EAS monitor to 8 kHz (with automatic resampling from higher-rate sources) so SAME FSK decoding runs at an efficient rate without wasting CPU on unnecessary bandwidth.
- Surfaced both the source and decoder sample rates in the monitor status API so operators can verify the tap is resampling correctly instead of assuming 22.05 kHz.
- Matched the streaming decoder sample rate to the active ingest source so SAME correlation and preamble detection run at the correct frequency instead of drifting off-sync when sources run at 44.1 kHz.
- Exposed the ingest-driven sample rate in the broadcast adapter stats returned with the EAS monitor status so operators can confirm the tap is aligned with the source.
- Added broadcast subscription health (queue depth, underruns, last audio time) to the continuous monitor API so the dashboard shows when audio is actually flowing and operators can see the tap is healthy instead of guessing through empty fields.
- Throttled repetitive buffer underrun warnings from the monitor's broadcast adapter while still counting them for visibility, preventing log spam when sources are temporarily quiet.
- Exposed broadcast queue stats and the currently active source in `/api/audio/metrics` so VU meters can distinguish "no signal" from transport failures and display accurate runtime state.
- Filled the continuous monitor status API with the streaming decoder's health, rate, and sync metrics so every dashboard field renders and operators can confirm the monitor is actively processing audio.
- Tagged live audio metrics with each source's runtime status so the VU meters reflect whether inputs are running instead of dimming as if they were offline.
- Added a selectable streaming mode on the audio monitor that prefers the built-in HTTPS stream by default and only opts into Icecast when operators explicitly choose it, reducing stalls when external ports are blocked.
- Filter placeholder artwork metadata values (e.g., `null`, `undefined`, root-only paths) in the audio monitor so browsers stop
- Corrected the default Icecast external port variable so Icecast URLs use the configured `ICECAST_EXTERNAL_PORT` rather than
- Hardened the SDR audio monitoring stack by adding an auto-healing ingest controller that restarts stalled/error sources,
- Added a differential RBDS symbol slicer so FM demodulation correctly reconstructs PI/PS/RadioText metadata and keeps the latest
- Hardened the SoapySDR receiver implementation by mapping stream error codes (including SOAPY_SDR_NOT_LOCKED) to descriptive
- Disabled the CAP poller's optional SDR capture orchestration by default so its RadioManager hooks stay idle unless the poller
- Forced OLED templates with manually positioned lines to default to no-wrapping in the renderer so preview cards and physical
- Updated the OLED layout migration to use uniquely named bind parameters so Alembic can compile the update statement without colliding with column names, preventing the `bindparam() name 'name' is reserved` failure during upgrades.
- Added an automatic SoapySDR fallback that retries opening receivers without the serial filter when the initial connection fails, letting Airspy radios initialize even if the driver rejects the serialized arguments.
- Updated the OLED layout migration to JSON-serialize `template_data` before persisting it to PostgreSQL so upgrades no longer crash with `can't adapt type 'dict'` errors.
- Rebuilt the EAS Station wordmark as an inline SVG partial that inherits theme colors for its accent bars and lettering, so the logo automatically matches whichever palette operators choose without filters or manual assets.
- Updated the navigation bar and hero sections on the Help, About, Privacy, Terms, and Version pages to consume the new partial, eliminating duplicate markup and keeping the refreshed layout consistent in every mode.
- Introduced two new UI themes, **Midnight** and **Tide**, complete with theme-switcher entries and CSS variable palettes so operators can choose between a deep slate dark mode and a crisp coastal light mode.
- Published NOAA, FEMA IPAWS, and ARRL resource badges plus a curated "Trusted Field Resources" section on the Help page so the most requested links are visual, organized, and no longer broken.
- Modernized the Help & Operations Guide layout with hero quick links, an operations flow mini-timeline, refreshed typography, and a reorganized assistance section for a more professional flow.
- Added dedicated Help-page utility styles that sharpen quick-link tiles, timeline steps, and resource cards, ensuring the guide matches the rest of the dashboard polish.
- Added a refresh-status meta block on the dashboard map card that now shows the last update time, refresh source, and a live
- Replaced the fixed interval timer with a scheduler that pauses during manual refreshes, resumes after success or failure, and
- Updated the dashboard refresh action so manual, automatic, keyboard, and debug triggers all share the same code path,
- Default location snapshots now seed `area_terms` with an empty list rather than mirroring the removed environment variable,
- Removed the CAP poller's area-term fallback so alerts only appear on `/alerts` when their SAME or UGC codes match the
- Fixed duplicate DOM element declarations on the Weekly Test Automation page that threw JavaScript errors and prevented saved
- Ensured the RWT scheduler always opens a Flask application context before touching the
- Added an offline alert self-test harness plus `scripts/run_alert_self_test.py` so operators can replay bundled RWT captures,
- Folded the alert self-test harness into the **Tools → Alert Verification** dashboard so operators can replay bundled or custom
- Consolidated the alert self-test workflow into the Alert Verification dashboard so operators validate decoding, analytics,
- Added comprehensive `utilities.css` with gradient, card, badge, spacing, layout, typography, shadow, border, visibility, and animation utilities
- Created reusable template component partials in `templates/components/` for metric cards, stat cards, page headers, status badges, and data lists
- Built new professional version page (`/help/version`) with tabbed interface featuring Overview, Changelog, Features, System Info, and JSON API tabs
- Added `changelog_parser.py` utility to parse CHANGELOG.md files and extract structured version history
- Integrated git commit information display (hash, branch, date, message) on version page
- Added visual timeline visualization for changelog with animated current version marker
- Added comprehensive feature matrix showing all installed system components and their availability status
- Added copy-to-clipboard functionality for JSON API output
- Fixed inconsistent gradient implementations across templates by centralizing in utilities.css
- Fixed missing CSS files (design-system.css, components.css) not being loaded in base template
- Improved dark theme compatibility for version page components
- Updated `base.html` template to include all CSS files in proper order: design-system, base, components, utilities, layout, and enhancements
- Replaced basic version page with comprehensive tabbed interface showing full release history from parsed CHANGELOG.md
- Enhanced version route in `routes_monitoring.py` to include git metadata and parsed changelog data
- Standardized gradient usage across all templates with new utility classes (.gradient-primary, .gradient-success, etc.)
- Improved version page accessibility with URL hash-based tab navigation
- Clarified the commercial license offer notes pricing covers software only and excludes any hardware costs.
- Extended `/api/system_status` and `/api/system_health` with hostname, primary IPv4, uptime, and primary-interface metadata
- Surfaced the Weekly Test Automation console with a county management side panel, Broadcast navigation entry, and in-product callouts so operators can edit RWT schedules and default SAME codes entirely from the UI.
- Added a curated OLED showcase rotation (system overview, alerts, network beacon, IPAWS poll watch, audio health, and audio
- Enforced Argon Industria OLED reservations by blocking BCM pins 2, 3, 4, and 14 (physical header block 1-8) from GPIO configuration, greying them out in the GPIO Pin Map, and surfacing guidance in setup, environment, and hardware docs.
- Provisioned default OLED status screens with system, alert, and audio telemetry plus on-device button shortcuts (short press to advance rotation, long press for a live snapshot).
- Added Argon Industria SSD1306 OLED module support with full configuration tooling and display workflows
- Introduced `app_core/oled.py` with luma.oled-based controller, new `OLED_*` environment variables, and runtime initialization hooks
- Extended screen renderer, manager, and `/api/screens` endpoints with an `oled` display type alongside LED and VFD rotations
- Updated admin Environment editor, setup wizard, and hardware reference docs for OLED installation and configuration guidance
- ...32 more
- Removed caching from `/api/audio/metrics` and set explicit no-store headers so VU meters and live audio telemetry refresh in
- Hardened backup API endpoints by validating backup names to block path traversal before
- Removed the CAP poller's area-term fallback so `/alerts` only surfaces entries that explicitly name the configured SAME or
- Ensured the continuous EAS monitor auto-initializes on demand so the audio monitoring page no longer stalls when the monitor
- Added comprehensive audio ingest pipeline for unified capture from SDR, ALSA, and file sources
- Implemented `app_core/audio/ingest.py` with pluggable source adapters and PCM normalization
- Added peak/RMS metering and silence detection with PostgreSQL storage
- Built web UI at `/settings/audio-sources` for source management with real-time metering
- Exposed configuration for capture priority and failover in environment variables
- Documented the Weekly Test Automation county list regression addressed in 2.11.4 so QA can trace the scheduler fix through the
- ...86 more
- Refined the theming system with higher-contrast logo treatments and added Aurora, Nebula, and Sunset presets to expand the built-in palette while keeping the wordmark legible across gradients.
- Renamed the "EAS Workflow" console to **Broadcast Builder** and linked the Weekly Test Automation page throughout the Broadcast menu and workflow hero banner so automation tooling is obvious to operators.
- **Consolidated stream support in Audio Sources system** - Removed stream support from RadioReceiver model and UI, centralizing all HTTP/M3U stream configuration through the Audio Sources page where StreamSourceAdapter already provided full functionality
- Removed `source_type` and `stream_url` fields from RadioReceiver database model
- RadioReceiver now exclusively handles SDR hardware (RTL-SDR, Airspy)
- Added Stream (HTTP/M3U) option to Audio Sources UI dropdown
- Added stream configuration fields (URL, format) to Audio Sources modal
- Updated navigation to point to `/settings/audio` instead of deprecated `/audio/sources` route
- Clear separation of concerns: Radio = RF hardware, Audio = all audio ingestion sources
- Enhanced AGENTS.md with bug screenshot workflow, documentation update requirements, and semantic versioning conventions
- ...3 more
- OLED alert rotations now preempt normal playlists when `skip_on_alert` is enabled, prioritizing the most severe alert and
- `/api/alerts` now returns each alert's source and (when available) the cached EAS narration text, allowing custom OLED/LED
- Prevented the `20251113_add_serial_mode_to_led_sign_status` Alembic migration from
- Added an offline pyttsx3 text-to-speech provider so narration can be generated without
- Authored dedicated `docs/reference/ABOUT.md` and `docs/guides/HELP.md` documentation describing the system mission, software stack, and operational playbooks, with cross-links from the README for quick discovery.
- Exposed in-app About and Help pages so operators can read the mission overview and operations guide directly from the dashboard navigation.
- Documented open-source dependency attributions in the docs and surfaced
- Inserted the mandatory display-position byte in LED sign mode fields so M-Protocol
- Surface offline pyttsx3 narration failures in the Manual Broadcast Builder with
- Detect missing libespeak dependencies when pyttsx3 fails and surface
- Detect missing ffmpeg dependencies and empty audio output from pyttsx3 so the
- Surface actionable pyttsx3 dependency hints when audio decoding fails so
- ...32 more
- Documented why the platform remains on Python 3.12 instead of the new Python 3.13 release across the README and About surfaces,
- Documented Debian 14 (Trixie) 64-bit as the validated Raspberry Pi host OS while clarifying that the container image continues to ship on Debian Bookworm via the `python:3.12-slim-bookworm` base.
- Documented the release governance workflow across the README, ABOUT page, Terms of Use, master roadmap, and site footer so version numbering, changelog discipline, and regression verification remain mandatory for every contribution.
- Suppressed automatic EAS generation for Special Weather Statements and Dense Fog Advisories to align with standard activation practices.
- Clarified in the README and dependency notes that PostgreSQL with PostGIS must run in a dedicated container separate from the application services.
- Clarified the update instructions to explicitly pull the Experimental branch when refreshing deployments.
- Documented the expectation that deployments supply their own PostgreSQL/PostGIS host and simplified Compose instructions to run only the application services.
- Reworked the EAS Output tab with an interactive Manual Broadcast Builder and refreshed the README/HELP documentation to cover the browser-based workflow.
- Enhanced the Manual Broadcast Builder with a hierarchical state→county SAME picker, a deduplicated PSSCCC list manager, a live `ZCZC-ORG-EEE-PSSCCC+TTTT-JJJHHMM-LLLLLLLL-` preview with field-by-field guidance, and refreshed docs that align with commercial encoder terminology.
- Added a one-touch **Quick Weekly Test** preset to the Manual Broadcast Builder so operators can load the configured SAME counties, test status, and sample script before generating audio.
- ...1 more
- Allow first-time deployments to create the initial administrator from a dedicated
- Restore SDR audio monitor adapters on-demand for all audio ingest APIs, eliminating the recurring 503 responses and broken
- Backfill SDR squelch columns automatically when legacy deployments haven't run the
- Added an audio-monitor provisioning API and UI workflow that auto-starts SDR Icecast streams, surfaces RBDS programme data, and exposes squelch/carrier telemetry directly from the radio settings page for immediate listening checks.
- Enabled configurable squelch thresholds, timing, and carrier-loss alarms for SDR receivers with service-specific defaults tuned for Raspberry Pi deployments, reducing false positives while keeping CPU usage low.
- Removed the `APP_BUILD_VERSION` environment override so persistent `.env` files can no longer pin stale release numbers; the UI now always reflects the repository `VERSION` manifest.
- Ensured the version resolver invalidates its cache when `APP_BUILD_VERSION` or the `VERSION` file changes so dashboards display
- Disabled caching on the built-in documentation viewer routes to prevent browsers and reverse proxies from serving outdated
- Added automatic cache-busting query parameters to all Flask-served static asset URLs so envoy/nginx layers fetch freshly deployed bundles instead of stale copies (Screenshot_7-11-2025_75931_easstation.com.jpeg).
- Corrected the documentation viewer's Mermaid block detection to support Windows-style line endings so diagrams render instead of showing raw code.
- Refreshed system version metadata on each request so the footer and monitoring endpoints display the latest release after version bumps.
- **Resolved production nginx image regressions** - Ensured HTTPS container bundles required tooling and static assets
- Copied repository `static/` directory into the image to stop 404 errors for CSS, JS, and image assets
- Updated nginx configuration to use the modern `http2 on;` directive and silence deprecation warnings during startup
- Hardened admin location validation so statewide SAME/FIPS codes are always accepted and labelled consistently when saving.
- Fixed admin location settings so statewide SAME/FIPS codes remain saved when operators select entire states.
- Reformatted SAME plain-language summaries to omit appended FIPS and state code
- Display the per-location FIPS identifiers and state codes on the Audio Archive
- Backfilled missing plain-language SAME header summaries when loading existing
- Linked the admin location reference summary and API responses to the bundled
- Added an admin location reference API and dashboard card that surfaces the saved
- Prevented the public forecast zone catalog synchronizer from inserting duplicate
- Documented Raspberry Pi 5 (4 GB RAM) as the reference platform across the README, policy documents, and in-app help/about pages while noting continued Raspberry Pi 4 compatibility.
- The web server now falls back to a guarded setup mode when critical
- Added one-click backup and upgrade controls to the Admin System Operations panel, wrapping the existing CLI helpers in background tasks with status reporting.
- Delivered a WYSIWYG LED message designer with content-editable line cards, live colour/effect previews,
- Refactored the LED controller to accept structured line payloads, allowing nested colours, display modes,
- Enhanced the LED send API to normalise structured payloads, summarise mixed-format messages for history
- Inserted the mandatory display-position byte in LED sign mode fields so M-Protocol
- Updated ignore rules and documentation so generated EAS artifacts and runtime logs remain outside
- Aligned build metadata across environment defaults, the diagnostics endpoints, and the
- Refreshed the README to highlight core features, deployment steps, and configuration
- Added database-backed administrator authentication with PBKDF2 hashed passwords,
- Expanded the admin console with a user management tab, dedicated login page, and APIs
- Introduced `.env.example` alongside README instructions covering environment setup and
- Implemented the EAS broadcaster pipeline that generates SAME headers, synthesizes WAV
- Published `/admin/eas_messages` for browsing generated transmissions and downloading
- Switched administrator password handling to Werkzeug's PBKDF2 helpers while migrating
- Extended the database seed script to provision `admin_users`, `eas_messages`, and
- Persisted configurable location settings with admin APIs and UI controls for managing
- Delivered a manual NOAA alert import workflow with backend validation, a reusable CLI
- Enabled editing and deletion of stored alerts from the admin console, including audit
- Broadened boundary metadata with new hydrography groupings and preset labels for water
- Hardened manual import queries to enforce supported NOAA parameters and improved error
- Established the NOAA CAP alert monitoring stack with Flask, PostGIS persistence,
- Delivered the interactive Bootstrap-powered dashboard with alert history, statistics,
- Integrated optional LED sign controls with configurable presets, message scheduling,
- Recorded the originating feed for each CAP alert and poll cycle, exposing the source in the
- Normalised IPAWS XML payloads with explicit source tagging and circle-to-polygon conversion
- Automatically migrate existing databases to include `cap_alerts.source` and
- Surfaced poll provenance in the statistics dashboard, including the observed feed sources
- Documented the public forecast zone catalog synchronisation workflow and
- Normalized every database URL builder to require `POSTGRES_PASSWORD`, apply safe
- Trimmed duplicate database connection variables from the default `.env` file and
- Bumped the default `APP_BUILD_VERSION` to 2.3.0 across the application and sample
- Switch certbot issuance to standalone HTTP-01 mode so the container itself binds to port 80 during startup,
- Log the standalone challenge server activation so operators can confirm ACME connectivity when debugging
- Verify existing certificates against the system trust store and expiration before skipping issuance, so stale self-signed chains are purged and a new ACME request runs on startup.
- Log detailed reasons when certificate validation fails and remove the associated material, making it obvious when fallback artifacts block public issuance.
- Detect existing certificates issued by anything other than Let's Encrypt (including legacy self-signed chains)
- Extend the certificate cleanup routine to treat unknown issuers as invalid, guaranteeing that deployments replace
- Remove any lingering self-signed certificate directories (including suffixed variants) on
- Extend the certificate purge routine to clean historical self-signed material before certbot
- Purge the domain's existing `/etc/letsencrypt` material whenever a self-signed
- Force certbot to request a fresh certificate for self-signed domains by
- Detect legacy self-signed fallback certificates by inspecting the existing fullchain.pem and
- Remove invalid certificate files prior to issuing new ones so nginx never launches with the
- Detect previously generated self-signed certificates and automatically retry Let's Encrypt
- Tag self-signed fallbacks with a marker file and clear it after successful issuance to avoid
- Provision certbot in the nginx container via Python's package manager so Let's Encrypt
- Replaced bash-specific `[[ ... ]]` usage in the nginx initialization script with
Installed Features
Feature Status
This EAS Station ™ installation includes all core features and professional-grade components for 24/7 emergency alert monitoring and broadcasting. Visit the documentation for detailed feature guides.
System Information
Version Details
- System Name
- NOAA CAP Alerts System
- Version
- 2.75.0
- Author
- KR8MER Amateur Radio Emergency Communications
- Description
- Emergency alert system for Putnam County, OH
Git Information
- Commit Hash
9354a783- Branch
main- Commit Date
- 2026-05-21T21:29:16Z
- Message
- Update repository statistics [skip ci]
Time Information
- Timezone
- America/New_York
- Local Time
2026-05-22T16:19:12.306572-04:00- UTC Time
2026-05-22T20:19:12.306552+00:00
Features
- LED Signs
- Not Available
JSON API Response
This data is also available in JSON format at
/version for programmatic access.
{
"version": "2.75.0",
"name": "NOAA CAP Alerts System",
"author": "KR8MER Amateur Radio Emergency Communications",
"description": "Emergency alert system for Putnam County, OH",
"timezone": "America/New_York",
"led_available": false,
"vfd_available": false,
"oled_available": false,
"radio_available": true,
"python_version": "3.13.5",
"platform": "Linux-6.12.73+deb13-amd64-x86_64-with-glibc2.41",
"hostname": "ohc137",
"timestamp": "2026-05-22T20:19:12.306552+00:00",
"local_timestamp": "2026-05-22T16:19:12.306572-04:00"
}