feat(traceroute): participation picker on all sources — brings the strip to MQTT (epic phase 2) - #4427
Conversation
…ion picker Spec for the traceroute picker epic phase 2: a sourceId-scoped participation API (endpoints + intermediate hops, 7-day window, JS matching over Drizzle rows) plus a Node Details dropdown that makes the strip render on MQTT sources for the first time. Covers reuse inventory, file-by-file signatures, the picker/badge coexistence rules, the read||write permission gate, the test plan (repository, route via createRouteTestApp, perSource isolation, component/MQTT cases), and three work packages. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTfGTsPBskKYJUK8SSvYob
tracerouteParticipationKind() classifies how a node took part in a traceroute row — as an endpoint (fromNodeNum/toNodeNum) or an intermediate hop in route/routeBack. Endpoint match wins over hop match. Coerces node numbers with Number() for the PG/MySQL BIGINT-as-string shape; parseHopArray already tolerates malformed JSON. Covers §8.1: endpoint/hop matches both directions, endpoint precedence, non-participant, string-typed node numbers, malformed JSON, and the substring-match regression guard.
…phase 2 §4, WP1) Bounded, source-scoped, newest-first scan through the query builder, filtered in JS via tracerouteParticipationKind — there is no database-agnostic SQL for JSON-array hop membership, and raw SQL is banned outside migrations. scanLimit (default 2000) bounds memory; because the scan orders newest-first, exceeding it can only drop older participations. Covers §8.2 (endpoint/hop matches, time window, ordering, limit, scanLimit tradeoff, participation kind, fail-closed sourceId) and §8.3 (per-source isolation, including hop-only participation, and the explicit ALL_SOURCES opt-in).
…ase 2 §5, WP1) New route backing the Node Details traceroute picker — the only way an MQTT source (no origin node, so no own-request traceroute) can render the strip at all. sourceId is required (400 MISSING_SOURCE_ID, no ALL_SOURCES fallback — fail closed). Applies maskTraceroutesByChannel (#3092 channel gate) and strips routePositions from the projection (no hop-position leak surface). The two pre-existing handlers in this file are untouched. Covers §8.4 via createRouteTestApp: permission grant/deny per source, anonymous 403, validation ladder (sourceId/nodeNum/hours/limit), endpoint-level source isolation, hop-only participation, absent routePositions, hopCount null-vs-length, and channel masking.
…§6.1, WP1)
Client transport for GET /api/traceroutes/participation/:nodeNum.
Declares TracerouteParticipationEntry next to SignalTrendResult,
following getSignalTrend's declare-here + unwrap-data precedent —
request() deliberately does not unwrap {success,data} (CLAUDE.md).
Structurally satisfies TracerouteStripInput; no adapter needed.
Covers §8.5: encoded query string (sourceId + optional hours/limit),
unwraps data.entries, and returns [] when data is absent.
…route req.user is already typed globally via the Express.Request augmentation in src/types/auth.ts — the (req as any).user cast was unnecessary and tripped the @typescript-eslint/no-explicit-any lint ratchet.
… wiring (epic phase 2 WP2) Adds the client-side half of the traceroute participation picker: a one-shot useNodeTraceroutes hook, the TracerouteParticipationPicker component (native <select>, renders only at >=2 entries), and the MessagesTab coexistence rules (gate widened to read||write, newest-of-both default selection, reset on node change / fresh traceroute, badges/age and the strip memo keyed off the displayed row instead of only the poll row). This is what unlocks the traceroute strip on MQTT sources, which have no own-request traceroute to show. Codes against the §6.1 API contract from TRS_PHASE2_SPEC.md; WP1 landed apiService.getTracerouteParticipation / TracerouteParticipationEntry in parallel in this same worktree. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTfGTsPBskKYJUK8SSvYob
… a QueryClientProvider MessagesTab.composeFocus.test.tsx and MessagesTab.txDisabled.test.tsx render MessagesTab directly (no QueryClientProvider), unlike MessagesTab.tracerouteStrip.test.tsx which wraps it in one. The traceroute participation picker (epic phase 2, WP2) added an unconditional useNodeTraceroutes -> useQuery call in MessagesTab, which throws "No QueryClient set, use QueryClientProvider" in both files. Stub the hook to a deterministic empty list, same pattern already used in MessagesTab.tracerouteStrip.test.tsx. Checked every other src/components/*.test.tsx referencing MessagesTab (NodesTab.test.tsx) — it only mentions MessagesTab in comments and never renders it, so no further stragglers. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTfGTsPBskKYJUK8SSvYob
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTfGTsPBskKYJUK8SSvYob
…rticipation-picker # Conflicts: # src/server/routes/tracerouteRoutes.ts
|
Claude finished @Yeraze's task —— View job PR Review: feat(traceroute): participation picker on all sourcesTasks
Overall AssessmentSolid, well-structured feature addition. The code is clean, follows project conventions, and the PR description accurately reflects the implementation. The test coverage is thorough. I have a few observations ranging from minor nits to one worth discussing. Backend
Clean predicate. The One edge case worth documenting (not a bug): a node that appears in both
The scan-then-JS-filter approach is a reasonable workaround for the JSON-column-search problem across three backends; the 2000-row Route
Frontend
Using
The three-way priority logic (explicit pick → newest-of-both by timestamp → fallback) is well-reasoned and the comment explains all the cases. One subtle point: when
These badges fire when Box gate widening (
Test CoverageTests are comprehensive and well-structured:
One gap: there's no test exercising the Security
SummaryThe PR is in good shape. The main points worth a second look before merge:
None of these are blockers. The feature is well-reasoned and the implementation correctly follows project conventions. |
…narrowed sourceId type - Hidden "Latest" placeholder for the select's value="" state so the picker no longer implies the first option is selected when the displayed row came from the poll (review item 2). - getTraceroutesInvolvingNode narrows opts.sourceId from SourceScope to string, making an ALL_SOURCES caller unrepresentable (review item 3). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BTfGTsPBskKYJUK8SSvYob
|
Review feedback triage — addressed in the latest commit:
Declined:
🤖 Generated with Claude Code |
|
Claude finished @Yeraze's task —— View job PR Review: feat(traceroute): participation picker on all sourcesTasks
Follow-up ReviewBoth addressed items are clean. Fix #2 — The hidden The test at line 275-314 covers exactly the right cases: Fix #3 — The inline comment explains the narrowing rationale clearly: "Narrowed to No new issues introduced. The two changes are minimal and additive; the rest of the implementation is unchanged. The two outstanding actionable items from the previous review are resolved. The declined items (#1 dead-code badge suppression, #4 deferred coexistence test) remain reasonable calls given the justifications provided. This PR is ready to merge. |
Summary
Phase 2 (final) of the Traceroute Strip Interactivity epic (plan:
docs/internal/dev-notes/TRACEROUTE_STRIP_INTERACTIVITY_EPIC.md, spec:TRS_PHASE2_SPEC.md). Adds a per-node traceroute picker to the Node Details traceroute section on every Meshtastic-family source — which is what makes the strip appear on MQTT sources at all, since they have no origin node and therefore no own-request traceroute.Backend
tracerouteParticipationKind()(src/utils/tracerouteSegments.ts): pure predicate classifying a row asendpoint/hop/ not-involved.Number()-coerced against PG/MySQL BIGINT strings; hop membership viaparseHopArray, immune to the substring bug aLIKE '%num%'approach would ship (regression test included).TraceroutesRepository.getTraceroutesInvolvingNode(): one bounded (2000), newest-first, source-scoped query-builder scan + JS filter — no raw SQL. Overflow can only drop older participations.GET /api/traceroutes/participation/:nodeNum—requirePermission('traceroute','read', {sourceIdFrom:'query'}),sourceIdrequired (400MISSING_SOURCE_ID, noALL_SOURCESfallback), envelope viaok/fail,maskTraceroutesByChannelapplied ([BUG] MeshCore telemetry retrieval fails on every repeater (meshcore-only setup, v4.6.1) #3092),routePositionsstripped from the projection, honesthopCount: number | null(no 999 sentinel).Frontend
useNodeTraceroutes(one-shot TanStack query, 7-day window) +TracerouteParticipationPicker(native<select>, renders only at ≥2 entries; labels = date · endpoints · hop count, relay-hop entries marked "relayed").entries[0]), so the shipped TCP request-flow (pending/failed badges, "last traced") stays byte-identical — badges and age now describe the displayed row. Pick clears on node change and on a fresh traceroute response. The render guard moves fromrecentTraceto the displayed row — the MQTT unlock.write→read || write(the box is a display; the request button still requires write). 6 new en.json keys, flat with inline defaults.Testing
success: true, JSON reporter). New: predicate tests (incl. substring trap), repository +perSourceisolation tests, route tests viacreateRouteTestApp(permission, missing-sourceId, bounds), ApiService test, picker component tests, MessagesTab gate/coexistence tests.tsc --noEmitclean; lint ratchet passes.!04c5b85c: picker lists 100 (capped) dated entries; strip renders on an MQTT source for the first time; picking a 2-hop entry re-renders 2→7 glyphs, "last traced" follows the pick; Phase 1 edge tooltips work on picked rows.Closes out the epic (Phase 1 was #4424).
🤖 Generated with Claude Code
https://claude.ai/code/session_01BTfGTsPBskKYJUK8SSvYob