Files
wtr/app/api/notifications/check/route.ts
zv ee55521803
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
chore: add prettier formatting
2026-06-14 20:26:56 +02:00

105 lines
3.4 KiB
TypeScript

import { NextResponse } from "next/server";
import { fetchMeteoWarnings } from "@/lib/server-warnings";
import {
getPushSubscriptions,
hasSentWarning,
markWarningSent,
pruneSentWarnings,
removePushSubscription,
} from "@/lib/push-store";
import { isWebPushConfigured, sendWarningNotification } from "@/lib/push-service";
import { warningMatchesCounty } from "@/lib/warning-regions";
import type { WeatherWarning } from "@/types/imgw";
export const runtime = "nodejs";
function getTimestamp(value: string | null) {
if (!value) return null;
const timestamp = new Date(value).getTime();
return Number.isNaN(timestamp) ? null : timestamp;
}
function isRelevantWarning(warning: WeatherWarning, now: number) {
const validTo = getTimestamp(warning.validTo);
return warning.kind === "meteo" && (validTo === null || validTo > now);
}
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 getErrorMessage(error: unknown) {
return error instanceof Error ? error.message : "Unknown notification check error.";
}
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 });
}
try {
const now = Date.now();
const warnings = (await fetchMeteoWarnings(AbortSignal.timeout(12_000))).filter((warning) =>
isRelevantWarning(warning, now),
);
const subscriptions = getPushSubscriptions().filter(
(subscription) => subscription.region === "PL" && subscription.enabled && subscription.province,
);
const activeWarningIds = new Set(warnings.map((warning) => warning.id));
let sent = 0;
let skipped = 0;
let failed = 0;
pruneSentWarnings(activeWarningIds);
for (const subscription of subscriptions) {
const matchingWarnings = warnings.filter((warning) =>
subscription.countyTeryt
? warningMatchesCounty(warning, subscription.countyTeryt)
: subscription.province !== null && warning.provinces.includes(subscription.province),
);
for (const warning of matchingWarnings) {
if (hasSentWarning(subscription.endpoint, warning.id)) {
skipped += 1;
continue;
}
try {
await sendWarningNotification(subscription, warning);
markWarningSent(subscription.endpoint, warning.id);
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,
subscriptions: subscriptions.length,
warnings: warnings.length,
sent,
skipped,
failed,
});
} catch (error) {
return NextResponse.json(
{
error: "Unable to check IMGW meteorological warnings.",
details: getErrorMessage(error),
},
{ status: 502 },
);
}
}