Commit Graph
267 Commits
Author SHA1 Message Date
agessaman 7e3ab04b53 feat(web-viewer): rebuild dashboard on a background snapshot
The landing page re-ran ~50 aggregate queries five times per load, then
repeated the whole sequence every 30 seconds forever — including in
backgrounded tabs. Against the live 1.44 GB database that was roughly 20
seconds of SQLite work per page load.

Move the work off the request path. A refresher thread in the viewer
process (which already runs migrations, so it works for a split-DB
install) writes two tables: daily_rollup, one row per local date, and
dashboard_snapshot, a single JSON row. A page load now reads one row.

Measured on the live database: first paint 6 requests -> 2,
/api/dashboard/summary p50 1.1 ms (304 in 0.8 ms), /api/stats 130 ms,
and a 0.32 s refresh once a minute in the background.

Make the numbers mean what they say:

- Window selectors are built from each source's retention. The page
  offered "30d" and "All" against tables pruned at 7 days, so three of
  four choices returned the same figure under a label that denied it.
- The incoming-packet chart reports its measured window instead of
  claiming 7 days for a table pruned at 3 — it sat beside a genuine
  7-day contacts chart inviting an invalid comparison.
- Days with no source data store NULL and render as gaps. Writing 0
  would put a fake cliff at every retention boundary.
- Signal metrics are stored as sums and counts, never means, so any
  window re-aggregates correctly.
- Delta chips compare the last two complete calendar days and say so;
  the headline above them is a rolling 24 hours.
- Unmapped role ordinals (type0..type15) bucket into "Unknown".
- SNR comes from message_stats, where it is populated on every row, not
  from complete_contact_tracking, where it is populated on 7%.

Kill the json_extract scans: packet_stream gains denormalized
route_type_name, payload_type_name, path_len and bytes_per_hop, written
at capture time. Aggregating those from JSON cost 3-6 s per query.
Existing rows convert a bounded batch per tick rather than in one
migration that would rewrite ~180 MB into the WAL and stall bot startup.
A partial index serves as the backfill worklist — without it the "any
rows left?" probe is a full scan costing 4.6 s per tick, and it costs
that after the backfill finishes, because finding nothing still means
reading everything.

Also: replace the per-contact hop-prefix scan with the existing bucketed
matcher and memoize the 7-day chunk set (264 ms -> 35 ms on a synthetic
100k-row database, regression-locked by a test); move the dashboard's JS
and CSS to static files, which removes the CSP nonce requirement for the
bulk of the page; and give cleanup_old_stats a future-timestamp guard,
without which rows dated 2103 are never older than the cutoff and so
live forever.

Deletes the orphaned /stats page, unreachable from the nav and rendering
stub charts that never populated. /api/stats stays as a shim with every
key name intact plus Deprecation and Sunset headers.

All schema changes are additive, so a downgraded codebase can read the
data; it would however need the new schema_version rows removed, since
MigrationRunner rejects versions it does not know.
2026-07-29 21:56:09 -07:00
agessaman 9ae04987e7 fix(install-service,commands): enhance permission handling and cache path for service account
- Added a command to set read and execute permissions for the virtual environment directory, ensuring the service account can access necessary files without modification rights.
- Updated the cache path for the Open-Meteo API client to ensure it resides in the service-writable state directory, preventing access issues during plugin load.
2026-07-29 20:09:12 -07:00
agessaman 96c4a01788 fix(commands,clients): correct scheduling, parsing and formatting defects
Remaining findings from the same triage pass. None are security
relevant; each produces a wrong user-visible result.

- greeter: treat rollout_started_at as UTC. It is written by SQLite
  CURRENT_TIMESTAMP, but datetime.timestamp() read the naive value as
  local time, shifting the backfill cutoff by the host's offset (7h on
  PT). Users who posted inside that window were never marked as already
  greeted and could be sent a welcome they should not have received.

- greeter: write greeted_at in SQLite's own format. The rollout backfill
  used isoformat() while every other path used CURRENT_TIMESTAMP. "T"
  sorts above a space, so ORDER BY greeted_at interleaved the two
  formats wrongly, corrupting duplicate cleanup and the web viewer's
  recently-greeted list.

- greeter: let an empty `channels =` disable the command. BaseCommand
  reads an empty value as disabled-on-channels, but the greeter
  collapsed "key absent" and "key present but empty" into one fallback
  and kept greeting via monitor_channels.

- greeter: stop comma-splitting greeting text. channel_greetings split
  entries on ",", so "Public:Welcome to the mesh, {sender}!" was stored
  as "Welcome to the mesh" with the placeholder silently dropped. A
  fragment now starts a new entry only when the text before its first
  colon looks like a channel name, which keeps commas, URLs and clock
  times attached to the greeting they belong to.

- wxsim_parser: match condition abbreviations longest-first. Substring
  matching in dict order let RAIN shadow CHNC. RAIN, so five conditions
  lost their "chance" qualifier (rain, snow, drizzle, t-storm, and
  FAIR-P.C.).

- wxsim_parser: re-anchor "now" on each parse. current_date and
  current_year were fixed at construction and wx_command builds one
  parser at startup, so after a few days forecast dates rolled back a
  year and staleness checks read permanently true.

- thesportsdb_client: hold the rate-limit read/sleep/write under a lock.
  Concurrent callers read the same last_request_time, slept the same
  interval and fired together, bursting past the 2.1s throttle. This
  exposes an unrelated request fan-out problem in fetch_league_scores;
  filed in TODO.md rather than fixed here.

- alert_command: stop duplicating the first incident. The tail treated
  any single-line buffer as "header only" and appended incidents[0],
  but messages after the first carry no header, so a final chunk holding
  one incident had incident 0 pasted onto it. The same confusion inside
  the loop also let a second incident be appended past the 130-character
  limit.

- feed: reject non-positive poll intervals. -1 is truthy and was stored,
  and the poller's `now - last_check >= interval` then treats the feed
  as permanently due and re-fetches the URL every cycle; 0 was ignored
  while still reporting success. The web viewer, which is the primary
  editor, had no validation at all, and a JSON null there raised a
  TypeError that aborted the poll cycle for every feed rather than one.
  feed_manager falls back to the default for rows written before this.

- transmission_tracker: age out confirmed transmissions that have
  repeats. Cleanup removed only repeat_count == 0, so repeated records
  accumulated for the lifetime of the process, which matters on a Pi
  Zero. Repeat counts are already persisted to packet_stream, so the
  longer 30-minute retention loses nothing.

- mesh_graph: weighted-merge avg_hop_position when promoting an edge.
  Promotion overwrote the average with the single new observation, so an
  edge averaging 2.0 over 4 observations became 9.0 instead of 3.4 and
  skewed subsequent path scoring.

- multitest_command: show the path that ends exactly at the display LCP.
  The suffix helpers are correct in isolation; the loss happens in the
  cluster formatters, where _shrink_display_lcp refuses to shrink a
  single-token LCP and the trunk then rendered as "96 ┐", meaning
  "everything continues past here" and dropping the bare 96 route while
  the header still counted it. Now uses the file's existing "├ common"
  marker. Empty suffixes are also no longer handed to the nested
  renderer, which mapped them onto the same [] as a route ending at the
  inner LCP and drew a row for a route that did not exist.

- sports_mappings: stop shadowing seven unique team nicknames. In one
  flat dict a repeated key silently drops the earlier team, so hawks
  resolved to the NBA Hawks rather than the Seahawks alias it was added
  as, blazers to Kamloops rather than Portland, and rockets to Kelowna
  rather than Houston. First definition now wins for hawks, giants,
  jets, rangers, kings, blazers and rockets; every shadowed team keeps
  its unambiguous full-name alias. The 55 city and abbreviation
  collisions (chicago, sf, la, ...) are genuinely ambiguous and stay
  last-wins, now pinned by a test so a new collision fails loudly
  instead of passing unnoticed.
2026-07-28 20:08:57 -07:00
agessaman 4e49c0557f fix(path,capture): restore silently dead packet and path-scoring paths
Four code paths that produced no result, or a wrong one, without ever
logging an error. Each looked correct in review and failed only against
real inputs.

- packet_capture: read RAW_DATA from the "payload" field. meshcore's
  reader dispatches RAW_DATA as {"SNR", "RSSI", "payload"}, but the
  handler looked for "data" and "raw_hex" and returned early when it
  found neither, so every RAW_DATA event was dropped with no log line.
  Existing tests passed because they fabricated {"data": ...}. The
  sibling RX_LOG handler already preferred "payload".

- packet_capture: key both RF correlation caches on uppercase hex.
  RX_LOG_DATA cached prefixes in the lowercase the wire supplies, while
  RAW_DATA uppercased before looking up, so the two never correlated and
  no RAW_DATA packet ever picked up cached SNR/RSSI. This was masked
  until now by the drop above. The event's own SNR/RSSI are also folded
  into the lowercase spelling the rest of the pipeline reads, via an
  explicit None check so a legitimate 0 is not discarded.

