Compare commits

...

2 Commits

Author SHA1 Message Date
zv
310da3a87f Load env files in notification worker 2026-06-12 21:26:52 +02:00
zv
ea1dd4b5cb Clarify notification worker app dependency 2026-06-12 21:18:56 +02:00
3 changed files with 39 additions and 3 deletions

View File

@@ -28,7 +28,7 @@ npm run start
npm run notifications:worker npm run notifications:worker
``` ```
Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani formattera. `npm run build` uruchamia kontrolę TypeScript wykonywaną przez Next.js. `npm run notifications:worker` uruchamia osobny proces Node do cyklicznego wywoływania endpointów Web Push na self-hostingu. Nie opisuj ani nie uruchamiaj nieistniejących komend jako standardowego workflow. Repozytorium nie ma obecnie skryptu testów, osobnego skryptu type-check ani formattera. `npm run build` uruchamia kontrolę TypeScript wykonywaną przez Next.js. `npm run notifications:worker` uruchamia osobny proces Node do cyklicznego wywoływania endpointów Web Push na self-hostingu; wczytuje `.env` i `.env.local`, ale nadal wymaga równolegle działającego `npm run start` albo poprawnego `WTR_APP_URL`. Nie opisuj ani nie uruchamiaj nieistniejących komend jako standardowego workflow.
## Konwencje kodu ## Konwencje kodu

View File

@@ -46,9 +46,12 @@ npm run start
Worker powiadomień uruchamiany obok aplikacji: Worker powiadomień uruchamiany obok aplikacji:
```bash ```bash
npm run start
npm run notifications:worker npm run notifications:worker
``` ```
Worker musi mieć dostęp do działającej aplikacji Next.js. Domyślnie odpytuje `http://127.0.0.1:3000`; jeśli aplikacja działa pod innym adresem albo portem, ustaw `WTR_APP_URL`. Skrypt workera sam wczytuje `.env` i `.env.local`, a zmienne podane bezpośrednio w shellu mają pierwszeństwo.
## Źródła danych ## Źródła danych
Bieżące pomiary i komunikaty pochodzą z rzeczywistych publicznych danych IMGW: Bieżące pomiary i komunikaty pochodzą z rzeczywistych publicznych danych IMGW:
@@ -151,7 +154,7 @@ NOTIFICATIONS_WARNING_INTERVAL_MINUTES=5
NOTIFICATIONS_MORNING_BRIEF_TIME=07:00 NOTIFICATIONS_MORNING_BRIEF_TIME=07:00
``` ```
Klucze VAPID można wygenerować poleceniem `npx web-push generate-vapid-keys`. Worker `npm run notifications:worker` domyślnie odpytuje `http://127.0.0.1:3000`, sprawdza ostrzeżenia co 5 minut i wysyła poranny brief po 07:00 czasu polskiego. Jeśli używasz zewnętrznego crona zamiast workera, cron ostrzeżeń powinien wywoływać `GET /api/notifications/check` z nagłówkiem `Authorization: Bearer <NOTIFICATIONS_CRON_SECRET>` albo `x-cron-secret`. Drugi cron, ustawiony na około 07:00 czasu polskiego, powinien wywoływać `GET /api/notifications/daily-brief` z tym samym nagłówkiem. Klucze VAPID można wygenerować poleceniem `npx web-push generate-vapid-keys`. Worker `npm run notifications:worker` wczytuje `.env` i `.env.local`, domyślnie odpytuje `http://127.0.0.1:3000`, więc `npm run start` musi działać równolegle na tym samym hoście albo `WTR_APP_URL` musi wskazywać właściwy adres aplikacji. Worker sprawdza ostrzeżenia co 5 minut i wysyła poranny brief po 07:00 czasu polskiego. Jeśli używasz zewnętrznego crona zamiast workera, cron ostrzeżeń powinien wywoływać `GET /api/notifications/check` z nagłówkiem `Authorization: Bearer <NOTIFICATIONS_CRON_SECRET>` albo `x-cron-secret`. Drugi cron, ustawiony na około 07:00 czasu polskiego, powinien wywoływać `GET /api/notifications/daily-brief` z tym samym nagłówkiem.
## Wdrożenie na Vercel ## Wdrożenie na Vercel

