export type ServerHealthState = "online" | "offline" | "degraded" | "initializing" | "unknown" export type OperationalStatus = "healthy" | "degraded" | "stale" | "not_reported" | "unknown" export interface StatusDiagnostic { state: OperationalStatus reason?: string source?: string lastSuccessfulUpdate?: string lastError?: string historicalStallTimeouts?: number hint?: string scope?: { bucket?: string prefix?: string set?: string timeout?: string } } export interface PeerHealthDiagnostic extends StatusDiagnostic { nodeId: string isLocal?: boolean } export interface ClusterDiagnostics { peerHealth: StatusDiagnostic storageReadiness: StatusDiagnostic usageFreshness: StatusDiagnostic listingHealth: StatusDiagnostic workloadAdmission: StatusDiagnostic peers: PeerHealthDiagnostic[] membership: Array<{ nodeId: string; gridHost?: string; isLocal?: boolean }> topologyDrives: Array<{ nodeId: string poolIndex?: number setIndex?: number diskIndex?: number }> pools: Array<{ poolIndex?: number drivesPerSet?: number endpointCount?: number }> } export interface ServerInfo { endpoint?: string state?: ServerHealthState version?: string uptime?: number drives?: Array<{ uuid?: string drive_path?: string path?: string usedspace?: number totalspace?: number availspace?: number state?: string }> network?: Record } export interface SystemInfo { buckets?: { count?: number } objects?: { count?: number } servers?: ServerInfo[] backend?: { backendType?: string onlineDisks?: number offlineDisks?: number unknownDisks?: number totalDrivesPerSet?: number[] } adminDiscovery?: { clusterSnapshot: string } } export interface RunningStatusTopology { source: "v4" | "v3" | "reported" expectedServers?: number reportedServers?: number expectedDrives?: number reportedDrives?: number unreportedServers?: number unreportedDrives?: number incomplete: boolean } export interface RunningStatusView { servers?: ServerInfo[] serverSummary?: Record topology: RunningStatusTopology } export interface DataUsageInfo { total_capacity?: number total_free_capacity?: number total_used_capacity?: number } export interface StorageInfo { backend?: { BackendType?: string StandardSCParity?: string | number RRSCParity?: string | number } } export interface MetricsInfo { aggregated?: { scanner?: { current_cycle?: number current_cycle_active?: boolean current_started?: string cycle_complete_times?: string[] } } } type JsonRecord = Record function asRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {} } function asArray(value: unknown): T[] { return Array.isArray(value) ? (value as T[]) : [] } function asNumber(value: unknown): number | undefined { if (typeof value === "number" && Number.isFinite(value)) return value if (typeof value === "string" && value.trim() !== "") { const parsed = Number(value) if (Number.isFinite(parsed)) return parsed } return undefined } function asNonNegativeNumber(value: unknown): number | undefined { const number = asNumber(value) return number !== undefined && number >= 0 ? number : undefined } function asString(value: unknown): string | undefined { return typeof value === "string" && value.trim() !== "" ? value : undefined } function asBoolean(value: unknown): boolean | undefined { return typeof value === "boolean" ? value : undefined } function asStringOrNumber(value: unknown): string | number | undefined { return typeof value === "string" || (typeof value === "number" && Number.isFinite(value)) ? value : undefined } function asTimestamp(value: unknown): string | undefined { const timestamp = asString(value) return timestamp && !Number.isNaN(Date.parse(timestamp)) ? timestamp : undefined } function asUnixTimestamp(value: unknown): string | undefined { const seconds = asNonNegativeNumber(value) if (seconds === undefined || seconds === 0) return undefined const timestamp = new Date(seconds * 1000) return Number.isNaN(timestamp.getTime()) ? undefined : timestamp.toISOString() } function asSafeText(value: unknown): string | undefined { const text = asString(value)?.trim() return text ? text.slice(0, 500) : undefined } function asSafeAdminPath(value: unknown): string | undefined { const path = asString(value) return path?.startsWith("/") && !path.startsWith("//") && !path.includes("\\") ? path : undefined } export function normalizeServerHealthState(value: unknown): ServerHealthState { const state = asString(value)?.toLowerCase() if (state === "online" || state === "offline" || state === "degraded" || state === "initializing") { return state } return "unknown" } function normalizeDrive(value: unknown): NonNullable[number] | undefined { const source = asRecord(value) if (!Object.keys(source).length) return undefined return { uuid: asString(source.uuid ?? source.UUID), drive_path: asString(source.drive_path ?? source.drivePath ?? source.DrivePath), path: asString(source.path ?? source.Path), usedspace: asNonNegativeNumber(source.usedspace ?? source.usedSpace ?? source.UsedSpace), totalspace: asNonNegativeNumber(source.totalspace ?? source.totalSpace ?? source.TotalSpace), availspace: asNonNegativeNumber(source.availspace ?? source.availableSpace ?? source.AvailableSpace), state: asString(source.state ?? source.State)?.toLowerCase(), } } function normalizeServer(value: unknown): ServerInfo | undefined { const source = asRecord(value) if (!Object.keys(source).length) return undefined const network = Object.fromEntries( Object.entries(asRecord(source.network ?? source.Network)).flatMap(([key, state]) => { const normalizedState = asString(state)?.toLowerCase() return normalizedState ? [[key, normalizedState]] : [] }), ) return { endpoint: asString(source.endpoint ?? source.Endpoint), state: normalizeServerHealthState(source.state ?? source.State), version: asString(source.version ?? source.Version), uptime: asNonNegativeNumber(source.uptime ?? source.Uptime), drives: asArray(source.drives ?? source.Drives).flatMap((drive) => { const normalized = normalizeDrive(drive) return normalized ? [normalized] : [] }), network, } } function unwrapInfoRecord(value: unknown): JsonRecord { const record = asRecord(value) const wrapped = record.info ?? record.Info return wrapped && typeof wrapped === "object" && !Array.isArray(wrapped) ? (wrapped as JsonRecord) : record } function normalizeCountInfo(value: unknown): { count?: number } | undefined { const record = asRecord(value) if (record.error ?? record.Error) return undefined const count = asNonNegativeNumber(record.count ?? record.Count) return count === undefined ? undefined : { count } } export function normalizeSystemInfo(value: unknown): SystemInfo { const response = asRecord(value) const source = unwrapInfoRecord(value) const backend = asRecord(source.backend ?? source.Backend) const discovery = asRecord( response.admin_discovery ?? response.adminDiscovery ?? source.admin_discovery ?? source.adminDiscovery, ) const clusterSnapshotPath = asSafeAdminPath(discovery.clusterSnapshot ?? discovery.cluster_snapshot) const buckets = normalizeCountInfo(source.buckets ?? source.Buckets) const objects = normalizeCountInfo(source.objects ?? source.Objects) const rawServers = source.servers ?? source.Servers const servers = Array.isArray(rawServers) ? rawServers.flatMap((server) => { const normalized = normalizeServer(server) return normalized ? [normalized] : [] }) : undefined const totalDrivesPerSet = asArray(backend.totalDrivesPerSet ?? backend.TotalDrivesPerSet).flatMap( (value) => { const count = asNonNegativeNumber(value) return count === undefined ? [] : [count] }, ) return { ...(buckets ? { buckets } : {}), ...(objects ? { objects } : {}), ...(servers ? { servers } : {}), ...(Object.keys(backend).length ? { backend: { backendType: asString(backend.backendType ?? backend.BackendType), onlineDisks: asNonNegativeNumber(backend.onlineDisks ?? backend.OnlineDisks), offlineDisks: asNonNegativeNumber(backend.offlineDisks ?? backend.OfflineDisks), unknownDisks: asNonNegativeNumber(backend.unknownDisks ?? backend.UnknownDisks), ...(totalDrivesPerSet.length ? { totalDrivesPerSet } : {}), }, } : {}), ...(clusterSnapshotPath ? { adminDiscovery: { clusterSnapshot: clusterSnapshotPath } } : {}), } } function normalizeOperationalStatus(value: unknown): OperationalStatus { const state = asString(value)?.trim().toLowerCase().replaceAll("-", "_").replaceAll(" ", "_") if (["healthy", "ready", "online", "ok", "supported", "fresh", "available"].includes(state ?? "")) { return "healthy" } if (["stale", "outdated", "expired"].includes(state ?? "")) return "stale" if (["not_reported", "notreported", "unsupported", "disabled", "unavailable"].includes(state ?? "")) { return "not_reported" } if ( ["degraded", "unhealthy", "failed", "failure", "offline", "unreachable", "unresolved", "error", "timeout"].includes( state ?? "", ) ) { return "degraded" } return "unknown" } function normalizeScope(value: unknown): StatusDiagnostic["scope"] { const scope = asRecord(value) const bucket = asSafeText(scope.bucket ?? scope.bucket_name ?? scope.bucketName) const prefix = asSafeText(scope.prefix ?? scope.path) const setValue = asStringOrNumber(scope.set ?? scope.set_index ?? scope.setIndex) const timeoutValue = asStringOrNumber(scope.timeout ?? scope.timeout_ms ?? scope.timeoutMs) const normalized = { ...(bucket ? { bucket } : {}), ...(prefix ? { prefix } : {}), ...(setValue !== undefined ? { set: String(setValue) } : {}), ...(timeoutValue !== undefined ? { timeout: typeof timeoutValue === "number" ? `${timeoutValue} ms` : String(timeoutValue) } : {}), } return Object.keys(normalized).length ? normalized : undefined } function normalizeDiagnostic(value: unknown): StatusDiagnostic { if (typeof value === "string") return { state: normalizeOperationalStatus(value) } const record = asRecord(value) const nestedStatus = asRecord(record.status ?? record.Status) const state = normalizeOperationalStatus( record.condition ?? record.Condition ?? record.state ?? record.State ?? (typeof record.status === "string" ? record.status : undefined) ?? nestedStatus.state ?? nestedStatus.State, ) const reason = asSafeText(record.reason ?? record.Reason ?? nestedStatus.reason ?? nestedStatus.Reason) const source = asSafeText(record.source ?? record.Source ?? nestedStatus.source ?? nestedStatus.Source) const lastSuccessfulUpdate = asTimestamp( record.last_successful_update ?? record.lastSuccessfulUpdate ?? record.last_success ?? record.lastSuccess ?? nestedStatus.last_successful_update ?? nestedStatus.lastSuccessfulUpdate, ) ?? asUnixTimestamp(record.last_success_unix_secs ?? record.lastSuccessUnixSecs) const lastError = asSafeText( record.last_error ?? record.lastError ?? record.error ?? nestedStatus.last_error ?? nestedStatus.lastError, ) const scope = normalizeScope(record.scope ?? record.context ?? nestedStatus.scope ?? nestedStatus.context) const historicalStallTimeouts = asNonNegativeNumber( record.internode_stall_timeouts_total ?? record.internodeStallTimeoutsTotal, ) const hint = asSafeText(record.hint ?? record.Hint) return { state, ...(reason ? { reason } : {}), ...(source ? { source } : {}), ...(lastSuccessfulUpdate ? { lastSuccessfulUpdate } : {}), ...(lastError ? { lastError } : {}), ...(historicalStallTimeouts !== undefined ? { historicalStallTimeouts } : {}), ...(hint ? { hint } : {}), ...(scope ? { scope } : {}), } } function aggregateDiagnostics(diagnostics: StatusDiagnostic[], fallback?: StatusDiagnostic): StatusDiagnostic { if (!diagnostics.length) return fallback ?? { state: "unknown" } const priority: Record = { degraded: 0, stale: 1, unknown: 2, not_reported: 3, healthy: 4, } return diagnostics.reduce((worst, current) => (priority[current.state] < priority[worst.state] ? current : worst)) } function firstDiagnosticRecord(...values: unknown[]) { return values.find((value) => typeof value === "string" || Object.keys(asRecord(value)).length > 0) } export function normalizeClusterDiagnostics(value: unknown): ClusterDiagnostics | undefined { const response = asRecord(value) const wrappedSnapshot = response.snapshot ?? response.Snapshot const snapshot = wrappedSnapshot === undefined ? response : asRecord(wrappedSnapshot) if (!Object.keys(snapshot).length) return undefined const summary = asRecord(snapshot.summary ?? snapshot.Summary) const components = asRecord( snapshot.components ?? snapshot.Components ?? snapshot.component_status ?? snapshot.componentStatus, ) const membershipRecord = asRecord(snapshot.membership ?? snapshot.Membership) const membership = asArray(membershipRecord.nodes ?? membershipRecord.Nodes).flatMap((value) => { const node = asRecord(value) const nodeId = asSafeText(node.node_id ?? node.nodeId ?? node.NodeId) if (!nodeId) return [] const gridHost = asSafeText(node.grid_host ?? node.gridHost ?? node.GridHost) const isLocal = asBoolean(node.is_local ?? node.isLocal ?? node.IsLocal) return [{ nodeId, ...(gridHost ? { gridHost } : {}), ...(isLocal !== undefined ? { isLocal } : {}) }] }) const topologyDrives = asArray(membershipRecord.drives ?? membershipRecord.Drives).flatMap((value) => { const drive = asRecord(value) const nodeId = asSafeText(drive.node_id ?? drive.nodeId ?? drive.NodeId) if (!nodeId) return [] const poolIndex = asNonNegativeNumber(drive.pool_index ?? drive.poolIndex ?? drive.PoolIndex) const setIndex = asNonNegativeNumber(drive.set_index ?? drive.setIndex ?? drive.SetIndex) const diskIndex = asNonNegativeNumber(drive.disk_index ?? drive.diskIndex ?? drive.DiskIndex) return [ { nodeId, ...(poolIndex !== undefined ? { poolIndex } : {}), ...(setIndex !== undefined ? { setIndex } : {}), ...(diskIndex !== undefined ? { diskIndex } : {}), }, ] }) const poolState = asRecord(snapshot.pool_state ?? snapshot.poolState ?? snapshot.PoolState) const pools = asArray(poolState.pools ?? poolState.Pools).flatMap((value) => { const pool = asRecord(value) if (!Object.keys(pool).length) return [] const poolIndex = asNonNegativeNumber(pool.pool_index ?? pool.poolIndex ?? pool.PoolIndex) const drivesPerSet = asNonNegativeNumber(pool.drives_per_set ?? pool.drivesPerSet ?? pool.DrivesPerSet) const endpointCount = asNonNegativeNumber(pool.endpoint_count ?? pool.endpointCount ?? pool.EndpointCount) return [ { ...(poolIndex !== undefined ? { poolIndex } : {}), ...(drivesPerSet !== undefined ? { drivesPerSet } : {}), ...(endpointCount !== undefined ? { endpointCount } : {}), }, ] }) const peerRecord = asRecord(snapshot.peer_health ?? snapshot.peerHealth ?? snapshot.PeerHealth) const peers = asArray(peerRecord.peers ?? peerRecord.Peers).flatMap((value) => { const peer = asRecord(value) const nodeId = asSafeText(peer.node_id ?? peer.nodeId ?? peer.NodeId) if (!nodeId) return [] const diagnostic = normalizeDiagnostic(peer.status ?? peer.Status ?? peer) const isLocal = asBoolean(peer.is_local ?? peer.isLocal ?? peer.IsLocal) return [{ ...diagnostic, nodeId, ...(isLocal !== undefined ? { isLocal } : {}) }] }) const explicitPeerHealth = firstDiagnosticRecord( components.peer_health, components.peerHealth, summary.peer_health, summary.peerHealth, ) const peerHealth = explicitPeerHealth ? normalizeDiagnostic(explicitPeerHealth) : aggregateDiagnostics(peers, { state: "not_reported" }) const runtime = asRecord(snapshot.runtime_status ?? snapshot.runtimeStatus ?? snapshot.RuntimeStatus) const explicitStorage = firstDiagnosticRecord( snapshot.storage_readiness, snapshot.storageReadiness, components.storage_readiness, components.storageReadiness, components.storage, ) const storageReady = asBoolean(runtime.storage_ready ?? runtime.storageReady) const degradedReasons = asArray(runtime.degraded_reasons ?? runtime.degradedReasons) .flatMap((reason) => asSafeText(reason) ?? []) .join(", ") const storageReadiness = explicitStorage ? normalizeDiagnostic(explicitStorage) : storageReady === undefined ? { state: "not_reported" as const } : { state: storageReady ? ("healthy" as const) : ("degraded" as const), ...(!storageReady && degradedReasons ? { reason: degradedReasons } : {}), } const explicitUsage = firstDiagnosticRecord( snapshot.usage_freshness, snapshot.usageFreshness, snapshot.usage_cache, snapshot.usageCache, components.usage_freshness, components.usageFreshness, components.usage_cache, components.usageCache, components.usage, ) const usageFreshness = explicitUsage ? normalizeDiagnostic(explicitUsage) : { state: "not_reported" as const } const explicitListing = firstDiagnosticRecord( snapshot.listing, snapshot.metacache, snapshot.listing_metacache, snapshot.listingMetacache, components.listing, components.metacache, components.listing_metacache, components.listingMetacache, ) const listingHealth = explicitListing ? normalizeDiagnostic(explicitListing) : { state: "not_reported" as const } const explicitWorkloadAdmission = firstDiagnosticRecord( snapshot.workload_admission_status, snapshot.workloadAdmissionStatus, components.workload_admission, components.workloadAdmission, ) const workloadAdmission = explicitWorkloadAdmission ? normalizeDiagnostic(explicitWorkloadAdmission) : { state: "not_reported" as const } return { peerHealth, storageReadiness, usageFreshness, listingHealth, workloadAdmission, peers, membership, topologyDrives, pools, } } function getEndpointIdentity(value: string | undefined) { if (!value) return undefined const trimmed = value.trim().toLowerCase().replace(/\/$/, "") try { const url = new URL(trimmed.includes("://") ? trimmed : `http://${trimmed}`) return { host: url.host, hostname: url.hostname } } catch { const host = trimmed.replace(/^https?:\/\//, "") return { host, hostname: host.split(":")[0] } } } function getMemberIdentities(member: ClusterDiagnostics["membership"][number]) { return [getEndpointIdentity(member.nodeId), getEndpointIdentity(member.gridHost)].filter( (identity): identity is NonNullable> => Boolean(identity), ) } function findMatchingMember(server: ServerInfo, diagnostics: ClusterDiagnostics) { const serverIdentity = getEndpointIdentity(server.endpoint) if (!serverIdentity) return undefined const exactMatches = diagnostics.membership.filter((member) => getMemberIdentities(member).some((identity) => identity.host === serverIdentity.host), ) if (exactMatches.length === 1) return exactMatches[0] const hostnameMatches = diagnostics.membership.filter((member) => getMemberIdentities(member).some((identity) => identity.hostname === serverIdentity.hostname), ) return hostnameMatches.length === 1 ? hostnameMatches[0] : undefined } function findPeerDiagnostic(server: ServerInfo, diagnostics: ClusterDiagnostics) { const member = findMatchingMember(server, diagnostics) return member ? diagnostics.peers.find((peer) => peer.nodeId === member.nodeId) : undefined } function hasOnlyHealthyDrives(server: ServerInfo) { return Boolean( server.drives?.length && server.drives.every((drive) => ["ok", "online"].includes(drive.state?.trim().toLowerCase() ?? "")), ) } export function resolveServerHealth( server: ServerInfo, diagnostics?: ClusterDiagnostics, ): { state: ServerHealthState; reason?: string; source: "legacy" | "peer" } { const legacyState = normalizeServerHealthState(server.state) if (!diagnostics) return { state: legacyState, source: "legacy" } const peer = findPeerDiagnostic(server, diagnostics) if (!peer) return { state: legacyState, source: "legacy" } if (peer.state === "degraded") { return { state: "degraded", ...(peer.reason ? { reason: peer.reason } : {}), source: "peer" } } if ( (peer.state === "unknown" || peer.state === "stale" || peer.state === "not_reported") && (legacyState === "unknown" || (legacyState === "degraded" && hasOnlyHealthyDrives(server))) ) { return { state: "unknown", ...(peer.reason ? { reason: peer.reason } : {}), source: "peer" } } return { state: legacyState, source: "legacy" } } export function resolveUsageFreshness( diagnostic: StatusDiagnostic | undefined, context: { hasData: boolean; error?: string; lastUpdatedAt?: Date | null }, ): StatusDiagnostic { if (diagnostic && diagnostic.state !== "unknown") return diagnostic if (context.error && context.hasData) { return { state: "stale", reason: context.error, ...(context.lastUpdatedAt ? { lastSuccessfulUpdate: context.lastUpdatedAt.toISOString() } : {}), } } if (context.error) return { state: "unknown", reason: context.error } return diagnostic ?? { state: "unknown" } } export function normalizeStorageInfo(value: unknown): StorageInfo { const source = unwrapInfoRecord(value) const backend = asRecord(source.backend ?? source.Backend) if (!Object.keys(backend).length) return {} return { backend: { BackendType: asString(backend.BackendType ?? backend.backendType), StandardSCParity: asStringOrNumber(backend.StandardSCParity ?? backend.standardSCParity), RRSCParity: asStringOrNumber(backend.RRSCParity ?? backend.rrSCParity), }, } } export function normalizeDataUsageInfo(value: unknown): DataUsageInfo { const source = unwrapInfoRecord(value) const totalCapacity = asNonNegativeNumber(source.total_capacity ?? source.totalCapacity ?? source.TotalCapacity) const totalFreeCapacity = asNonNegativeNumber( source.total_free_capacity ?? source.totalFreeCapacity ?? source.TotalFreeCapacity, ) const totalUsedCapacity = asNonNegativeNumber( source.total_used_capacity ?? source.totalUsedCapacity ?? source.TotalUsedCapacity, ) return { ...(totalCapacity !== undefined ? { total_capacity: totalCapacity } : {}), ...(totalFreeCapacity !== undefined ? { total_free_capacity: totalFreeCapacity } : {}), ...(totalUsedCapacity !== undefined ? { total_used_capacity: totalUsedCapacity } : {}), } } export function normalizeMetricsInfo(value: unknown): MetricsInfo { const source = unwrapInfoRecord(value) 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) => { const timestamp = asTimestamp(item) return timestamp ? [timestamp] : [] }, ) 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 } : {}), }, }, } } 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, ): Record { const summary: Record = { online: 0, offline: 0, degraded: 0, initializing: 0, unknown: 0, } for (const server of servers ?? []) { summary[resolveServerHealth(server, diagnostics).state] += 1 } return summary } function getUniqueMembership(diagnostics: ClusterDiagnostics | undefined) { const members = diagnostics?.membership ?? [] return Array.from(new Map(members.map((member) => [member.nodeId, member])).values()) } function findServerIndexForMember( member: ClusterDiagnostics["membership"][number], members: ClusterDiagnostics["membership"], servers: ServerInfo[], usedIndexes: Set, ) { const memberIdentities = getMemberIdentities(member) const available = servers.flatMap((server, index) => { if (usedIndexes.has(index)) return [] const identity = getEndpointIdentity(server.endpoint) return identity ? [{ identity, index }] : [] }) const exactMatches = available.filter(({ identity }) => memberIdentities.some((memberIdentity) => memberIdentity.host === identity.host), ) if (exactMatches.length) return exactMatches[0].index const memberHostnames = new Set(memberIdentities.map((identity) => identity.hostname)) const hostnameMatches = available.filter(({ identity }) => memberHostnames.has(identity.hostname)) if (hostnameMatches.length !== 1) return undefined const sharedMemberHostname = members.some( (candidate) => candidate !== member && getMemberIdentities(candidate).some((identity) => memberHostnames.has(identity.hostname)), ) const sharedServerHostname = servers.some((server, index) => { if (index === hostnameMatches[0].index) return false const identity = getEndpointIdentity(server.endpoint) return identity ? memberHostnames.has(identity.hostname) : false }) return sharedMemberHostname || sharedServerHostname ? undefined : hostnameMatches[0].index } function reconcileTopologyServers(system: SystemInfo, diagnostics: ClusterDiagnostics | undefined) { const reportedServers = system.servers ?? [] const membership = getUniqueMembership(diagnostics) if (!membership.length) { return { servers: system.servers, reportedServers: system.servers?.length, expectedServers: undefined, } } const usedIndexes = new Set() let matchedServers = 0 const servers = membership.map((member): ServerInfo => { const serverIndex = findServerIndexForMember(member, membership, reportedServers, usedIndexes) if (serverIndex !== undefined) { usedIndexes.add(serverIndex) matchedServers += 1 return reportedServers[serverIndex] } return { endpoint: member.gridHost ?? member.nodeId, state: "unknown", drives: [], network: {}, } }) reportedServers.forEach((server, index) => { if (!usedIndexes.has(index)) servers.push(server) }) return { servers, reportedServers: matchedServers, expectedServers: membership.length, } } function sumDefined(values: Array) { return values.some((value) => value !== undefined) ? values.reduce((total, value) => total + (value ?? 0), 0) : undefined } export function buildRunningStatusView(system: SystemInfo, diagnostics?: ClusterDiagnostics): RunningStatusView { const reconciled = reconcileTopologyServers(system, diagnostics) const v4ExpectedDrives = diagnostics?.topologyDrives.length ? new Set( diagnostics.topologyDrives.map( (drive) => `${drive.poolIndex ?? ""}:${drive.setIndex ?? ""}:${drive.diskIndex ?? ""}:${drive.nodeId}`, ), ).size : sumDefined(diagnostics?.pools.map((pool) => pool.endpointCount) ?? []) const v3ExpectedDrives = system.backend?.totalDrivesPerSet?.reduce((total, count) => total + count, 0) const reportedDrives = sumDefined([ system.backend?.onlineDisks, system.backend?.offlineDisks, system.backend?.unknownDisks, ]) const hasV4Topology = reconciled.expectedServers !== undefined || v4ExpectedDrives !== undefined const expectedDrives = v4ExpectedDrives ?? v3ExpectedDrives const source = hasV4Topology ? "v4" : v3ExpectedDrives !== undefined ? "v3" : "reported" const unreportedServers = reconciled.expectedServers !== undefined && reconciled.reportedServers !== undefined ? Math.max(0, reconciled.expectedServers - reconciled.reportedServers) : undefined const unreportedDrives = expectedDrives !== undefined && reportedDrives !== undefined ? Math.max(0, expectedDrives - reportedDrives) : undefined const incomplete = Boolean( (reconciled.expectedServers !== undefined && reconciled.reportedServers !== reconciled.expectedServers) || (expectedDrives !== undefined && reportedDrives !== expectedDrives), ) return { servers: reconciled.servers, serverSummary: reconciled.servers ? summarizeServerStates(reconciled.servers, diagnostics) : undefined, topology: { source, ...(reconciled.expectedServers !== undefined ? { expectedServers: reconciled.expectedServers } : {}), ...(reconciled.reportedServers !== undefined ? { reportedServers: reconciled.reportedServers } : {}), ...(expectedDrives !== undefined ? { expectedDrives } : {}), ...(reportedDrives !== undefined ? { reportedDrives } : {}), ...(unreportedServers !== undefined ? { unreportedServers } : {}), ...(unreportedDrives !== undefined ? { unreportedDrives } : {}), incomplete, }, } } export function formatRelativeTime(timestamp: string | Date, locale: string | undefined, now = Date.now()) { const time = timestamp instanceof Date ? timestamp.getTime() : Date.parse(timestamp) if (!Number.isFinite(time)) return undefined const difference = time - now const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [ ["year", 31_536_000_000], ["month", 2_592_000_000], ["day", 86_400_000], ["hour", 3_600_000], ["minute", 60_000], ["second", 1_000], ] const [unit, size] = units.find(([, unitSize]) => Math.abs(difference) >= unitSize) ?? units.at(-1)! return new Intl.RelativeTimeFormat(locale, { numeric: "auto" }).format(Math.round(difference / size), unit) }