diff --git a/dashboard-frontend/src/components/NodeDetail.tsx b/dashboard-frontend/src/components/NodeDetail.tsx
index 54570f9..31016d0 100644
--- a/dashboard-frontend/src/components/NodeDetail.tsx
+++ b/dashboard-frontend/src/components/NodeDetail.tsx
@@ -97,7 +97,13 @@ export default function NodeDetail({
}
})
- // Sort by SNR descending
+ // SNR quality bands (also the legend behind the colored quality dots):
+ // >12 excellent — reliable mesh hop
+ // 8-12 good
+ // 5-8 fair — works in clear conditions
+ // 3-5 marginal — will drop under load
+ // <3 poor — intermittent
+ // Sort by SNR descending
return neighborData.sort((a, b) => b.snr - a.snr)
}, [node, edges, nodes])
diff --git a/dashboard-frontend/src/components/NodeTable.tsx b/dashboard-frontend/src/components/NodeTable.tsx
index aed0930..b3e234a 100644
--- a/dashboard-frontend/src/components/NodeTable.tsx
+++ b/dashboard-frontend/src/components/NodeTable.tsx
@@ -210,13 +210,13 @@ export default function NodeTable({
className="px-3 py-2 text-left cursor-pointer hover:text-slate-200"
onClick={() => handleSort('battery_level')}
>
- Battery
Per-adapter tunables (thresholds, freshness windows, toggles, curation lists). Changes take effect on the next handler call -- no container restart needed. - Sentence templates, emoji, and translation maps live in code by design. + Sentence templates, emoji, and translation maps live in code by design — see the CODE rule under Adapter Config & the CODE Rule in Reference. The LLM context toggle on each card gates whether that adapter's data lands in the system prompt when you DM the bot; broadcasts are unaffected.
{allAdapters.map((adapter) => { diff --git a/dashboard-frontend/src/pages/Alerts.tsx b/dashboard-frontend/src/pages/Alerts.tsx index e6faa5c..dd4ec28 100644 --- a/dashboard-frontend/src/pages/Alerts.tsx +++ b/dashboard-frontend/src/pages/Alerts.tsx @@ -553,7 +553,7 @@ export default function Alerts() {No active subscriptions.
- Manage subscriptions via !subscribe on mesh
+ Manage subscriptions via !subscribe on mesh. Broadcasts arrive with one of three prefixes — New: (first sight), Update: (material change), or Active: (clock-driven reminder while the event is still live). See Broadcast Types and Reminder System in Reference.
- NWS-AHPS stream gauge thresholds curated for the nwis_handler. Disabled rows are - ignored at envelope time. Changes propagate to the handler on the next event. + NWS-AHPS stream gauge thresholds for the USGS NWIS handler. Each row pairs a USGS site_id with a human gauge name, lat/lon, and four flood thresholds (Action / Minor / Moderate / Major, all in feet). Disabled rows still ingest into gauge_readings -- they don't broadcast. The USGS lookup button auto-populates name + coords + thresholds from USGS Site Service + NWS NWPS when this adapter is on native feed_source; Central-feed mode disables it (see Reference → OR-not-AND for why). Changes take effect on the next event.
{adding &&+ FIRMS hotspots are fast but noisy; WFIGS incidents are accurate but slow. + The Fire Tracker fuses both feeds and a per-pixel attribution graph so a + single fire's name, declared acreage, real-time perimeter movement, and + spotting events all land as separate broadcasts on the mesh. +
+ +Six fire-family alert categories, in order of when they fire during an incident's lifecycle:
+
+ Twice a day (default 06:00 and 18:00 Mountain Time) the bot runs an LLM
+ summary across every active fire and the last 24 h of growth + spotting
+ events, then broadcasts one terse line to the mesh. Shape:{' '}
+ "Fires today: Cache Peak 1,847 ac +200 NE; Twin Peaks 320 ac stable; possible new fire 15 mi from Cache Peak."{' '}
+ Configure the schedule and timezone under
+ When a FIRMS hotspot lands, the bot walks every active fire (those not
+ yet tombstoned) and matches by Haversine distance to that fire's running
+ centroid. If the pixel is within the fire's
+ Pixels that match no fire feed the cluster detector instead: if at least{' '}
+
+ Each VIIRS pass groups pixels into a
+ Once a pass closes its perimeter (a GeoJSON polygon stored on the
+ fire), every subsequent attributed pixel runs a point-in-polygon test.
+ Pixels outside the polygon with a vertex distance ≥{' '}
+
+ Every broadcast the bot sends to the mesh carries a one-word prefix that + tells you what kind of update it is. Three types: +
++ The bot tracks first-broadcast time and last-broadcast time separately + on every event row, so a New: prefix is only emitted once even after a + container restart. Update: respects per-adapter cooldowns (WFIGS is 8 h + by default; ITD 511 is per-incident). Active: is the reminder system, + covered in the next section. +
+
+ Some events stay live for days. A wildfire doesn't go out because
+ WFIGS stopped publishing updates; a geomagnetic storm doesn't end
+ because SWPC went quiet on the wire. The reminder system fires a
+ clock-driven{' '}
+
+ When a WFIGS update declares an incident closed, the bot stamps{' '}
+
+ Per-adapter on/off lives in
- MeshAI isn't just commands — you can ask it questions in plain English. "How's the mesh doing?" "Is there any ducting?" "What's the fire situation?" "How's traffic on I-84?" It uses the live environmental data and mesh health data to answer. + Bang commands are the short, predictable interface. For anything that + doesn't map cleanly to a single command — "how's the mesh doing?", + "is there any ducting?", "why didn\'t I hear about anything today?" + — you can DM the bot in plain English. The LLM DM path covers the + same data the commands cover, plus the dispatcher drop audit, with + honest "no data" answers when a feed is quiet. Full catalog under{' '} + LLM DM + Queries. +
+
+ Bang commands like
+ When you DM the bot a question, the env_reporter layer assembles up to + seven data blocks and injects them into the LLM's system prompt. Each + block maps to one adapter: +
++ The bot is told to answer only from the blocks in the system + prompt. If a block is empty (no recent quakes, no active NWS alerts), + the response is honest about it: "No active weather alerts right now," + not a fabricated "144 earthquakes worldwide in the past 24 hours." + That clamp closes the failure mode where the LLM defaulted to its + training data when local tables were quiet. +
+ +
+ The
+ The bot has no general internet access. Questions that need data the + env_reporter doesn't carry ("what's the weather forecast tomorrow", + "who's the current president") fall back to whatever the configured + LLM backend knows from training. The grounding clamp keeps the bot + from inventing local data, but it can't keep the LLM from speculating + about non-local topics. +
++ Every environmental adapter pulls its data from one of two places: +
++ An adapter is set to either Central or{' '} + native, never both. Running both at the same time is what the + codebase calls the AND-mode anti-pattern: two independent + poll loops on the same upstream feed, duplicate broadcasts, duplicate + cursor state, no shared dedup. The Spokane-class leak (cross-state + broadcasts that escaped the bbox filter in May 2026) was caused by + an inadvertent AND-mode on the traffic adapter; the fix made the + gate enforce mutual exclusion at boot and on every config save. +
+ +
+ Set
+ On the GUI, adapters with no Central counterpart yet show + their Central button disabled with a "native only" tooltip. That's + not an AND state; the adapter is still single-source, just locked to + native by upstream availability. +
+ +
+ You'll see "AND-model anti-pattern" referenced in two places: the
+ USGS-lookup button on Gauge Sites (disabled when the USGS adapter is
+ on Central, because doing a one-off direct USGS poll from the GUI
+ while the runtime is on Central is precisely the AND-mode this rule
+ forbids) and the env_routes 404 response on{' '}
+
+ The Adapter Config page is the single hub for ~50 GUI-editable knobs + across the 13 adapters that touch the broadcast pipeline. Changes + take effect on the next handler call — no container restart needed + for most keys. +
+ ++ Not everything tunable becomes a GUI row. The codebase splits along + one rule: +
++ If you find yourself wanting to add a wire-string template or an + emoji to the GUI, stop — that's CODE. If you want to change a + threshold or a curation list, the GUI is the right place. +
+ ++ Most keys take effect on the next handler call (the env_store re-reads + from the database). A short list requires a container restart, because + they govern startup-only wiring: +
++ When you save one of those keys via the GUI, a yellow Restart-Required + banner surfaces at the top of the page with a "Restart now" button. + Until you click it, the on-disk config and the running config + intentionally disagree — that's the OR-not-AND gate refusing to + transition mid-flight. +
+ +
+ Each adapter's card on Adapter Config carries a per-adapter
+ "LLM context" switch. When off, that adapter's
+ Two curation tables drive the broadcast text the bot puts on the mesh. + Both are CRUD UIs with per-row enable/disable; both fall through to + fallback chains when a row is missing or disabled. +
+ ++ Stream gauge thresholds for the USGS NWIS handler. Each row pairs a + USGS site_id with a human gauge name, lat/lon, and four NWS-AHPS + flood thresholds in feet: Action, Minor, Moderate, Major. The + handler compares an incoming gauge reading to those thresholds and + emits the right broadcast severity. +
++ USGS lookup button — when you add a new row in + native-feed mode, the lookup queries the USGS Site Service plus NWS + NWPS to auto-populate name, coordinates, and flood stages. In + central-feed mode the button is disabled with a tooltip: a one-off + direct USGS poll from the GUI while the runtime is on Central is the + AND-mode anti-pattern the architecture forbids. Enter values + manually or pull them from Central. +
+
+ Disabled rows are ignored at dispatch time. The
+ corresponding gauge still ingests into
+ Lookup table for the "X mi {'<'}bearing{'>'} of {'<'}town{'>'}" suffix + in broadcast text. When a fire or NWS alert renders, the bot walks an + anchor chain to figure out where to say it is: +
++ Each row carries a name (lowercased on save), state, lat/lon, and an + enable flag. The "lowercased on save" rule keeps "Almo" / "ALMO" / + "almo" from being three distinct rows. Disabled rows fall through to + the next anchor in the chain — the broadcast text still goes out, it + just uses a different anchor. +
++ Example broadcast text rendered from a Town Anchors row:{' '} + "🔥 New: Cache Peak Fire (WF), 3 mi N of Almo: 250 ac, 0% contained, @ 42.118,-113.643" +
+
+ MeshAI persists state in a single SQLite database
+ (
+ A migration failure leaves the database at the prior version and
+ raises in the runner. Container logs surface the SQL error;{' '}
+
- Lookup table for the "X mi <bearing> of <town>" anchor in wire-string rendering. - Disabled rows fall through to the generic anchor chain. + Lookup table for the "X mi <bearing> of <town>" suffix in the bot's broadcast text. + When a fire or NWS alert renders, the bot walks: Photon nearest-town → this table → landclass → + county/state → bare coords. Disabled rows fall through to the next anchor in the chain; the + broadcast still goes out, it just uses a different anchor. Example: "3 mi N of Almo". + See Reference → Curation: Gauges & Towns for the full chain.
{adding &&