v1.0.0 marks the first stable release. Major highlights include zero-hop neighbor discovery, a rebuilt snapshot-backed web-viewer dashboard, improved transport recovery, location and rain features, World Cup support, safer feed and outbound HTTP handling, command-prefix enhancements, and significant performance and security work on the web-viewer.
The configuration format, command syntax, service layout, and web-viewer API are now considered stable; breaking changes to these will only occur with a major version bump.
Added
- Zero-hop neighbor discovery in the packet capture service, ported from
meshcore-packet-capture(itself a port of the observer firmware’s neighbors feature). At a configurable interval (12–336 h, default 24), the bot discovers which repeaters it hears directly and logs confirmed links with measured SNR.[PacketCapture] neighbors_enabledtoggles the feature (off by default); each enabled broker publishes the snapshot once this is on (mqttN_neighborsdefaults true, so set false to exclude a broker). The neighbors topic is formed by swapping the last segment of each broker’s packets topic (templated asmeshcore/{IATA}/{PUBLIC_KEY}/neighbors, matching firmware). If noiatais set, location-routed topics are skipped with warning, preventing publishing tomeshcore/XYZ/.... Snapshots are not retained, sinceheard_secs_agois relative. - Confirmed direct links now form the strongest evidence class in the database: both public keys (32 bytes each) plus a first-party RF measurement, as compared to path inference, which has only prefixes and no keys. Links are stored in
neighbor_links(adjacency, migration 22) andneighbor_observations(per-cycle history, pruned byneighbor_observations_retention_days, default 365). - Mesh graph integration: a Neighbors Only evidence mode on the mesh page and in
GET /api/mesh/edges?evidence=neighbors, deriving edges solely fromneighbor_links. These edges carry complete public keys and real SNR, and render as heavier lines; confirmed neighbors are annotated and treated as provenance-trusted when initializing the map. Ifneighbors_feed_mesh_graph(default on) is enabled, these are also written tomesh_connections. - The
neighborsDM command lets admins run discovery cycles on demand (the scheduled interval has a 12h floor, making testing impractical otherwise). The command acknowledges immediately and follows with a report DM after the listen window. Useful to add to[Admin_ACL] admin_commands, as each invocation spends airtime. - Optional region-scope collection (
neighbors_collect_scopes, off by default): each request can lock the bot’s radio command for up to ~25s. For repeaters lacking a stored path, the library temporarily rewrites their contact path to reach zero-hop. The default cycle uses a single radio command plus a passive listen window, keeping the bot responsive. - Automatic reconnects for serial, BLE, and TCP transport, including plugin re-subscription after reconnect.
- Minute-level rain/precipitation nowcasts, with optional proactive notifications.
- World Cup command and live event announcement service.
- Opt-in sender-language detection for greeting replies, using keyword-first detection; a
langdetectextra is available for longer inputs. - Centralized location/geocoding helpers, now shared across weather, AQI, path, and related commands.
- New web-viewer plugin settings, node settings, multi-byte evidence views, and paginated contacts APIs.
- Database restore utilities and hardened migrations for config, state, logs, and local plugins.
- Flexible command prefixes: support for single, multiple, or decorative prefixes; permissive/strict matching modes; and optional support for bare commands.
- Per-channel flood scope configuration for granular message routing control.
- Optional packet-capture payload decoding:
GRP_TXTchannel messages are decrypted andADVERTs are parsed into a nesteddecodedobject. MQTT publishing is off by default and configurable per broker viamqttN_include_decoded. Packet-log rotation (off/size/time) is now configurable. {hops}and{hops_label}placeholders for path command replies, plus an RSSI placeholder for test command responses.- Full startup-time configuration validation, surfacing missed/invalid sections or keys that previously failed silently. The
validate_config.py --stricttool can pre-check configs. - DARC MOWAS alerts: German region IDs (Regionalschlüssel) are mapped to MeshCore scopes, ensuring each alert is targeted only to its relevant regions.
- NWS gridpoint as the US source for precipitation-nowcasts.
- LICENSE file (MIT), and matching
pyproject.tomllicense metadata, so packaged wheels and distributions now carry the license as reflected in the README.
Changed
-
Minimum
meshcoreversion is now 2.3.8 (was 2.3.6), required forsend_node_discover_req,req_regions_sync, and bounded/serialized BLE writes. -
The web-viewer dashboard is now built from a background snapshot, instead of recomputing stats 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 just that data (not ~50 aggregate queries five times per load). -
Daily trends now span full retention:
message_stats(and similar) are pruned at 7 days,packet_streamat 3, so formerly a 30-day chart had gaps. Signal metrics are now stored as sums and counts, never means, enabling correct windowed re-aggregation. -
Added
/api/dashboard/{summary,series,top,windows,refresh}endpoints. Thesummaryendpoint carries a strong ETag, so the dashboard’s 30s polling usually results in a bodyless 304, with polling disabled when the tab is hidden. -
New dashboard tiles and charts: routing mix (flood vs direct), hop-count and path-length histograms, 30-day multibyte adoption trends, busiest repeaters, and role breakdowns.
-
Introduced a one-hop neighbors panel (24h/7d selectors): shows nodes whose adverts reached this radio in a single hop, sorted by weakest measured link. Membership comes from path evidence, not
complete_contact_tracking.hop_count(which notoriously overestimates zero-hop contacts: live database example, 800 zero-hop contacts, but only 68 verified with a one-hop path; their SNR piles up in a tight band, and their RSSI clusters near –45 dBm—a classic sign of recording one strong local link against every node, not hundreds of unique radios). SNR is only shown where path evidence and stored hop count agree; otherwise, “no signal reading” is shown instead of reusing another link’s measurement. -
Payload-type breakdown is now displayed next to routing type breakdown, categories accumulate into "Other" rather than truncating lists, so bar totals remain accurate.
-
Hop-distance chart now combines two distributions: nodes by their closest advert path, and flood packets by their travelled hop count on arrival. Node count vs packet count: both normalized to their own totals, tooltips show raw counts. Live mesh: node counts peak at 2-3 hops, decay quickly; flood packets peak at 5 hops, longer tail—evidence of local neighborhoods picking up flood from afar.
Note: source tables use different units.
observed_paths.path_lengthis a byte count, so hops =path_length / bytes_per_hop(ex: 3-hop multibyte path is 6 or 9 bytes);packet_stream.path_lenis a hop count with a separate byte count. Applying the wrong rule to a source silently rescales axes; both are verified by tests. This replaces a raw path-length chart (which misread bytes as hops) and one using unreliable stored hop counts.Flood hop buckets with <0.1% of samples are not drawn—the long tail fades twenty hops out, where bars are <1 pixel tall. The withheld sample count is shown beneath (1,467 packets, 0.9%, live mesh: axis shrinks from 64 to 44 buckets). Percentages are against the full series, never just the rendered subset. The node series is never thresholded.
The chart still calculates the full protocol path range: a 64-byte path is 64 hops if one byte per hop. The old dashboard’s
BETWEEN 0 AND 32filter dropped 5,654 real flood packets (some from 63 hops) and could erase nodes with true minimum paths past 32 hops—this is now fixed. -
New
[Web_Viewer]settings added:dashboard_snapshot_enabled,dashboard_snapshot_interval_seconds,dashboard_snapshot_history_days, anddashboard_packet_backfill_rows. -
Partial index
idx_packet_stream_undimensionedused as the packet backfill worklist. Otherwise, searching for rows without dimensions required a full table scan (slow even after backfill, since finding none still read everything). -
Dashboard time-window selectors reflect each table’s retention: previously "30d" and "All" were shown against 7-day-retained data, so multiple choices returned identical counts.
-
The incoming-packet chart no longer claims 7-day retention:
packet_streamis pruned at 3 days, so the covered window is measured/labelled from actual data—and now can be compared to a true 7-day contacts chart. -
packet_streamnow has denormalized columns:route_type_name,payload_type_name,path_len,bytes_per_hop, written at capture time. Queries no longer depend on expensivejson_extract, and old rows are backfilled in batches, not as a single, table-rewriting migration. -
Dashboard JS and CSS are now static files, eliminating the main need for a CSP nonce in the page.
-
cleanup_old_statsnow also deletes rows from the future, which otherwise weren’t caught by normal retention filtering and were seen dated as far out as 2103. -
Multi-byte mesh graph path splitting/aggregation now runs inside SQLite, not Python, using lifetime edge coalescing and applying time windows afterward to preserve identity and semantics. Graph startup no longer sorts unnecessarily; a table-specific index (
mesh_connections(last_seen)) improves window/retention queries. -
Graph persistence for new installs defaults to batched writes: a flush means one batch/transaction instead of checking/upserting each edge individually, minimizing WAL churn and SD card writes, while immediate/hybrid options remain as alternatives.
-
Feed polling now supports bounded responses 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 to the current page/data.
-
Service install scripts now keep executable code root-owned, while config/runtime remains service-account-writable only.
-
The help command now obeys its own
channelsoverride, falling back tomonitor_channelsonly if none specified. -
Webhook service now starts before the radio connection and returns HTTP 503 until the bot is connected, reducing early connection errors.
-
Weather alerts now recognize NWS “no coverage here” responses and no longer treat them as errors.
Deprecated
- The
GET /api/statsendpoint. All key names remain, but responses now includeDeprecationandSunsetheaders; use/api/dashboard/*instead. Removal is scheduled for the next major release.
Removed
- The
/statspage, which was unreachable in navigation and displayed stub charts that never loaded data. - The dashboard’s Live Activity feed, which duplicated
/realtimeand required three SocketIO subscriptions and per-packet re-rendering. Dashboard polling now incurs only a single snapshot read per poll.
Fixed
- Data retention now runs after startup and then daily—no more waiting 24h of uninterrupted uptime for the initial cleanup. Its timer is independent of the nightly maintenance mail.
- Retention deletes now commit in tunable chunks and yield between batches, so a large first cleanup doesn't monopolize the SQLite writer lock (especially helpful on SD card installs).
- Linux service installers now set 1GB RAM and 200% CPU limits, which allows Raspberry Pi web-viewer/graph workloads enough headroom.
- Systemd restart limits now actually take effect:
StartLimitInterval/StartLimitBurstwere previously under[Service](ignored by systemd 230+), so the default of 5 restarts/10s always applied. WithRestartSec=10, the limiter couldn't trip and a failed bot could spin endlessly. Now set under[Unit]asStartLimitIntervalSec=60,StartLimitBurst=3—persistent failures now stop after three restart attempts. Fixed for both the shipped unit and the one built byscripts/build-deb.sh. - The mesh map now coalesces live edge events, serializes reloads, pauses refreshes in hidden-browser tabs, and caches concurrent multi-byte aggregation jobs, preventing the graph from overloading CPU/SQLite on large meshes.
- The web-viewer’s rotating-file logger now honors
[Logging] log_level, while the journal handler always retains an INFO floor to skip debug log duplication. - Closed outbound HTTP SSRF bypasses (including IPv4-mapped IPv6 and redirect/DNS rebinding corner cases).
- Hardened behavior for config rollback, scheduler operation claims, feed queue deduplication, and blocking provider calls.
- User-controlled web-viewer content is now escaped; Discord mentions are neutralized in output.
- Restored Python 3.10 compatibility and expanded CI coverage to Python 3.13.
NEW_CONTACTadverts are now classified as known or new, instead of always reporting as newly discovered.- The standalone installer now preserves custom commands/symlinks, and will roll back a failed executable sync instead of leaving the service in a half-updated state.
- Startup validation now reports unknown or misspelled keys, even in
*_Commandsections—with did-you-mean suggestions—instead of only checking section names and a hardcoded[Connection]pair. !aqi,!rain,!snow,!aurora,!prefix,!alert, and!gwxcommands no longer block the event loop during geocoding; location resolution is now run off-thread, as forecast fetching already was.!prefixwas previously the worst offender, reverse-geocoding per repeater with the event loop stalled.- The Nominatim rate limiter now reserves slots before request, so concurrent geocodes can’t slip through and breach the 1 req/s policy; caches are now locked against concurrent eviction.
- The web-viewer footer now matches the
!versioncommand on dev and detached tags; a detached checkout on a release tag now reports the tag (notHEAD-<sha>). [Feed_Manager]numeric limits are now clamped to valid minimums.max_items_per_checkbelow 1 no longer triggers negative Python slices;max_posts_per_checknow enforced before, not after, an item is sent; values below 1 are set to 1.feed_request_timeoutbelow 1 can’t disable HTTP timeouts now.max_message_lengthbelow 4 no longer expands the message it’s meant to cap.
Notes for downgrades
All schema changes are additive: two new tables, four new nullable columns, and new indexes; so data itself is compatible with older code. However, MigrationRunner halts startup if it sees a schema version it doesn't know. Downgrading below this release requires deleting the corresponding schema_version rows.
Contributors
Special thanks to @rlwilliamson-dev for the rain nowcast and NWS gridpoint data, and to @fmoessbauer for MOWAS region-scope mapping and code style improvements.