View File

@@ -1,8 +1,14 @@
import { existsSync, readFileSync } from "node:fs";
const DEFAULT_APP_URL = "http://127.0.0.1:3000"; const DEFAULT_APP_URL = "http://127.0.0.1:3000";
const DEFAULT_WARNING_INTERVAL_MINUTES = 5; const DEFAULT_WARNING_INTERVAL_MINUTES = 5;
const DEFAULT_MORNING_BRIEF_TIME = "07:00"; const DEFAULT_MORNING_BRIEF_TIME = "07:00";
const LOOP_INTERVAL_MS = 30_000; const LOOP_INTERVAL_MS = 30_000;
const initialEnvKeys = new Set(Object.keys(process.env));
loadEnvFile(".env", false);
loadEnvFile(".env.local", true);
const appUrl = normalizeAppUrl(process.env.WTR_APP_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_APP_URL); const appUrl = normalizeAppUrl(process.env.WTR_APP_URL ?? process.env.NEXT_PUBLIC_APP_URL ?? DEFAULT_APP_URL);
const cronSecret = process.env.NOTIFICATIONS_CRON_SECRET ?? ""; const cronSecret = process.env.NOTIFICATIONS_CRON_SECRET ?? "";
const warningIntervalMinutes = readPositiveNumber(process.env.NOTIFICATIONS_WARNING_INTERVAL_MINUTES, DEFAULT_WARNING_INTERVAL_MINUTES); const warningIntervalMinutes = readPositiveNumber(process.env.NOTIFICATIONS_WARNING_INTERVAL_MINUTES, DEFAULT_WARNING_INTERVAL_MINUTES);
@@ -12,6 +18,22 @@ let lastWarningCheckAt = 0;
let lastMorningBriefDate = ""; let lastMorningBriefDate = "";
let isRunningTick = false; let isRunningTick = false;
function loadEnvFile(path, overrideLoadedValues) {
if (!existsSync(path)) return;
const lines = readFileSync(path, "utf8").split(/\r?\n/);
for (const line of lines) {
const trimmedLine = line.trim();
if (!trimmedLine || trimmedLine.startsWith("#")) continue;
const separatorIndex = trimmedLine.indexOf("=");
if (separatorIndex === -1) continue;
const key = trimmedLine.slice(0, separatorIndex).trim();
const rawValue = trimmedLine.slice(separatorIndex + 1).trim();
if (!key || initialEnvKeys.has(key)) continue;
if (!overrideLoadedValues && process.env[key] !== undefined) continue;
process.env[key] = rawValue.replace(/^['"]|['"]$/g, "");
}
}
function normalizeAppUrl(value) { function normalizeAppUrl(value) {
return value.replace(/\/+$/, ""); return value.replace(/\/+$/, "");
} }
@@ -55,6 +77,17 @@ async function callNotificationEndpoint(path) {
return body; return body;
} }
function formatWorkerError(error) {
if (error?.cause?.code === "ECONNREFUSED") {
return [
`wtr. notification worker: app is not reachable at ${appUrl}.`,
"Start the Next.js app first with `npm run start`, or set WTR_APP_URL to the running app URL.",
].join(" ");
}
if (error instanceof Error) return error.message;
return String(error);
}
async function tick() { async function tick() {
if (isRunningTick) return; if (isRunningTick) return;
isRunningTick = true; isRunningTick = true;
@@ -72,7 +105,7 @@ async function tick() {
lastMorningBriefDate = warsawTime.dateKey; lastMorningBriefDate = warsawTime.dateKey;
} }
} catch (error) { } catch (error) {
console.error(error); console.error(formatWorkerError(error));
} finally { } finally {
isRunningTick = false; isRunningTick = false;
} }