import { NextResponse } from "next/server"; import { fetchServerForecast } from "@/lib/server-forecast"; import { fetchMeteoWarnings } from "@/lib/server-warnings"; import { getPushSubscriptions, hasSentTomorrowBrief, markTomorrowBriefSent, removePushSubscription } from "@/lib/push-store"; import { isWebPushConfigured, sendTomorrowBriefNotification } from "@/lib/push-service"; import { buildTomorrowWeatherBrief } from "@/lib/weather-brief"; import { DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils"; export const runtime = "nodejs"; function isCronAuthorized(request: Request) { const secret = process.env.NOTIFICATIONS_CRON_SECRET; if (!secret) return process.env.NODE_ENV !== "production"; const authHeader = request.headers.get("authorization"); const secretHeader = request.headers.get("x-cron-secret"); return authHeader === `Bearer ${secret}` || secretHeader === secret; } function getWarsawDateKey(date = new Date(), dayOffset = 0) { const parts = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Warsaw", year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(date); const part = (type: Intl.DateTimeFormatPartTypes) => parts.find((entry) => entry.type === type)?.value ?? ""; if (dayOffset === 0) return `${part("year")}-${part("month")}-${part("day")}`; const shiftedDate = new Date(Date.UTC(Number(part("year")), Number(part("month")) - 1, Number(part("day")) + dayOffset, 12)); const shiftedParts = new Intl.DateTimeFormat("en-CA", { timeZone: "Europe/Warsaw", year: "numeric", month: "2-digit", day: "2-digit", }).formatToParts(shiftedDate); const shiftedPart = (type: Intl.DateTimeFormatPartTypes) => shiftedParts.find((entry) => entry.type === type)?.value ?? ""; return `${shiftedPart("year")}-${shiftedPart("month")}-${shiftedPart("day")}`; } export async function GET(request: Request) { if (!isCronAuthorized(request)) { return NextResponse.json({ error: "Unauthorized." }, { status: 401 }); } if (!isWebPushConfigured()) { return NextResponse.json({ error: "Web Push is not configured." }, { status: 503 }); } const now = new Date(); const targetDateKey = getWarsawDateKey(now, 1); const subscriptions = getPushSubscriptions().filter((subscription) => ( subscription.enabled && subscription.tomorrowBriefEnabled && Number.isFinite(subscription.latitude) && Number.isFinite(subscription.longitude) )); let warningsUnavailable = false; const warnings = await fetchMeteoWarnings(AbortSignal.timeout(12_000)).catch(() => { warningsUnavailable = true; return []; }); let sent = 0; let skipped = 0; let failed = 0; for (const subscription of subscriptions) { if (hasSentTomorrowBrief(subscription.endpoint, targetDateKey)) { skipped += 1; continue; } try { const forecast = await fetchServerForecast(subscription.latitude as number, subscription.longitude as number); const brief = buildTomorrowWeatherBrief({ forecast, warnings, province: subscription.province, countyTeryt: subscription.countyTeryt, locationName: subscription.locationName ?? "wtr.", language: subscription.language, windSpeedUnit: subscription.windSpeedUnit ?? DEFAULT_WIND_SPEED_UNIT, now, }); if (!brief) { skipped += 1; continue; } await sendTomorrowBriefNotification(subscription, brief); markTomorrowBriefSent(subscription.endpoint, targetDateKey); sent += 1; } catch (error) { failed += 1; const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null; if (statusCode === 404 || statusCode === 410) removePushSubscription(subscription.endpoint); } } return NextResponse.json({ ok: true, date: targetDateKey, subscriptions: subscriptions.length, warningsUnavailable, sent, skipped, failed, }); }