chore(release): prepare 1.1.0

Fold the Unreleased section into 1.1.0 and date it. The 1.1.0 heading was
written on 2026-09-13 but never tagged, so everything merged since belongs in
it; the section also carried two separate "### Added" headings from earlier
merges, now consolidated. All 79 entries preserved.

Extend the v1.0.0 -> v1.1.0 upgrade guide with the meshcore 2.3.14 requirement,
migration 24, and the configuration and behavior changes that landed after the
heading was first written.
This commit is contained in:
agessaman
2026-09-20 11:44:34 -07:00
parent cc6ca00b4e
commit 31e56b3766
4 changed files with 243 additions and 185 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ file is kept only for historical reference.
## Outstanding known issues
As of the **v1.1.0 release candidate**.
As of **v1.1.0**.
### High priority
+199 -181
View File
@@ -6,6 +6,8 @@ semantic versioning.
## [Unreleased]
## [1.1.0] - 2026-09-20
### Added
- Region-code monitoring and an optional automatic warning to senders whose
@@ -73,6 +75,7 @@ semantic versioning.
mention takes the place of the random human descriptor, keeping the translated
sentence intact, and is dropped when it would push the reply past the channel body
budget. DMs are unaffected.
- `generate_website.py` accepts `--link-css URL` and `--embed-css FILE` to layer
custom CSS on top of the built-in style chosen with `--style` (#288). Embedded
CSS is appended to the page's `<style>` block, keeping the page a single file;
@@ -85,6 +88,7 @@ semantic versioning.
card so a user can add the bot and DM it without waiting for an advert. Useful
for bots that do not advertise. Enabled by default; disable with
`[Contact_Command] enabled = false`.
- `generate_website.py` now lists the local commands installed under
`[Bot] local_dir_path` and omits commands disabled in the configuration file
(#287). It also reads the `<local_dir_path>/config.ini` overlay the way the
@@ -93,80 +97,6 @@ semantic versioning.
own helpers, so `[Jokes] joke_enabled = false` is honored rather than silently
ignored.
### Changed
- `meshcore` now requires 2.3.14 or newer. Before 2.3.13, `send_msg_with_retry`
reported ACKed DMs as failures: it subscribed to the ACK only after `send_msg`
returned, so an ACK queued right behind `MSG_SENT` was dispatched with no
listener, and each attempt accepted only its own ACK code, so a late ACK
answering an earlier attempt was ignored (meshcore_py#108). The bot logged
"no ACK received after retries" and skipped the delivery bookkeeping for
messages the recipient had in fact received.
### Fixed
- MQTT brokers on `waev.app` now default to a JWT lifetime they accept (#248).
waev.app refuses a token whose `exp` is more than an hour past its `iat`, so the
project-wide 24-hour default never authenticated there and the operator saw only a
bare auth failure. Those hosts now get a 3600s TTL with renewal at 3500s unless a
value was configured for them, per broker or globally, in which case the configured
value still wins. The choice is logged. Only `waev.app` and its subdomains match; no
other broker's defaults change.
- The `[Keywords]` newline instructions now say the escape is a *backslash* and that
`/n` is two literal characters (#277), and they no longer use `test` as the example
keyword. `test` and `t` are served by the built-in test command, not the plain
keyword path, so the mesh-info placeholders listed in that same block render empty
there. Both facts are now stated where the example lives, along with the
configparser continuation form, which needs no escape at all.
- The packet-capture docs no longer show a naive local timestamp in the published
packet and status examples (#276). The bot has published UTC with a `Z` suffix since
v1.0.0; only the examples still read as local time.
- `outgoing_flood_scope_override = none` is now read as global flood on the
send path, as it already was everywhere else. `send_channel_message` tested
the raw value against a fixed tuple, so the lowercase spelling became the
region `#none` and sent scoped.
- Striped and hovered table rows in the web viewer's dark mode no longer render
Bootstrap's light-theme text color on a dark background (about 1.3:1
contrast). The dark overrides set a background but not a color, so every
`.table-striped` page was affected.
- `[Joke_Command] joke_enabled` and `[DadJoke_Command] dadjoke_enabled` are now
listed in the shared legacy-alias table. Both commands accepted that spelling
at runtime through their own fallback, but the settings UI read only the
`[Jokes]` spelling, so a bot disabled the same-section way showed as enabled
on the plugin settings page.
- A region-scoped channel message now restores global flood even when
`set_flood_scope` raises. The restore only ran in the `finally` around the
send, so a set that raised left the device pinned to that region and every
later send — channel replies, DMs, scheduled sends — went out under it.
- A DM waiting for its ACK no longer holds the radio. Radio commands were
serialized per call, so a DM's retry loop kept every other command waiting
through all of its ACK timeouts (up to ~36 s with the default three attempts),
stalling channel replies, other DMs, and scheduled sends. The lock now covers
one frame and the radio's immediate reply, and ACK and remote-response waits
run outside it, so neighbor scope requests no longer stall replies either. A
region-scoped channel message now holds the radio from setting its flood scope
through restoring it, so no other send goes out under that scope.
- A radio connection is no longer accepted when every channel read times out or
returns no usable channel data (#266). Startup and reconnect now retry the
channel scan three times, keep an empty result out of the valid cache and
database, then fail the connection cleanly so the normal restart/reconnect
path can try again instead of running a bot that cannot route replies.
- Daily Weather Service forecasts now retry transient Open-Meteo failures at
5, 15, and 30 minutes after the original run (#264). HTTP 429, 500, 502, 503,
and 504 responses plus transport failures use one replaceable retry job,
while permanent HTTP errors stop immediately and a successful retry sends
the forecast only once.
- `help <command> <subcommand>` now resolves help for the base command while
preserving the full message for context-aware help text (#285). Exact
multi-word aliases such as `dad joke` and `ps aux` still take precedence
over the base-command fallback.
## [1.1.0] - 2026-09-13
### Added
- Local commands and local services (dropped into `local/commands` and
`local/service_plugins`) now appear in the web viewer's Plugins settings page
alongside the built-in ones, tagged with `source: "local"`. Settings edited
@@ -223,8 +153,195 @@ semantic versioning.
the bot's default; `wx_international` was reaching for the private
`_response_translator` ContextVar to do this.
- Shlink is now supported as a URL shortener alongside v.gd / is.gd, selected with
`short_url_website_service = shlink` under `[External_Data]`. It authenticates with
`short_url_website_api_key` in an `X-Api-Key` header and needs `short_url_website`
set to your own instance — there is no default, and the bot skips shortening rather
than sending the key to a host you did not configure.
- `shorten` and `if_nonempty` response-template filters. `shorten` runs a value
through the configured shortener and falls back to the original URL when shortening
fails, so a clause is never lost to a network error. `if_nonempty:L` replaces a
non-empty value with literal `L` and clears otherwise, which is how a whole clause
is hidden rather than labelled: `{packet_hash|if_nonempty:"https://…/{packet_hash}"|shorten}`
prints nothing at all when RF correlation fails, instead of a broken link. Both
filters also answer to their other spellings — `shorten_url` in a template,
`shorten_url` in a feed format, `if_notempty` — so a chain copied between a feed
format and a command `response_format` works unchanged either way.
- Response-template filter arguments may be double-quoted, and a quoted argument may
contain nested `{field}` placeholders: `{d|prefix_if_nonempty:"Dist {sender}: "}`.
The quote ends the argument, so further filters can follow it. An unquoted
`prefix_if_nonempty` argument still consumes the rest of the placeholder, which is
what lets its literal contain `|`, so that form must stay last in its chain.
- `mqttN_keepalive` (default 60) sets the MQTT PINGREQ interval per broker. It was
hardcoded at 60 before, which is long for websockets through a proxy that drops
idle connections.
- `hops_min:N` response-template filter, alongside `pathbytes_min:N`. It clears a
field unless the message actually travelled at least N hops, so
`{firstlast_distance|hops_min:1|prefix_if_nonempty: | F/L Dist: }` drops the whole
clause on a direct message. The distance placeholders render `N/A` when there is no
path, and `prefix_if_nonempty` treats that as a value and prints its label, so a
gate was needed; `pathbytes_min` was the only one available and it asks how the path
is *encoded*, which meant throwing away a measurable one-byte multi-hop distance to
suppress the direct case. `hops_min` asks about the route instead. An unknown hop
count clears the field rather than guessing.
- `{packet_hash}` placeholder for `[Keywords]` responses, the test command's
`response_format` and the path command's `reply_prefix`: the 16-char MeshCore
packet identity hash (uppercase hex) of the packet that carried the request, so a
reply can be tied back to a specific transmission when comparing paths. It comes
only from the routing info of an RF packet actually correlated to the message, and
renders empty otherwise, so a hash from an unrelated transmission is never shown.
- **Scheduled messages can be managed from the web viewer** (#174). A new Schedule page
lists every `[Scheduled_Messages]` entry with its next run time and offers add, edit
and delete. Changes are written to `config.ini` and applied by a queued config reload,
so no restart is needed. The schedule builder composes the cron key from plain-language
options and previews the next five runs; entries the bot cannot run are shown as
**Not scheduled** with the reason rather than hidden. It edits the same config section
the bot already uses, so there is no second source of truth.
- Documented installing with `pipx`, which sidesteps PEP 668 on Debian 12+, Ubuntu
23.04+, Fedora and Arch (#222), including where `config.ini`, the database and
`local/` live — everything resolves relative to the config file's directory, so an
absolute `--config` is what makes a pipx install deterministic.
- Migration 23: nullable `snr` / `rssi` columns on `observed_paths` for
zero-hop advert rows.
- `{cmd:<command> [args]}` placeholders in `[Scheduled_Messages]`: a scheduled message
can embed the reply of any bot command, so a recurring forecast is
`0 6,12,18 * * * = Public:{cmd:wx Seattle}` rather than a per-service schedule
setting. The command runs for its text only and transmits nothing itself
(`CommandManager.render_command_output`); unknown, disabled, admin-only, timing-out
and silent commands expand to nothing rather than airing raw placeholder text.
Bounded by the new `[Bot] scheduled_command_timeout_seconds` (default 30). Two
non-configurable airtime guards apply: a schedule using `{cmd:...}` must not fire
more often than every 15 minutes (rejected at startup, measured by the tightest gap
so `0,1 * * * *` counts as 60 seconds), and the command's own `cooldown_seconds` is
still enforced.
- `{path_distance}` is now available in the path command's `[Path_Command] reply_prefix`,
reporting total distance travelled (sender → hops → bot, e.g. `12.4km`) and rendering
empty when any node in the chain has no usable coordinates. The prefix now supports the
same pipe filters as the test command's `response_format`, so
`{path_distance|prefix_if_nonempty:📏 }` drops the label along with the value.
- `install-service.sh --install-extras` installs the optional profanity-filter and
geocoding packages without prompting, for unattended installs and upgrades. It
takes precedence over the in-place `--update-venv` path, so the two can be
combined.
- `[PacketCapture] observer_name` — an optional name reported as the `origin` of
MQTT packet and status payloads. It lets the observer/analyzer identity differ
from the MeshCore RF node, which is useful when one bot name is already taken
by the radio's advertised name. Unset (the default) keeps the previous
behavior: the connected device name, falling back to `[Bot] bot_name`.
- `local_translation_path` in `[Localization]` points at your own translation
catalog, merged over the distributed one key by key, so you can translate a
local command or override a single shipped string without editing a file that
an upgrade will replace. Defaults to the `translations/` directory inside
`[Bot] local_dir_path`, resolved to an absolute path so it does not depend on
the working directory.
### Changed
- `meshcore` now requires 2.3.14 or newer. Before 2.3.13, `send_msg_with_retry`
reported ACKed DMs as failures: it subscribed to the ACK only after `send_msg`
returned, so an ACK queued right behind `MSG_SENT` was dispatched with no
listener, and each attempt accepted only its own ACK code, so a late ACK
answering an earlier attempt was ignored (meshcore_py#108). The bot logged
"no ACK received after retries" and skipped the delivery bookkeeping for
messages the recipient had in fact received.
- `{elapsed}` in test/keyword replies renders as seconds once the delay is a
second or more (`1.5s` instead of `1500ms`), so a typical ack stays shorter
(#275). Sub-second times still print as milliseconds.
- Response templates are parsed by a character-by-character state machine rather
than by splitting on delimiters. Placeholders can now nest (`{"Dist: {d|hops_min:1}"}`)
and filter arguments can be quoted. Field values are substituted into the output
and never re-scanned, so a sender-supplied phrase still cannot inject a placeholder.
- Web viewer navigation is grouped: Radio, Scheduled Messages, Greeter, Feeds, Plugins
and Configuration now sit under a single **Settings** gear menu, leaving Dashboard,
Real-time, Contacts, Mesh Graph and Logs on the bar. The current page is highlighted,
including the gear when a settings page is open.
- Added notes on connecting to waev.app MQTT brokers to the `packet_capture.md` file.
### Fixed
- MQTT brokers on `waev.app` now default to a JWT lifetime they accept (#248).
waev.app refuses a token whose `exp` is more than an hour past its `iat`, so the
project-wide 24-hour default never authenticated there and the operator saw only a
bare auth failure. Those hosts now get a 3600s TTL with renewal at 3500s unless a
value was configured for them, per broker or globally, in which case the configured
value still wins. The choice is logged. Only `waev.app` and its subdomains match; no
other broker's defaults change.
- The `[Keywords]` newline instructions now say the escape is a *backslash* and that
`/n` is two literal characters (#277), and they no longer use `test` as the example
keyword. `test` and `t` are served by the built-in test command, not the plain
keyword path, so the mesh-info placeholders listed in that same block render empty
there. Both facts are now stated where the example lives, along with the
configparser continuation form, which needs no escape at all.
- The packet-capture docs no longer show a naive local timestamp in the published
packet and status examples (#276). The bot has published UTC with a `Z` suffix since
v1.0.0; only the examples still read as local time.
- `outgoing_flood_scope_override = none` is now read as global flood on the
send path, as it already was everywhere else. `send_channel_message` tested
the raw value against a fixed tuple, so the lowercase spelling became the
region `#none` and sent scoped.
- Striped and hovered table rows in the web viewer's dark mode no longer render
Bootstrap's light-theme text color on a dark background (about 1.3:1
contrast). The dark overrides set a background but not a color, so every
`.table-striped` page was affected.
- `[Joke_Command] joke_enabled` and `[DadJoke_Command] dadjoke_enabled` are now
listed in the shared legacy-alias table. Both commands accepted that spelling
at runtime through their own fallback, but the settings UI read only the
`[Jokes]` spelling, so a bot disabled the same-section way showed as enabled
on the plugin settings page.
- A region-scoped channel message now restores global flood even when
`set_flood_scope` raises. The restore only ran in the `finally` around the
send, so a set that raised left the device pinned to that region and every
later send — channel replies, DMs, scheduled sends — went out under it.
- A DM waiting for its ACK no longer holds the radio. Radio commands were
serialized per call, so a DM's retry loop kept every other command waiting
through all of its ACK timeouts (up to ~36 s with the default three attempts),
stalling channel replies, other DMs, and scheduled sends. The lock now covers
one frame and the radio's immediate reply, and ACK and remote-response waits
run outside it, so neighbor scope requests no longer stall replies either. A
region-scoped channel message now holds the radio from setting its flood scope
through restoring it, so no other send goes out under that scope.
- A radio connection is no longer accepted when every channel read times out or
returns no usable channel data (#266). Startup and reconnect now retry the
channel scan three times, keep an empty result out of the valid cache and
database, then fail the connection cleanly so the normal restart/reconnect
path can try again instead of running a bot that cannot route replies.
- Daily Weather Service forecasts now retry transient Open-Meteo failures at
5, 15, and 30 minutes after the original run (#264). HTTP 429, 500, 502, 503,
and 504 responses plus transport failures use one replaceable retry job,
while permanent HTTP errors stop immediately and a successful retry sends
the forecast only once.
- `help <command> <subcommand>` now resolves help for the base command while
preserving the full message for context-aware help text (#285). Exact
multi-word aliases such as `dad joke` and `ps aux` still take precedence
over the base-command fallback.
- Service installers no longer leave `venv/bin/pip` and other console scripts
pointing at the temporary build environment after an atomic virtualenv swap.
`--update-venv` also repairs already-broken shebangs before replacing the old
@@ -432,10 +549,12 @@ semantic versioning.
least one operator running under `tmux` with nothing to restart it. The confirmation
spells out that the bot stops completely and only returns if systemd or Docker
restarts it.
- Corrected the `[External_Data] repeater_prefix_api_url` comment, which claimed that
leaving it empty "disables prefix command functionality" (#70). Empty is the normal
setup: the prefix command answers from the bot's own database. The option only adds
an optional external dataset, and its JSON contract is now documented.
- `message_stats.path` no longer reports another packet's route (#80). When RF
correlation failed, `find_recent_rf_data` fell back to the most recent packet in the
cache, and the caller attributed that packet's route to the message — which is how a
@@ -445,24 +564,29 @@ semantic versioning.
left unresolved instead of being fabricated. This also stopped a wrong edge being
written to the mesh graph, and stopped the `path` command receiving another packet's
`routing_info`. SNR and RSSI still use the fallback as before.
- Dashboard **One-hop neighbours** now lists radios this node heard directly
(MeshCore hop count 0: empty RF path), not originators of 1-hop relayed
adverts. Empty-path adverts are stored in `observed_paths` with SNR/RSSI;
neighbor-discover cycles refresh SNR on those rows. A one-time backfill
copies recent zero-hop ADVERTs out of `packet_stream`.
- **Airplanes / ADS-B** no longer depends on the public airplanes.live API,
which now returns HTTP 403 for unregistered clients (#244). Default
endpoint is `https://api.adsb.lol/v2/`; existing `api_url` values pointing
at `api.airplanes.live` are remapped automatically. Local readsb URLs are
unchanged.
- `cmd` no longer lists commands that are disabled in `config.ini`. Commands
with no `[<Name>_Command]` section at all are still listed, as before.
- Stale-contact cleanup no longer retries forever (#176). When the device refuses to
remove a contact the contact stays in the list, so every sweep re-selected it and
logged the same failure again — hundreds of `Failed to remove stale contact` warnings
that only a restart cleared. A contact is now dropped from cleanup after
3 consecutive refusals, with one summary warning explaining that the list may stay
near its limit. A successful removal clears the count.
- Contacts whose device clock was never set are no longer treated as stale. MeshCore
seeds an unset clock with a hardcoded time — `1715770351` (15 May 2024) or
`1772323200` (1 Mar 2026) — so a never-synced node advertises that seed rather than
@@ -472,6 +596,7 @@ semantic versioning.
`722 days ago` entries in #176 were: the 15 May 2024 seed, not contacts last heard
in 2024. A raw `0` (decoding to 1970) is covered too, and genuine adverts near a
seed are unaffected.
- The same unset-clock check now guards every purge path, not just stale-contact
cleanup. `_get_repeaters_for_purging`, `_get_companions_for_purging` and
`purge_old_repeaters` all ranked an unset clock as maximum age, so an active node
@@ -483,113 +608,6 @@ semantic versioning.
The reply sent over the mesh is unchanged — it still carries only the exception
text, with no filesystem path and no extra airtime.
### Changed
- `{elapsed}` in test/keyword replies renders as seconds once the delay is a
second or more (`1.5s` instead of `1500ms`), so a typical ack stays shorter
(#275). Sub-second times still print as milliseconds.
- Response templates are parsed by a character-by-character state machine rather
than by splitting on delimiters. Placeholders can now nest (`{"Dist: {d|hops_min:1}"}`)
and filter arguments can be quoted. Field values are substituted into the output
and never re-scanned, so a sender-supplied phrase still cannot inject a placeholder.
- Web viewer navigation is grouped: Radio, Scheduled Messages, Greeter, Feeds, Plugins
and Configuration now sit under a single **Settings** gear menu, leaving Dashboard,
Real-time, Contacts, Mesh Graph and Logs on the bar. The current page is highlighted,
including the gear when a settings page is open.
- Added notes on connecting to waev.app MQTT brokers to the `packet_capture.md` file.
### Added
- Shlink is now supported as a URL shortener alongside v.gd / is.gd, selected with
`short_url_website_service = shlink` under `[External_Data]`. It authenticates with
`short_url_website_api_key` in an `X-Api-Key` header and needs `short_url_website`
set to your own instance — there is no default, and the bot skips shortening rather
than sending the key to a host you did not configure.
- `shorten` and `if_nonempty` response-template filters. `shorten` runs a value
through the configured shortener and falls back to the original URL when shortening
fails, so a clause is never lost to a network error. `if_nonempty:L` replaces a
non-empty value with literal `L` and clears otherwise, which is how a whole clause
is hidden rather than labelled: `{packet_hash|if_nonempty:"https://…/{packet_hash}"|shorten}`
prints nothing at all when RF correlation fails, instead of a broken link. Both
filters also answer to their other spellings — `shorten_url` in a template,
`shorten_url` in a feed format, `if_notempty` — so a chain copied between a feed
format and a command `response_format` works unchanged either way.
- Response-template filter arguments may be double-quoted, and a quoted argument may
contain nested `{field}` placeholders: `{d|prefix_if_nonempty:"Dist {sender}: "}`.
The quote ends the argument, so further filters can follow it. An unquoted
`prefix_if_nonempty` argument still consumes the rest of the placeholder, which is
what lets its literal contain `|`, so that form must stay last in its chain.
- `mqttN_keepalive` (default 60) sets the MQTT PINGREQ interval per broker. It was
hardcoded at 60 before, which is long for websockets through a proxy that drops
idle connections.
- `hops_min:N` response-template filter, alongside `pathbytes_min:N`. It clears a
field unless the message actually travelled at least N hops, so
`{firstlast_distance|hops_min:1|prefix_if_nonempty: | F/L Dist: }` drops the whole
clause on a direct message. The distance placeholders render `N/A` when there is no
path, and `prefix_if_nonempty` treats that as a value and prints its label, so a
gate was needed; `pathbytes_min` was the only one available and it asks how the path
is *encoded*, which meant throwing away a measurable one-byte multi-hop distance to
suppress the direct case. `hops_min` asks about the route instead. An unknown hop
count clears the field rather than guessing.
- `{packet_hash}` placeholder for `[Keywords]` responses, the test command's
`response_format` and the path command's `reply_prefix`: the 16-char MeshCore
packet identity hash (uppercase hex) of the packet that carried the request, so a
reply can be tied back to a specific transmission when comparing paths. It comes
only from the routing info of an RF packet actually correlated to the message, and
renders empty otherwise, so a hash from an unrelated transmission is never shown.
- **Scheduled messages can be managed from the web viewer** (#174). A new Schedule page
lists every `[Scheduled_Messages]` entry with its next run time and offers add, edit
and delete. Changes are written to `config.ini` and applied by a queued config reload,
so no restart is needed. The schedule builder composes the cron key from plain-language
options and previews the next five runs; entries the bot cannot run are shown as
**Not scheduled** with the reason rather than hidden. It edits the same config section
the bot already uses, so there is no second source of truth.
- Documented installing with `pipx`, which sidesteps PEP 668 on Debian 12+, Ubuntu
23.04+, Fedora and Arch (#222), including where `config.ini`, the database and
`local/` live — everything resolves relative to the config file's directory, so an
absolute `--config` is what makes a pipx install deterministic.
- Migration 23: nullable `snr` / `rssi` columns on `observed_paths` for
zero-hop advert rows.
- `{cmd:<command> [args]}` placeholders in `[Scheduled_Messages]`: a scheduled message
can embed the reply of any bot command, so a recurring forecast is
`0 6,12,18 * * * = Public:{cmd:wx Seattle}` rather than a per-service schedule
setting. The command runs for its text only and transmits nothing itself
(`CommandManager.render_command_output`); unknown, disabled, admin-only, timing-out
and silent commands expand to nothing rather than airing raw placeholder text.
Bounded by the new `[Bot] scheduled_command_timeout_seconds` (default 30). Two
non-configurable airtime guards apply: a schedule using `{cmd:...}` must not fire
more often than every 15 minutes (rejected at startup, measured by the tightest gap
so `0,1 * * * *` counts as 60 seconds), and the command's own `cooldown_seconds` is
still enforced.
- `{path_distance}` is now available in the path command's `[Path_Command] reply_prefix`,
reporting total distance travelled (sender → hops → bot, e.g. `12.4km`) and rendering
empty when any node in the chain has no usable coordinates. The prefix now supports the
same pipe filters as the test command's `response_format`, so
`{path_distance|prefix_if_nonempty:📏 }` drops the label along with the value.
- `install-service.sh --install-extras` installs the optional profanity-filter and
geocoding packages without prompting, for unattended installs and upgrades. It
takes precedence over the in-place `--update-venv` path, so the two can be
combined.
- `[PacketCapture] observer_name` — an optional name reported as the `origin` of
MQTT packet and status payloads. It lets the observer/analyzer identity differ
from the MeshCore RF node, which is useful when one bot name is already taken
by the radio's advertised name. Unset (the default) keeps the previous
behavior: the connected device name, falling back to `[Bot] bot_name`.
- `local_translation_path` in `[Localization]` points at your own translation
catalog, merged over the distributed one key by key, so you can translate a
local command or override a single shipped string without editing a file that
an upgrade will replace. Defaults to the `translations/` directory inside
`[Bot] local_dir_path`, resolved to an absolute path so it does not depend on
the working directory.
## [1.0.0] — 2026-08-07
v1.0.0 marks the first stable release. It adds zero-hop neighbor discovery, a
+3 -3
View File
@@ -7,14 +7,14 @@ tracked here — see [CHANGELOG.md](CHANGELOG.md) for shipped changes and
The Inline TODOs section at the bottom is auto-generated by
`python scripts/update_todos.py` (see [Auto-Update](#auto-update)).
**Last updated:** 2026-09-13. The v1.1.0 release-candidate suite reports 55%
coverage, and `fail_under = 50`.
**Last updated:** 2026-09-20. The v1.1.0 suite reports 57% coverage, and
`fail_under = 50`.
---
## Test coverage
Coverage is **55%** at the v1.1.0 release candidate. The enforced floor is 50%.
Coverage is **57%** as of v1.1.0. The enforced floor is 50%.
Hardware- and API-dependent modules cap the realistic ceiling, so the list below
is ordered by value rather than by percentage. Per-module figures below are from
+40
View File
@@ -11,6 +11,15 @@ command, service-plugin, or web-viewer interfaces introduced in v1.0.0. Existing
configuration files continue to work. Back up `config.ini`, `local/`, and the bot
database before upgrading, then use the normal installer or container update path.
### Dependencies
`meshcore` must be 2.3.14 or newer. This is a hard requirement, not a
recommendation: before 2.3.13, `send_msg_with_retry` reported ACKed DMs as
failures, so a bot on an older library logs "no ACK received after retries" for
messages the recipient did receive, and skips the delivery bookkeeping for them.
The normal installer and container paths pick this up on their own; a manual venv
needs `pip install -U -r requirements.txt`.
### Database
Migration 23 adds nullable `snr` and `rssi` columns to `observed_paths`. The bot
@@ -18,6 +27,14 @@ applies it automatically and records it in `schema_version`; existing path rows
rows without a direct RF measurement retain `NULL` values. The migration is
transactional and safe to run more than once.
Migration 24 creates `region_scope_daily` and `region_warning_events` for the
region-scope tallies described below. Both are new tables, so nothing existing is
rewritten, and the migration is safe to run more than once. `region_scope_daily`
is written for every channel message once `[Region_Warnings] track_traffic` is on
(the default); it holds one row per channel per local date, so the growth is
bounded by channel count rather than traffic. `region_warning_events` is pruned by
`[Data_Retention] region_warning_retention_days` (default 90).
### Configuration and local extensions
- `[Test_Command] distance_unit` controls printed path distances. Its default,
@@ -33,6 +50,16 @@ transactional and safe to run more than once.
- The Plugins page now discovers local commands and services and saves their new
sections to `local/config.ini`. A section already present in the base config keeps
its existing owner.
- `[Region_Warnings]` is new. Scope **tallying** is on by default and costs no
airtime; the **warning** is off by default (`enabled = false`) and, when enabled,
starts in dry run (`dry_run = true`). Nothing is transmitted on upgrade unless you
turn both of those around. See `docs/region-warnings.md`.
- `[Data_Retention] region_warning_retention_days` (default 90) prunes the warning
event log.
- `[Hello_Command] include_sender` (default off) names the user the hello reply is
answering. Existing hello replies are unchanged.
- `[Contact_Command] enabled` (default on) adds a `contact` command that replies with
the bot's own contact card. Set it to `false` if you do not want it.
### Behavior changes
@@ -53,6 +80,19 @@ transactional and safe to run more than once.
between runs.
- Proactive weather output, `!wx`, and `!gwx` share localized alert formatting.
Russian is included, and local catalogs can override individual strings.
- MQTT brokers on `waev.app` now default to a 3600s JWT TTL with renewal at 3500s,
because waev rejects a longer-lived token. An explicit `mqttN_jwt_ttl_seconds` or
global `jwt_ttl_seconds` still wins, so existing configurations are unchanged; the
substitution is logged when it applies. No other broker's defaults move.
- The Radio page gains **Region Scopes** and **Default Region Scope** cards, so the
bot's own flood scopes and the radio's firmware default can be set from the web
viewer instead of by hand. Note that the bot leaves the radio in forced-unscoped
mode after any scoped send, so the firmware default stops applying until the bot
scopes another one — the card says so too.
- A DM waiting for its ACK no longer holds the radio, so channel replies, other DMs,
and scheduled sends are no longer stalled behind one DM's retries. A region-scoped
channel send does still hold the radio from setting the scope through restoring it,
so nothing else goes out under that scope.
## Upgrading from v0.8 → v0.9