chore: add prettier formatting
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
All checks were successful
CI / Lint, typecheck and build (push) Successful in 9m56s
This commit is contained in:
@@ -26,6 +26,12 @@ export async function GET(request: Request) {
|
||||
headers: { "Cache-Control": "public, s-maxage=120, stale-while-revalidate=300" },
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json({ error: region === "PL" ? "IMGW Hybrid service is unavailable." : "Open-Meteo current conditions are unavailable." }, { status: 502 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
region === "PL" ? "IMGW Hybrid service is unavailable." : "Open-Meteo current conditions are unavailable.",
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { isImgwNoProductsResponse, readImgwResponseBody } from "@/lib/imgw-empty-response";
|
||||
|
||||
const ALLOWED_PATHS = new Set([
|
||||
"synop",
|
||||
"hydro",
|
||||
"meteo",
|
||||
"warningsmeteo",
|
||||
"warningshydro",
|
||||
"product",
|
||||
]);
|
||||
const ALLOWED_PATHS = new Set(["synop", "hydro", "meteo", "warningsmeteo", "warningshydro", "product"]);
|
||||
const IMGW_BASE_URL = "https://danepubliczne.imgw.pl/api/data";
|
||||
const USER_AGENT = "wtr./1.0 (weather data proxy)";
|
||||
|
||||
@@ -30,7 +23,11 @@ export async function GET(_: Request, context: { params: Promise<{ path: string[
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await readImgwResponseBody(response);
|
||||
if ((resource === "warningsmeteo" || resource === "warningshydro") && response.status === 404 && isImgwNoProductsResponse(body.json)) {
|
||||
if (
|
||||
(resource === "warningsmeteo" || resource === "warningshydro") &&
|
||||
response.status === 404 &&
|
||||
isImgwNoProductsResponse(body.json)
|
||||
) {
|
||||
return NextResponse.json([], {
|
||||
headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600" },
|
||||
});
|
||||
|
||||
@@ -49,28 +49,32 @@ export async function GET(request: Request) {
|
||||
headers: { Accept: "application/json", "User-Agent": USER_AGENT },
|
||||
});
|
||||
if (!response.ok) return NextResponse.json({ error: "Reverse geocoding is unavailable." }, { status: 502 });
|
||||
const data = await response.json() as RawReverseLocation;
|
||||
const name = data.address?.city
|
||||
?? data.address?.town
|
||||
?? data.address?.village
|
||||
?? data.address?.municipality
|
||||
?? data.name
|
||||
?? data.display_name?.split(",")[0]?.trim();
|
||||
const data = (await response.json()) as RawReverseLocation;
|
||||
const name =
|
||||
data.address?.city ??
|
||||
data.address?.town ??
|
||||
data.address?.village ??
|
||||
data.address?.municipality ??
|
||||
data.name ??
|
||||
data.display_name?.split(",")[0]?.trim();
|
||||
if (!name) return NextResponse.json({ error: "Place name is unavailable." }, { status: 404 });
|
||||
const countryCode = data.address?.country_code?.toUpperCase() ?? null;
|
||||
return NextResponse.json({
|
||||
name,
|
||||
province: data.address?.state ?? null,
|
||||
district: data.address?.county ?? null,
|
||||
country: data.address?.country ?? null,
|
||||
countryCode,
|
||||
admin1: data.address?.state ?? null,
|
||||
admin2: data.address?.county ?? null,
|
||||
timezone: null,
|
||||
region: getWeatherRegionForCountryCode(countryCode),
|
||||
}, {
|
||||
headers: { "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=604800" },
|
||||
});
|
||||
return NextResponse.json(
|
||||
{
|
||||
name,
|
||||
province: data.address?.state ?? null,
|
||||
district: data.address?.county ?? null,
|
||||
country: data.address?.country ?? null,
|
||||
countryCode,
|
||||
admin1: data.address?.state ?? null,
|
||||
admin2: data.address?.county ?? null,
|
||||
timezone: null,
|
||||
region: getWeatherRegionForCountryCode(countryCode),
|
||||
},
|
||||
{
|
||||
headers: { "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=604800" },
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Reverse geocoding is unavailable." }, { status: 502 });
|
||||
}
|
||||
|
||||
@@ -30,26 +30,31 @@ export async function GET(request: Request) {
|
||||
try {
|
||||
const response = await fetch(`${GEOCODING_URL}?${params}`, { next: { revalidate: 86400 } });
|
||||
if (!response.ok) return NextResponse.json({ error: "Location search is unavailable." }, { status: 502 });
|
||||
const data = await response.json() as { results?: RawLocation[] };
|
||||
const data = (await response.json()) as { results?: RawLocation[] };
|
||||
const results = (data.results ?? []).flatMap((location) => {
|
||||
if (!location.id || !location.name || !Number.isFinite(location.latitude) || !Number.isFinite(location.longitude)) return [];
|
||||
if (!location.id || !location.name || !Number.isFinite(location.latitude) || !Number.isFinite(location.longitude))
|
||||
return [];
|
||||
const countryCode = location.country_code?.toUpperCase() ?? null;
|
||||
return [{
|
||||
id: location.id,
|
||||
name: location.name,
|
||||
latitude: location.latitude as number,
|
||||
longitude: location.longitude as number,
|
||||
province: location.admin1 ?? null,
|
||||
district: location.admin2 ?? null,
|
||||
country: location.country ?? null,
|
||||
countryCode,
|
||||
admin1: location.admin1 ?? null,
|
||||
admin2: location.admin2 ?? null,
|
||||
timezone: location.timezone ?? null,
|
||||
region: getWeatherRegionForCountryCode(countryCode),
|
||||
}];
|
||||
return [
|
||||
{
|
||||
id: location.id,
|
||||
name: location.name,
|
||||
latitude: location.latitude as number,
|
||||
longitude: location.longitude as number,
|
||||
province: location.admin1 ?? null,
|
||||
district: location.admin2 ?? null,
|
||||
country: location.country ?? null,
|
||||
countryCode,
|
||||
admin1: location.admin1 ?? null,
|
||||
admin2: location.admin2 ?? null,
|
||||
timezone: location.timezone ?? null,
|
||||
region: getWeatherRegionForCountryCode(countryCode),
|
||||
},
|
||||
];
|
||||
});
|
||||
return NextResponse.json(results, {
|
||||
headers: { "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=604800" },
|
||||
});
|
||||
return NextResponse.json(results, { headers: { "Cache-Control": "public, s-maxage=86400, stale-while-revalidate=604800" } });
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Location search is unavailable." }, { status: 502 });
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { fetchMeteoWarnings } from "@/lib/server-warnings";
|
||||
import { getPushSubscriptions, hasSentWarning, markWarningSent, pruneSentWarnings, removePushSubscription } from "@/lib/push-store";
|
||||
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";
|
||||
@@ -40,8 +46,12 @@ export async function GET(request: Request) {
|
||||
|
||||
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 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;
|
||||
@@ -50,9 +60,11 @@ export async function GET(request: Request) {
|
||||
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)
|
||||
));
|
||||
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;
|
||||
@@ -65,7 +77,8 @@ export async function GET(request: Request) {
|
||||
sent += 1;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
const statusCode =
|
||||
typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
if (statusCode === 404 || statusCode === 410) removePushSubscription(subscription.endpoint);
|
||||
}
|
||||
}
|
||||
@@ -80,9 +93,12 @@ export async function GET(request: Request) {
|
||||
failed,
|
||||
});
|
||||
} catch (error) {
|
||||
return NextResponse.json({
|
||||
error: "Unable to check IMGW meteorological warnings.",
|
||||
details: getErrorMessage(error),
|
||||
}, { status: 502 });
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Unable to check IMGW meteorological warnings.",
|
||||
details: getErrorMessage(error),
|
||||
},
|
||||
{ status: 502 },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { fetchServerForecast } from "@/lib/server-forecast";
|
||||
import { fetchMeteoWarnings } from "@/lib/server-warnings";
|
||||
import { getPushSubscriptions, hasSentMorningBrief, markMorningBriefSent, removePushSubscription } from "@/lib/push-store";
|
||||
import {
|
||||
getPushSubscriptions,
|
||||
hasSentMorningBrief,
|
||||
markMorningBriefSent,
|
||||
removePushSubscription,
|
||||
} from "@/lib/push-store";
|
||||
import { isWebPushConfigured, sendMorningBriefNotification } from "@/lib/push-service";
|
||||
import { buildWeatherBrief } from "@/lib/weather-brief";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils";
|
||||
@@ -49,18 +54,19 @@ export async function GET(request: Request) {
|
||||
|
||||
const now = new Date();
|
||||
const briefTime = normalizeTime(process.env.NOTIFICATIONS_MORNING_BRIEF_TIME, DEFAULT_MORNING_BRIEF_TIME);
|
||||
const subscriptions = getPushSubscriptions().filter((subscription) => (
|
||||
subscription.morningBriefEnabled
|
||||
&& Number.isFinite(subscription.latitude)
|
||||
&& Number.isFinite(subscription.longitude)
|
||||
));
|
||||
const subscriptions = getPushSubscriptions().filter(
|
||||
(subscription) =>
|
||||
subscription.morningBriefEnabled &&
|
||||
Number.isFinite(subscription.latitude) &&
|
||||
Number.isFinite(subscription.longitude),
|
||||
);
|
||||
let warningsUnavailable = false;
|
||||
const hasPolishSubscriptions = subscriptions.some((subscription) => subscription.region === "PL");
|
||||
const warnings = hasPolishSubscriptions
|
||||
? await fetchMeteoWarnings(AbortSignal.timeout(12_000)).catch(() => {
|
||||
warningsUnavailable = true;
|
||||
return [];
|
||||
})
|
||||
warningsUnavailable = true;
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
let sent = 0;
|
||||
let skipped = 0;
|
||||
@@ -79,7 +85,11 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const forecast = await fetchServerForecast(subscription.latitude as number, subscription.longitude as number, subscription.region);
|
||||
const forecast = await fetchServerForecast(
|
||||
subscription.latitude as number,
|
||||
subscription.longitude as number,
|
||||
subscription.region,
|
||||
);
|
||||
const brief = buildWeatherBrief({
|
||||
forecast,
|
||||
warnings: subscription.region === "PL" ? warnings : [],
|
||||
@@ -100,7 +110,8 @@ export async function GET(request: Request) {
|
||||
sent += 1;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
const statusCode =
|
||||
typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
if (statusCode === 404 || statusCode === 410) removePushSubscription(subscription.endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,12 @@ import { NextResponse } from "next/server";
|
||||
import { upsertPushSubscription, removePushSubscription } from "@/lib/push-store";
|
||||
import { isWebPushConfigured } from "@/lib/push-service";
|
||||
import { normalizeProvinceName } from "@/lib/provinces";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT, isTemperatureUnit, isWindSpeedUnit } from "@/lib/weather-utils";
|
||||
import {
|
||||
DEFAULT_TEMPERATURE_UNIT,
|
||||
DEFAULT_WIND_SPEED_UNIT,
|
||||
isTemperatureUnit,
|
||||
isWindSpeedUnit,
|
||||
} from "@/lib/weather-utils";
|
||||
import type { Language } from "@/lib/i18n";
|
||||
import type { BrowserPushSubscription } from "@/types/notifications";
|
||||
import type { WeatherRegion } from "@/types/weather-region";
|
||||
@@ -12,10 +17,12 @@ export const runtime = "nodejs";
|
||||
function isBrowserPushSubscription(value: unknown): value is BrowserPushSubscription {
|
||||
if (!value || typeof value !== "object") return false;
|
||||
const subscription = value as Partial<BrowserPushSubscription>;
|
||||
return typeof subscription.endpoint === "string"
|
||||
&& subscription.endpoint.startsWith("https://")
|
||||
&& typeof subscription.keys?.p256dh === "string"
|
||||
&& typeof subscription.keys.auth === "string";
|
||||
return (
|
||||
typeof subscription.endpoint === "string" &&
|
||||
subscription.endpoint.startsWith("https://") &&
|
||||
typeof subscription.keys?.p256dh === "string" &&
|
||||
typeof subscription.keys.auth === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export async function POST(request: Request) {
|
||||
@@ -23,7 +30,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Web Push is not configured." }, { status: 503 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null) as {
|
||||
const body = (await request.json().catch(() => null)) as {
|
||||
subscription?: unknown;
|
||||
province?: unknown;
|
||||
region?: unknown;
|
||||
@@ -63,7 +70,8 @@ export async function POST(request: Request) {
|
||||
tomorrowBriefEnabled: body?.tomorrowBriefEnabled === true,
|
||||
latitude: typeof body?.latitude === "number" && Number.isFinite(body.latitude) ? body.latitude : null,
|
||||
longitude: typeof body?.longitude === "number" && Number.isFinite(body.longitude) ? body.longitude : null,
|
||||
locationName: typeof body?.locationName === "string" && body.locationName.trim() ? body.locationName.trim().slice(0, 80) : null,
|
||||
locationName:
|
||||
typeof body?.locationName === "string" && body.locationName.trim() ? body.locationName.trim().slice(0, 80) : null,
|
||||
timezone: typeof body?.timezone === "string" && body.timezone.trim() ? body.timezone.trim().slice(0, 80) : null,
|
||||
countyTeryt: typeof body?.countyTeryt === "string" && /^\d{4}$/.test(body.countyTeryt) ? body.countyTeryt : null,
|
||||
temperatureUnit: isTemperatureUnit(rawTemperatureUnit) ? rawTemperatureUnit : DEFAULT_TEMPERATURE_UNIT,
|
||||
@@ -76,7 +84,7 @@ export async function POST(request: Request) {
|
||||
}
|
||||
|
||||
export async function DELETE(request: Request) {
|
||||
const body = await request.json().catch(() => null) as { endpoint?: unknown } | null;
|
||||
const body = (await request.json().catch(() => null)) as { endpoint?: unknown } | null;
|
||||
if (typeof body?.endpoint !== "string" || !body.endpoint) {
|
||||
return NextResponse.json({ error: "Invalid push subscription endpoint." }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export async function POST(request: Request) {
|
||||
return NextResponse.json({ error: "Web Push is not configured." }, { status: 503 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => null) as { endpoint?: unknown } | null;
|
||||
const body = (await request.json().catch(() => null)) as { endpoint?: unknown } | null;
|
||||
if (typeof body?.endpoint !== "string" || !body.endpoint) {
|
||||
return NextResponse.json({ error: "Invalid push subscription endpoint." }, { status: 400 });
|
||||
}
|
||||
@@ -23,7 +23,8 @@ export async function POST(request: Request) {
|
||||
await sendTestNotification(subscription);
|
||||
return NextResponse.json({ ok: true });
|
||||
} catch (error) {
|
||||
const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
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({ error: "Unable to send test notification." }, { status: 502 });
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
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 {
|
||||
getPushSubscriptions,
|
||||
hasSentTomorrowBrief,
|
||||
markTomorrowBriefSent,
|
||||
removePushSubscription,
|
||||
} from "@/lib/push-store";
|
||||
import { isWebPushConfigured, sendTomorrowBriefNotification } from "@/lib/push-service";
|
||||
import { buildTomorrowWeatherBrief } from "@/lib/weather-brief";
|
||||
import { DEFAULT_TEMPERATURE_UNIT, DEFAULT_WIND_SPEED_UNIT } from "@/lib/weather-utils";
|
||||
@@ -37,14 +42,17 @@ function getLocalDateParts(timeZone: string | null, date = new Date(), dayOffset
|
||||
const time = `${part("hour")}:${part("minute")}`;
|
||||
if (dayOffset === 0) return { dateKey, time };
|
||||
|
||||
const shiftedDate = new Date(Date.UTC(Number(part("year")), Number(part("month")) - 1, Number(part("day")) + dayOffset, 12));
|
||||
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: 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 ?? "";
|
||||
const shiftedPart = (type: Intl.DateTimeFormatPartTypes) =>
|
||||
shiftedParts.find((entry) => entry.type === type)?.value ?? "";
|
||||
return { dateKey: `${shiftedPart("year")}-${shiftedPart("month")}-${shiftedPart("day")}`, time };
|
||||
}
|
||||
|
||||
@@ -58,18 +66,19 @@ export async function GET(request: Request) {
|
||||
|
||||
const now = new Date();
|
||||
const briefTime = normalizeTime(process.env.NOTIFICATIONS_TOMORROW_BRIEF_TIME, DEFAULT_TOMORROW_BRIEF_TIME);
|
||||
const subscriptions = getPushSubscriptions().filter((subscription) => (
|
||||
subscription.tomorrowBriefEnabled
|
||||
&& Number.isFinite(subscription.latitude)
|
||||
&& Number.isFinite(subscription.longitude)
|
||||
));
|
||||
const subscriptions = getPushSubscriptions().filter(
|
||||
(subscription) =>
|
||||
subscription.tomorrowBriefEnabled &&
|
||||
Number.isFinite(subscription.latitude) &&
|
||||
Number.isFinite(subscription.longitude),
|
||||
);
|
||||
let warningsUnavailable = false;
|
||||
const hasPolishSubscriptions = subscriptions.some((subscription) => subscription.region === "PL");
|
||||
const warnings = hasPolishSubscriptions
|
||||
? await fetchMeteoWarnings(AbortSignal.timeout(12_000)).catch(() => {
|
||||
warningsUnavailable = true;
|
||||
return [];
|
||||
})
|
||||
warningsUnavailable = true;
|
||||
return [];
|
||||
})
|
||||
: [];
|
||||
let sent = 0;
|
||||
let skipped = 0;
|
||||
@@ -88,7 +97,11 @@ export async function GET(request: Request) {
|
||||
}
|
||||
|
||||
try {
|
||||
const forecast = await fetchServerForecast(subscription.latitude as number, subscription.longitude as number, subscription.region);
|
||||
const forecast = await fetchServerForecast(
|
||||
subscription.latitude as number,
|
||||
subscription.longitude as number,
|
||||
subscription.region,
|
||||
);
|
||||
const brief = buildTomorrowWeatherBrief({
|
||||
forecast,
|
||||
warnings: subscription.region === "PL" ? warnings : [],
|
||||
@@ -109,7 +122,8 @@ export async function GET(request: Request) {
|
||||
sent += 1;
|
||||
} catch (error) {
|
||||
failed += 1;
|
||||
const statusCode = typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
const statusCode =
|
||||
typeof error === "object" && error !== null && "statusCode" in error ? Number(error.statusCode) : null;
|
||||
if (statusCode === 404 || statusCode === 410) removePushSubscription(subscription.endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user