mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
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>
This commit is contained in:
parent
24cb6a31df
commit
de1e58aa71
19 changed files with 1870 additions and 219 deletions
|
|
@ -216,15 +216,15 @@ def test_webhook_channel_uses_webhook_renderer():
|
|||
|
||||
# ============================================================
|
||||
# PER-FAMILY MESHCORE ROUTING — end-to-end threading guard
|
||||
# (regression guard for the broadcast send-path gap)
|
||||
# Updated for the explicit-per-mesh model (meshcore_broadcast/mesh_broadcast)
|
||||
# ============================================================
|
||||
|
||||
def test_broadcast_threads_meshcore_channel_through_factory():
|
||||
"""create_channel(rule) -> MeshBroadcastChannel.deliver must pass BOTH
|
||||
channel=<broadcast_channel> AND meshcore_channel=<name> to send_message.
|
||||
def test_mesh_broadcast_routes_to_meshtastic_only():
|
||||
"""mesh_broadcast passes transport='meshtastic' and channel index to
|
||||
send_message. meshcore_channel is NOT passed (auto-fan removed).
|
||||
|
||||
This is the regression guard for the gap where the rule's
|
||||
meshcore_channel never reached connector.send_message.
|
||||
Regression guard: before this model, mesh_broadcast also threaded
|
||||
meshcore_channel through; now it is Meshtastic-only.
|
||||
"""
|
||||
from meshai.config import NotificationRuleConfig
|
||||
from meshai.notifications.channels import create_channel
|
||||
|
|
@ -234,7 +234,7 @@ def test_broadcast_threads_meshcore_channel_through_factory():
|
|||
name="toggle:fire",
|
||||
delivery_type="mesh_broadcast",
|
||||
broadcast_channel=1,
|
||||
meshcore_channel="AIDA",
|
||||
meshcore_channel="AIDA", # present in config but must NOT flow to send_message
|
||||
)
|
||||
|
||||
channel = create_channel(rule, mock_connector)
|
||||
|
|
@ -254,54 +254,63 @@ def test_broadcast_threads_meshcore_channel_through_factory():
|
|||
mock_connector.send_message.assert_called_once()
|
||||
kwargs = mock_connector.send_message.call_args.kwargs
|
||||
assert kwargs.get("channel") == 1
|
||||
# The load-bearing assertion: the name was NOT dropped.
|
||||
assert kwargs.get("meshcore_channel") == "AIDA"
|
||||
assert kwargs.get("transport") == "meshtastic"
|
||||
# meshcore_channel must NOT be present (no auto-fan).
|
||||
assert "meshcore_channel" not in kwargs or kwargs.get("meshcore_channel") is None
|
||||
|
||||
|
||||
def test_broadcast_meshcore_channel_none_passed_through():
|
||||
"""meshcore_channel=None (family not on MeshCore) => send_message still
|
||||
receives meshcore_channel=None (MeshCore child skipped downstream)."""
|
||||
def test_meshcore_broadcast_routes_to_meshcore_only():
|
||||
"""meshcore_broadcast passes meshcore_channel=name and transport='meshcore'
|
||||
to send_message. This is the explicit MeshCore-only delivery path."""
|
||||
from meshai.config import NotificationRuleConfig
|
||||
from meshai.notifications.channels import create_channel
|
||||
from meshai.notifications.channels import MeshCoreBroadcastChannel, create_channel
|
||||
|
||||
# Simulate a CompositeTransport connector with a meshcore child.
|
||||
mock_connector = MagicMock()
|
||||
mock_connector._by_name = {"meshcore": MagicMock(), "meshtastic": MagicMock()}
|
||||
mock_connector.send_message.return_value = True
|
||||
|
||||
rule = NotificationRuleConfig(
|
||||
name="toggle:weather",
|
||||
delivery_type="mesh_broadcast",
|
||||
broadcast_channel=0,
|
||||
meshcore_channel=None,
|
||||
name="toggle:fire",
|
||||
delivery_type="meshcore_broadcast",
|
||||
meshcore_channel="AIDA",
|
||||
)
|
||||
|
||||
channel = create_channel(rule, mock_connector)
|
||||
assert isinstance(channel, MeshCoreBroadcastChannel)
|
||||
|
||||
payload = NotificationPayload(
|
||||
message="weather alert",
|
||||
category="weather_warning",
|
||||
severity="priority",
|
||||
message="fire alert",
|
||||
category="fire",
|
||||
severity="immediate",
|
||||
timestamp=time.time(),
|
||||
event_type="weather_warning",
|
||||
event_type="fire",
|
||||
chunk_index=0,
|
||||
)
|
||||
|
||||
assert asyncio.run(channel.deliver(payload, rule)) is True
|
||||
|
||||
mock_connector.send_message.assert_called_once()
|
||||
kwargs = mock_connector.send_message.call_args.kwargs
|
||||
assert kwargs.get("channel") == 0
|
||||
assert "meshcore_channel" in kwargs
|
||||
assert kwargs.get("meshcore_channel") is None
|
||||
assert kwargs.get("meshcore_channel") == "AIDA"
|
||||
assert kwargs.get("transport") == "meshcore"
|
||||
assert kwargs.get("destination") is None
|
||||
|
||||
|
||||
def test_broadcast_render_loop_threads_meshcore_channel():
|
||||
"""Non-prechunked path (renderer loop) also threads meshcore_channel
|
||||
on every chunk send."""
|
||||
"""Non-prechunked path (renderer loop) for meshcore_broadcast threads
|
||||
meshcore_channel on every chunk send."""
|
||||
from meshai.config import NotificationRuleConfig
|
||||
from meshai.notifications.channels import create_channel
|
||||
|
||||
mock_connector = MagicMock()
|
||||
# Connector has a meshcore child so the no-op guard passes.
|
||||
mock_connector._by_name = {"meshcore": MagicMock(), "meshtastic": MagicMock()}
|
||||
mock_connector.send_message.return_value = True
|
||||
|
||||
rule = NotificationRuleConfig(
|
||||
name="toggle:fire",
|
||||
delivery_type="mesh_broadcast",
|
||||
broadcast_channel=2,
|
||||
delivery_type="meshcore_broadcast",
|
||||
meshcore_channel="AIDA",
|
||||
)
|
||||
channel = create_channel(rule, mock_connector)
|
||||
|
|
@ -318,5 +327,5 @@ def test_broadcast_render_loop_threads_meshcore_channel():
|
|||
assert asyncio.run(channel.deliver(payload, rule)) is True
|
||||
assert mock_connector.send_message.call_count >= 2
|
||||
for call in mock_connector.send_message.call_args_list:
|
||||
assert call.kwargs.get("channel") == 2
|
||||
assert call.kwargs.get("meshcore_channel") == "AIDA"
|
||||
assert call.kwargs.get("transport") == "meshcore"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue