Skip to content

Rank nodes by 7-day availability, and keep their counts across restarts - #601

Merged
Babissimo merged 2 commits into
mainfrom
node-availability
Sep 24, 2026
Merged

Babissimo merged 2 commits into
mainfrom
node-availability

Conversation

@Babissimo

Copy link
Copy Markdown
Contributor

Item 4 of the console split (ClickUp 123zgec4k7v, epic 123zgec4k7k). The leaderboard's uptime becomes 7-day availability, and a node's counts survive a restart.

Needs offworldlabs/retina-analytics#36. The submodule points at that PR's commit, which stays on retina-analytics main once the PR is merge-committed, so this PR should merge after it.

Why

Uptime was the time since a node last registered with this process, and every reconnect, config change and startup reset it. Nothing persisted it either, so after a deploy every node read the same few minutes: all online nodes showed "0h 4m" on production on 2026-09-23. total_detections, the column the leaderboard ranks by, was not persisted, so the ranking also restarted with every deploy.

What changes

Backend (first commit):

  • The submodule bump brings availability: the share of the minutes the server was up in which a node delivered a frame, over 7 days and 24 hours. See the library PR for the definition.
  • server_clock_task marks each minute this process is up, every 20 s. It starts five minutes after boot, so the minutes nodes spend reconnecting after a restart count for no one. On production's last restart (10:59:36 today), the frame rate was back to steady state within 40 s.
  • The state snapshot carries each node's metrics (its week of minutes, first_seen, and the running counts including frames, so reputation's detections-per-frame reading stays sane) and the server's minutes. They are restored over what startup priming registered. This adds about 1.8 KB per node.
  • PublicLeaderboardRow.uptime_s becomes availability_7d.
  • e2e/specs/nodes.spec.ts asserts the new metrics fields on the deployed API in place of uptime_s.

Dashboard (second commit):

  • Leaderboard, overview, node detail, network health and node management show availability where they showed uptime. The leaderboard sorts on it, with a node not yet measured last.
  • The node's own page also gives the 24-hour figure and how much of the week the figure rests on.
  • The percentage rounds down, so only a node that missed nothing reads 100%.
  • The knowledge base no longer says reputation is based on uptime, and gains an Availability entry.

Choices you may want to overrule

  • A node is measured from when it was first seen, not over the full week, so a node that joined yesterday reads its availability since then. The figure for every node rests on less than a week until a week after this deploys; the node page says how much.
  • A blocked node's frames still count as delivered. Availability is whether it sends; reputation is whether it is believed.
  • Minutes are marked when a frame is processed, not when it arrives, so frames the queue drops under overload count against the node. That matters on retina-test at high frame rates, not on production's volume.

Tests

  • Library: 15 new tests, each guard checked by a control mutation.
  • Backend: snapshot round trip of metrics and server minutes, the clock's tick and grace, and the leaderboard field. Controls fail each one.
  • Dashboard: the formatter, the leaderboard column and sort, and the node page's three rows.
  • Locally: backend full suite with CI's flags, 5000 passed; dashboard 1352 passed; pre-commit, dashboard and e2e typecheck, and lint are clean. uv lock --check with the Dockerfile's uv 0.12.5 is clean, so no relock is needed.

🤖 Generated with Claude Code

@claude

This comment has been minimized.

Babissimo and others added 2 commits September 24, 2026 14:01
The leaderboard's uptime was the time since a node last registered with
this process, which every reconnect, config change and startup reset, and
which nothing persisted: after a deploy every node read the same few
minutes. total_detections, the column the leaderboard ranks by, was not
persisted either, so the ranking also restarted with every deploy.

retina-analytics now measures availability, the share of the minutes this
server was up in which a node delivered a frame, over 7 days and 24 hours.
The server's side of that is a clock task marking each minute the process
is up. It starts five minutes after boot, because nodes take a while to
reconnect after a restart and those minutes are the server's, not theirs.

The snapshot carries each node's metrics (its week of minutes, when it was
first seen, and its running counts) and the server's minutes with them,
restored over what startup priming registered. The leaderboard row
publishes availability_7d in place of uptime_s, and the e2e spec asserts
the new fields on the deployed API.

Ticket: https://app.clickup.com/t/123zgec4k7v

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The five pages that showed a node's uptime (leaderboard, overview, node
detail, network health, node management) show its 7-day availability
instead. The node's own page adds the 24-hour figure and how much of the
week the figure rests on, which for a new node, or in the first week after
this ships, is less than the whole of it. The leaderboard sorts on it, with
a node not yet measured last.

The percentage rounds down, so a node that missed one minute in a week
reads 99.9% rather than a perfect 100.0%.

