feat(#1845): filter nodes by how long they have been silent - #1973
Merged
Conversation
The issue asks to "flag repeaters that stopped communicating for x days" so an operator can find remote gear that has died. The Nodes page had Active/Stale with thresholds fixed at 72h for infra and 24h for the rest, and a Last Heard filter that selects nodes heard WITHIN a window. Neither answers "show me what has been quiet for over a week", which is the question in the title. Adds a "Silent for" select next to Last Heard: 1d, 3d, 7d, 14d, 30d, each labelled with how many nodes it would select. Counts are computed BEFORE the silence filter is applied, so the dropdown keeps showing what the other windows would select instead of collapsing to the one already chosen. The choice persists in localStorage like the neighbouring filters, and is mirrored into the URL as ?silent=, so a view can be pasted to whoever owns the silent gear. URL sync is wrapped in try/catch: it is a convenience and must never stop the filter from working. One definition of freshness, not two. getNodeStatus was relay-aware (#1598) while nodes.js separately computed statusAge from the ADVERT timestamp alone. Filtering on the latter would list a repeater as silent for ten days while its own badge on the same row said active, for exactly the nodes #1598 exists to protect. So the freshness rule is extracted into window.getEffectiveHeardMs in roles.js and getNodeStatus now calls it. Same behaviour, one source. A node never heard from at all scores Infinity, so it matches every window rather than dropping out of the filter entirely. Tests: 666 to 680 passed, 0 failed. Fourteen cases on getEffectiveHeardMs and getNodeStatus: NaN rather than 0 when nothing is known (0 is a real timestamp and would sort as very old instead of unknown), the full _liveSeen > _lastHeard > last_heard > last_seen precedence, a recent relay beating a stale advert, a stale relay NOT dragging a fresh advert backwards, relay alone being enough, room counting as infra while companion does not (last_relayed on a companion is meaningless and must not rescue it), case-insensitive roles, the legacy two-argument call shape, and the 72h boundary at 71h and 73h. One of those tests failed on first run and the test was wrong, not the code: 9e7 ms is 25h, which is correctly ACTIVE for infra. Fixed and the boundary is now asserted explicitly so the next person does not repeat it. No new API, no new column, no server change. Cache busting needs no bump: __BUST__ is substituted at startup in cmd/server/main.go:570. Not done: no alerting. @fokcuk asked on the thread to be notified when a repeater goes silent, which needs subscriber identity, an evaluation loop and delivery, none of which exist. That is a separate product surface and a separate issue. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQS3XLoPD98yu9pxdRujqg
Contributor
|
I'm tagged multiple times but I have not interacted on this before now. Might you have meant to tag @fokcuk ? |
# Conflicts: # test-frontend-helpers.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1845 for the question in its title. The alerting ask that came later in the thread is deliberately not in here; see the bottom.
The gap
@Jonher937 asked to flag repeaters that stopped communicating for x days, to find remote gear that has died. Today the Nodes page has Active/Stale with thresholds fixed at 72h for infra and 24h for everything else, plus a Last Heard filter that selects nodes heard within a window. Neither answers "show me what has been quiet for over a week".
What this adds
A
Silent forselect beside Last Heard: 1d, 3d, 7d, 14d, 30d, each labelled with the number of nodes it would select.Counts are computed before the silence filter is applied, so the dropdown keeps showing what the other windows would select instead of collapsing to the one already chosen. The choice persists in
localStoragelike the neighbouring filters and is mirrored into the URL as?silent=7d, so the view can be pasted to whoever owns the silent gear. The URL sync is wrapped in try/catch, because it is a convenience and must never stop the filter working.The part worth reviewing: one definition of freshness, not two
getNodeStatushas been relay-aware since #1598, whilenodes.jsseparately computedstatusAgefrom the ADVERT timestamp alone. Filtering on the latter would have listed a repeater as silent for ten days while its own badge on the same row said active, and it would have done so for exactly the nodes #1598 exists to protect.So the freshness rule is extracted into
window.getEffectiveHeardMsinroles.js, andgetNodeStatusnow calls it. Behaviour is unchanged, there is now one source. Reviewers should look hardest at that refactor rather than at the select.A node never heard from at all scores
Infinity, so it matches every window instead of silently dropping out of the filter.Why the thresholds are fixed values and not derived
I measured the alternative before writing this, on a 1179-repeater mesh, and posted it on #1611: replacing a fixed threshold with
3 x per-node advert medianfixes 8 false "silent" flags and newly mis-flags 28 currently-active nodes, because a 3h-median node gets a 9h threshold. Raising the global default to 144h rescues 7 and hides 27 genuinely dead repeaters. Both are net-negative. A user-chosen window sidesteps the whole question: the operator picks what "too long" means for their mesh, which is what @Jonher937 asked for in the first place.Verification
test-frontend-helpers.js: 666 → 680 passed, 0 failed. Fourteen cases coveringNaNrather than0when nothing is known (0 is a real timestamp and would sort as very old rather than unknown), the full_liveSeen > _lastHeard > last_heard > last_seenprecedence, a recent relay beating a stale advert, a stale relay not dragging a fresh advert backwards, relay alone sufficing,roomcounting as infra whilecompaniondoes not (alast_relayedon a companion is meaningless and must not rescue it), case-insensitive roles, the legacy(role, ms)call shape, and the 72h boundary asserted at 71h and 73h.9e7ms is 25h, which is correctly active for infra. Fixed, and the boundary is now asserted explicitly so nobody repeats it.eslinton the changed files: 0 errors. The warnings present are the same ones master already reports.__BUST__is substituted at startup incmd/server/main.go:570.Not done
No alerting. @fokcuk asked on the thread for notification when a repeater they look after goes silent, which is a different product: subscriber identity, an evaluation loop and delivery, none of which exist today. That deserves its own issue and a design call rather than being shimmed into
nodes.js, which is also what the triage concluded. This PR gives the operator the view; it does not push to them.Sizes and boundaries (1d/3d/7d/14d/30d) are a judgement call. Say the word if a different set fits real operator habits better.