- path_inference: run the bot path-validation bonus. Its scoring block
  sat inside an `if (len(decoded_path_hex) % path_n) != 0` branch, and
  for a uniform-width path that remainder is always 0, so the body was
  unreachable. A candidate with a perfect historical path match scored
  0.0 instead of 0.3. The web sibling was indented correctly.

- path_inference: compare path segments case-insensitively in the web
  variant. decoded_nodes came from the uppercased path_context while
  stored_nodes were lowercased, so a segment match was impossible for
  any path containing a hex letter; a perfect match scored 0.15 rather
  than 0.3. This raises some web decode confidence values, so the
  module docstring's byte-for-byte parity note now records the
  deliberate exception. No node resolves to a different repeater purely
  from case.

Also resolves node hop position by index rather than by value, so a
1-byte prefix appearing twice in one path no longer gives every
occurrence the neighbours and final-hop status of the first. This is
threaded through the shared engine and used by the web decode path,
which returns a list. The bot `path` command deliberately does not pass
it: repeater_info is keyed by node_id, so resolving each occurrence
separately would let the later hop overwrite the earlier one's display.
Fixing that needs repeater_info re-keyed by hop index.
2026-07-28 20:08:23 -07:00
agessaman 9a24ed1bf2 chore: update documentation and improve configuration handling
- Added a tracked `LICENSE` file and updated `pyproject.toml` to include license metadata.
- Enhanced `CHANGELOG.md` with recent changes and clarifications.
- Updated `config.ini.example` and related documentation to reflect clamping behavior for numeric limits in `[Feed_Manager]`.
- Improved startup validation to suggest corrections for unknown and misspelled keys.
- Refactored geocoding and HTTP request handling to run off-thread, preventing event loop stalls.
- Added thread safety to cache management in geocoding functions to avoid race conditions.
2026-07-28 16:56:43 -07:00
agessaman 717f9a8ef3 feat(i18n): opt-in auto-detect sender language for replies
Reworked implementation of the multilingual-response feature (issue #218)
as a reusable, framework-level capability instead of logic bolted onto
HelloCommand.

- modules/lang_detector.py: keyword-first detection (reliable for the short
  greetings typical on mesh) with optional langdetect for longer text;
  results are constrained to languages that actually have a translation file.
- core.py: cache Translator instances by language (get_translator) so
  per-message switching never re-reads files, and expose available_languages()
  derived from translations/*.json. Store translation_path; keep cache/path
  consistent on config reload.
- base_command.py: task-local translator override via a ContextVar (safe under
  asyncio concurrency, no global mutation), plus detect_response_language() and
  a respond_in_sender_language() context manager any command can opt into.
- hello_command.py: build the reply inside respond_in_sender_language(); the
  only awaited call (send_response) stays outside the override window.
- Gated by [Localization] auto_detect_language (default false); langdetect is
  an optional [lang] extra, not a hard dependency.

Tests: unit coverage for the detector and the base-command wiring.
2026-07-28 12:44:53 -07:00
agessaman 388c2925cf chore(ci): fix mypy Optional inference and ruff import order
Finish the lint/type cleanup so all CI gates pass:

- location.py: declare lat/lon as Optional[float] up front. The
  fallback/coordinates/repeater branches bind plain floats while the
  zipcode/city branches bind float | None from best-effort geocoding, so
  a single Optional declaration keeps mypy from pinning the first binding.
- rain_command.py: sort imports to ruff/isort order and mark the
  US_STATE_ABBRS / titlecase_location re-exports noqa: F401. They are
  listed in __all_location_reexports__ rather than __all__, so ruff does
  not recognize them as intentional re-exports on its own.
- test_location_characterization.py: drop a stray blank line (ruff E303).
2026-07-22 14:54:31 -07:00
agessamanandClaude Opus 4.8 03a1d4056b Merge origin/dev into codex/p0-security-hardening
Resolve the feeds.html conflict by keeping the branch's XSS-safe DOM
construction of the feed-details view and re-adding dev's per-feed and
reset-all error buttons via addEventListener instead of inline onclick,
since /feeds is a nonce-CSP page where inline handlers are blocked.

Update tests/test_feed_manager_extended.py::TestPollFeedPosting to patch
the SafeUrlPolicy.validate_async path (this branch's SSRF refactor) rather
than the removed module-level validate_external_url symbol, matching the
idiom already used throughout that test file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:28:13 -07:00
agessaman 3a896ec1ea feat(location): enhance geocoding and error handling in location resolution
- Added asynchronous support for location resolution, improving performance and responsiveness.
- Introduced detailed error handling for invalid latitude and longitude inputs, providing specific feedback to users.
- Implemented a caching mechanism for geocoding results to optimize repeated lookups.
- Refactored the AQI command to streamline location handling and improve clarity in error messages.
- Expanded unit tests to cover new geocoding features and error scenarios, ensuring robust functionality.
2026-07-20 09:35:13 -07:00
agessaman 6cb5c34a9a feat(location): enhance location resolution and geocoding functionality
- Introduced a shared API for location resolution using `modules.location`, allowing for better handling of place lookups (coordinates, ZIP codes, city names).
- Updated geocoding functions to accept both strings and structured dictionaries, improving flexibility in location queries.
- Refactored existing commands (AQI, Rain, Wx) to utilize the new location resolution methods, streamlining the codebase and enhancing maintainability.
- Removed redundant location handling logic from commands, centralizing functionality in the new location module.
- Added tests to ensure proper classification and resolution of various location formats, including multi-word international cities and ZIP codes with surrounding whitespace.
2026-07-19 18:23:00 -07:00
agessaman 1f0ec48448 Harden feed, config reload, and wx async paths 2026-07-16 15:40:20 -07:00
agessaman a24d756abd Harden outbound URL and async provider boundaries 2026-07-16 15:22:12 -07:00
agessaman 119fce2211 feat(command-manager): implement DM length guard and auto-splitting for oversized messages
- Added a mechanism to check the byte length of direct messages (DMs) before sending, ensuring they do not exceed the firmware's maximum limit.
- Implemented a method to split oversized messages into smaller chunks, preserving UTF-8 encoding and avoiding mid-codepoint splits.
- Enhanced logging to provide warnings when messages are auto-split, including details on the number of chunks created.
- Updated the DM sending logic to handle both single and split messages efficiently.
- Introduced a new static method for splitting text into UTF-8 chunks, improving message handling across the application.
2026-07-16 14:56:42 -07:00
agessaman f804d05d7f feat(command-manager): implement DM length guard and auto-splitting for oversized messages
- Added a mechanism to check the byte length of direct messages (DMs) before sending, ensuring they do not exceed the firmware's maximum limit.
- Implemented a method to split oversized messages into smaller chunks, preserving UTF-8 encoding and avoiding mid-codepoint splits.
- Enhanced logging to provide warnings when messages are auto-split, including details on the number of chunks created.
- Updated the DM sending logic to handle both single and split messages efficiently.
- Introduced a new static method for splitting text into UTF-8 chunks, improving message handling across the application.
2026-07-16 12:49:38 -07:00
agessaman 8c5b932634 refactor(config): single source for legacy enabled-key aliases
The legacy [Section] enabled alias map (e.g. [Jokes] joke_enabled,
[Stats] stats_enabled) was duplicated in BaseCommand.get_config_value and
modules/settings_schema.py, so runtime behavior and the web settings UI
could drift apart. Both now read config_schema.LEGACY_ENABLED_ALIASES.
2026-07-12 11:57:28 -07:00
agessaman 2e8ccf283a fix(settings-ui): make config reload honor deletions and cached command settings
- reload_config: clear all sections before re-reading so keys deleted from
  config.ini (e.g. rows removed in the settings UI) don't survive in memory
  (ConfigParser.read merges instead of replacing)
- reload_config: re-instantiate command plugins so settings cached in
  __init__ (including 'enabled') take effect on reload
- /api/plugins save: treat dynamic_sections / repeating_blocks absent from
  the payload as 'don't touch' instead of 'delete all managed keys'
- settings view: respect per-plugin settings_enabled_default so opt-in
  commands (Announcements, Greeter) display as off when unconfigured
- ruff: drop unused import, organize command_manager imports
- add tests for ini_writer, settings_schema, settings_store and the
  /api/plugins endpoints (38 cases) plus reload regression tests
2026-07-12 11:57:28 -07:00
agessaman 3ccfd63613 feat(config): add config panel for modifying plugin settings, with config schema in each command and service plugin. 2026-07-12 11:57:28 -07:00
agessaman 95b0601980 feat(command_prefix, config): enhance command prefix handling and documentation
- Updated `config.ini.example` to support single, multiple, and decorative command prefixes, improving flexibility in command invocation.
- Refactored `CommandManager` and `BaseCommand` to utilize new command prefix parsing logic, allowing for better handling of multiple prefixes and optional bare commands.
- Enhanced documentation to clarify command prefix configuration options and behavior, ensuring users understand how to set and use prefixes effectively.
- Added unit tests to verify the correct functionality of command prefix handling, including support for permissive and strict prefix modes.
2026-07-07 09:37:28 -07:00
agessaman 18f231b142 feat(config, command): add hops placeholders to path command responses
- Updated configuration examples to include new `{hops}` and `{hops_label}` placeholders for path command replies, enhancing response detail.
- Modified `BaseCommand` to support hop count retrieval and formatting, ensuring accurate display of hop information in responses.
- Enhanced unit tests to verify correct formatting of hop-related placeholders in response messages, improving overall command functionality.
2026-07-06 11:29:47 -07:00
agessaman b59fa9cec9 feat(config, docs): enhance rain command and stats collection configuration
- Updated the `[Rain_Command]` section in `config.ini.example` to include support for snow alongside rain, improving the command's functionality.
- Enhanced documentation for the rain command to reflect the new snow alias and clarify response behavior based on the selected keyword.
- Added a new `collect_stats` option in the `[Stats_Command]` section, allowing stats collection to be enabled independently of the user-facing command, with updated documentation to explain its behavior.
- Improved the web viewer documentation to clarify how stats are collected and displayed, ensuring users understand the configuration options.
2026-06-28 13:28:23 -07:00
agessaman 24fdfb2370 feat(command_manager, help_command): improve command prefix handling and help command visibility
- Enhanced `CommandManager` to persist normalized message content, preventing prefix stripping from interfering with command matching.
- Updated `BaseCommand` to skip prefix handling if already normalized, ensuring all commands can be matched correctly.
- Modified `HelpCommand` to ensure unused commands are listed even when command statistics are present, improving user accessibility to all commands.
- Added unit tests to verify the correct behavior of command matching and help command functionality under various scenarios.
2026-06-28 08:42:58 -07:00
agessaman 986fe67c01 feat(test_command): add RSSI placeholder support in response formatting
- Enhanced the `TestCommand` class to include the `rssi` value in the response format, allowing for more detailed output.
- Updated the example configuration to reflect the new placeholder usage for `rssi`.
- Added a unit test to verify that the `rssi` placeholder expands correctly in the response message.
2026-06-27 16:41:11 -07:00
agessaman 7e377ffeb6 feat(wx_command): update keyword handling for wx command delegation
- Modified `WxCommand` to use its own keywords along with any aliases from `BaseCommand` when delegating to `GlobalWxCommand` for the Open-Meteo weather provider.
- Added a new test fixture for `WxCommand` configured to delegate to `GlobalWxCommand`, ensuring that aliases are preserved and correctly matched in keyword checks.
- Enhanced unit tests to verify the delegation behavior and keyword matching for both standard and aliased commands.
2026-06-27 15:57:31 -07:00
agessaman d772b8a04e feat(weather): integrate NWS coverage check for alerts
- Added a new function `nws_http_means_no_coverage` to determine if NWS HTTP status codes indicate no coverage for a location.
- Updated `WxCommand` and `WeatherService` classes to utilize this function, improving error handling for NOAA alerts.
- Introduced a lazy loading mechanism for `_nws_alerts_available` to manage alert availability based on NWS coverage status.
- Enhanced logging to provide clearer warnings when NWS alerts are unavailable due to HTTP errors.
2026-06-27 15:52:15 -07:00
agessaman 5aa84c4ffb feat(worldcup): enhance scoreboard fetching with date range support
- Added new utility functions `espn_dates_for_local_day` and `filter_events_local_day` to handle local timezone date calculations and event filtering.
- Updated `ESPNClient` to support optional date parameters in `fetch_scoreboard_with_calendar` and `fetch_match_states` methods, allowing for more precise data retrieval.
- Modified `WorldCupCommand` and `WorldCupLiveService` to utilize the new date range functionality, ensuring accurate event reporting based on local time.
- Implemented unit tests to verify the correct behavior of date handling and event filtering in various scenarios.
2026-06-21 10:34:28 -07:00
agessaman 21676f0792 feat(worldcup): add support for announcing disallowed goals and enhance goal tracking
- Introduced a new configuration option `announce_disallowed` to allow notifications for goals overturned by VAR.
- Updated `WorldCupLiveService` to handle disallowed goals, including formatting for previously announced scorers.
- Enhanced `ESPNClient` to ensure accurate goal tracking and state management.
- Added unit tests to validate the new disallowed goal functionality and ensure correct behavior during matches.
2026-06-17 22:11:24 -07:00
agessaman 4bf60622ff refactor(rain): enhance fetch_precip_series_nws with caching support
- Added cache_ttl parameter to fetch_precip_series_nws to enable caching of results for improved performance.
- Implemented logic to reuse cached results based on location and cache expiration.
- Updated tests to ensure proper handling of cache_ttl without causing errors.
- Refactored WorldCupFastcastClient to streamline connection handling and improve readability.
2026-06-15 15:26:55 -07:00
Adam GessamanandGitHub b45b23bdd2 Merge pull request #196 from rlwilliamson-dev/feat/nws-nowcast-source
feat(rain): NWS gridpoint nowcast source for US (Open-Meteo fallback)
2026-06-15 14:59:08 -07:00
Adam GessamanandGitHub af6ef9fc10 Merge pull request #195 from rlwilliamson-dev/feat/rain-enhancements
feat(rain): nowcast enhancements + tests (amounts, capitals, snow/ice, !snow, probability, changeover)
2026-06-15 14:56:52 -07:00
rlwilliamson-dev 2cec9973dc feat(rain): use NWS gridpoint as the US precip-nowcast source
The Open-Meteo forecast model smooths away scattered, pop-up convection, so the rain nowcast (the rain/snow command and the proactive push) could read 0.0 in / ~12% and stay silent while rain was actually falling. Observed near Nashville: Open-Meteo reported 0.0 in across the next 3 h while NWS's own gridpoint showed 65-74% probability with measurable QPF, and thunderstorms were occurring.

Add fetch_precip_series_nws(), which builds the same nowcast-series shape from the NWS gridpoint forecast (6-hour QPF + hourly PoP + weather type). Each hour's precip is its QPF share, zeroed when that hour's PoP is below a floor, so the predicted rain-start tracks the hourly probability instead of snapping to coarse 6-hour QPF boundaries. Both fetchers now prefer NWS for US points and fall back to Open-Meteo where NWS has no coverage (outside the US) or on failure, so the command and the push agree and the model's convective blind spot no longer silences the alert.

Pure helpers (_iso_duration_hours, _nws_hourly, _nws_weather_code) are unit-tested; the NWS-weather -> WMO-code mapping keeps bucket classification (rain/snow/thunder/...) identical to the Open-Meteo path.
2026-06-15 08:33:21 -05:00
agessaman 7537c76c63 feat(worldcup): add World Cup command and service configuration
- Introduced World Cup command and service settings in `config.ini.example`, `config.ini.minimal-example`, and `config.ini.quickstart` to enable FIFA World Cup score tracking and updates.
- Enhanced `espn_client.py` with methods to fetch tournament calendar and match states, supporting the new World Cup features.
- Updated `sports_command.py` to resolve World Cup nation names and integrate with the new command.
- Added translations for World Cup commands and messages in multiple languages, ensuring comprehensive support for users.
- Included mappings for World Cup nations in `sports_mappings.py` to facilitate accurate team identification.
2026-06-14 19:33:20 -07:00
rlwilliamson-dev ffa5595fff feat(rain): nowcast enhancements (amounts, capitals, snow/ice, !snow, probability, changeover) + tests
Builds on the merged rain/snow nowcast (#193): ten enhancements plus
end-to-end, proactive, and live-smoke test coverage. All new behavior is
config-gated or additive, so existing deployments are unaffected by default.

Enhancements
- Precip amount estimate "(est 0.2 in)" on the command and the proactive push;
  snow shown as real depth (Open-Meteo snowfall, cm); freezing rain tagged "in ice".
- Bare country / US state resolves to its capital with a heads-up
  (self-contained modules/region_capitals.py; no pycountry/us dependency).
- join_location() dedupes "Spain, Spain" / city-states.
- !snow alias + neutral !nowcast; each looks for its own precip family across
  the window, else falls back with a "No snow, but rain ..." cross-type line.
- Keyword-aware help (help rain / help snow).
- Precip probability shown "(..., 70%)"; the proactive incoming alert is gated
  to >= [Weather_Service] rain_nowcast_min_probability (default 50).
- Rain<->snow changeover line when the window holds both families.
- Borderline temperature tag (30-38F).
- Short-lived series cache shared by the command and the proactive poll.

Tests
- test_rain_command_e2e.py: drives RainCommand.execute() end to end and asserts
  the exact rendered reply across dry/incoming/raining, snow depth, cross-type
  mismatch, changeover, ice, temp tag, region capitals, config toggles, DM
  budget, and keyword-aware help.
- test_rain_proactive_e2e.py: drives Weather_Service._check_rain_nowcast() for
  the probability gate, snow depth, ending notice, and once-per-episode dedup.
- test_rain_live_smoke.py: opt-in (RAIN_LIVE_SMOKE=1) live Open-Meteo check for
  upstream schema drift; skipped in CI.
- Shared scaffolding in tests/unit/_rain_harness.py.

New config: [Rain_Command] show_amount/amount_unit/show_probability/show_temp/
cache_seconds/zip_city_lookup; [Weather_Service] rain_nowcast_min_probability/
rain_nowcast_cache_seconds/rain_nowcast_show_amount/rain_nowcast_amount_unit.
ruff + mypy clean.
2026-06-06 16:18:52 -05:00
rlwilliamson-dev e5666b7cce feat(rain): minute-level rain nowcast command + proactive incoming/ending push
Add a rain nowcast (Open-Meteo 15-minutely precipitation — worldwide, no API
key) as both an on-demand `rain`/`nowcast` command and an opt-in Weather_Service
push that announces rain starting and stopping at the bot's position.

- modules/commands/rain_command.py — command plus pure, unit-tested
  fetch/analyze/dedup/label helpers (also reused by the service)
- modules/service_plugins/weather_service.py — background poller mirroring the
  existing weather-alert poll pattern; ships disabled (opt-in)
- Location labels resolve to "City, ST" (US) / "City, Country" (non-US)
- 34 unit tests; rain_command added to the strict-mypy module list
2026-06-03 18:56:24 -05:00
agessaman 9b437efb05 feat(utils): add public_key_has_prefix function and refactor path command logic
- Introduced a new utility function `public_key_has_prefix` to check if a public key starts with a specified prefix in a case-insensitive manner.
- Updated the `PathCommand` class to utilize the new utility function for public key prefix matching, enhancing code readability and maintainability.
- Refactored logic in `PathCommand` and `BotDataViewer` to streamline the handling of recent repeaters based on recency scores, improving clarity in the filtering process.
2026-06-01 15:27:27 -07:00
agessaman 33f50a9fdc chore(docs): update documentation URL and bump version to 0.9.2
- Changed the documentation URL in the meshcore-bot.service file to point to the correct repository.
- Updated the project version in pyproject.toml from 0.9.1 to 0.9.2 for consistency in versioning.
- Modified the User-Agent string in dadjoke_command.py to reflect the new repository URL.
2026-05-17 14:42:05 -07:00
agessaman 74b7de6f72 feat(flood_scope): enhance flood scope handling across services
- Introduced optional regional TC_FLOOD scope configuration for various services, allowing for more granular control over message routing.
- Updated the CommandManager to resolve and apply flood scopes from messages, configuration sections, and explicit parameters, ensuring correct message delivery.
- Enhanced service plugins to utilize the new flood scope functionality, including weather, earthquake, and webhook services.
- Added unit tests to verify the correct resolution and application of flood scopes in different scenarios, ensuring robust functionality.
2026-05-16 12:08:24 -07:00
agessaman 4a0b989f5a feat(scheduler): optional flood scope for scheduled channel messages
Parse channel:#scope:body in [Scheduled_Messages], pass scope to
send_channel_message; extend schedule list and docs.
2026-05-14 11:32:20 -07:00
agessaman 70b2256752 feat(test): Test_Command response_format with piped path filters
Add format_piped_template (pathbytes_min, prefix_if_nonempty), utils
bytes-per-hop helpers, and [Test_Command] format priority over Keywords.
2026-05-12 19:30:42 -07:00
agessaman 13f22e4d7b feat(path_command): add reply prefix and minimum path bytes configuration
- Introduced `reply_prefix` to prepend a customizable string to path command replies, supporting placeholders for dynamic content.
- Added `minimum_path_bytes` setting to control the resolution of repeater names based on the number of bytes per hop, enhancing path command behavior.
- Updated relevant documentation and translations to reflect these new configuration options.
- Implemented unit tests to ensure correct functionality of the new features.
2026-05-12 17:23:22 -07:00
agessaman 5879838d8b feat(command_manager): Fix flood scope on keyword and RandomLine channel replies
Route process_message keyword/RandomLine sends through send_response so
reply_scope reaches set_flood_scope; add optional command_id to
send_response for transmission tracking.
2026-05-10 19:20:22 -07:00
agessaman 4d35e103a2 refactor(scheduler): enhance scheduled message configuration and parsing
- Updated the scheduled message format in `config.ini.example` to support 5-field cron expressions and preset aliases, replacing the deprecated HHMM format.
- Improved the `MessageScheduler` class to parse and validate new schedule formats, logging warnings for deprecated usage.
- Adjusted the `ScheduleCommand` to display scheduled messages with their respective cron or preset labels.
- Added unit tests to ensure correct parsing and handling of various schedule formats, including legacy HHMM and cron expressions.
2026-05-05 14:12:02 -07:00
agessaman d34ad24e9b feat(weather): add default city configuration for wx commands
- Introduced a new configuration option `default_city` in `config.ini.example` for fallback city names when no location is provided.
- Updated `wx_command.py` and `wx_international.py` to utilize the `default_city` for location disambiguation, enhancing user experience when companion location is unavailable.
- Improved logging to reflect the use of default city or bot location as fallback options.
2026-04-25 21:15:51 -07:00
agessaman 6d80ad03da feat(trace_command): enhance path parsing for multibyte nodes and update documentation
- Updated the TraceCommand class to support parsing of 1-byte, 2-byte, and 3-byte hex node paths.
- Improved the _extract_path_from_message and _parse_path_arg methods to handle multibyte node formats and enforce consistent length checks.
- Enhanced help text and usage examples to reflect new path formats.
- Added unit tests for multibyte path parsing and reciprocal path building to ensure correct functionality.
2026-04-25 20:28:41 -07:00
agessaman 63592bdd65 feat(commands, config): implement path byte length requirements for commands
- Added configuration options to specify minimum path byte length requirements for the path, test, and multitest commands.
- Introduced methods to enforce these requirements and handle failure responses.
- Updated the packet capture service to correctly calculate packet hashes using the raw wire byte length.
- Enhanced unit tests to verify the correct behavior of the new path byte length logic.
2026-04-25 09:05:17 -07:00
agessaman b5cce55604 feat(config): add configurable timeouts for web viewer integration
Introduced optional timeout settings in the configuration for various web viewer operations, including edge and node post timeouts, SQLite connection timeout, and requeue timeout. Updated the web viewer integration to utilize these settings, enhancing flexibility and reliability. Added commands to inspect the resolved configuration with sensitive keys redacted, and updated documentation accordingly.
2026-04-16 19:56:47 -07:00
agessaman 90fdd0c77a feat: add cmd_reference_url option for Cmd_Command
Introduced a new configuration option `cmd_reference_url` in the Cmd_Command section, allowing users to override the default `cmd` output with a link to a full documentation page. Updated the CmdCommand class to utilize this new setting and modified related documentation and tests to ensure proper functionality.
2026-04-15 09:26:19 -07:00
agessaman 313dfccf75 fix: Align send suppression and security compatibility behavior
Make outbound send suppression consistent by honoring both radio-offline and zombie states across command-manager and scheduler paths. Preserve strict SSRF defaults while adding explicit private-feed URL opt-ins, persist allow_local_smtp from notifications config writes, reconcile zombie alert setting precedence, and replace deprecated UTC timestamp calls with timezone-aware UTC usage.
2026-04-14 12:18:02 -07:00
agessaman af6f693cff refactor: update max_results configuration for airplanes command
Changed the default value of max_results in config.ini.example from 0 (no limit) to 3, ensuring a more manageable output size for aircraft listings. Updated the AirplanesCommand class to reflect this new default and added logic to handle message size constraints when formatting the aircraft list. This refactor improves the user experience by preventing excessive data in responses while maintaining functionality.
2026-04-14 11:35:00 -07:00
agessaman 52227ae6ac refactor: replace fortune command with random lines fortunes command
Removed the fortune command and its associated files, transitioning to existing new random lines command that retrieves fortunes from a different file structure. Updated configuration settings in config.ini.example to reflect this change, including the new file path for fortunes. Adjusted the .gitignore to accommodate the new directory structure for random lines. This refactor enhances the organization of fortune-related content and improves maintainability.
2026-04-14 11:27:40 -07:00
agessaman 46d3fab125 fix: keep airplanes output single-message
Prevent multi-message aircraft list replies by capping formatted output to one frame, preserving anti-flood behavior on mesh channels.

Made-with: Cursor
2026-04-14 10:11:06 -07:00