🚧 WIP: batched status

This commit is contained in:
Fred KISSIE
2026-07-20 20:37:52 +01:00
parent 4c873e7c48
commit 9cc3190e3a
2 changed files with 85 additions and 44 deletions
+55 -32
View File
@@ -264,9 +264,7 @@ export function computeBuckets(
// Shift by the client's offset before formatting so the label reflects
// their local calendar date rather than the UTC date of dayStartSec
const dateStr = new Date(
(dayStartSec + tzOffsetMinutes * 60) * 1000
)
const dateStr = new Date((dayStartSec + tzOffsetMinutes * 60) * 1000)
.toISOString()
.slice(0, 10);
@@ -305,13 +303,10 @@ export function computeBuckets(
return { buckets, totalDowntime };
}
export interface BatchedStatusHistoryResponse {
entityType: string;
entityIds: number[];
days: StatusHistoryDayBucket[];
overallUptimePercent: number;
totalDowntimeSeconds: number;
}
export type BatchedStatusHistoryResponse = Record<
string,
StatusHistoryResponse
>;
export async function getBatchedStatusHistory(
entityType: string,
@@ -345,8 +340,8 @@ export async function getBatchedStatusHistory(
// Fetch the last known state before the window so that entities that
// haven't changed status recently still show the correct status rather
// than appearing as "no_data".
const [lastKnownEvent] = await logsDb
.select()
const lastKnownEvents = await logsDb
.selectDistinctOn([statusHistory.entityId, statusHistory.timestamp])
.from(statusHistory)
.where(
and(
@@ -355,29 +350,57 @@ export async function getBatchedStatusHistory(
lt(statusHistory.timestamp, startSec)
)
)
.orderBy(desc(statusHistory.timestamp))
.limit(1);
.orderBy(desc(statusHistory.timestamp));
const priorStatus = lastKnownEvent?.status ?? null;
const eventStatusMap: Record<
number,
{
events: typeof events;
lastKnownEvent: (typeof lastKnownEvents)[number] | null;
}
> = {};
const { buckets, totalDowntime } = computeBuckets(
events,
days,
priorStatus
);
const totalWindow = days * 86400;
const overallUptime =
totalWindow > 0
? Math.max(0, ((totalWindow - totalDowntime) / totalWindow) * 100)
: 100;
for (const event of events) {
if (!eventStatusMap[event.entityId]) {
eventStatusMap[event.entityId] = {
events: [],
lastKnownEvent:
lastKnownEvents.find(
(ev) => ev.entityId === event.entityId
) ?? null
};
}
eventStatusMap[event.entityId].events.push(event);
}
const result: BatchedStatusHistoryResponse = {
entityType,
entityIds,
days: buckets,
overallUptimePercent: Math.round(overallUptime * 100) / 100,
totalDowntimeSeconds: totalDowntime
};
const result: BatchedStatusHistoryResponse = {};
for (const entityId in eventStatusMap) {
const event = eventStatusMap[Number(entityId)];
const priorStatus = event.lastKnownEvent?.status ?? null;
const { buckets, totalDowntime } = computeBuckets(
event.events,
days,
priorStatus
);
const totalWindow = days * 86400;
const overallUptime =
totalWindow > 0
? Math.max(
0,
((totalWindow - totalDowntime) / totalWindow) * 100
)
: 100;
result[entityId] = {
entityType,
entityId: Number(entityId),
days: buckets,
overallUptimePercent: Math.round(overallUptime * 100) / 100,
totalDowntimeSeconds: totalDowntime
};
}
// await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL);
return result;
+30 -12
View File
@@ -11,8 +11,35 @@ import {
StatusHistoryResponse
} from "@server/lib/statusHistory";
const siteParamsSchema = z.object({
siteId: z.string().transform((v) => parseInt(v, 10))
const siteIdParamsSchema = z.object({
days: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 90)),
// Minutes to add to UTC to get the requesting client's local time
// (e.g. Australia/Sydney standard time is 600). Optional and
// defaults to 0 (UTC) so older clients keep the prior behavior.
tzOffsetMinutes: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 0)),
siteIds: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
const raw = Array.isArray(val) ? val : [val];
const nums = raw
.map((v) =>
typeof v === "string" ? parseInt(v, 10) : Number(v)
)
.filter((n) => Number.isInteger(n) && n > 0);
const unique = [...new Set(nums)];
return unique.length ? unique : undefined;
}, z.array(z.number().int().positive()))
.openapi({
description: "Filter by siteIds (repeat query param)"
})
});
export async function getBatchedSiteStatusHistory(
@@ -21,16 +48,7 @@ export async function getBatchedSiteStatusHistory(
next: NextFunction
): Promise<any> {
try {
const parsedParams = siteParamsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const parsedQuery = statusHistoryQuerySchema.safeParse(req.query);
const parsedQuery = siteIdParamsSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(