From 3bbfaf41652b62faeb26cf8b29901f9f8fc58b7d Mon Sep 17 00:00:00 2001 From: Alper Gundogdu Date: Tue, 29 Sep 2026 13:27:01 +0100 Subject: [PATCH] feat(fleet): grey out nodes with no settlement in the last 24h A node's dot pulses while its newest settlement is inside the panel's 24h window and turns solid grey once it falls outside, keeping the "settled N ago" text. Rows with no settlement on file drop the "idle" label and show the operator alone. Co-Authored-By: Claude Opus 5.5 --- app/globals.css | 5 ++++ components/site/FleetStatus.test.ts | 37 ++++++++++++++++++++++++++--- components/site/FleetStatus.tsx | 21 +++++++++------- lib/stats.test.ts | 14 +++++++++++ lib/stats.ts | 12 ++++++++++ 5 files changed, 77 insertions(+), 12 deletions(-) diff --git a/app/globals.css b/app/globals.css index ed23c36..d65fe44 100644 --- a/app/globals.css +++ b/app/globals.css @@ -668,6 +668,11 @@ a.hero-status:hover .arrow { animation: fleet-pulse 2.4s ease-in-out infinite; } +.fleet-pulse-stale { + background: color-mix(in srgb, var(--paper) 45%, transparent); + border: none; +} + @keyframes fleet-pulse { 0%, 100% { diff --git a/components/site/FleetStatus.test.ts b/components/site/FleetStatus.test.ts index 3adb0ca..d228664 100644 --- a/components/site/FleetStatus.test.ts +++ b/components/site/FleetStatus.test.ts @@ -51,6 +51,16 @@ const stats: Stats = { amount: "500000", epoch: 7, }, + { + txHash: "0xdef", + logIndex: 0, + blockNumber: 90, + timestamp: 1_790_000_000 - 24 * 60 * 60, + operator: "0x00000000000000000000000000000000000000a4", + bytesDelivered: "1000000000", + amount: "500000", + epoch: 6, + }, ], nodes: { "0x01": { @@ -65,6 +75,10 @@ const stats: Stats = { operator: "0x00000000000000000000000000000000000000a3", region: "unknown", }, + "0x04": { + operator: "0x00000000000000000000000000000000000000a4", + region: "US", + }, }, }; @@ -108,14 +122,31 @@ describe("FleetPanel", () => { it("renders one row per registered node and the 24h aggregates", () => { const { text } = panel({ status: "ok", stats, fetchedAt }); - expect(text).toContain("3nodes registered"); + expect(text).toContain("4nodes registered"); expect(text).toContain("de0x0000…00a1 · settled 4 min ago"); - expect(text).toContain("de0x0000…00a2 · idle"); - expect(text).toContain("n/a0x0000…00a3 · idle"); + expect(text).toContain("us0x0000…00a4 · settled 24 h ago"); + expect(text).toContain("de0x0000…00a2n/a0x0000…00a3Σ served"); + expect(text).not.toContain("idle"); expect(text).toContain("$1.50"); expect(text).not.toContain("catching up"); }); + // A node pulses while its newest settlement is inside the 24h window, greys + // out once it falls outside, and stays an empty ring with none on file. + it("pulses recent nodes and greys out stale ones", () => { + const { tree } = panel({ status: "ok", stats, fetchedAt }); + const pulses = findAll(tree, "span") + .map((el) => attrs(el).className) + .filter((c): c is string => typeof c === "string") + .filter((c) => c.startsWith("fleet-pulse")); + expect(pulses).toEqual([ + "fleet-pulse fleet-pulse-active pulse-0", + "fleet-pulse fleet-pulse-stale", + "fleet-pulse", + "fleet-pulse", + ]); + }); + it("flags a backfilling index", () => { const { text } = panel({ status: "ok", diff --git a/components/site/FleetStatus.tsx b/components/site/FleetStatus.tsx index 8506e6d..22feb96 100644 --- a/components/site/FleetStatus.tsx +++ b/components/site/FleetStatus.tsx @@ -5,6 +5,7 @@ import { PANEL_CAPTIONS } from "@/lib/copy"; import { FLEET_WINDOW_HOURS, fleetView, + settledInWindow, type StatsResult, timeAgo, } from "@/lib/stats"; @@ -76,19 +77,21 @@ export function FleetPanel({ {n.region} {n.operator} - - {" · "} - {n.lastSettled !== null && result.status === "ok" - ? `settled ${timeAgo(n.lastSettled, result.fetchedAt)}` - : "idle"} - + {n.lastSettled !== null && result.status === "ok" && ( + + {" · settled "} + {timeAgo(n.lastSettled, result.fetchedAt)} + + )} )) diff --git a/lib/stats.test.ts b/lib/stats.test.ts index 79437f1..152ced1 100644 --- a/lib/stats.test.ts +++ b/lib/stats.test.ts @@ -6,6 +6,7 @@ import { formatBytes, formatUsdc, loadStats, + settledInWindow, type Stats, STATS_URL, TERMINAL_SESSIONS, @@ -122,6 +123,19 @@ describe("timeAgo", () => { }); }); +describe("settledInWindow", () => { + const now = 1_790_000_000_000; + it.each([ + [1_790_000_000 - 4 * 60, true], + [1_790_000_000 - 24 * 3600 + 1, true], + [1_790_000_000 - 24 * 3600, false], + [1_790_000_000 - 3 * 86_400, false], + [null, false], + ])("%s → %s", (lastSettled, inWindow) => { + expect(settledInWindow(lastSettled, now)).toBe(inWindow); + }); +}); + describe("terminalSessions", () => { const stats: Stats = { ...base, diff --git a/lib/stats.ts b/lib/stats.ts index a202171..dfafc3f 100644 --- a/lib/stats.ts +++ b/lib/stats.ts @@ -227,6 +227,18 @@ export type FleetView = { caughtUp: boolean; }; +/** Whether a node's newest settlement falls inside the fleet window as of + * `now` (ms): its row pulses while this holds and greys out after. */ +export function settledInWindow( + lastSettled: number | null, + now: number, +): boolean { + return ( + lastSettled !== null && + now / 1000 - lastSettled < FLEET_WINDOW_HOURS * 60 * 60 + ); +} + /** Each value as a fraction of the series max; all zeros stay zeros. */ function levels(values: string[]): number[] { const big = values.map((v) => BigInt(v));