Merge branch 'bluewave-labs:develop' into feat/improved-status-bar-messages

This commit is contained in:
SkyKing_PX authored and GitHub committed 2026-04-16 13:40:10 +02:00
commit 675ec40836
9 files changed
+132 -11

No files matched your search

@@ -25,10 +25,7 @@ export const MonitorStatBoxes = ({
}
const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0;
const timeSinceLastFailure =
timeOfLastFailure > 0
? Date.now() - timeOfLastFailure
: Date.now() - new Date(monitorStats?.createdAt || 0).getTime();
const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0;
// Determine time since last check
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
@@ -43,6 +43,7 @@ export const AddTeamMemberDialog = ({
const roleOptions: { value: UserRole; label: string }[] = [
{ value: "admin", label: t("common.auth.roles.admin") },
{ value: "user", label: t("common.auth.roles.user") },
{ value: "demo", label: t("common.auth.roles.demo") },
];
const handleClose = () => {
@@ -48,6 +48,7 @@ export const InviteTeamMemberDialog = ({
const roleOptions: { value: UserRole; label: string }[] = [
{ value: "admin", label: t("common.auth.roles.admin") },
{ value: "user", label: t("common.auth.roles.user") },
{ value: "demo", label: t("common.auth.roles.demo") },
];
const handleGenerateToken = async (data: InviteFormData) => {
+2 -2
View File
@@ -32,8 +32,8 @@ class AuthRoutes {
this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById);
this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById);
this.router.patch("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser);
this.router.delete("/user", verifyJWT, this.authController.deleteUser);
this.router.patch("/user", verifyJWT, upload.single("profileImage"), isAllowed(["admin", "superadmin", "user"]), this.authController.editUser);
this.router.delete("/user", verifyJWT, isAllowed(["admin", "superadmin", "user"]), this.authController.deleteUser);
}
getRouter() {
@@ -200,8 +200,8 @@ export class NotificationsService implements INotificationsService {
};
deleteById = async (id: string, teamId: string): Promise<Notification> => {
const deleted = await this.notificationsRepository.deleteById(id, teamId);
await this.monitorsRepository.removeNotificationFromMonitors(id);
const deleted = await this.notificationsRepository.deleteById(id, teamId);
return deleted;
};
}
@@ -257,12 +257,18 @@ export class StatusService implements IStatusService {
};
}
// With a full window, a single raw check must not change monitor.status; only the sliding-window threshold can trigger a transition.
let newStatus = monitor.status;
// With a full window, a single raw check must not change UNLESS we are initializing. Otherwise, only the sliding-window threshold can trigger a transition.
let newStatus: MonitorStatus;
if (monitor.status === "initializing") {
newStatus = status === true ? "up" : "down";
} else {
newStatus = monitor.status;
}
let statusChanged = false;
// First evaluate reachability-based status changes, which apply to all monitor types and take precedence over hardware breaches.
const reachabilityResult = this.computeReachability(monitor.status, monitor.statusWindow, monitor.statusWindowThreshold);
const reachabilityResult = this.computeReachability(newStatus, monitor.statusWindow, monitor.statusWindowThreshold);
if (reachabilityResult.transitioned) {
newStatus = reachabilityResult.nextStatus;
statusChanged = true;
@@ -273,7 +279,7 @@ export class StatusService implements IStatusService {
const hardwarePayload = statusResponse.payload as HardwareStatusPayload | undefined;
if (monitor.type === "hardware" && hardwarePayload?.data) {
const hardware = this.computeHardwareStatus({
currentStatus: monitor.status,
currentStatus: newStatus,
reachabilityDown: newStatus === "down",
metrics: hardwarePayload.data,
thresholds: {
@@ -72,5 +72,31 @@ describe("Heartbeat job: down detection", () => {
const storedMonitor = await h.monitorsRepo.findById("mon-1", "team-1");
expect(storedMonitor.status).toBe("up");
expect(h.incidentsRepo.getAll()).toHaveLength(0);
// Regression guard for #3438: a sub-threshold blip must produce NO notification
// dispatches. Before PR #3505, a failing check below threshold silently wrote
// status="down" and the next passing check fired a spurious "Recovered" event.
expect(h.notificationsService.handleNotifications).not.toHaveBeenCalled();
});
it("fires zero notifications on a sub-threshold blip (regression #3438)", async () => {
// Dedicated end-to-end guard for the #3438 bug. Distinct from "stays up when
// failures are below threshold" because it pins the notification-dispatch
// contract independently of status/incident assertions, so a future refactor
// that reintroduces the silent-down-write can't sneak past by happening to
// leave status and incidents in the right shape.
const monitor = makeMonitor();
h.monitorsRepo.seed(monitor);
// Failing check below threshold (window [t,t,t,t,f], 20%)
h.setNextResponse(false, 503);
await h.heartbeatJob(monitor);
// Passing check — pre-fix, this is where the spurious "Recovered" fired
h.setNextResponse(true, 200);
await h.heartbeatJob(monitor);
expect((await h.monitorsRepo.findById("mon-1", "team-1")).status).toBe("up");
expect(h.incidentsRepo.getAll()).toHaveLength(0);
expect(h.notificationsService.handleNotifications).not.toHaveBeenCalled();
});
});
@@ -123,6 +123,57 @@ describe("Heartbeat job: hardware threshold breach", () => {
expect(incidents[0].resolutionType).toBe("automatic");
});
it("transitions breached hardware monitor to down when reachability also fails", async () => {
// A hardware monitor currently in "breached" state starts failing its reachability
// checks enough to cross the statusWindow threshold. "down" must take precedence
// over "breached" — when both conditions are true, the user wants the escalated
// alert. Unit-covered in statusService.test.ts via computeReachability; this is
// the end-to-end counterpart that exercises the full heartbeat path including
// incident-lifecycle handling.
const monitor = makeMonitor({
type: "hardware",
status: "breached",
cpuAlertThreshold: 80,
cpuAlertCounter: 0,
memoryAlertThreshold: 100,
memoryAlertCounter: 5,
diskAlertThreshold: 100,
diskAlertCounter: 5,
tempAlertThreshold: 100,
tempAlertCounter: 5,
selectedDisks: [],
// Window is fully healthy — the monitor is breached from hardware metrics,
// not from reachability. The test drives the window from clean → failing.
statusWindow: [true, true, true, true, true],
});
h.monitorsRepo.seed(monitor);
// Seed an active breached incident matching the initial state.
await h.incidentsRepo.create({
monitorId: "mon-1",
teamId: "team-1",
status: true,
statusCode: 9999,
message: "CPU: 90% (threshold: 80%)",
});
// Drive reachability failure: 3 consecutive failing heartbeats.
// Window evolves: [t,t,t,t,t] → [t,t,t,t,f] (20%) → [t,t,t,f,f] (40%) → [t,t,f,f,f] (60%)
// At 60% >= threshold, and current status "breached" !== "down", reachability fires.
// Reachability sends HTTP-shaped responses with no hardware payload, so the
// hardware block is correctly skipped for these checks.
h.setNextResponse(false, 503);
for (let i = 0; i < 3; i++) {
await h.heartbeatJob(monitor);
}
const stored = await h.monitorsRepo.findById("mon-1", "team-1");
expect(stored.status).toBe("down");
// Notification dispatch must have fired for the transition.
expect(h.notificationsService.handleNotifications).toHaveBeenCalled();
});
it("does not create duplicate incidents while remaining breached", async () => {
const monitor = makeMonitor({
type: "hardware",
@@ -418,6 +418,45 @@ describe("StatusService", () => {
expect(result.monitor.status).toBe("down");
});
it("resumes from 'initializing' to 'up' on a passing check with a full window (regression: pause/resume)", async () => {
// When a monitor is paused and resumed, togglePauseById sets status='initializing'
// but does NOT reset statusWindow. On the next check the window is already full,
// so the warmup early-return is skipped. Pre-fix, newStatus was seeded from
// monitor.status ('initializing') and computeReachability never transitioned
// out of 'initializing' on a passing window, so the monitor was stuck forever.
const monitor = makeMonitor({
statusWindow: [true, true, true, true, true],
statusWindowSize: 5,
statusWindowThreshold: 80,
status: "initializing",
});
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 }));
expect(result.monitor.status).toBe("up");
});
it("resumes from 'initializing' to 'down' on a failing check with a full window (regression: pause/resume mirror)", async () => {
// Mirror of the pause/resume regression with a failing check: should flip to 'down'
// immediately rather than stay stuck in 'initializing'.
const monitor = makeMonitor({
statusWindow: [false, false, false, false, false],
statusWindowSize: 5,
statusWindowThreshold: 80,
status: "initializing",
});
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 }));
expect(result.monitor.status).toBe("down");
});
it("during warmup (window not full) the stored status tracks the raw check", async () => {
// Early-return branch: monitor.status is written to reflect the current check and
// no status-changed notification is emitted. This is the service's documented warmup behavior.