diff --git a/app/(dashboard)/status/page.tsx b/app/(dashboard)/status/page.tsx index 314027d0..7b8d3a95 100644 --- a/app/(dashboard)/status/page.tsx +++ b/app/(dashboard)/status/page.tsx @@ -6,7 +6,12 @@ import { Button } from "@/components/ui/button" import { Spinner } from "@/components/ui/spinner" import { usePerformanceData, type PerformanceDataSource } from "@/hooks/use-performance-data" import { usePermissions } from "@/hooks/use-permissions" -import { buildRunningStatusView, formatRelativeTime, resolveUsageFreshness } from "@/lib/performance-data" +import { + buildRunningStatusView, + formatRelativeTime, + isScannerCycleActive, + resolveUsageFreshness, +} from "@/lib/performance-data" import { cn } from "@/lib/utils" import { RiArchiveDrawerFill, @@ -97,10 +102,9 @@ export default function PerformancePage() { ) const scannerStartedAt = metricsInfo.aggregated?.scanner?.current_started - const scannerCycle = metricsInfo.aggregated?.scanner?.current_cycle const scannerCompleteTimes = metricsInfo.aggregated?.scanner?.cycle_complete_times ?? [] const scannerCompletedAt = scannerCompleteTimes.at(-1) - const scannerActive = (scannerCycle ?? 0) > 0 + const scannerActive = isScannerCycleActive(metricsInfo) const scannerStatus = scannerActive ? t("Scanning") : scannerCompletedAt ? t("Idle") : t("Never run") const scannerDuration = useMemo(() => { if (!scannerStartedAt) return undefined diff --git a/lib/performance-data.ts b/lib/performance-data.ts index 2abf68ba..5a478dba 100644 --- a/lib/performance-data.ts +++ b/lib/performance-data.ts @@ -111,6 +111,7 @@ export interface MetricsInfo { aggregated?: { scanner?: { current_cycle?: number + current_cycle_active?: boolean current_started?: string cycle_complete_times?: string[] } @@ -630,6 +631,7 @@ export function normalizeMetricsInfo(value: unknown): MetricsInfo { const aggregated = asRecord(source.aggregated ?? source.Aggregated) const scanner = asRecord(aggregated.scanner ?? aggregated.Scanner) const currentCycle = asNonNegativeNumber(scanner.current_cycle ?? scanner.currentCycle) + const currentCycleActive = asBoolean(scanner.current_cycle_active ?? scanner.currentCycleActive) const currentStarted = asTimestamp(scanner.current_started ?? scanner.currentStarted) const cycleCompleteTimes = asArray(scanner.cycle_complete_times ?? scanner.cycleCompleteTimes).flatMap( (item) => { @@ -638,12 +640,15 @@ export function normalizeMetricsInfo(value: unknown): MetricsInfo { }, ) - if (currentCycle === undefined && !currentStarted && !cycleCompleteTimes.length) return {} + if (currentCycle === undefined && currentCycleActive === undefined && !currentStarted && !cycleCompleteTimes.length) { + return {} + } return { aggregated: { scanner: { ...(currentCycle !== undefined ? { current_cycle: currentCycle } : {}), + ...(currentCycleActive !== undefined ? { current_cycle_active: currentCycleActive } : {}), ...(currentStarted ? { current_started: currentStarted } : {}), ...(cycleCompleteTimes.length ? { cycle_complete_times: cycleCompleteTimes } : {}), }, @@ -651,6 +656,11 @@ export function normalizeMetricsInfo(value: unknown): MetricsInfo { } } +export function isScannerCycleActive(metricsInfo: MetricsInfo): boolean { + const scanner = metricsInfo.aggregated?.scanner + return scanner?.current_cycle_active ?? (scanner?.current_cycle ?? 0) > 0 +} + export function summarizeServerStates( servers: ServerInfo[] | undefined, diagnostics?: ClusterDiagnostics, diff --git a/tests/lib/performance-data.test.js b/tests/lib/performance-data.test.js index 9208ae4d..4b40a585 100644 --- a/tests/lib/performance-data.test.js +++ b/tests/lib/performance-data.test.js @@ -4,6 +4,7 @@ import assert from "node:assert/strict" import { buildRunningStatusView, formatRelativeTime, + isScannerCycleActive, normalizeClusterDiagnostics, normalizeDataUsageInfo, normalizeMetricsInfo, @@ -246,15 +247,31 @@ test("normalizeMetricsInfo rejects missing and invalid scanner timestamps", () = ) }) -test("normalizeMetricsInfo keeps the scanner cycle state for active and idle displays", () => { - assert.equal( - normalizeMetricsInfo({ aggregated: { scanner: { current_cycle: 3 } } }).aggregated?.scanner?.current_cycle, - 3, - ) - assert.equal( - normalizeMetricsInfo({ aggregated: { scanner: { current_cycle: 0 } } }).aggregated?.scanner?.current_cycle, - 0, - ) +test("normalizeMetricsInfo keeps explicit scanner activity and legacy cycle fallback", () => { + const activeFirstCycle = normalizeMetricsInfo({ + aggregated: { scanner: { current_cycle: 0, current_cycle_active: true } }, + }) + assert.equal(activeFirstCycle.aggregated?.scanner?.current_cycle, 0) + assert.equal(activeFirstCycle.aggregated?.scanner?.current_cycle_active, true) + assert.equal(isScannerCycleActive(activeFirstCycle), true) + + const explicitIdle = normalizeMetricsInfo({ + aggregated: { scanner: { current_cycle: 3, current_cycle_active: false } }, + }) + assert.equal(explicitIdle.aggregated?.scanner?.current_cycle_active, false) + assert.equal(isScannerCycleActive(explicitIdle), false) + + const activityOnly = normalizeMetricsInfo({ aggregated: { scanner: { current_cycle_active: false } } }) + assert.equal(activityOnly.aggregated?.scanner?.current_cycle_active, false) + + const camelCaseActivity = normalizeMetricsInfo({ + aggregated: { scanner: { currentCycle: 0, currentCycleActive: true } }, + }) + assert.equal(camelCaseActivity.aggregated?.scanner?.current_cycle_active, true) + assert.equal(isScannerCycleActive(camelCaseActivity), true) + + assert.equal(isScannerCycleActive(normalizeMetricsInfo({ aggregated: { scanner: { current_cycle: 3 } } })), true) + assert.equal(isScannerCycleActive(normalizeMetricsInfo({ aggregated: { scanner: { current_cycle: 0 } } })), false) }) test("formatRelativeTime follows the active locale and advances with the clock", () => { diff --git a/tests/lib/performance-status-source.test.js b/tests/lib/performance-status-source.test.js index 17df4f80..e5daf311 100644 --- a/tests/lib/performance-status-source.test.js +++ b/tests/lib/performance-status-source.test.js @@ -11,6 +11,7 @@ test("status page passes every normalized admin info state into infrastructure h assert.match(source, /initializingServers=\{serverSummary\?\.initializing\}/) assert.match(source, /unknownDisks=\{systemInfo\.backend\?\.unknownDisks\}/) assert.match(source, /topology=\{runningStatus\.topology\}/) + assert.match(source, /isScannerCycleActive\(metricsInfo\)/) assert.doesNotMatch(source, /unknownDisks=.*\?\? 0/) }) diff --git a/tests/lib/running-status-safety.test.js b/tests/lib/running-status-safety.test.js index 4c19952b..d08ce62d 100644 --- a/tests/lib/running-status-safety.test.js +++ b/tests/lib/running-status-safety.test.js @@ -56,7 +56,7 @@ test("running status distinguishes unknown values and exposes recovery feedback" assert.match(pageSource, /lastUpdatedAt/) assert.match(pageSource, /sourceErrors/) assert.match(pageSource, /order-2 xl:order-3 xl:col-span-2/) - assert.match(pageSource, /scannerCycle/) + assert.match(pageSource, /isScannerCycleActive\(metricsInfo\)/) assert.match(pageSource, /t\("Scanner Status"\)/) assert.doesNotMatch(pageSource, /dayjs\(last\)/) assert.doesNotMatch(pageSource, /t\("Uptime"\)/)