The knowledge base said reputation was based on uptime. It is not: it is
judged on the heartbeat, agreement with ADS-B and trusted neighbours, and a
plausible detection rate, which is what it now says. Availability gets its
own entry.

Ticket: https://app.clickup.com/t/123zgec4k7v

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Claude finished @Babissimo's task in 1m 56s —— View job


Review of PR #601

  • Gather context (PR body, changed files list)
  • Read repo guidelines (CLAUDE.md, ONBOARDING.md)
  • Diff against origin/main for new changes since last review
  • Review backend changes (server_clock_task, state_snapshot, admin routes, constants, tests)
  • Review dashboard changes (format.ts, leaderboard/node/overview/admin pages, tests)
  • Post final review

I checked git diff origin/main...HEAD against the branch's current head (85d9ad1) and it's unchanged from the last review pass — both commits (f07ad4e, 85d9ad1) predate that review, and the file list matches exactly. I re-verified the whole diff independently rather than just repeating the prior comment.

Review

Solid, well-scoped change. The two-commit split (backend/library-facing, then dashboard) is clean, and the snapshot round-trip and clock-grace behavior are both exercised by real tests with controls, not just happy-path assertions.

Correctness — verified

  • server_clock_task (backend/services/tasks/periodic.py:108-113) sleeps SERVER_CLOCK_GRACE_S before its first tick, so the post-restart reconnect window is excluded from every node's denominator. test_server_clock.py checks both the marked and grace-suppressed cases with MinuteRing.bits.
  • restore_snapshot() (backend/services/state_snapshot.py:275-281) restores node_metrics/server_minutes and runs after prime_pipeline_at_startup() in main.py:189-203, so it correctly overwrites whatever cold-start priming registered. Old snapshots without these keys degrade gracefully via .get(..., {}).
  • formatAvailability (dashboard/src/utils/format.ts:17-20) floors to one decimal before formatPercent, so 0.9999 reads 99.9% rather than rounding to 100.0% — matches the "only a perfect week reads 100%" requirement. formatAvailability(0) correctly renders 0.0%, not the dash (verified fmt's null/undefined/NaN checks don't treat 0 as missing).
  • LeaderboardPage.tsx:41's sort ((b.availability_7d ?? -1) - (a.availability_7d ?? -1)) sinks unmeasured (null) nodes to the bottom under the fixed descending sort — the new sort test (LeaderboardPage.test.tsx) confirms ordering High, Low, Unmeasured.
  • test_publication.py's removal of the uptime_s tolerance-comparison special-case (it previously allowed a few ms of drift between two summaries) is correct: availability_7d is minute-granular, so two summaries taken milliseconds apart are now byte-identical, and the dict comparison can go back to a plain ==.
  • PublicLeaderboardRow.availability_7d: float | None plus the vulture_whitelist.py and PUBLIC_FIELDS set updates all move together consistently.
  • No /v1/nodes route or bound config touched, so no OpenAPI regen is required — correctly not included.

Minor / non-blocking (unchanged from the earlier pass)

  • dashboard/src/test/kvTable.test.tsx:56 still seeds metrics: { uptime_s: 60, ... }, a field that no longer exists on this response. Harmless (the test only counts <table> elements), but worth swapping to availability_7d in a follow-up for clarity.
  • NodeDetailPage.tsx:223 reuses formatDuration for "Availability measured over," which only ever renders in hours — a node measured for nearly a full week reads e.g. 168.0h rather than 7.0d. Fine as a reuse of an existing formatter, just a little awkward at the top of the range.
  • e2e/specs/nodes.spec.ts validates availability_7d's range and null-ness explicitly but only checks presence (not range) for availability_24h. Low-value gap given the shared code path.

Not independently reviewable from this checkout

  • libs/retina-analytics submodule isn't fetched here (no network access), so MinuteRing, NodeMetrics.record_frame/summary/to_state/from_state, and mark_server_up are taken on faith per the companion PR (Measure node availability over a trailing week, not time since registration retina-analytics#36). Everything on this repo's side that calls into them (snapshot round-trip, clock marking, leaderboard field) is exercised by the tests added here.
  • Couldn't run pre-commit or the backend/dashboard suites in this sandbox (no venv, no submodule checkout, no network). PR description states 5000 backend / 1352 dashboard tests passed locally, and uv lock --check was clean against the Dockerfile's pinned uv.

Nothing here blocks merge.

@Babissimo
Babissimo merged commit 1fb6b59 into main Sep 24, 2026
21 checks passed
@Babissimo
Babissimo deleted the node-availability branch September 24, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant