From bbed52a9623fafd71e73f6a59b4d276cb968808d Mon Sep 17 00:00:00 2001 From: anti Date: Tue, 28 Apr 2026 23:53:25 -0400 Subject: [PATCH] =?UTF-8?q?fix(bus):=20topic=20segments=20can't=20contain?= =?UTF-8?q?=20dots=20=E2=80=94=20service.added=20=E2=86=92=20service=5Fadd?= =?UTF-8?q?ed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bus topic segments are NATS-style tokens and the validator at bus/topics.py:402 rejects '.', '*', '>', whitespace. My W3 constants 'service.added' / 'service.removed' tripped this on every live add/remove call: ValueError: topic segment 'service.added' may not contain '.', ... Renamed both to underscore form: DECKY_SERVICE_ADDED = 'service_added'. Aligned the SSE forwarder's name mapping (decky..service_added → SSE event 'decky.service_added') and the frontend's useTopologyStream listener + MazeNET.tsx event handler. Also updated the wiki entry with a note about the underscore. --- decnet/bus/topics.py | 4 ++-- decnet/web/router/topology/api_events.py | 9 +++++++-- decnet_web/src/components/MazeNET/MazeNET.tsx | 4 ++-- decnet_web/src/components/MazeNET/useTopologyStream.ts | 8 ++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/decnet/bus/topics.py b/decnet/bus/topics.py index d06492eb..d3002e73 100644 --- a/decnet/bus/topics.py +++ b/decnet/bus/topics.py @@ -88,8 +88,8 @@ DECKY_MUTATION = "mutation" # ``topology_id``, and ``services`` (the post-mutation list). Consumers # that watch substrate shape (correlator, dashboard, profiler) reconcile # off these without waiting for the next decnet-state.json snapshot. -DECKY_SERVICE_ADDED = "service.added" -DECKY_SERVICE_REMOVED = "service.removed" +DECKY_SERVICE_ADDED = "service_added" +DECKY_SERVICE_REMOVED = "service_removed" # Attacker event types (second token under the ``attacker`` root). First # sighting, session boundary transitions, and score-threshold crossings diff --git a/decnet/web/router/topology/api_events.py b/decnet/web/router/topology/api_events.py index bc360af7..c5a6f3c4 100644 --- a/decnet/web/router/topology/api_events.py +++ b/decnet/web/router/topology/api_events.py @@ -177,10 +177,15 @@ def _sse_name_for(topic: str) -> str: ``topology..mutation.applied`` → ``mutation.applied`` ``topology..status`` → ``status`` - ``decky..service.added`` → ``decky.service.added`` - ``decky..service.removed`` → ``decky.service.removed`` + ``decky..service_added`` → ``decky.service_added`` + ``decky..service_removed`` → ``decky.service_removed`` Anything else is passed through unchanged so future topic families don't silently collapse onto a generic bucket. + + Bus topic segments are NATS-style tokens — no dots inside a segment + — which is why the leaf is ``service_added`` (underscore) here and + on the wire, not ``service.added``. The frontend's + ``useTopologyStream`` listens on the underscore form too. """ parts = topic.split(".", 2) if len(parts) < 3: diff --git a/decnet_web/src/components/MazeNET/MazeNET.tsx b/decnet_web/src/components/MazeNET/MazeNET.tsx index fe5cf022..6fe86783 100644 --- a/decnet_web/src/components/MazeNET/MazeNET.tsx +++ b/decnet_web/src/components/MazeNET/MazeNET.tsx @@ -679,8 +679,8 @@ const MazeNET: React.FC = () => { // patch local state so the chip set reflects shape without a full // re-hydrate. The post-mutation services list lives on the // payload; same shape the actor's POST/DELETE response carries. - if (event.name === 'decky.service.added' - || event.name === 'decky.service.removed') { + if (event.name === 'decky.service_added' + || event.name === 'decky.service_removed') { const p = event.payload ?? {}; const deckyName = typeof p.decky_name === 'string' ? p.decky_name : null; const services = Array.isArray(p.services) ? p.services as string[] : null; diff --git a/decnet_web/src/components/MazeNET/useTopologyStream.ts b/decnet_web/src/components/MazeNET/useTopologyStream.ts index 4e57beae..7c8fa33d 100644 --- a/decnet_web/src/components/MazeNET/useTopologyStream.ts +++ b/decnet_web/src/components/MazeNET/useTopologyStream.ts @@ -21,8 +21,8 @@ export type TopologyStreamEventName = // server. The payload carries decky_name + service_name + the // post-mutation services list, so a second tab can reconcile shape // without a refetch. - | 'decky.service.added' - | 'decky.service.removed'; + | 'decky.service_added' + | 'decky.service_removed'; export interface TopologyStreamEvent { name: TopologyStreamEventName | string; @@ -46,8 +46,8 @@ const NAMED_EVENTS: TopologyStreamEventName[] = [ 'mutation.applied', 'mutation.failed', 'status', - 'decky.service.added', - 'decky.service.removed', + 'decky.service_added', + 'decky.service_removed', ]; export function useTopologyStream({