fix: regression - incorrect uptime badge color in status pages (#8141)

This commit is contained in:
Louis Lam authored and GitHub committed 2026-11-10 02:59:30 +08:00
1 parent c64c6d950d
commit b376409720
4 files changed
+84 -2

No files matched your search

+22
View File
@@ -72,3 +72,25 @@ export function applyLatestBeats(
} }
heartbeatList[monitorID] = beats; heartbeatList[monitorID] = beats;
} }
/**
* Store the newest beat of each monitor from a REST heartbeat payload
* @param state Mixin state
* @param heartbeatList Beats per monitor, oldest first
* @param translate Translation function
* @returns void
*/
export function applyHeartbeatSnapshot(
state: BeatState,
heartbeatList: Record<number | string, unknown[]>,
translate: Translate
): void {
for (const monitorID in heartbeatList) {
const beats = heartbeatList[monitorID];
if (!beats || beats.length === 0) {
continue;
}
const beat = beats[beats.length - 1];
applyBeat(state, Number(monitorID), beat, beatStatusEntry(translate, beat));
}
}
+2
View File
@@ -635,6 +635,7 @@ import {
} from "../util.ts"; } from "../util.ts";
import Tag from "../components/Tag.vue"; import Tag from "../components/Tag.vue";
import VueMultiselect from "vue-multiselect"; import VueMultiselect from "vue-multiselect";
import { applyHeartbeatSnapshot } from "../beat-state";
const toast = useToast(); const toast = useToast();
dayjs.extend(duration); dayjs.extend(duration);
@@ -1075,6 +1076,7 @@ export default {
this.$root.heartbeatList = heartbeatList; this.$root.heartbeatList = heartbeatList;
this.$root.uptimeList = uptimeList; this.$root.uptimeList = uptimeList;
applyHeartbeatSnapshot(this.$root, heartbeatList, this.$t.bind(this));
const heartbeatIds = Object.keys(heartbeatList); const heartbeatIds = Object.keys(heartbeatList);
const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => { const downMonitors = heartbeatIds.reduce((downMonitorsAmount, currentId) => {
+17 -1
View File
@@ -84,6 +84,22 @@ test.describe("Public status page", () => {
expect(await badge.text()).toContain("<svg"); expect(await badge.text()).toContain("<svg");
}); });
test("shows the uptime badge in green for an up monitor", async ({ page }) => {
const pushUrl = await addPushMonitor(page, "Uptime Badge Monitor");
// One up beat gives a 100% uptime badge; push it before the status page is fetched
await page.request.get(pushUrl);
await addStatusPageWithMonitor(page, "Uptime Badge Demo", "uptime-badge-demo", "Uptime Badge Monitor");
await page.context().clearCookies();
await page.goto("./status/uptime-badge-demo");
const badge = page.getByTestId("monitor").locator(".badge");
await expect(badge).toHaveText(/%$/);
await expect(badge).toHaveClass(/bg-primary/);
});
test("shows an active maintenance window to logged-out visitors", async ({ page }) => { test("shows an active maintenance window to logged-out visitors", async ({ page }) => {
const pushUrl = await addPushMonitor(page, "Maintenance Monitor"); const pushUrl = await addPushMonitor(page, "Maintenance Monitor");
await addStatusPageWithMonitor(page, "Maintenance Demo", "maintenance-demo", "Maintenance Monitor"); await addStatusPageWithMonitor(page, "Maintenance Demo", "maintenance-demo", "Maintenance Monitor");
@@ -107,7 +123,7 @@ test.describe("Public status page", () => {
await page.context().clearCookies(); await page.context().clearCookies();
await page.goto("./status/maintenance-demo"); await page.goto("./status/maintenance-demo");
await expect(page.locator(".bg-maintenance")).toContainText("Planned Window"); await expect(page.locator(".alert.bg-maintenance")).toContainText("Planned Window");
await expect(page.getByTestId("monitor-name")).toContainText("Maintenance Monitor"); await expect(page.getByTestId("monitor-name")).toContainText("Maintenance Monitor");
}); });
}); });
+43 -1
View File
@@ -1,6 +1,6 @@
import { describe, test } from "node:test"; import { describe, test } from "node:test";
import assert from "node:assert"; import assert from "node:assert";
import { applyBeat, applyLatestBeats, beatStatusEntry } from "../../src/beat-state"; import { applyBeat, applyHeartbeatSnapshot, applyLatestBeats, beatStatusEntry } from "../../src/beat-state";
import { DOWN, MAINTENANCE, PENDING, UP } from "../../src/util"; import { DOWN, MAINTENANCE, PENDING, UP } from "../../src/util";
/** /**
@@ -78,6 +78,48 @@ describe("applyLatestBeats()", () => {
}); });
}); });
describe("applyHeartbeatSnapshot()", () => {
/**
* Build empty mixin state
* @returns {object} State
*/
function emptyState() {
return { lastHeartbeatList: {}, statusList: {}, lastBeatRaw: {} };
}
test("fills the badge state from a REST payload with string monitor ids", () => {
const state = emptyState();
const beats = [{ status: UP }];
applyHeartbeatSnapshot(state, { 7: beats }, translate);
assert.strictEqual(state.lastHeartbeatList[7], beats[0]);
assert.deepStrictEqual(state.statusList[7], { text: "Up", color: "primary" });
});
test("uses the newest beat of every monitor", () => {
const state = emptyState();
const heartbeatList = {
1: [{ status: DOWN }, { status: UP }],
2: [{ status: DOWN }],
};
applyHeartbeatSnapshot(state, heartbeatList, translate);
assert.deepStrictEqual(state.statusList[1], { text: "Up", color: "primary" });
assert.deepStrictEqual(state.statusList[2], { text: "Down", color: "danger" });
});
test("leaves monitors without beats untouched", () => {
const state = { lastHeartbeatList: { 1: { status: UP } }, statusList: {}, lastBeatRaw: {} };
applyHeartbeatSnapshot(state, { 2: [] }, translate);
assert.strictEqual(Object.keys(state.lastHeartbeatList).length, 1);
assert.deepStrictEqual(state.statusList, {});
});
});
describe("applyBeat()", () => { describe("applyBeat()", () => {
test("updates the three maps in place", () => { test("updates the three maps in place", () => {
const state = { lastHeartbeatList: {}, statusList: {}, lastBeatRaw: {} }; const state = { lastHeartbeatList: {}, statusList: {}, lastBeatRaw: {} };