From 18b1e034fd7e9702933a35df05e7001e4f67f78b Mon Sep 17 00:00:00 2001 From: Jean-Pierre Martineau Date: Sun, 26 Jul 2026 17:26:39 -0400 Subject: [PATCH] Node detail: light route labels through coalesced hop ids MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Routes tab mutes every label off the selected route, testing membership against the route's raw draw hops. But the path graph folds a short hop into the wide id it can only be among the drawn routes (a 1-byte trace hop beside the same relay's full contact id), so a route recorded at the short width greyed the very marker its white line rides through — TSFCT SUTTON-680M's bf→8e→3d route muted YUL-Cartierville. Decide membership in the graph's own id space instead, by running the layers through the same coalesce the widget applies. Co-Authored-By: Claude Fable 5 --- meshterm/ui/node_detail_screen.py | 7 ++++++- tests/test_node_detail.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/meshterm/ui/node_detail_screen.py b/meshterm/ui/node_detail_screen.py index 3152f82..40cd2cf 100644 --- a/meshterm/ui/node_detail_screen.py +++ b/meshterm/ui/node_detail_screen.py @@ -75,6 +75,7 @@ from .pathgraph import ( LabelOf, LabelRgbOf, PathLayer, + _coalesce_prefixes, bidir_clusters, render_path_graph, ) @@ -649,7 +650,11 @@ class NodeDetailScreen(Screen): # A node keeps its name hue only while it sits on the selected route (its endpoints # always do); every other node in the fan fades its label to the same grey its line # went, so the picture reads as *this* route rather than the whole tangle. - on_route = set(rv.routes[sel].draw) | {SRC_NODE, DST_NODE} + # Membership is decided in the graph's own id space: the widget folds a short hop + # into the wide id it can only be among these routes (a 1-byte trace hop beside the + # same relay's full contact id), so the raw draw hops would grey the very marker the + # selected line rides through. + on_route = set(_coalesce_prefixes(layers)[sel].hops) | {SRC_NODE, DST_NODE} base_rgb = rv.label_rgb_of assert base_rgb is not None diff --git a/tests/test_node_detail.py b/tests/test_node_detail.py index d141a41..3642d87 100644 --- a/tests/test_node_detail.py +++ b/tests/test_node_detail.py @@ -572,6 +572,37 @@ def test_node_detail_enter_on_a_route_row_opens_its_trace() -> None: assert resolved == ["trace"] and screen.selected_spec() == "a1,f2,a1" +def test_route_labels_light_through_a_coalesced_hop() -> None: + """A route drawn from a short hop id lights the wide marker the graph folds it into. + + Route evidence reaches the page at mixed hash widths — a 1-byte trace hop (``3d``) + beside the same relay's full id on a sibling route. The graph coalesces the short id + into the one wide marker, so the label muting must test membership against the *drawn* + ids: keyed on the raw draw hops, the selected route's own relay rendered muted while + the white line rode straight through it (the TSFCT SUTTON-680M report). + """ + lit: list[str] = [] + + def base_rgb(node: str) -> tuple[int, int, int]: + lit.append(node) # the stage's muting wrapper only consults us for on-route nodes + return (200, 200, 200) + + routes = _RoutesView( + routes=[ + _Route(draw=("bf61f2fb1d9e", "3d63c6429436"), spec="s0", row=Text("wide")), + _Route(draw=("bf61f2fb1d9e", "3d"), spec="s1", row=Text("short")), + ], + glyph_of=lambda n: ("●", "#ffffff"), + label_of=lambda n: n[:2], + label_rgb_of=base_rgb, + ) + screen = _screen(routes=routes, tabs=[_Tab("Routes", "routes")]) + screen.note_viewport(30) + screen.handle("down") # select the short-hop route + screen.render_body(72) + assert "3d63c6429436" in lit # the relay it rides through keeps its hue + + def test_node_detail_route_list_windows_inside_the_page() -> None: """With more routes than fit, the list windows with edge markers — the graph and the action rows never leave the screen, however many routes a busy node has."""