fix(node): stop rendering saved-on-phone nodes as freshly heard - #6747
fix(node): stop rendering saved-on-phone nodes as freshly heard#6747jamesarich wants to merge 2 commits into
Conversation
Root-caused via #6682/#6693: Node.hopsAway defaults to the sentinel -1 instead of null, and nothing distinguished a locally-retained node from one the connected radio just reported this session. Together that made a legitimately cached row (multi-radio use, or a radio with a smaller NodeDB than the phone) render as if it were freshly heard with 0 hops, 0 SNR, 0 RSSI. - Add Node.hopsAwayOrNull, extending the existing snrOrNull/rssiOrNull pattern, and fix both copies of getRelayNode (the live one in Packet.kt and the unused one in Node.kt) whose plain minByOrNull { it.hopsAway } let the unresolved -1 sentinel look closer than a real hop count. - Track the current connection session's exact NodeDB membership via NodeManager.currentSessionNodeNums, published right after each Stage 2 handshake and reconciled against the same generation-race window already used for connectionIdentity. - Add a "Saved on phone" badge (node list rows + detail screen) for any node retained locally but absent from that snapshot, and stop those rows from claiming "online" from a cached, no-longer-current lastHeard (list rows, a11y description). New/updated tests across core:model, core:data, core:ui, core:database and feature:node; screenshot goldens regenerated for the new "Saved on phone" previews. Full baseline (spotlessApply/spotlessCheck/detekt/assembleDebug/test/allTests) and :screenshot-tests:validateDebugScreenshotTest are green. Fixes #6263 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
currentSessionNodeNums was captured once, from the Stage 2 NodeDB download, and never updated again for the life of the connection. Any node that announced itself afterwards — extremely common on a long-lived BLE connection — was therefore absent from the set and got badged "Saved on phone" with its online/fresh status suppressed, reintroducing the very "presence looks wrong" defect #6263 exists to fix, only inverted: genuinely fresh nodes now looked falsely stale. Every mid-session path that learns about a node through the radio carries a RadioSessionContext, and every local-only write (optimistic admin projections, shared-contact imports, fixed-position edits) carries none. That distinction is exactly the one the badge needs, so it drives the new private noteHeardInSession(): NodeInfo via handleReceivedUser, position, node status, PaxCounter, and the updateNodeForSession path that mesh telemetry and admin replies arrive on all extend the set; sessionless writes deliberately do not, since those nodes really are phone-only. Membership is claimed only where the mutation actually committed. For handleReceivedUser that means the winning CAS branch, using a new ReceivedUserTransition.sessionMemberNodeNum so a suppressed retired-number replay claims nothing and a stale noncanonical presentation credits the canonical row it yields to rather than the slot it vacates. The generation is re-read inside the StateFlow update so a concurrent session boundary makes it a no-op instead of resurrecting a number into the new session's set, and an already-present member returns the same instance rather than copying a set of up to MAX_IN_MEMORY_NODES on every inbound packet. The converse gap is documented rather than fixed: a node evicted from the radio's own bounded NodeDB mid-session keeps its membership, because firmware sends no eviction notification and the phone has no signal short of re-downloading the NodeDB. Withholding a badge is the safe direction. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up: session membership now grows mid-sessionAdversarial review of this branch caught a high-severity bug that undermined the whole point of the PR. Good that it did not ship. The bug. The fix. Every mid-session path that learns about a node through the radio carries a Three details worth flagging for re-review:
On the converse gap the reviewer raised (should a node evicted from the radio's own bounded NodeDB mid-session lose membership): documented in the KDoc rather than deferred, because it is not implementable as stated — firmware sends no eviction notification, so the phone has no signal short of re-downloading the whole NodeDB. Withholding a badge is the safe direction: it declines to claim locally-retained history rather than falsely claiming it. Tests. Nine new cases in Validation: |
|
Parking this one in draft. The fix itself (session-membership tracking, sentinel-zero cleanup) is solid and verified, but the "Saved on phone" badge as a user-facing UX concept needs more thought before shipping — a persistent badge on node rows risks reading as noise/confusion rather than useful signal, especially for the common case of "just hasn't been heard from since reconnect yet." Not rejecting the approach, just not confident enough in the UX to merge as-is. Will revisit. |
Summary
Root-caused via #6682 / #6693 (see the diagnosis comment on #6263):
Node.hopsAwaydefaults to the sentinel-1instead ofnull, and nothing distinguished a locally-retained node from one the connected radio actually reported this session. Together, a legitimately cached row (multi-radio use, or a radio whose own NodeDB is smaller/has been wiped relative to the phone's) rendered as if it were freshly heard, with 0 hops, 0 SNR, 0 RSSI.Node.hopsAwayOrNull, extending the existingsnrOrNull/rssiOrNullnull-safety pattern already used inNodeItem.kt.getRelayNode— the one actually wired into production (Packet.Companion.getRelayNode, used byDebugViewModel) and the unused duplicate inNode.Companion— whose plainminByOrNull { it.hopsAway }let the unresolved-1sentinel look "closer" than a real, known hop count.NodeManager.currentSessionNodeNums: published right after each Stage 2 handshake completes, and reconciled against the same generation-race window already used forconnectionIdentity(a delayedclearStaleConnectionIdentitycollector must not blank a snapshot already published for the active generation).lastHeard— in the visible list rows and in the TalkBack accessibility description — since that's the same false-freshness claim the badge exists to correct.Scope notes / deliberate non-changes
CommonGetNodeDetailsUseCase'sisSavedOnPhonederivation has no direct unit test. That class has 8 constructor dependencies and no existing test fixture at all (pre-existing gap, not introduced here); the logic itself is a one-line derivation (sessionNodeNums != null && node.num !in sessionNodeNums), and the session-membership flow it derives from is exhaustively covered inNodeManagerImplTestandNodeListViewModelTest.Testing
direnv exec . ./gradlew spotlessApply spotlessCheck detekt assembleDebug test allTests— BUILD SUCCESSFUL, all tests (including every new/updated one) passed../gradlew :screenshot-tests:updateDebugScreenshotTestthen:screenshot-tests:validateDebugScreenshotTest— BUILD SUCCESSFUL. Only 6 new golden PNGs were produced (the new "Saved on phone" previews); no existing golden changed, confirming the default (isSavedOnPhone = false) rendering is pixel-identical to before.Node.hopsAwayOrNull(null + zero-value pair),Packet.getRelayNode/Node.getRelayNodesentinel-ordering regression,NodeManager.currentSessionNodeNumsgeneration-reconciliation (including the delayed-collector race),MeshConfigFlowManagerImplpublish-before-ready ordering,NodeListViewModelpass-through, and Compose-level badge/online-suppression coverage inNodeItemZeroMetricsTest+BuildNodeDescriptionTest.Fixes #6263