mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 04:32:42 +00:00
A typed apiFetch wrapper owns the envelope unwrap, the ok-check and the error read for every client call — two hundred call sites across the app migrate onto it and a lint rule keeps raw api fetches out for good. The legacy timezone helpers fold into the tz module and test fixtures drop personal names.
210 lines
6.9 KiB
TypeScript
210 lines
6.9 KiB
TypeScript
/**
|
||
* Test script for notification delivery and reminder check.
|
||
*
|
||
* Usage (inside Docker container):
|
||
* npx tsx scripts/test-notifications.ts
|
||
*
|
||
* Or from host:
|
||
* docker compose exec app npx tsx scripts/test-notifications.ts
|
||
*
|
||
* What it does:
|
||
* 1. Lists all configured notification channels
|
||
* 2. Sends a test notification through each enabled channel
|
||
* 3. Runs the reminder check logic once with verbose logging
|
||
*/
|
||
|
||
import { PrismaClient } from "@/generated/prisma/client";
|
||
import { PrismaPg } from "@prisma/adapter-pg";
|
||
import { decrypt } from "@/lib/crypto";
|
||
import { dispatchNotification } from "@/lib/notifications/dispatcher";
|
||
import { parseScheduleRecurrence } from "@/lib/medication-schedule";
|
||
import { getUserTodayBounds, getDayOfWeekInTz } from "@/lib/tz/local-day";
|
||
|
||
const DATABASE_URL = process.env.DATABASE_URL!;
|
||
|
||
function createPrisma() {
|
||
const adapter = new PrismaPg({ connectionString: DATABASE_URL });
|
||
return new PrismaClient({ adapter });
|
||
}
|
||
|
||
function parseTimeToMinutes(value: string): number {
|
||
const [h, m] = value.split(":").map(Number);
|
||
if (!Number.isFinite(h) || !Number.isFinite(m)) return 0;
|
||
return h * 60 + m;
|
||
}
|
||
|
||
// getUserTodayBounds and getDayOfWeekInTz imported from @/lib/tz/local-day
|
||
|
||
const dayLabels = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
|
||
|
||
async function main() {
|
||
const prisma = createPrisma();
|
||
|
||
try {
|
||
console.log("=== HealthLog Notification Test ===\n");
|
||
|
||
// ── Step 1: List channels ──
|
||
console.log("── 1. Notification Channels ──\n");
|
||
const channels = await prisma.notificationChannel.findMany({
|
||
include: { user: { select: { id: true, username: true } } },
|
||
});
|
||
|
||
if (channels.length === 0) {
|
||
console.log("❌ Keine Notification-Channels konfiguriert!");
|
||
console.log(
|
||
" → Gehe zu Einstellungen > Benachrichtigungen und richte einen Kanal ein.\n",
|
||
);
|
||
return;
|
||
}
|
||
|
||
for (const ch of channels) {
|
||
const status = ch.enabled ? "✅ aktiv" : "⏸️ deaktiviert";
|
||
let configInfo = "";
|
||
try {
|
||
const config = JSON.parse(decrypt(ch.config));
|
||
if (ch.type === "TELEGRAM") {
|
||
configInfo = `Chat-ID: ${config.chatId}`;
|
||
} else if (ch.type === "NTFY") {
|
||
configInfo = `Topic: ${config.topic}`;
|
||
} else {
|
||
configInfo = "Web Push";
|
||
}
|
||
} catch {
|
||
configInfo = "(Config nicht lesbar)";
|
||
}
|
||
console.log(
|
||
` ${ch.type} [${status}] — User: ${ch.user.username} — ${configInfo}`,
|
||
);
|
||
}
|
||
|
||
// ── Step 2: Send test notification ──
|
||
console.log("\n── 2. Test-Notification senden ──\n");
|
||
const enabledChannels = channels.filter((ch) => ch.enabled);
|
||
|
||
if (enabledChannels.length === 0) {
|
||
console.log("❌ Keine aktivierten Channels — überspringe Test.\n");
|
||
} else {
|
||
const userIds = [...new Set(enabledChannels.map((ch) => ch.userId))];
|
||
for (const userId of userIds) {
|
||
console.log(` Sende Test-Notification an User ${userId}...`);
|
||
try {
|
||
await dispatchNotification({
|
||
eventType: "SYSTEM_ALERT",
|
||
userId,
|
||
title: "🔔 Test-Notification",
|
||
message:
|
||
"<b>HealthLog Test:</b> Wenn du diese Nachricht siehst, funktionieren deine Benachrichtigungen!",
|
||
});
|
||
console.log(" ✅ Dispatched (best-effort, check deine Kanäle)\n");
|
||
} catch (err) {
|
||
console.error(" ❌ Fehler:", err);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Step 3: Reminder check dry-run ──
|
||
console.log("── 3. Reminder-Check (Dry-Run) ──\n");
|
||
|
||
const now = new Date();
|
||
console.log(` Aktuelle Zeit (UTC): ${now.toISOString()}`);
|
||
|
||
const appSettings = await prisma.appSettings.findUnique({
|
||
where: { id: "singleton" },
|
||
select: { reminderMissedMinutes: true },
|
||
});
|
||
const missedMinutes = appSettings?.reminderMissedMinutes ?? 240;
|
||
console.log(` Verpasst-Schwellenwert: ${missedMinutes} Minuten\n`);
|
||
|
||
const medications = await prisma.medication.findMany({
|
||
where: { active: true },
|
||
include: {
|
||
schedules: true,
|
||
user: { select: { id: true, username: true, timezone: true } },
|
||
},
|
||
});
|
||
|
||
if (medications.length === 0) {
|
||
console.log(" Keine aktiven Medikamente gefunden.\n");
|
||
return;
|
||
}
|
||
|
||
for (const med of medications) {
|
||
const userTz = med.user.timezone || "Europe/Berlin";
|
||
const { start: todayStart, end: todayEnd } = getUserTodayBounds(
|
||
now,
|
||
userTz,
|
||
);
|
||
const currentTime = now.toLocaleTimeString("de-DE", {
|
||
timeZone: userTz,
|
||
hour: "2-digit",
|
||
minute: "2-digit",
|
||
hour12: false,
|
||
});
|
||
const todayDow = getDayOfWeekInTz(now, userTz);
|
||
|
||
console.log(
|
||
` 📋 ${med.name} (${med.dose}) — User: ${med.user.username}`,
|
||
);
|
||
console.log(
|
||
` Timezone: ${userTz}, Lokale Zeit: ${currentTime}, Tag: ${dayLabels[todayDow]}`,
|
||
);
|
||
console.log(
|
||
` Notifications: ${med.notificationsEnabled ? "✅ an" : "❌ aus"}`,
|
||
);
|
||
console.log(` Schedules: ${med.schedules.length}`);
|
||
|
||
for (const schedule of med.schedules) {
|
||
const recurrence = parseScheduleRecurrence(schedule.daysOfWeek);
|
||
const endMins = parseTimeToMinutes(schedule.windowEnd);
|
||
const currentMins = parseTimeToMinutes(currentTime);
|
||
const minutesPastEnd = currentMins - endMins;
|
||
|
||
const dayMatch =
|
||
recurrence.daysOfWeek.length === 0 ||
|
||
recurrence.daysOfWeek.includes(todayDow);
|
||
const windowPassed = currentMins > endMins;
|
||
const thresholdPassed = minutesPastEnd > missedMinutes;
|
||
|
||
let status: string;
|
||
if (!dayMatch) {
|
||
status = "⏭️ Heute kein geplanter Tag";
|
||
} else if (!windowPassed) {
|
||
status = `🟢 Fenster noch offen (endet um ${schedule.windowEnd})`;
|
||
} else if (!thresholdPassed) {
|
||
status = `🟡 Fenster vorbei seit ${minutesPastEnd} Min (Threshold: ${missedMinutes} Min)`;
|
||
} else {
|
||
status = `🔴 Missed-Threshold erreicht (${minutesPastEnd} Min > ${missedMinutes} Min)`;
|
||
}
|
||
|
||
const daysInfo =
|
||
recurrence.daysOfWeek.length > 0
|
||
? ` [${recurrence.daysOfWeek.map((d) => dayLabels[d]).join(", ")}]`
|
||
: " [Täglich]";
|
||
|
||
console.log(
|
||
` ${schedule.windowStart}–${schedule.windowEnd}${daysInfo}: ${status}`,
|
||
);
|
||
}
|
||
|
||
// Count existing events today
|
||
const eventCount = await prisma.medicationIntakeEvent.count({
|
||
where: {
|
||
medicationId: med.id,
|
||
userId: med.user.id,
|
||
scheduledFor: { gte: todayStart, lte: todayEnd },
|
||
},
|
||
});
|
||
console.log(` Heutige Events: ${eventCount}\n`);
|
||
}
|
||
|
||
console.log("=== Test abgeschlossen ===");
|
||
} finally {
|
||
await prisma.$disconnect();
|
||
}
|
||
}
|
||
|
||
main().catch((err) => {
|
||
console.error("Test failed:", err);
|
||
process.exit(1);
|
||
});
|