Use user timezone for maintenance and official Plutonium manifest
This commit is contained in:
+47
-12
@@ -1,6 +1,9 @@
|
||||
import { validTimeZone } from './time-zone.js';
|
||||
|
||||
export interface MaintenanceSchedule {
|
||||
enabled: boolean;
|
||||
time: string;
|
||||
timeZone: string;
|
||||
nextAt: string | null;
|
||||
}
|
||||
export interface PlutoniumRelease {
|
||||
@@ -37,19 +40,51 @@ export function parsePlutoniumRevision(text: string): number | null {
|
||||
return match ? Number(match[1]) : null;
|
||||
}
|
||||
|
||||
export function parseChangelogRevision(html: string): number {
|
||||
const headings = [...html.matchAll(/<h2\b[^>]*>([\s\S]*?)<\/h2>/gi)];
|
||||
const revisions = headings.map(h => parsePlutoniumRevision(h[1].replace(/<[^>]*>/g, ' ')))
|
||||
.filter((r): r is number => r !== null);
|
||||
if (!revisions.length) throw new Error('The Plutonium changelog did not contain a release heading.');
|
||||
return Math.max(...revisions);
|
||||
export const PLUTONIUM_MANIFEST_URL = 'https://cdn.plutoniummod.com/updater/prod/info.json';
|
||||
|
||||
export function parsePlutoniumManifest(value: unknown): number {
|
||||
const manifest = value as { product?: unknown; revision?: unknown } | null;
|
||||
if (!manifest || manifest.product !== 'plutonium-core-prod' || typeof manifest.revision !== 'number'
|
||||
|| !Number.isSafeInteger(manifest.revision) || manifest.revision < 1) {
|
||||
throw new Error('The production updater manifest did not contain a valid Plutonium revision.');
|
||||
}
|
||||
return manifest.revision;
|
||||
}
|
||||
|
||||
export function nextMaintenanceAt(time: string, now = new Date()): string {
|
||||
if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(time)) throw new Error('Choose a valid UTC time (HH:mm).');
|
||||
const next = new Date(now);
|
||||
export function nextMaintenanceAt(time: string, now = new Date(), timeZone = 'UTC'): string {
|
||||
if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(time)) throw new Error('Choose a valid time (HH:mm).');
|
||||
if (!validTimeZone(timeZone)) throw new Error('Choose a valid timezone.');
|
||||
if (!Number.isFinite(now.getTime())) throw new Error('Invalid schedule date.');
|
||||
const [hour, minute] = time.split(':').map(Number);
|
||||
next.setUTCHours(hour, minute, 0, 0);
|
||||
if (next <= now) next.setUTCDate(next.getUTCDate() + 1);
|
||||
return next.toISOString();
|
||||
const target = hour * 60 + minute;
|
||||
const formatter = new Intl.DateTimeFormat('en-CA', {
|
||||
timeZone, year: 'numeric', month: '2-digit', day: '2-digit',
|
||||
hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
|
||||
});
|
||||
function local(at: number) {
|
||||
const parts = Object.fromEntries(formatter.formatToParts(at).map(p => [p.type, p.value]));
|
||||
return { day: `${parts.year}-${parts.month}-${parts.day}`, minute: Number(parts.hour) * 60 + Number(parts.minute) };
|
||||
}
|
||||
const today = local(now.getTime()).day;
|
||||
const start = Math.floor(now.getTime() / 60_000) * 60_000;
|
||||
const days = new Map<string, { at: number; minute: number }>();
|
||||
// Bounded minute search avoids assuming whole-hour offsets or 24-hour days.
|
||||
// A missing wall time runs at the first valid minute after the DST gap;
|
||||
// a repeated wall time runs only on its first occurrence that local day.
|
||||
for (let at = start - 26 * 3_600_000; at <= start + 50 * 3_600_000; at += 60_000) {
|
||||
const wall = local(at);
|
||||
if (wall.day < today || wall.minute < target) continue;
|
||||
const previous = days.get(wall.day);
|
||||
if (!previous || wall.minute < previous.minute) days.set(wall.day, { at, minute: wall.minute });
|
||||
}
|
||||
const next = [...days.values()].map(d => d.at).filter(at => at > now.getTime()).sort((a, b) => a - b)[0];
|
||||
if (next === undefined) throw new Error('Could not calculate the next maintenance time.');
|
||||
return new Date(next).toISOString();
|
||||
}
|
||||
|
||||
/** Present an existing schedule in the viewing user's zone without rewriting it. */
|
||||
export function maintenanceTimeInZone(schedule: MaintenanceSchedule, timeZone: string): string {
|
||||
if (schedule.timeZone === timeZone) return schedule.time;
|
||||
const at = schedule.nextAt ?? nextMaintenanceAt(schedule.time, new Date(), schedule.timeZone);
|
||||
return new Intl.DateTimeFormat('en-GB', { timeZone, hour: '2-digit', minute: '2-digit', hourCycle: 'h23' }).format(new Date(at));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user