feat: add global weather support
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m54s

This commit is contained in:
zv
2026-06-14 15:53:34 +02:00
parent d572c9cc53
commit 2182297adc
45 changed files with 739 additions and 212 deletions

View File

@@ -16,15 +16,27 @@ function isCronAuthorized(request: Request) {
return authHeader === `Bearer ${secret}` || secretHeader === secret;
}
function getWarsawDateKey(date = new Date()) {
const DEFAULT_MORNING_BRIEF_TIME = "07:00";
function normalizeTime(value: string | undefined, fallback: string) {
return value && /^\d{2}:\d{2}$/.test(value) ? value : fallback;
}
function getLocalDateParts(timeZone: string | null, date = new Date()) {
const parts = new Intl.DateTimeFormat("en-CA", {
timeZone: "Europe/Warsaw",
timeZone: timeZone || "Europe/Warsaw",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hourCycle: "h23",
}).formatToParts(date);
const part = (type: Intl.DateTimeFormatPartTypes) => parts.find((entry) => entry.type === type)?.value ?? "";
return `${part("year")}-${part("month")}-${part("day")}`;
return {
dateKey: `${part("year")}-${part("month")}-${part("day")}`,
time: `${part("hour")}:${part("minute")}`,
};
}
export async function GET(request: Request) {
@@ -36,35 +48,43 @@ export async function GET(request: Request) {
}
const now = new Date();
const dateKey = getWarsawDateKey(now);
const briefTime = normalizeTime(process.env.NOTIFICATIONS_MORNING_BRIEF_TIME, DEFAULT_MORNING_BRIEF_TIME);
const subscriptions = getPushSubscriptions().filter((subscription) => (
subscription.enabled
&& subscription.morningBriefEnabled
subscription.morningBriefEnabled
&& Number.isFinite(subscription.latitude)
&& Number.isFinite(subscription.longitude)
));
let warningsUnavailable = false;
const warnings = await fetchMeteoWarnings(AbortSignal.timeout(12_000)).catch(() => {
warningsUnavailable = true;
return [];
});
const hasPolishSubscriptions = subscriptions.some((subscription) => subscription.region === "PL");
const warnings = hasPolishSubscriptions
? await fetchMeteoWarnings(AbortSignal.timeout(12_000)).catch(() => {
warningsUnavailable = true;
return [];
})
: [];
let sent = 0;
let skipped = 0;
let failed = 0;
for (const subscription of subscriptions) {
const localTime = getLocalDateParts(subscription.timezone, now);
if (localTime.time < briefTime) {
skipped += 1;
continue;
}
const dateKey = localTime.dateKey;
if (hasSentMorningBrief(subscription.endpoint, dateKey)) {
skipped += 1;
continue;
}
try {
const forecast = await fetchServerForecast(subscription.latitude as number, subscription.longitude as number);
const forecast = await fetchServerForecast(subscription.latitude as number, subscription.longitude as number, subscription.region);
const brief = buildWeatherBrief({
forecast,
warnings,
warnings: subscription.region === "PL" ? warnings : [],
province: subscription.province,
countyTeryt: subscription.countyTeryt,
countyTeryt: subscription.region === "PL" ? subscription.countyTeryt : null,
locationName: subscription.locationName ?? "wtr.",
language: subscription.language,
temperatureUnit: subscription.temperatureUnit ?? DEFAULT_TEMPERATURE_UNIT,
@@ -87,7 +107,7 @@ export async function GET(request: Request) {
return NextResponse.json({
ok: true,
date: dateKey,
time: briefTime,
subscriptions: subscriptions.length,
warningsUnavailable,
sent,