fix(meshcore): preserve real Last Heard across reconnect (#3645)#3650
Conversation
On MeshCore companion reconnect, refreshContacts() set every contact's lastSeen to Date.now() and discarded the companion-reported advert time, so persistContact wrote the reconnect wall-clock into meshcore_nodes.lastHeard — surfacing as "Last Heard: just now" for every node after a disconnect/reconnect, matching the report. The companion reports each contact's last advert time (`last_advert`, epoch seconds) in the get_contacts response. Use it for lastSeen (seconds → ms, mirroring MeshCoreContactDetailPanel's guard), falling back to Date.now() only when the device didn't report one. Also populate the in-memory `lastAdvert` field (seconds) so the contact-detail panel's "Last Advert" is accurate. This both preserves Last Heard across reconnect (the device-reported value is stable, not the moving reconnect clock) and uses the actual advert timestamp for nodes the companion heard while MeshMonitor was disconnected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011JEaCGwY9Wz8jeV4e22GW4
|
Claude finished @Yeraze's task —— View job PR Review: fix(meshcore): preserve real Last Heard across reconnectTasks:
SummaryThis is a clean, minimal fix for a real bug. The root cause analysis is accurate, the fix is correct, and the test coverage is solid. No blocking issues. Code Review
|
| Scenario | Covered |
|---|---|
Advert time used (not now) |
✅ line 48 |
| Seconds→ms conversion verified | ✅ line 62 |
lastAdvert field set in seconds |
✅ line 65 |
lastHeard persisted to DB |
✅ lines 68–71 |
Fallback to now when absent (last_advert: 0) |
✅ line 74 |
| Stability across repeated reconnects | ✅ line 87 |
Test quality is high. vi.setSystemTime is used correctly to lock the fallback clock, and the stability test cleanly demonstrates that the fix is monotone (device value doesn't get clobbered by subsequent reconnect wall-clocks).
Minor observation — missing edge cases (non-blocking):
last_advertisundefined(field missing from device response entirely) vs.0— the current code handles this correctly (typeof c.last_advert === 'number'returns false forundefined, soadvertSec = 0→ fallback), but a test for theundefinedcase would document that behavior explicitly.- The
< 1e12guard (already-in-ms branch) is not tested — if a future device ever reports milliseconds directly, the guard is in place but is currently untested.
Neither gap is a blocker, just potential additions if you want belt-and-suspenders coverage.
sourceId scoping: upsertNode is asserted with 'src-a' (line 69) — good, this confirms the per-source scoping invariant is exercised.
Verdict
✅ Approve — Correct fix, clean implementation, well-tested. The two missing edge cases in tests are optional improvements, not blockers.
Summary
Closes #3645.
On MeshCore companion reconnect, every node's Last Heard reset to the reconnect time, discarding the real advert timestamps.
Root cause
refreshContacts()(which runs on connect/reconnect) set every contact'slastSeentoDate.now()and ignored the companion-reported advert time.persistContactwriteslastHeard ← lastSeen, someshcore_nodes.lastHeardbecame the reconnect wall-clock for every node — surfacing as "Last Heard: just now".Fix
The companion reports each contact's last advert time (
last_advert, epoch seconds) in theget_contactsresponse.refreshContacts()now:lastSeenfromlast_advert(seconds → ms, with the same< 1e12guardMeshCoreContactDetailPaneluses), falling back toDate.now()only when the device didn't report one;lastAdvertfield (seconds) so the contact-detail panel's "Last Advert" is accurate too.This satisfies both expected behaviors from the issue: Last Heard is preserved across reconnect (the device-reported value is stable, not the moving reconnect clock), and the actual advert timestamp is used for nodes the companion heard while MeshMonitor was disconnected.
Testing
meshcoreManager.lastHeard.test.ts— advert-time used (notnow), seconds→ms conversion,lastHeardpersisted, fallback-to-now when absent, and stability across repeated reconnects.pathUpdate+contactPersistencesuites still green.tsc -p tsconfig.server.jsonclean.🤖 Generated with Claude Code