Skip to content

Commit 40dfe49

Browse files
authored
fix: minor status page component bugs (#1943)
1 parent 066eba6 commit 40dfe49

File tree

3 files changed

+104
-10
lines changed

3 files changed

+104
-10
lines changed

packages/api/src/router/pageComponent.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from "zod";
22

3-
import { type SQL, and, asc, desc, eq, inArray, sql } from "@openstatus/db";
3+
import { type SQL, and, asc, desc, eq, inArray, ne, sql } from "@openstatus/db";
44
import {
55
monitor,
66
page,
@@ -169,7 +169,26 @@ export const pageComponentRouter = createTRPCRouter({
169169
)
170170
.all();
171171

172-
if (existingComponents.length >= pageComponentLimit) {
172+
// Count components on OTHER pages in this workspace
173+
const otherPagesComponentCount = await tx
174+
.select({ id: pageComponent.id })
175+
.from(pageComponent)
176+
.where(
177+
and(
178+
eq(pageComponent.workspaceId, opts.ctx.workspace.id),
179+
ne(pageComponent.pageId, opts.input.pageId),
180+
),
181+
)
182+
.all();
183+
184+
const inputComponentCount =
185+
opts.input.components.length +
186+
opts.input.groups.reduce((sum, g) => sum + g.components.length, 0);
187+
188+
const totalAfterUpdate =
189+
otherPagesComponentCount.length + inputComponentCount;
190+
191+
if (totalAfterUpdate > pageComponentLimit) {
173192
throw new TRPCError({
174193
code: "FORBIDDEN",
175194
message: "You reached your page component limits.",

packages/api/src/router/statusPage.utils.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,70 @@ describe("getUptime", () => {
805805

806806
expect(uptime).toBe("100%");
807807
});
808+
809+
it("should clamp report durations to the lookback window and never go negative", () => {
810+
const data = Array.from({ length: 45 }, (_, i) =>
811+
createStatusData(i, 100, 0, 0),
812+
);
813+
// Create reports that started well before the 45-day window
814+
const events: Event[] = Array.from({ length: 15 }, (_, i) => {
815+
const from = new Date();
816+
from.setDate(from.getDate() - (60 + i * 30)); // 60 to 480 days ago
817+
const to = new Date(from);
818+
to.setDate(to.getDate() + 2); // each 2 days long
819+
return {
820+
id: i + 1,
821+
name: `Old Report ${i}`,
822+
from,
823+
to,
824+
type: "report" as const,
825+
status: "degraded" as const,
826+
};
827+
});
828+
829+
const uptime = getUptime({
830+
data,
831+
events,
832+
barType: "manual",
833+
cardType: "manual",
834+
});
835+
836+
// Reports are entirely outside the window, so uptime should be 100%
837+
expect(Number.parseFloat(uptime)).toBe(100);
838+
});
839+
840+
it("should clamp partially overlapping reports to the window boundary", () => {
841+
const data = Array.from({ length: 45 }, (_, i) =>
842+
createStatusData(i, 100, 0, 0),
843+
);
844+
// Report started 50 days ago (before window) and ended 40 days ago (inside window)
845+
const from = new Date();
846+
from.setDate(from.getDate() - 50);
847+
const to = new Date();
848+
to.setDate(to.getDate() - 40);
849+
const events: Event[] = [
850+
{
851+
id: 1,
852+
name: "Overlapping Report",
853+
from,
854+
to,
855+
type: "report",
856+
status: "degraded",
857+
},
858+
];
859+
860+
const uptime = getUptime({
861+
data,
862+
events,
863+
barType: "manual",
864+
cardType: "manual",
865+
});
866+
867+
// Only ~5 days should count (from window start to report end), not 10 days
868+
const uptimeNum = Number.parseFloat(uptime);
869+
expect(uptimeNum).toBeGreaterThan(85);
870+
expect(uptimeNum).toBeLessThan(100);
871+
});
808872
});
809873

810874
describe("duration card type", () => {

packages/api/src/router/statusPage.utils.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -850,14 +850,28 @@ export function getUptime({
850850
barType: "absolute" | "dominant" | "manual";
851851
cardType: "requests" | "duration" | "dominant" | "manual";
852852
}): string {
853+
// Clamp event durations to the data lookback window to avoid
854+
// events outside the window producing negative uptime values.
855+
const timestamps = data.map((d) => new Date(d.day).getTime());
856+
const windowStart = timestamps.length > 0 ? Math.min(...timestamps) : 0;
857+
const windowEndDate = new Date(
858+
timestamps.length > 0 ? Math.max(...timestamps) : Date.now(),
859+
);
860+
windowEndDate.setUTCHours(23, 59, 59, 999);
861+
const windowEnd = windowEndDate.getTime();
862+
863+
function clampedDuration(item: Event): number {
864+
if (!item.from) return 0;
865+
const from = Math.max(item.from.getTime(), windowStart);
866+
const to = Math.min((item.to || new Date()).getTime(), windowEnd);
867+
return Math.max(0, to - from);
868+
}
869+
853870
if (barType === "manual") {
854871
const duration = events
855872
// NOTE: we want only user events
856873
.filter((e) => e.type === "report")
857-
.reduce((acc, item) => {
858-
if (!item.from) return acc;
859-
return acc + ((item.to || new Date()).getTime() - item.from.getTime());
860-
}, 0);
874+
.reduce((acc, item) => acc + clampedDuration(item), 0);
861875

862876
const total = data.length * MILLISECONDS_PER_DAY;
863877

@@ -867,10 +881,7 @@ export function getUptime({
867881
if (cardType === "duration") {
868882
const duration = events
869883
.filter((e) => e.type === "incident")
870-
.reduce((acc, item) => {
871-
if (!item.from) return acc;
872-
return acc + ((item.to || new Date()).getTime() - item.from.getTime());
873-
}, 0);
884+
.reduce((acc, item) => acc + clampedDuration(item), 0);
874885

875886
const total = data.length * MILLISECONDS_PER_DAY;
876887
return `${Math.floor(((total - duration) / total) * 10000) / 100}%`;

0 commit comments

Comments
 (0)