Add timezone offset to fix bad display

This commit is contained in:
Owen
2026-07-16 16:09:35 -04:00
parent a48ef77ee5
commit a88b79e066
5 changed files with 82 additions and 29 deletions
+49 -18
View File
@@ -8,26 +8,42 @@ const STATUS_HISTORY_CACHE_TTL = 60; // seconds
function statusHistoryCacheKey(
entityType: string,
entityId: number,
days: number
days: number,
tzOffsetMinutes: number
): string {
return `statusHistory:${entityType}:${entityId}:${days}`;
return `statusHistory:${entityType}:${entityId}:${days}:${tzOffsetMinutes}`;
}
// Returns the epoch seconds of the most recent local-calendar-day midnight,
// where "local" is defined by tzOffsetMinutes (minutes to ADD to UTC to get
// local time, e.g. Australia/Sydney standard time is 600). Defaults to 0
// (UTC) so callers that don't pass a timezone keep the original behavior.
function localMidnightSec(tzOffsetMinutes: number): number {
const localNow = new Date(Date.now() + tzOffsetMinutes * 60_000);
localNow.setUTCHours(0, 0, 0, 0);
return Math.floor(localNow.getTime() / 1000) - tzOffsetMinutes * 60;
}
export async function getCachedStatusHistory(
entityType: string,
entityId: number,
days: number
days: number,
tzOffsetMinutes: number = 0
): Promise<StatusHistoryResponse> {
const cacheKey = statusHistoryCacheKey(entityType, entityId, days);
const cacheKey = statusHistoryCacheKey(
entityType,
entityId,
days,
tzOffsetMinutes
);
const cached = await cache.get<StatusHistoryResponse>(cacheKey);
if (cached !== undefined) {
return cached;
}
// Anchor to UTC midnight so the query window aligns with stable calendar days
const utcToday = new Date();
utcToday.setUTCHours(0, 0, 0, 0);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const startSec = todayMidnightSec - days * 86400;
const events = await logsDb
@@ -63,7 +79,8 @@ export async function getCachedStatusHistory(
const { buckets, totalDowntime } = computeBuckets(
events,
days,
priorStatus
priorStatus,
tzOffsetMinutes
);
const totalWindow = days * 86400;
const overallUptime =
@@ -99,11 +116,19 @@ export const statusHistoryQuerySchema = z
days: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 90))
.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))
})
.pipe(
z.object({
days: z.number().int().min(1).max(365)
days: z.number().int().min(1).max(365),
tzOffsetMinutes: z.number().int().min(-720).max(840)
})
);
@@ -133,15 +158,15 @@ export function computeBuckets(
id: number;
}[],
days: number,
priorStatus: string | null = null
priorStatus: string | null = null,
tzOffsetMinutes: number = 0
): { buckets: StatusHistoryDayBucket[]; totalDowntime: number } {
const nowSec = Math.floor(Date.now() / 1000);
// Anchor bucket boundaries to UTC midnight so dates are stable calendar days
// and don't drift as the cache expires and is recomputed
const utcToday = new Date();
utcToday.setUTCHours(0, 0, 0, 0);
const todayMidnightSec = Math.floor(utcToday.getTime() / 1000);
// Anchor bucket boundaries to local midnight (UTC when tzOffsetMinutes is
// 0) so dates are stable calendar days for the requesting client and
// don't drift as the cache expires and is recomputed
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const buckets: StatusHistoryDayBucket[] = [];
let totalDowntime = 0;
@@ -237,7 +262,13 @@ export function computeBuckets(
)
: 100;
const dateStr = new Date(dayStartSec * 1000).toISOString().slice(0, 10);
// 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
)
.toISOString()
.slice(0, 10);
const hasAnyData = currentStatus !== null || dayEvents.length > 0;