- Implemented caching for multi-byte mesh aggregation, allowing concurrent requests to share computation and reducing CPU load. - Updated the web viewer to coalesce live edge events and refresh the mesh graph every 30 seconds, improving responsiveness on busy networks. - Adjusted logging behavior in the web viewer to respect the configured log level, ensuring efficient logging without excessive duplicate entries. - Added configuration options for mesh graph caching duration, enhancing user control over performance settings.
26 KiB
Changelog
All notable changes to this project are documented here. The format loosely follows Keep a Changelog, and the project adheres to semantic versioning.
[Unreleased]
Added
-
Rebuilt web-viewer dashboard, served from a background snapshot instead of recomputing statistics on every request. A refresher thread in the viewer process writes
daily_rollup(one row per local date) anddashboard_snapshot(a single JSON row), so a page load reads one row rather than running ~50 aggregate queries five times over. -
Daily trends that outlive retention:
message_statsand friends are pruned at 7 days andpacket_streamat 3, so a 30-day chart had nothing to draw from until now. Signal metrics are stored as sums and counts, never means, so any window re-aggregates correctly. -
New
/api/dashboard/{summary,series,top,windows,refresh}endpoints.summarysends a strongETag, so the page's 30-second poll is normally a bodyless304, and polling stops entirely while the tab is hidden. -
New dashboard tiles and charts: routing mix (flood vs direct), hop-count and path-length histograms, a 30-day multibyte adoption trend, busiest repeaters, and a role mix.
-
One-hop neighbours panel, with a 24-hour / 7-day selector: the nodes whose advert reached this radio in a single hop, weakest measured link first. Membership is derived from observed path evidence rather than from
complete_contact_tracking.hop_count, which is not trustworthy — it claims 800 zero-hop contacts on the live database while only 68 have any one-hop path to corroborate it, their stored SNR piles up in a 1.5 dB band (655 of 800 between 11.25 and 12.75 dB), and their RSSI clusters near -45 dBm. That is the signature of one strong local link being recorded against every node whose traffic arrived through it, not of hundreds of separate radios. SNR is therefore shown only where the path evidence and the stored hop count agree; the rest read "no signal reading" rather than borrowing another link's measurement. -
Payload-type mix beside the routing mix, over the same packets — what the traffic is, next to how it is routed. Category lists roll their tail into "Other" rather than truncating, so the bars still sum to the total printed beside them.
-
Hop-distance chart carrying two distributions: nodes by their closest advert path, and arriving flood packets by how far they had already travelled. One counts nodes and the other packets, so both are drawn as a share of their own total with raw counts in the tooltip. On the live mesh nodes peak at 2-3 hops and fall away quickly while flood traffic peaks at 5 with a much longer tail — a nearby neighbourhood absorbing flood from well beyond it.
Note that the two source tables measure paths in different units:
observed_paths.path_lengthis a byte count, so hops arepath_length / bytes_per_hop(a 3-hop multibyte path is 6 or 9), whilepacket_stream.path_lenis already a hop count with its byte length kept separately aspath_byte_length. Applying either rule to the other table silently rescales an axis; both are pinned by tests. This replaces the earlier raw path-length chart, which read bytes as hops, and the chart built on the untrustworthy stored hop count.Hop buckets holding under 0.1% of the flood series are not drawn — the tail decays for around twenty hops in bars under a pixel tall — and the amount withheld is stated beneath the chart (1,467 packets, 0.9%, on the live mesh, taking the axis from 64 buckets to 44). Percentages remain shares of the full series rather than of the drawn subset, so hiding the tail cannot inflate the bars that remain. The node series is never thresholded.
The chart still computes the full protocol range: a 64-byte path is 64 hops at one byte per hop. The old dashboard's
BETWEEN 0 AND 32filters, carried forward at first, discarded 5,654 flood packets arriving from as far as 63 hops, and because that limit applies after the per-node minimum it would erase a node whose closest path was longer than 32 hops rather than plotting it at the far end. -
New
[Web_Viewer]settings:dashboard_snapshot_enabled,dashboard_snapshot_interval_seconds,dashboard_snapshot_history_days, anddashboard_packet_backfill_rows. -
Partial index
idx_packet_stream_undimensionedas the backfill worklist. Probing for remaining un-dimensioned rows was otherwise a full table scan, and it cost the same 4.6 seconds after the backfill finished as during it, because finding nothing still meant reading everything.
Changed
- Time-window selectors are now built from each source's configured retention. The dashboard previously offered "30d" and "All" against tables pruned at 7 days, so three of the four choices returned the same number under a label that claimed otherwise.
- The incoming-packet chart no longer claims to show 7 days.
packet_streamis pruned at 3 days, so the covered window is now measured and labelled from the data — and shown beside a genuinely 7-day contacts chart it can be compared to. packet_streamgained denormalizedroute_type_name,payload_type_name,path_len, andbytes_per_hopcolumns, written at capture time. Aggregating these viajson_extractcost 3–6 seconds per query on a large database. Existing rows are converted a bounded batch per refresh rather than in one table-rewriting migration.- Dashboard JavaScript and CSS moved to static files, removing the CSP nonce requirement for the bulk of the page's code.
cleanup_old_statsnow also deletes rows dated implausibly far in the future. Such rows are never older than the retention cutoff, so they were immortal — observed in the wild dated 2103.- Multi-byte mesh graph path splitting and aggregation now run in SQLite
instead of materializing every retained path in Python. Time windows are
applied after lifetime edge coalescing to preserve existing graph identity
and count semantics. Graph startup also skips an unnecessary sort, and a
table-specific
mesh_connections(last_seen)index supports window and retention queries. - Graph persistence defaults to batched writes for new installations. A flush uses one upsert batch and transaction instead of probing every edge before writing it, reducing WAL churn and SD-card writes while preserving immediate and hybrid strategies as explicit options.
Fixed
- Data retention now runs shortly after startup and then daily. It no longer requires 24 hours of uninterrupted uptime before the first cleanup, and its timer remains independent from the nightly maintenance email.
- Retention deletes now commit in configurable chunks and yield between batches, preventing a large first cleanup from monopolizing SQLite's writer lock on SD-card installations.
- Linux service installers now use 1GB memory and 200% CPU ceilings, providing Raspberry Pi graph and web-viewer workloads with practical headroom.
- The mesh map now coalesces live edge events, serializes graph reloads, pauses hidden-tab refreshes, and caches concurrent multi-byte aggregation requests, preventing an open graph page from creating a CPU and SQLite I/O request storm on busy meshes.
- The web-viewer rotating-file handler now honors
[Logging] log_level, while its journal handler retains an INFO floor to avoid duplicate debug writes.
Removed
- The orphaned
/statspage, which was unreachable from the navigation and rendered stub charts that never populated. - The dashboard's Live Activity feed. It opened three SocketIO subscriptions
and re-rendered on every packet to duplicate a page that already exists at
/realtime, so the dashboard now costs one snapshot read per poll and nothing else.
Deprecated
GET /api/stats. Every key name is preserved and the response now carriesDeprecationandSunsetheaders; use/api/dashboard/*instead. It will be removed at the next major version.
Notes for downgrades
All schema changes are additive — two new tables, four new nullable columns, and
new indexes — so the data itself is safe to read with older code. However,
MigrationRunner fails startup on encountering an applied migration version it
does not know about, so downgrading below this release requires deleting the
corresponding schema_version rows.
1.0.0 — 2026-07-28
v1.0.0 marks the first stable release. It adds transport recovery, location and rain improvements, World Cup support, safer feed and outbound HTTP handling, command-prefix enhancements, and substantial web-viewer performance and security work.
The configuration format, command syntax, service layout, and web-viewer API are now considered stable; breaking changes to them will come with a major version bump.
Added
- Automatic serial, BLE, and TCP transport reconnect handling, including service plugin re-subscription after reconnect.
- Minute-level rain and precipitation nowcasts with optional proactive notifications.
- World Cup command and live event announcement service.
- Opt-in sender-language detection for localized greeting replies, with
keyword-first detection and an optional
langdetectextra for longer text. - Centralized location resolution and geocoding helpers shared by weather, AQI, path, and related commands.
- Web-viewer plugin settings, node settings, multi-byte evidence views, and paginated contacts APIs.
- Database restore tooling and hardened service-layout migration for configuration, state, logs, and local plugins.
- Flexible command prefixes: single, multiple, or decorative prefixes, with permissive and strict matching modes and optional bare commands.
- Per-channel flood scope configuration for more granular message routing.
- Optional packet-capture payload decoding —
GRP_TXTchannel messages are decrypted andADVERTs parsed into a nesteddecodedobject. Publishing it to MQTT is off by default and set per broker viamqttN_include_decoded. Packet-log rotation (off/size/time) is configurable. {hops}and{hops_label}placeholders for path command replies, and an RSSI placeholder for test command responses.- Configuration is validated on every startup, surfacing misspelled sections and
keys that previously failed silently.
validate_config.py --strictchecks a config before upgrading. - DARC MOWAS alerts map German region IDs (Regionalschlüssel) to MeshCore scopes, limiting each alert to the regions it was issued for.
- NWS gridpoint data as the US precipitation-nowcast source.
- A tracked
LICENSEfile (MIT) and matchingpyproject.tomllicense metadata, so built wheels and packages carry the license the README has always declared.
Changed
- Feed polling now has bounded response and item limits, duplicate queue protection, per-feed serialization, and configurable post limits.
- Direct-message responses are split at MeshCore byte limits without breaking UTF-8.
- Mesh graph and contacts queries scope enrichment work to the requested page or visible data.
- Service installs keep executable code root-owned while configuration and runtime state remain writable only by the service account.
- The help command now respects its own
channelsoverride, falling back to the globalmonitor_channelsonly when no help command is loaded. - The webhook service starts before the radio connection and returns HTTP 503 until the bot is connected, narrowing the window for connection refusals.
- Weather alerts recognize NWS responses that mean "no coverage here" and stop reporting them as errors.
Fixed
- Closed outbound HTTP SSRF bypasses, including IPv4-mapped IPv6 and redirect/DNS rebinding cases.
- Hardened configuration reload rollback, scheduler operation claims, feed queue deduplication, and blocking weather-provider calls.
- Escaped user-controlled web-viewer content and neutralized Discord mentions.
- Restored Python 3.10 compatibility and expanded CI coverage through Python 3.13.
NEW_CONTACTadverts are classified as known or new instead of always being logged as newly discovered.- The standalone installer preserves custom alternative commands and symlinks, and rolls back a partial executable sync rather than restarting a half-updated tree.
- Startup validation now actually reports unknown and misspelled keys — including
in
*_Commandsections — with a "did you mean" suggestion, instead of only checking section names and a hardcoded[Connection]pair. !aqi,!rain,!snow,!aurora,!prefix,!alert, and!gwxno longer block the event loop while geocoding; location resolution runs off-thread like the forecast fetch already did.!prefixwas the worst case, reverse-geocoding once per matching repeater with the loop stalled throughout.- The Nominatim rate limiter reserves its slot before the request instead of recording it afterwards, so concurrent geocodes can no longer clear the gate together and breach the 1 req/s policy. The geocode caches are locked against concurrent eviction.
- The web viewer footer and the
!versioncommand agree on dev and detached-tag checkouts; a detached checkout on a release tag reports that tag rather thanHEAD-<sha>. [Feed_Manager]numeric limits are clamped to sane minimums.max_items_per_checkbelow 1 no longer takes Python's negative-slice meaning;max_posts_per_checkis enforced before an item is sent rather than after, while configured values below 1 are clamped to 1;feed_request_timeoutbelow 1 no longer disables the HTTP timeout outright; andmax_message_lengthbelow 4 no longer lengthens the message it is meant to cap.
Contributors
Thanks to @rlwilliamson-dev for the rain nowcast work and the NWS gridpoint source, and to @fmoessbauer for the MOWAS region-scope mapping and code-style fixes.
0.9.3 — 2026-05-30
Changed
- Bridged Discord messages set
allowed_mentionsto an empty list, so@everyone,@here, and role mentions arrive as plain text instead of pinging the channel.
Documentation
- Expanded the command reference for
cmd,version,weather, andpathwith usage examples and configuration options, and documented theRandomLineconfigurable triggers. - Marked the global
[Aliases]section deprecated in favor of per-commandaliases =keys, and clarified the[Rate_Limits]and[Webhook]sections. - Emphasized web-viewer security practices in the viewer documentation.
0.9.2 — 2026-05-17
Fixed
- Webhook channel lookup strips a leading
#, so posts match hashtag channels cached from the radio. - The webhook endpoint returns HTTP 500 when the mesh send fails, instead of reporting success.
Changed
- Packet capture applies log levels from its own verbose/debug settings rather than setting the global logger level, and logs a per-packet summary whose level follows those flags.
- Clarified how
outgoing_flood_scope_overrideandflood_scopesinteract, with more informative scope-resolution logging and RF-correlation eligibility checks. - Corrected the documentation URL in the systemd unit and the command User-Agent.
0.9.1 — 2026-05-16
The theme of this release is flood-scope control: which slice of the mesh a given outgoing message is flooded to.
Added
- Optional regional
TC_FLOODscope configuration across services (weather, earthquake, webhook).CommandManagerresolves the scope from the incoming message, the owning config section, or an explicit parameter. - Optional flood scope for scheduled channel messages via
channel:#scope:bodyin[Scheduled_Messages]. - Five-field cron expressions and preset aliases for
[Scheduled_Messages]. The legacyHHMMform is still parsed and warns. reply_prefixandminimum_path_bytessettings for the path command.[Test_Command] response_formatsupports piped path filters (pathbytes_min,prefix_if_nonempty) and takes priority over[Keywords].- Global and per-broker MQTT JWT settings:
jwt_ttl_secondsandjwt_renewal_interval. send_channel_messageaccepts an explicit timestamp, enabling bit-identical message replication and chronological display ordering.- DARC MOWAS retransmits bit-identical messages when a repeater ack is missing, so an emergency alert is not lost to a dropped ack.
Fixed
- Direct-message responses route by
sender_pubkeyrather thansender_id, preventing misrouting when several nodes share a display name. - Keyword and
RandomLinechannel replies now carry their configured flood scope. - Scheduled sends are staggered by a deterministic delay
(
scheduled_message_max_stagger_seconds, default 1.5) and skip the global user rate limit, so simultaneous jobs are no longer dropped. Per-channel andbot_txlimits still apply. - Mesh graph pending-update flushing no longer deadlocks.
- The feed manager checks lock status before acquiring it to prevent coroutine pileup, and the scheduler processes messages without blocking its main thread.
- Advert flag parsing uses bitwise operations so invalid flag values degrade to a warning instead of failing to parse.
- DARC MOWAS message chunks get ascending timestamps, giving receivers correct ordering and deduplication.
- Service names strip leading and trailing underscores, so
<foo>_Serviceresolves to<foo>rather than<foo>_.
Contributors
Thanks to @fmoessbauer for the MOWAS reliability work and the service-name fix (#182, #183).
0.9.0 — 2026-04-17
v0.9.0 is a large release that focuses on operational reliability, observability, and
deployment ergonomics. The headline additions are the authenticated real-time web
viewer, a full APScheduler rewrite, multi-arch Docker images, .deb packaging, a
migration-versioned aiosqlite DB, and numerous message-handling and radio-health
hardening fixes.
Highlights
- Real-time web viewer: auth, contact management, live packet/message/log/mesh streaming, admin config editor, maintenance tools, DB backup UI, API Explorer tab, and early-start initializing banner.
- Radio reliability: zombie-radio detection with health probe and banner alerts,
radio-offline fail state, send suppression during outages,
asyncio.wait_forguards onsend_advert/disconnect_radio/reboot_radio, radio debug mode toggle, packet-capture restart-storm prevention, auto-restart and reconnect logic. - Scheduler migration: scheduler slimmed and switched to APScheduler; maintenance moved to its own module; signal-driven graceful shutdown and config reload; backup scheduler fire-window fix (BUG-024).
- Database: aiosqlite
AsyncDBManager, versioned migrations indb_manager, safer ALTER-TABLE startup migrations forchannel_operationsandfeed_message_queue(BUG-002), improved connection lifecycle across modules (BUG-017). - Packaging:
.debbuild viascripts/build-deb.sh, multi-arch Docker images with SBOM + provenance,check-package-data.shdist verification, ncurses config TUI (scripts/config_tui.py), bot admin HTTP server +reload_config.sh. - Rate limiting & safety: per-channel rate limiting, per-user cooldown defaults tightened, thread-safe rate limiter with LRU SNR/RSSI caches, inbound webhook relay with bearer-token auth, SSRF hardening and log-injection sanitization, allow-local SMTP flag.
- Commands:
!schedule,!version,!pathgeographic scoring toggle, airplanes (full list, no truncation), weather (high/low display, Open-Meteo model selection, MQTT weather, location fallback, multi-day forecasts), fortune (BSD format), RandomLine, configurable command reference URL.
Added
- Authenticated web viewer with real-time streams (
packet_stream,command_stream,message_stream,log_stream,mesh_graph) — see93f73a1,a15827b,23f652f,4685ea7,da2e39c,ae52be4,9be5166,6246a81. - Web viewer admin config editor with password redaction and CSRF protection
(
3a9f710,8bea10c); live banner polling and early-start banner (23f652f). - API Explorer tab and actionable error messages in the viewer (
a15827b,75be386). - Zombie-radio detection, health probe, timeout guards, and alert system (
d0ae737,8b14c40); radio-offline fail state with send suppression and auto-restart (51ab5d3); radio debug logging mode with web UI toggle (9ce6970). - APScheduler-based scheduler, maintenance module, graceful shutdown via Unix
signals, and config-reload support (
aa2677b,07a2db4,904303f). .debpackaging, multi-arch Docker build pipeline with SBOM + provenance, ncurses config TUI (c7f2bdb,5b6f282,da1e68f).- Bot admin HTTP server +
reload_config.shCLI (773b80f). - Inbound webhook relay with bearer-token authentication (
d07cca6). - Per-channel rate limiting (
25eb7cc) and thread-safe rate limiter with LRU SNR and RSSI caches (ea0e25d). !versioncommand and web-viewer footer version string (issue #91,883b67d,fbf3995).!schedulecommand listing scheduled messages and advert interval (97e5c59).!pathgeographic scoring toggle (2a3a787) and multibyte path chart rendering (fbf3995,c6a7355).- Fortune command reading BSD fortune files (
13c10fd) and RandomLine command (a4d5f54);cmd_reference_urloption forCmd_Command(90fdd0c). - MQTT weather support, Open-Meteo model selection, location fallback, multi-day
forecasts, and high/low temperature display (
9d768a3,5f6eced,206753a,3735f26,d9ea209). - Airplanes command sends all aircraft without truncation (
7403c1e); keeps single-message output (46d3fab). - CI log-injection regression check (
ce4fa8e); lint gates for ruff, mypy, eslint, and shellcheck (e1cf2eb/a12797f).
Changed
- Upgraded
meshcoreto>= 2.3.6, which also supplies upstream fixes for:can't convert negative int to unsignedon flood contacts (issue #126) — the library now convertsout_path_len == -1to255before packing. Commitba52c3badds belt-and-braces defensive wire-field rebuilding in_ensure_contact_meshcore_path_encoding.KeyError('msg_hash')asyncio parser spam (issue #83) — the newmeshcore_parser.pyguards with'msg_hash' in l.
max_response_hopsdefault in shipped config templates lowered from 10 → 7 (issue #161).requires-pythonraised to>= 3.10(Python 3.9 dropped;meshcore >= 2.3.6requires 3.10+). Ruff target bumped topy310, CI matrix now covers 3.11, 3.12, and 3.13.- Web-viewer subscription handlers are silent; the navbar indicator reflects socket
state (
1ee84f2). - Scheduler now uses
add_done_callback(fire-and-forget) instead of blockingfuture.result(timeout=X)to avoid TimeoutError spam and loop stalls (BUG-015). - Command aliases moved from global
[Aliases]section to per-commandaliases =keys (14d3c0c). - Channel messages now reserve an extra 10-byte budget for regional flood scope
(
4ee2079). - Web-viewer password is emphasized but no longer strictly required (
8b6ccc9). - Configuration docs clarified for monitored channels,
max_response_hops, and public-channel guard (20c4ea4,4bf0929). - Discord bridge supports multiple webhooks per channel (
0cd23e8).
Fixed
- #126 (negative
out_path_len): fixed viameshcore >= 2.3.6dep bump plus defensive handling in_ensure_contact_meshcore_path_encoding. - #83 (
KeyError('msg_hash')asyncio spam): fixed viameshcore >= 2.3.6dep bump. - Web-viewer status-ack tests now assert the silent UX instead of the removed
emit('status', …)calls (tests/test_web_viewer.py). send_advert()guarded withasyncio.wait_for(timeout=30)to prevent event-loop lockup (22e1b2b,329905d).packetcapturerestart storm during radio reconnect (f09b214).- Scheduler
RuntimeErroron threadsafefuture.resulthandled (7b01242). - Web-viewer config-item retrieval no longer triggers interpolation errors
(
ad09e8b). - Path length calculation and hash mode in
MessageHandlercorrected (ba52c3b). - Mention handling, reply-match base function, and command-class inheritance fixes
(
8bea10c,9d4b142,56be1e7,277491f). - Path validation hardened (
6e8204c); scheduler duplicate run + mypy fallback types (8b68644). - Shutdown hardened — single stop, viewer cleanup, MQTT log teardown, scheduler
drain (
e058da4). - Discord-bridge channel-key normalization test alignment (
4178371,f971e97). - BUG-001 .. BUG-029 — see
BUGS.mdv0.9.0 section for the full list.
Security
- SSRF hardening in outbound HTTP (
54aeb28) with explicit CGN-network check invalidate_external_url(2a80f76). - Log-injection sanitization applied to user-supplied log lines (
54aeb28); CI regression check added (ce4fa8e). allow_local_smtpflag for opt-in local SMTP relay usage (54aeb28).- SMTP SSRF guard import restored in
scheduler.py(c543cac). - CSRF protection in the web viewer (
3a9f710).
Infrastructure
- Initial test suite, pytest timeouts, coverage threshold, and tracking files
(
9de9230,ba32acc,c95ddf6). - Test-coverage expansion for commands, web viewer, and infrastructure (
9be5166). - MQTT live-test framework and packet fixtures (
a667e3c). - Per-test timeout in
pytest.inito prevent CI hangs (d7cf0d5). - Makefile + virtual-environment bootstrap (
c2149bc).
Documentation
- README, config example,
docs/configuration.md, and BUGS.md updated throughout v0.9.0. - Discord integration, kg7qin integration notes (
f2936be,de6279c).