mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
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>
This commit is contained in:
parent
11bac716d0
commit
0460462485
38 changed files with 2502 additions and 2342 deletions
50
work/tests/test_meshcore_context_config.py
Normal file
50
work/tests/test_meshcore_context_config.py
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
"""Config round-trip tests for the MeshCore passive-context block.
|
||||
|
||||
Verifies that ``meshcore_context`` survives save_config -> load_config, and
|
||||
that a YAML lacking the section yields the dataclass defaults (proving the
|
||||
generic nested-dataclass loader branch handles it with no special-casing).
|
||||
"""
|
||||
|
||||
import yaml
|
||||
|
||||
from meshai.config import (
|
||||
Config,
|
||||
MeshCoreContextConfig,
|
||||
load_config,
|
||||
save_config,
|
||||
)
|
||||
|
||||
|
||||
def test_meshcore_context_round_trip(tmp_path):
|
||||
cfg = Config()
|
||||
cfg.meshcore_context = MeshCoreContextConfig(
|
||||
enable_passive_context=False,
|
||||
observe_channels=["#aida", "#general"],
|
||||
ignore_contacts=["a1b2", "SpamNode"],
|
||||
respond_to_dms=False,
|
||||
)
|
||||
|
||||
path = tmp_path / "config.yaml"
|
||||
save_config(cfg, path)
|
||||
loaded = load_config(path)
|
||||
|
||||
mc = loaded.meshcore_context
|
||||
assert isinstance(mc, MeshCoreContextConfig)
|
||||
assert mc.enable_passive_context is False
|
||||
assert mc.observe_channels == ["#aida", "#general"]
|
||||
assert mc.ignore_contacts == ["a1b2", "SpamNode"]
|
||||
assert mc.respond_to_dms is False
|
||||
|
||||
|
||||
def test_meshcore_context_defaults_when_absent(tmp_path):
|
||||
# A minimal YAML with no meshcore_context section at all.
|
||||
path = tmp_path / "config.yaml"
|
||||
path.write_text(yaml.safe_dump({"timezone": "America/Boise"}))
|
||||
|
||||
loaded = load_config(path)
|
||||
mc = loaded.meshcore_context
|
||||
assert isinstance(mc, MeshCoreContextConfig)
|
||||
assert mc.enable_passive_context is True
|
||||
assert mc.observe_channels == []
|
||||
assert mc.ignore_contacts == []
|
||||
assert mc.respond_to_dms is True
|
||||
85
work/tests/test_meshcore_context_filter.py
Normal file
85
work/tests/test_meshcore_context_filter.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
"""Unit tests for the MeshCore passive-context / bot-behavior filter.
|
||||
|
||||
Covers the pure module-level helper ``mc_context_allows`` — no hardware,
|
||||
no meshcore lib, no event loop. MeshMessage and MeshCoreContextConfig are
|
||||
constructed directly.
|
||||
"""
|
||||
|
||||
from meshai.config import MeshCoreContextConfig
|
||||
from meshai.connector import MeshMessage
|
||||
from meshai.transport.meshcore_transport import mc_context_allows
|
||||
|
||||
|
||||
def _dm(sender_id="a1b2", sender_name="Alice", text="hi"):
|
||||
return MeshMessage(
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
text=text,
|
||||
channel=0,
|
||||
is_dm=True,
|
||||
transport="meshcore",
|
||||
)
|
||||
|
||||
|
||||
def _chan(channel=1, text="hello", sender_id=None, sender_name=None):
|
||||
marker = f"chan:{channel}"
|
||||
return MeshMessage(
|
||||
sender_id=sender_id or marker,
|
||||
sender_name=sender_name or marker,
|
||||
text=text,
|
||||
channel=channel,
|
||||
is_dm=False,
|
||||
transport="meshcore",
|
||||
)
|
||||
|
||||
|
||||
def test_cfg_none_always_passes():
|
||||
assert mc_context_allows(None, _dm(), {}) is True
|
||||
assert mc_context_allows(None, _chan(), {}) is True
|
||||
|
||||
|
||||
def test_defaults_pass_through():
|
||||
cfg = MeshCoreContextConfig() # empty lists, passive on, respond_to_dms on
|
||||
idx_to_name = {1: "#general"}
|
||||
assert mc_context_allows(cfg, _chan(channel=1), idx_to_name) is True
|
||||
assert mc_context_allows(cfg, _dm(), idx_to_name) is True
|
||||
|
||||
|
||||
def test_observe_channels_filters_by_name():
|
||||
cfg = MeshCoreContextConfig(observe_channels=["#aida"])
|
||||
idx_to_name = {1: "#general", 2: "#aida"}
|
||||
# idx 1 -> #general -> not in observe list -> DROPPED
|
||||
assert mc_context_allows(cfg, _chan(channel=1), idx_to_name) is False
|
||||
# idx 2 -> #aida -> in observe list -> PASSES
|
||||
assert mc_context_allows(cfg, _chan(channel=2), idx_to_name) is True
|
||||
# idx 3 -> no name mapping -> DROPPED
|
||||
assert mc_context_allows(cfg, _chan(channel=3), idx_to_name) is False
|
||||
|
||||
|
||||
def test_ignore_contacts_matches_id_or_name():
|
||||
cfg = MeshCoreContextConfig(ignore_contacts=["a1b2"])
|
||||
# matched by sender_id
|
||||
assert mc_context_allows(cfg, _dm(sender_id="a1b2", sender_name="Alice"), {}) is False
|
||||
# matched by sender_name
|
||||
assert mc_context_allows(cfg, _dm(sender_id="ffff", sender_name="a1b2"), {}) is False
|
||||
# unrelated DM passes
|
||||
assert mc_context_allows(cfg, _dm(sender_id="c3d4", sender_name="Bob"), {}) is True
|
||||
|
||||
|
||||
def test_respond_to_dms_false_drops_dms_only():
|
||||
cfg = MeshCoreContextConfig(respond_to_dms=False)
|
||||
idx_to_name = {1: "#general"}
|
||||
# any DM dropped
|
||||
assert mc_context_allows(cfg, _dm(), idx_to_name) is False
|
||||
assert mc_context_allows(cfg, _dm(sender_id="zzzz", sender_name="Zed"), idx_to_name) is False
|
||||
# channel msgs unaffected (passive still on)
|
||||
assert mc_context_allows(cfg, _chan(channel=1), idx_to_name) is True
|
||||
|
||||
|
||||
def test_passive_disabled_drops_channel_but_dm_still_respected():
|
||||
cfg = MeshCoreContextConfig(enable_passive_context=False)
|
||||
idx_to_name = {1: "#general"}
|
||||
# channel msg dropped
|
||||
assert mc_context_allows(cfg, _chan(channel=1), idx_to_name) is False
|
||||
# DM still respects respond_to_dms (True by default) -> passes
|
||||
assert mc_context_allows(cfg, _dm(), idx_to_name) is True
|
||||
Loading…
Add table
Add a link
Reference in a new issue