diff --git a/app/(dashboard)/rebalance/page.tsx b/app/(dashboard)/rebalance/page.tsx index 0beee2ef..8249f461 100644 --- a/app/(dashboard)/rebalance/page.tsx +++ b/app/(dashboard)/rebalance/page.tsx @@ -49,6 +49,14 @@ function isFailedRebalancePool(pool: PoolSummary) { return ["failed", "error"].includes(pool.status.trim().toLowerCase()) } +function formatCleanupWarnings(pool: PoolSummary) { + const warnings = pool.cleanupWarnings + if (!warnings.count) return "--" + + const context = [warnings.lastBucket, warnings.lastObject].filter(Boolean).join("/") + return context ? `${warnings.count} (${context})` : String(warnings.count) +} + export default function RebalancePage() { const { t } = useTranslation() const dialog = useDialog() @@ -178,6 +186,7 @@ export default function RebalancePage() { ...pool, status: statusPool.status || pool.status, progress: statusPool.progress, + cleanupWarnings: statusPool.cleanupWarnings, } }) }, [overview.pools, status?.pools]) @@ -279,24 +288,37 @@ export default function RebalancePage() { {t("Bytes Moved")} {t("Objects")} {t("Versions")} + {t("Cleanup Warnings")} {pools.length === 0 ? ( - + {t("No Data")} ) : ( pools.map((pool) => ( - + {pool.name} {pool.status || "--"} {formatBytesValue(pool.used)} {formatBytesValue(pool.progress.bytes)} {formatNumberValue(pool.progress.objects)} {formatNumberValue(pool.progress.versions)} + + {pool.cleanupWarnings.count > 0 ? ( + + {formatCleanupWarnings(pool)} + + ) : ( + "--" + )} + )) )} diff --git a/lib/pool-operations.ts b/lib/pool-operations.ts index 87075b1d..3935765e 100644 --- a/lib/pool-operations.ts +++ b/lib/pool-operations.ts @@ -29,6 +29,14 @@ export interface PoolUsageProgress { elapsed: number } +export interface RebalanceCleanupWarnings { + count: number + lastMessage?: string + lastBucket?: string + lastObject?: string + lastAt?: string +} + export interface PoolDecommissionSummary { startTime?: string startSize: number @@ -54,6 +62,7 @@ export interface PoolSummary { lastUpdate?: string status: string progress: PoolUsageProgress + cleanupWarnings: RebalanceCleanupWarnings decommission: PoolDecommissionSummary } @@ -138,6 +147,17 @@ function normalizeProgress(value: unknown): PoolUsageProgress { } } +function normalizeCleanupWarnings(value: unknown): RebalanceCleanupWarnings { + const record = asRecord(value) + return { + count: asNumber(record.count || record.Count), + lastMessage: asString(record.lastMsg || record.LastMsg || record.lastMessage || record.LastMessage) || undefined, + lastBucket: asString(record.lastBucket || record.LastBucket) || undefined, + lastObject: asString(record.lastObject || record.LastObject) || undefined, + lastAt: asString(record.lastAt || record.LastAt) || undefined, + } +} + function normalizeDecommissionSummary(value: unknown): PoolDecommissionSummary { const record = asRecord(value) return { @@ -185,12 +205,12 @@ function hasExplicitProgressPercent(record: JsonRecord): boolean { function deriveRebalanceProgressPercent(status: string, pools: PoolSummary[]): number { const state = normalizeState(status) - if (["completed", "complete", "success", "finished"].includes(state)) return 100 + if (["completed", "complete", "success", "finished"].includes(state) && pools.every(isCompletedRebalancePool)) { + return 100 + } if (pools.length === 0) return 0 - const finishedPools = pools.filter((pool) => - ["completed", "complete", "success", "finished"].includes(normalizeState(pool.status)), - ).length + const finishedPools = pools.filter(isCompletedRebalancePool).length return Math.round((finishedPools / pools.length) * 100) } @@ -224,6 +244,7 @@ function normalizePool(value: unknown, index: number): PoolSummary { rawUsagePercent > 0 && rawUsagePercent <= 1 ? rawUsagePercent * 100 : rawUsagePercent, ) const progress = normalizeProgress(record.progress || record.Progress) + const cleanupWarnings = normalizeCleanupWarnings(record.cleanupWarnings || record.CleanupWarnings) const rawId = record.id ?? record.poolId ?? record.pool ?? record.index ?? record.ID ?? record.PoolID ?? record.poolIndex @@ -238,6 +259,7 @@ function normalizePool(value: unknown, index: number): PoolSummary { lastUpdate: asString(record.lastUpdate || record.LastUpdate || record.updatedAt || record.UpdatedAt) || undefined, status: pickStatus(record), progress, + cleanupWarnings, decommission, } } @@ -327,6 +349,14 @@ function normalizeState(value: string): string { return value.trim().toLowerCase() } +function isIdleRebalancePool(pool: PoolSummary): boolean { + return ["", "none", "not_started", "not-started", "idle"].includes(normalizeState(pool.status)) +} + +function isCompletedRebalancePool(pool: PoolSummary): boolean { + return ["completed", "complete", "success", "finished"].includes(normalizeState(pool.status)) +} + function deriveStatusFromPools(pools: PoolSummary[]): string { if (pools.length === 0) return "" @@ -340,13 +370,11 @@ function deriveStatusFromPools(pools: PoolSummary[]): string { ) { return "running" } - if ( - states.every((state) => - ["completed", "complete", "success", "finished", "none", "not_started", "not-started"].includes(state), - ) - ) { + if (pools.every(isCompletedRebalancePool)) { return "completed" } + if (pools.some(isCompletedRebalancePool) && pools.some(isIdleRebalancePool)) return "running" + if (pools.every(isIdleRebalancePool)) return "" return "" } diff --git a/tests/lib/pool-operations.test.js b/tests/lib/pool-operations.test.js index 7aa59f86..dc54eb37 100644 --- a/tests/lib/pool-operations.test.js +++ b/tests/lib/pool-operations.test.js @@ -115,15 +115,31 @@ test("normalizeRebalanceStatus reads progress and pool details", () => { id: "reb-1", status: "running", progress: { bytes: "1000", objects: "10", versions: "3", eta: "12", elapsed: "6" }, - pools: [{ id: 1, used: "20", progress: { bytes: "400" } }], + pools: [ + { + id: 1, + used: "20", + progress: { bytes: "400" }, + cleanupWarnings: { + count: 1, + lastMsg: "cleanup warning", + lastBucket: "test-bucket", + lastObject: "object-a", + lastAt: "2026-06-12T00:00:00Z", + }, + }, + ], }) assert.equal(status.id, "reb-1") assert.equal(status.progressPercent, 40) assert.equal(status.pools[0]?.progress.bytes, 400) + assert.equal(status.pools[0]?.cleanupWarnings.count, 1) + assert.equal(status.pools[0]?.cleanupWarnings.lastMessage, "cleanup warning") + assert.equal(status.pools[0]?.cleanupWarnings.lastBucket, "test-bucket") }) -test("normalizeRebalanceStatus derives status from pool-only response", () => { +test("normalizeRebalanceStatus does not complete mixed completed and idle pools", () => { const status = normalizeRebalanceStatus({ id: "3be0831f-4315-4adb-9904-3bb0609b3bfc", pools: [ @@ -154,11 +170,11 @@ test("normalizeRebalanceStatus derives status from pool-only response", () => { stoppedAt: null, }) - assert.equal(status.status, "completed") - assert.equal(status.progressPercent, 100) + assert.equal(status.status, "running") + assert.equal(status.progressPercent, 50) assert.equal(status.pools[0]?.status, "Completed") assert.equal(status.pools[1]?.status, "None") - assert.equal(deriveRebalanceDisplayState(status, "supported"), "completed") + assert.equal(deriveRebalanceDisplayState(status, "supported"), "running") }) test("normalizeRebalanceStatus aggregates pool progress when totals are missing", () => { diff --git a/tests/lib/pool-overview-source.test.js b/tests/lib/pool-overview-source.test.js index a9781350..0fe836ea 100644 --- a/tests/lib/pool-overview-source.test.js +++ b/tests/lib/pool-overview-source.test.js @@ -16,7 +16,7 @@ test("rebalance page highlights failed pool rows", () => { assert.match(source, /function isFailedRebalancePool\(pool: PoolSummary\)/) assert.match(source, /\["failed", "error"\]\.includes\(pool\.status\.trim\(\)\.toLowerCase\(\)\)/) - assert.match(source, /className=\{cn\(isFailedRebalancePool\(pool\) && "bg-destructive\/10"\)\}/) + assert.match(source, /bg-destructive\/15 hover:bg-destructive\/20/) }) test("decommission page keeps decommission detail columns", () => {