Commit graph

25 commits

Author SHA1 Message Date
Matt Johnson
a4c218b300 fix(central): don't crash boot when Central is unreachable at startup
The Central NATS consumer's start() was called unguarded during boot, so if
Central was enabled but unreachable at startup, nats.connect() raised
NoServersError, propagated through bot.start(), and crashed the process —
crash-looping under Docker restart:unless-stopped.

Now _start_central_consumer_guarded() wraps start() in try/except: on failure
it logs a warning and continues booting (LLM bot, Meshtastic/MeshCore,
mesh-health, and native feeds all start), then a background retry loop
(30s->300s backoff) re-attempts the initial connect until it succeeds. Once
connected, NATS's own allow_reconnect handles runtime drops. The retry task is
cancelled cleanly on stop(). No retry is scheduled when nothing is
central-sourced.

Tests: +tests/test_central_boot_guard.py (11); 0 new failures.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 07:13:46 +00:00
5c0f4b42f3
fix(meshcore): reply to DMs via discovered DIRECT route, not flood (#23)
pyMC-companion FLOOD direct-messages are silently rejected by recipient
MeshCore nodes (43 sent / ~1 ack in 24h); DIRECT-routed packets deliver.
A bare inbound DM does NOT populate the contact's out_path on pyMC, so the
contact stays out_path_len=-1 (flood), and send_msg_with_retry actively
reset_path→flood, guaranteeing the broken route.

New behavior on a DM reply:
- _establish_direct_path(): path discovery (CMD 52 send_path_discovery_sync)
  so the recipient returns a PATH packet → pyMC writes a real out_path →
  works at ANY hop count. Fallback: seed from the sender's cached advert
  (get_advert_path CMD 42 → update_contact CMD 9) if discovery is empty.
- send via plain send_msg (CMD 2) — uses the learned path → DIRECT. Drops
  send_msg_with_retry (which forced flood). Logs the RESP_CODE_SENT route
  (direct/flood) so we can confirm.

Tests reworked for the discover-then-direct-send path; 0 new failures.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:25:40 -06:00
e7c8ef507e
fix(meshcore): resolve DM dest to full contact object before send (canonical pattern) (#22)
MeshCore DM replies were passed a bare 6-byte pubkey prefix to
send_msg_with_retry. Per every working meshcore project (meshcore_py
examples, meshcore-cli, meshcore-bot, meshcore-ha), the destination must be
resolved to the FULL contact object (dict w/ 64-hex public_key) after
ensure_contacts — otherwise the lib can't upgrade the prefix to the full key,
skips reset_path, blind-floods, gets no ACK, and the DM silently never
delivers (matches our live symptom: inbound + channel send work, DM reply dies).

- Add _resolve_contact(dest): ensure_contacts() then get_contact_by_key_prefix().
- DM branch now passes the resolved contact object to send_msg_with_retry;
  if the contact can't be resolved, log + return False (no blind-flood).
- Subscribe to EventType.ACK + log received ACKs (instrumentation to confirm
  whether ACKs reach the dispatcher at all).

Tests updated (+2); 0 new failures.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:28:26 -06:00
ddd47afa87
fix(meshcore): deliver DM replies (flood/ACK), save meshcore_context, test-llm, inbound logging (#21)
Four fixes surfaced by live testing (a MeshCore DM got no reply):

- DM REPLY DELIVERY (root cause): reply used the meshcore lib's fire-and-forget
  send_msg (MSG_SENT != delivered, no flood, no ACK) so replies to nodes without
  an established direct path silently vanished. Switch to send_msg_with_retry
  (contact resolve + flood fallback + ACK wait); a None return (no ACK) is now a
  real failure, not silent success. _run_coro timeout raised to 40s for the ACK cycle.
- 422 on save: register meshcore_context in config_loader SECTION_TO_FILE
  (config.yaml) — it was in VALID_SECTIONS but not the save-routing table.
- test-llm endpoint: called backend.generate() with (str, []) instead of
  (messages:list, system_prompt:str) → "string indices" error; fixed the call.
- Inbound observability + robustness: subscribe to CONTACT_MSG_RECV BEFORE
  start_auto_message_fetching (+ ensure_contacts) so a DM queued at connect isn't
  drained before the handler registers; add INFO/DEBUG logging across the inbound
  DM + dispatch + send path (was entirely unlogged).

Tests: +test_meshcore_dm_delivery, +test_fix_meshcore_save_and_llm_test; 0 new failures.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:54:16 -06:00
70a0fd1657
feat(context): GUI control for chat-context retention (days) + live apply (#20)
- Config → Settings → Context: relabel the raw "Max Age (sec)" field to
  "Chat context retention (days)" (days<->seconds conversion, min 1,
  default 14). Governs the shared per-mesh chat memory window.
- Make PUT /api/config/context apply LIVE: MeshContext.update_settings()
  updates max_age/observe_channels/ignore_nodes in place; config_routes
  refreshes the running MeshContext via app.state.mesh_context (mirrors
  the existing _refresh_toggle_filter pattern) so retention changes take
  effect without a restart.

Tests: +tests/test_context_hot_reload.py (10); 0 new failures.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:03:37 -06:00
a191200a12
feat(meshcore): decouple per-mesh LLM DM gate + mesh-scoped chat context (#19)
- router.should_respond branches on message.transport: MeshCore DMs are
  governed solely by meshcore_context.respond_to_dms (enforced at the
  transport); Meshtastic solely by bot.respond_to_dms. No global gate.
- MeshObservation tagged per-transport; the LLM "recent traffic" block is
  scoped to the originating mesh (keyword override for the other mesh),
  labeled by mesh so the model knows which it is describing.
- MeshCore observe_channels is now opt-in (empty = observe none).
- Chat-context retention 30d -> 14d (both meshes).
- Meshtastic integer channel-index filter no longer misapplied to MeshCore
  observations (their channel is a companion slot index).
- Frontend: relabel DM toggles per-mesh ("Answer direct messages",
  Meshtastic-only / MeshCore-only), remove the false channel-mention
  tooltips, opt-in wording for MeshCore observe-channels.

Tests: +tests/test_llm_scoping.py (10), context-filter updated for opt-in;
0 new failures (34 pre-existing).

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:39:26 -06:00
284fb5cbf2
MeshCore Contacts roster + Companion status (read-only) (#17)
* feat(dashboard): MeshCore Contacts roster + Companion status (read-only)

Expose the live companion's contact roster (get_contacts) and self/channel
status via /api/meshcore/contacts + /api/meshcore/self. Fill the Contacts
(roster table) and Companion (status + channels) pages. Telemetry auto-poll
comes next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(meshcore): self-advertisement (send-advert + advert-on-connect + periodic)

AIDA now announces itself: send_advert(flood=True) on every connect, an
optional periodic auto-advert (meshcore_advert_interval_seconds), and a
manual "Send Advert" button + POST /api/meshcore/advert. Makes the
companion discoverable/DM-able on the mesh.

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 16:14:10 -06:00
61ca0057e2
fix(nws): tighten hazard wording across all product types; kill dangling '— …' in L4 (#16)
Special Weather Statements (and other SPS/WSW/FFW/FLW products) rendered a
verbose raw hazard sentence on L3 that ate the packet budget, collapsing L4 to
a dangling 'Moving SW 24 mph —…' with every town lost (seen live in the
Activity Log).

- Add _tighten_hazard(): compacts free-form NWS hazard text into the terse SVR
  idiom for ALL branches — 'Wind gusts in excess of 45 mph' -> '45mph gusts',
  'in excess of' -> '>', '45 mph' -> '45mph', 'pea size hail' -> '0.25" hail'.
  Applied to the FFW/FLW and SPS/WSW/else branches (SVR already terse).
- Rework L4 assembly to be budget-aware BEFORE the final hard cap: try location
  forms richest->poorest (full list -> first->mid->last -> first->last ->
  first-only -> none) and only attach '— {locs}' when the whole message fits.
  If no location fits, degrade to motion-only; if even that overflows, drop L4.
  A dangling '— …' / trailing '—' is now structurally impossible.
- Tests: SPS worst-case (tightened + no dangling), WSW, pathological
  motion-only degrade, SVR no-dangling re-verify; shared dangling-separator
  assertion.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 15:22:07 -06:00
0460462485
Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing (#15)
* feat(dashboard): Phase A — 4-section nav; move Scheduled Broadcasts + Danger Zones off Routing

Regroup nav into GENERAL/MESHTASTIC/MESHCORE/DOCUMENTATION (<=5 pages each,
MT & MC mirror). Consolidate via tabs (Places, Nodes & Health, Contacts &
Companion) reusing existing components. Move Band Conditions, cold-start,
and fire digest to per-mesh Scheduled Broadcasts pages; move Danger Zones
to its own page. Routing keeps its sending rules unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): Phase B — clean identical Routing grids; relocate per-family gating to Data Feeds

Meshtastic Routing becomes an always-visible pure-delivery grid matching
MeshCore (no master-toggle expand/collapse). Per-family gating (enable/
severity/freshness/cooldown) moves to a Family Settings section on Data
Feeds. Sending rules + Notification Rules unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): Phase C — MeshCore bot-behavior parity (observe channels / ignore contacts / DMs)

Add meshcore context (observe channels by name, ignore contacts, DM policy)
and wire the MeshCore inbound path to honor it, mirroring Meshtastic's
observe/ignore filtering. Symmetric "Bot behavior" sections on both
Connection pages. Meshtastic path unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): Phase D — dedupe Environment/Adapter Config into one Data Feeds surface

Curated family panels are the single home for the shared adapter keys;
Adapter Config becomes an Advanced/raw escape hatch (owned keys no longer
double-editable). Surface include_in_llm_context per adapter. Fix the
adapter-config array-vs-object parsing (fire digest values now load).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): Phase E — Activity Log (per-mesh broadcast feed); remove subscription backend

Replace Alerts with an Activity Log fed by per-mesh broadcast logging
(transport+channel+success on mesh_broadcasts_out, additive migration).
Remove the entire subscription backend (commands, DM dispatch, storage,
API) and its UI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:57:24 -06:00
11bac716d0
feat(dashboard): send-test-message + MeshCore channel list (#14)
Add POST /api/mesh/test-send (fire a labeled test broadcast on a chosen
mesh+channel via the live transport) and GET /api/meshcore/channels
(surface the companion's enumerated channel names). "Send test message"
cards on both Connection pages, with the MeshCore one listing real
channels. Lets the operator confirm a mesh's send path on demand.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:06:31 -06:00
47b56adab5
refactor(transport): derive active transports from config, drop transport setting (#13)
* refactor(transport): derive active transports from config, drop transport setting

A mesh is active when its connection is configured: Meshtastic is the
always-on base; MeshCore runs whenever meshcore_host is set (blank = off);
both configured = both. Removes the transport mode field/toggle entirely
so there's no separate flag to miss.

* docs: fix stale transport comment after field removal

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-03 00:18:53 -06:00
de1e58aa71
feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm) (#11)
* feat(routing): MeshCore as first-class delivery types (meshcore_broadcast/dm)

Replace the composite auto-fan with explicit per-mesh delivery types so
each family independently controls broadcast/DM per severity on Meshtastic
AND MeshCore. mesh_broadcast->Meshtastic only, meshcore_broadcast->MeshCore
(by channel name), mesh_dm/meshcore_dm likewise; routing via the existing
transport hint. Adds meshcore_dm_contacts. Meshtastic-only configs
unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(routing): deliver meshcore_broadcast via meshcore_channel through CompositeTransport

The hinted _broadcast path passed the channel NAME on the `channel` kwarg,
which MeshCoreTransport ignores (it reads meshcore_channel), so
meshcore_broadcast silently no-op'd on transport=both configs. Route the
meshcore child via meshcore_channel and the meshtastic child via channel.
Fix the test that asserted the broken kwarg layout. Add the new delivery
types to the remaining enumeration/validation sites (channel-test endpoint,
scheduler digest chunking, danger-zone valid set).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): split Notifications into Meshtastic and MeshCore sections

Delineate per-mesh routing: each family configures Meshtastic delivery
(mesh_broadcast/mesh_dm, channel index, node IDs) and MeshCore delivery
(meshcore_broadcast/meshcore_dm, channel name, contacts) in separate
sections; shared settings (enable/severity/regions/email/webhook) once.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): first-class MeshCore nav section + dedicated pages

Group the sidebar into Meshtastic and MeshCore sections. Promote MeshCore
routing and connection to their own pages; move MeshCore routing out of
Notifications (which stays Meshtastic + shared family settings). Add
placeholder Contacts and Companion pages for the follow-on companion data
API. No backend change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(dashboard): parallel MT/MC nav order + symmetric connection links

Order both nav groups Connection/Routing/Mesh(Contacts)/Sources(Companion).
Replace the prominent MeshCore block on the Meshtastic Connection page with
a single subtle cross-link, mirrored on the MeshCore Connection page.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dashboard): focused Meshtastic Connection/Sources pages for MT/MC parity

Meshtastic Connection and Sources are now their own focused pages
(mirroring MeshCore), instead of deep-linking into the full Config page.
Global settings move to a restored top-level Config item. No duplicate
editors; connection cross-links are mirror-image between the two pages.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 22:42:14 -06:00
24cb6a31df
feat(dashboard): MeshCore transport + per-family routing GUI controls (#10)
* feat(dashboard): MeshCore transport + per-family routing GUI controls

Add Transport mode selector (Meshtastic/MeshCore/Both) and MeshCore
host/port fields to the Config Connection section, and an independent
per-family "MeshCore channel" number input in Notifications (blank = not
broadcast on MeshCore, sends null). Extends the ConnectionConfig and
per-family toggle TS types.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(routing): MeshCore routing by channel name, not index

MeshCore channels are {name,PSK} (up to 40+ slots, not Meshtastic's 0-7).
The send index is a fragile slot position, so store the channel NAME per
family and resolve name->slot against the companion's live channel table
at send time; never blind-send to an unresolved slot. GUI field becomes a
channel-name text box. meshtastic path unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(routing): thread per-family meshcore_channel through the broadcast send path

MeshBroadcastChannel now carries the rule's meshcore_channel name and
passes it to send_message, so per-family MeshCore routing actually fires
end-to-end (dispatcher -> channel -> composite -> MeshCoreTransport).
Meshtastic path unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:49:59 -06:00
6bc57709a2
feat(broadcast): fit all mesh message formats to the packet budget (#8)
Tighten broadcast formats to fit 140 chars with critical info preserved:
traffic (directions/milepost never abbreviated, narrative trimmed from
end), nws hazard wording tightened (towns already path-sampled), fires
(drop ID line + ** + discovery time), avy (advice -> first sentence),
satpass/quake safety cap. Fire digest broadcast disabled by default.
All budget-aware via the shared max_chars.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:49:33 -06:00
7c15fa3f09
feat(reply): cap interactive LLM replies to 3 mesh packets (#7)
Add terse-answer guidance to the interactive system prompt and a hard
ceiling of 3 packets (3 x connector.max_chars) on LLM replies, with an
"ask for more" indicator when truncated. Protects LoRa airtime from
runaway replies. Broadcast chunking unchanged.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:49:10 -06:00
ff3ded8ca2
feat(routing): independent per-family MeshCore channel (#9)
Add meshcore_channel (Optional, default None) to each notification family
toggle, routed independently of the Meshtastic broadcast_channel. On a
broadcast the Meshtastic child uses broadcast_channel and the MeshCore
child uses meshcore_channel; an unset meshcore_channel means the family
does NOT broadcast on MeshCore (no default, no parallel to Meshtastic).
Additive; Meshtastic-only behavior unchanged.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 14:47:52 -06:00
f3df7a0a6d
feat(transport): CompositeTransport for dual Meshtastic+MeshCore (Phase 4) (#6)
* feat(transport): CompositeTransport for dual Meshtastic+MeshCore (Phase 4)

Adds CompositeTransport (transport: both) that fans broadcasts to both
meshes, sizes to min(children) for uniform messages, and routes DM
replies back over the originating mesh via a transport hint threaded from
the inbound MeshMessage. Per-child self-filtering; supervisor watchdog now
resolves the Meshtastic child inside the composite. Additive/optional
throughout; single-transport behavior unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(sizing): fixed universal mesh budget (mesh_max_chars=140)

Replace per-transport / min-of-active max_chars with a single fixed
universal constant (mesh_max_chars, default 140 = MeshCore LCD). Every
message is built once against one deterministic budget regardless of
which radios are connected; no runtime variance, no per-transport
retooling. Meshtastic sizing intentionally moves 200 -> 140.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 12:33:17 -06:00
225a5d37df
refactor(sizing): single mesh packet budget via transport.max_chars (Phase 3) (#5)
Route all mesh message sizing (renderer, digest, reply chunker, NWS
one-packet fit) through the active transport's max_chars instead of
scattered literal 200s. Meshtastic pinned at 200 (byte-identical output);
MeshCore uses its configured ~140. Sets up uniform-to-smaller sizing for
the composite transport. No behavior change on the Meshtastic path.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:46:11 -06:00
316ae7351e
feat(transport): MeshCoreTransport over pyMC companion TCP (Phase 2) (#4)
* feat(transport): MeshCoreTransport over pyMC companion TCP (Phase 2)

Implements MeshCoreTransport (MeshTransport impl) using the meshcore lib
over TCP to a pyMC companion frame server, bridged behind the sync
interface via a dedicated event-loop thread. Outbound channel/DM sends,
inbound message normalization into MeshMessage(transport="meshcore"),
contact/self lookups. Factory wires transport="meshcore"; supervisor is
now transport-aware (Meshtastic watchdog guarded). Dormant unless
configured; meshtastic path unchanged; full suite matches baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(transport): sync loop-thread readiness before dispatch (MeshCore)

Wait on a threading.Event set from inside the event loop (via call_soon)
before dispatching the first coroutine in connect(), eliminating a startup
race where run_coroutine_threadsafe could be rejected by an is_running()
pre-check before run_forever() had begun spinning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(transport): MeshCore broadcasts use configured channel index

The channel arg carries Meshtastic-index semantics that don't map to
MeshCore's channel table; broadcasts now always use the configured
meshcore_channel_index (also fixes explicit channel=0 being treated as
falsy). DM path unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 10:15:39 -06:00
61278ece28
refactor(transport): introduce MeshTransport abstraction (Phase 1) (#3)
Behavior-preserving seam for a future MeshCore transport. Adds a
MeshTransport ABC + factory; renames MeshConnector -> MeshtasticTransport
(with a back-compat alias); generalizes MeshMessage additively (transport
tag, optional packet); adds a `transport` config field defaulting to
"meshtastic". No runtime behavior change; full suite matches baseline.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 09:33:50 -06:00
3c3ad3f42e
fix(nws): keep weather alerts in one packet without dropping storm path (#1)
Weather alerts ran ~250-310 chars and were blind-sliced to 200 in the
central consumer, silently dropping storm motion + the impacted-town
list. The town list was also pre-capped to 80 chars at parse time,
destroying the middle/end of the storm path before formatting.

- nws_handler: preserve the full impacted-town list; when the message
  overflows one mesh packet, sample the path (first -> middle -> last)
  instead of truncating the tail, so both path endpoints survive
- consumer: pass precomposed titles through verbatim (no [:200] chop)
- adapter_config: add nws.single_packet_max_chars (default 200)
- tests: path-sampling + short-list coverage

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 22:14:26 -06:00
e6bc101194 fix(nws): normalize severities to schema; drop CAP pre-filter
Remove the nws_handler broadcast_severities/warning_suffix_promotes
pre-filter (GATE A) that dropped sub-Severe NWS products before the
pipeline. All NWS alerts now normalize to routine/priority/immediate
(map_severity) and breadth is governed solely by the per-toggle
dispatcher threshold. Warning-class categories are promoted to
immediate so a wrong/missing CAP severity int cant under-rank a real
warning. broadcast_severities/warning_suffix_promotes are now inert
(marked deprecated). Fixes sub-Severe alerts (Special Weather
Statements / advisories) for Magic Valley / East Idaho never reaching
the mesh despite a routine toggle threshold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 00:12:27 +00:00
Ubuntu
e982cbbdf6 refactor(danger-zones): drop min_severity + position_max_age; table snow (grayed/inert)
- alert on any hazard touching a node (severity gate removed)
- alert regardless of position staleness; only skip nodes with no position
- snow tabled: grayed-out in GUI + skipped in correlator pending snowfall+elevation pipeline

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:56:22 +00:00
Ubuntu
95b1a23ef1 feat(danger-zones): configurable infra-node hazard correlation + fire age-gate
- danger_zones config section (isolated dataclass + danger_zones.yaml + GUI panel on Notifications page); defaults disabled + dry_run
- DangerZoneCorrelator: distance-based correlation of hazard events vs infra nodes (CLIENT_BASE/ROUTER/ROUTER_LATE), DM delivery, cooldown
- fire age-gate (wfigs max_declare_age_seconds) suppresses stale/closed fires announced as "New"
- tests: correlator + fire-gate boundary; wfigs fixture fix

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:33:54 +00:00
Ubuntu
2e1fb325f7 refactor: move source tree into work/, multi-stage Docker build, fix satpass
- Move all application source (meshai/, dashboard-frontend/, tests/,
  config/, docs/, Dockerfile, etc.) into work/ directory
- Add Node.js multi-stage build to Dockerfile for frontend compilation;
  remove compiled static assets from git tracking
- Fix satpass missing time windows: consolidation was splitting wire on
  newline and only putting line 1 in event.title, dropping the time
  window line that the composer uses for precomposed broadcasts
- Fix satpass burst flooding: stagger consolidation timers (+60s per
  pending pass) so Central batch publishes don't blast the mesh
- Update CI workflow build context to work/
- Anchor lib/ and data/ gitignore patterns to repo root to prevent
  false matches on nested directories
- Add dashboard-frontend/node_modules/ to .dockerignore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-16 03:40:31 +00:00