From 0988a61c1d490aa797f8b6d6eff4cbed7148767e Mon Sep 17 00:00:00 2001 From: Iven Schlenther Date: Wed, 22 Jul 2026 12:51:30 +0200 Subject: [PATCH] Resolve legacy monitor status issues: handle unknown statuses, fix stuck states from old boolean values, and add regression tests. Adjust BaseChart text color for theme compatibility. --- server/src/service/statusService.ts | 7 +++- .../test/unit/services/statusService.test.ts | 39 +++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) diff --git a/server/src/service/statusService.ts b/server/src/service/statusService.ts index 748960a37..db83de08f 100755 --- a/server/src/service/statusService.ts +++ b/server/src/service/statusService.ts @@ -1,7 +1,7 @@ import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import type { Check, CheckDiskInfo, CheckSnapshot } from "@/domain/checks/check.type.js"; -import type { Monitor, MonitorStatus } from "@/domain/monitors/monitor.type.js"; +import { MonitorStatuses, type Monitor, type MonitorStatus } from "@/domain/monitors/monitor.type.js"; import type { DockerStatusPayload, GameStatusPayload, @@ -218,9 +218,12 @@ export class StatusService implements IStatusService { const patch: Partial = {}; // Resolve "initializing" and "maintenance" up front, incidents should be created on initialization && down + // Unknown values (e.g. legacy boolean statuses from old versions) are resolved the same way, + // otherwise computeReachability can never transition them and they stay stuck forever. + const isKnownStatus = (MonitorStatuses as readonly string[]).includes(monitor.status); let newStatus: MonitorStatus = monitor.status; let statusChanged = false; - if (monitor.status === "initializing" || monitor.status === "maintenance") { + if (monitor.status === "initializing" || monitor.status === "maintenance" || !isKnownStatus) { newStatus = status === true ? "up" : "down"; patch.status = newStatus; statusChanged = newStatus === "down"; diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 9e66ddbec..071465d48 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.ts @@ -498,6 +498,45 @@ describe("StatusService", () => { expect(result.monitor.status).toBe("down"); }); + it("resolves a legacy boolean status ('true') to 'up' on a passing check (regression: pre-upgrade monitors stuck forever)", async () => { + // Old Checkmate versions stored monitor.status as a boolean. Pre-fix, a monitor + // carrying status='true' never matched the initializing override and + // computeReachability only transitions to 'up' from 'down', so the legacy value + // was written back unchanged on every evaluation — stuck forever while the UI + // rendered it as 'initializing'. + const monitor = makeMonitor({ + statusWindow: [true, true, true, true, true], + statusWindowSize: 5, + statusWindowThreshold: 80, + status: "true" as MonitorStatus, + }); + const { service, monitorsRepository } = createService(); + (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); + (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); + + const result = await service.updateMonitorStatus(makeStatusResponse({ status: true }), makeCheck({ status: true }), monitor); + + expect(result.monitor.status).toBe("up"); + expect(result.statusChanged).toBe(false); // was effectively up already — no notification storm + }); + + it("resolves a legacy boolean status ('false') to 'down' on a failing check (regression mirror)", async () => { + const monitor = makeMonitor({ + statusWindow: [false, false, false, false, false], + statusWindowSize: 5, + statusWindowThreshold: 80, + status: "false" as MonitorStatus, + }); + const { service, monitorsRepository } = createService(); + (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); + (monitorsRepository.updateById as jest.Mock).mockImplementation((_id: unknown, _tid: unknown, m: unknown) => Promise.resolve(m)); + + const result = await service.updateMonitorStatus(makeStatusResponse({ status: false }), makeCheck({ status: false }), monitor); + + expect(result.monitor.status).toBe("down"); + expect(result.statusChanged).toBe(true); // down must surface so an incident is opened + }); + it("during warmup, sub-threshold raw checks no longer flip stored status (regression: down-during-init incident not resolving)", async () => { // Once the monitor has left 'initializing', the warmup branch must NOT silently // flip status from raw checks. Otherwise a down check during warmup writes