mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Initial release v1.0.0
HealthLog — personal health-tracking PWA with weight, blood pressure, pulse, mood tracking, medication compliance, Withings integration, doctor report PDF export, and Dracula-themed UI.
This commit is contained in:
commit
38309932f3
301 files changed
+60041
No files matched your search
@@ -0,0 +1,375 @@
|
||||
/**
|
||||
* Demo Data Seed Script for HealthLog
|
||||
*
|
||||
* Creates realistic demo data:
|
||||
* - 1 admin user (demo/demo123demo123)
|
||||
* - 90 days of measurements (weight, BP, pulse, body fat, sleep, steps)
|
||||
* - 3 medications with schedules and intake history
|
||||
* - 90 days of mood entries
|
||||
* - App settings (registration disabled, English locale)
|
||||
*
|
||||
* Usage: npx tsx scripts/seed-demo.ts
|
||||
* Requires DATABASE_URL env var.
|
||||
*/
|
||||
|
||||
import pg from "pg";
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL;
|
||||
if (!DATABASE_URL) {
|
||||
console.error("DATABASE_URL is required");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const pool = new pg.Pool({ connectionString: DATABASE_URL });
|
||||
|
||||
// ── Helpers ──────────────────────────────────────
|
||||
|
||||
function cuid(): string {
|
||||
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let id = "c";
|
||||
for (let i = 0; i < 24; i++) {
|
||||
id += chars[Math.floor(Math.random() * chars.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
function daysAgo(n: number): Date {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - n);
|
||||
d.setHours(7 + Math.floor(Math.random() * 3), Math.floor(Math.random() * 60), 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
function formatDate(d: Date): string {
|
||||
return d.toISOString().split("T")[0];
|
||||
}
|
||||
|
||||
// Smooth random walk with mean reversion
|
||||
function randomWalk(start: number, target: number, days: number, volatility: number): number[] {
|
||||
const values: number[] = [start];
|
||||
for (let i = 1; i < days; i++) {
|
||||
const prev = values[i - 1];
|
||||
const drift = (target - prev) * 0.03; // mean reversion
|
||||
const noise = (Math.random() - 0.5) * volatility;
|
||||
values.push(Math.round((prev + drift + noise) * 10) / 10);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
// ── Main ─────────────────────────────────────────
|
||||
|
||||
async function seed() {
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
|
||||
console.log("Cleaning existing data...");
|
||||
await client.query("DELETE FROM mood_entries");
|
||||
await client.query("DELETE FROM medication_intake_events");
|
||||
await client.query("DELETE FROM medication_schedules");
|
||||
await client.query("DELETE FROM reminder_phase_configs");
|
||||
await client.query("DELETE FROM medications");
|
||||
await client.query("DELETE FROM measurements");
|
||||
await client.query("DELETE FROM user_achievements");
|
||||
await client.query("DELETE FROM audit_logs");
|
||||
await client.query("DELETE FROM sessions");
|
||||
await client.query("DELETE FROM passkeys");
|
||||
await client.query("DELETE FROM auth_challenges");
|
||||
await client.query("DELETE FROM notification_channels");
|
||||
await client.query("DELETE FROM push_subscriptions");
|
||||
await client.query("DELETE FROM api_tokens");
|
||||
await client.query("DELETE FROM data_backups");
|
||||
await client.query("DELETE FROM users");
|
||||
|
||||
// ── User ──────────────────────────────────
|
||||
console.log("Creating demo user...");
|
||||
const userId = cuid();
|
||||
// argon2id hash of "demo123demo123" — pre-computed
|
||||
// We'll use a bcrypt-compatible approach: register via API later
|
||||
// For now, insert with a placeholder and we'll set it via the app
|
||||
const passwordHash = "$argon2id$v=19$m=65536,t=3,p=4$Kips6OxPAl0vmspO9SoKZQ$oX9gLgwHVnnENCqBloyM13ewuqmhPnw8EpLoemS3MNI";
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO users (id, username, email, password_hash, role, height_cm, date_of_birth, gender, timezone, locale, onboarding_completed_at, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, NOW(), NOW(), NOW())`,
|
||||
[userId, "demo", "demo@healthlog.app", passwordHash, "ADMIN", 182.0, "1990-05-15", "MALE", "Europe/Berlin", "en"]
|
||||
);
|
||||
|
||||
// ── Measurements (90 days) ────────────────
|
||||
console.log("Creating 90 days of measurements...");
|
||||
const days = 90;
|
||||
|
||||
// Weight: 86.5 → trending down to ~82.5
|
||||
const weights = randomWalk(86.5, 82.0, days, 0.6);
|
||||
// Systolic BP: 132 → improving to ~124
|
||||
const sysBP = randomWalk(132, 124, days, 4);
|
||||
// Diastolic BP: 85 → improving to ~78
|
||||
const diaBP = randomWalk(85, 78, days, 3);
|
||||
// Pulse: 72 → slight improvement to ~68
|
||||
const pulse = randomWalk(72, 68, days, 3);
|
||||
// Body fat: 24.5% → trending to ~22%
|
||||
const bodyFat = randomWalk(24.5, 22.0, days, 0.4);
|
||||
// Sleep: 6.5h → improving to ~7.5h
|
||||
const sleep = randomWalk(6.5, 7.5, days, 0.8);
|
||||
// Steps: 5000 → improving to ~8000
|
||||
const steps = randomWalk(5000, 8500, days, 1500);
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const date = daysAgo(days - i);
|
||||
|
||||
// Weight (daily)
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'WEIGHT', $3, 'kg', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, weights[i], date]
|
||||
);
|
||||
|
||||
// Blood pressure (daily)
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'BLOOD_PRESSURE_SYS', $3, 'mmHg', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, Math.round(sysBP[i]), date]
|
||||
);
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'BLOOD_PRESSURE_DIA', $3, 'mmHg', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, Math.round(diaBP[i]), date]
|
||||
);
|
||||
|
||||
// Pulse (daily)
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'PULSE', $3, 'bpm', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, Math.round(pulse[i]), date]
|
||||
);
|
||||
|
||||
// Body fat (every 2-3 days)
|
||||
if (i % 2 === 0 || Math.random() > 0.5) {
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'BODY_FAT', $3, '%', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, bodyFat[i], date]
|
||||
);
|
||||
}
|
||||
|
||||
// Sleep (daily)
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'SLEEP_DURATION', $3, 'hours', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, Math.round(sleep[i] * 10) / 10, date]
|
||||
);
|
||||
|
||||
// Steps (daily)
|
||||
await client.query(
|
||||
`INSERT INTO measurements (id, user_id, type, value, unit, source, measured_at, created_at, updated_at)
|
||||
VALUES ($1, $2, 'ACTIVITY_STEPS', $3, 'steps', 'MANUAL', $4, $4, $4)`,
|
||||
[cuid(), userId, Math.round(steps[i]), date]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Medications ───────────────────────────
|
||||
console.log("Creating medications...");
|
||||
|
||||
// Medication 1: Ramipril (blood pressure)
|
||||
const med1Id = cuid();
|
||||
await client.query(
|
||||
`INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES ($1, $2, 'Ramipril', '5mg', true, true, $3, $3)`,
|
||||
[med1Id, userId, daysAgo(120)]
|
||||
);
|
||||
const sched1Id = cuid();
|
||||
await client.query(
|
||||
`INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label)
|
||||
VALUES ($1, $2, '08:00', '10:00', 'Morning')`,
|
||||
[sched1Id, med1Id]
|
||||
);
|
||||
|
||||
// Medication 2: Vitamin D3
|
||||
const med2Id = cuid();
|
||||
await client.query(
|
||||
`INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES ($1, $2, 'Vitamin D3', '2000 IE', true, true, $3, $3)`,
|
||||
[med2Id, userId, daysAgo(90)]
|
||||
);
|
||||
const sched2Id = cuid();
|
||||
await client.query(
|
||||
`INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label)
|
||||
VALUES ($1, $2, '08:00', '10:00', 'Morning')`,
|
||||
[sched2Id, med2Id]
|
||||
);
|
||||
|
||||
// Medication 3: Magnesium (evening)
|
||||
const med3Id = cuid();
|
||||
await client.query(
|
||||
`INSERT INTO medications (id, user_id, name, dose, active, notifications_enabled, created_at, updated_at)
|
||||
VALUES ($1, $2, 'Magnesium', '400mg', true, true, $3, $3)`,
|
||||
[med3Id, userId, daysAgo(60)]
|
||||
);
|
||||
const sched3Id = cuid();
|
||||
await client.query(
|
||||
`INSERT INTO medication_schedules (id, medication_id, window_start, window_end, label)
|
||||
VALUES ($1, $2, '20:00', '22:00', 'Evening')`,
|
||||
[sched3Id, med3Id]
|
||||
);
|
||||
|
||||
// ── Medication Intake Events ──────────────
|
||||
console.log("Creating intake events (90 days)...");
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
const date = daysAgo(days - i);
|
||||
|
||||
// Ramipril: 96% compliance (miss ~1 every 25 days)
|
||||
const takeMed1 = Math.random() > 0.04;
|
||||
if (takeMed1) {
|
||||
const takenTime = new Date(date);
|
||||
takenTime.setHours(8, Math.floor(Math.random() * 45), 0);
|
||||
await client.query(
|
||||
`INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, false, 'WEB', $5)`,
|
||||
[cuid(), userId, med1Id, date, takenTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4)`,
|
||||
[cuid(), userId, med1Id, date]
|
||||
);
|
||||
}
|
||||
|
||||
// Vitamin D3: 92% compliance
|
||||
const takeMed2 = Math.random() > 0.08;
|
||||
if (takeMed2) {
|
||||
const takenTime = new Date(date);
|
||||
takenTime.setHours(8, Math.floor(5 + Math.random() * 50), 0);
|
||||
await client.query(
|
||||
`INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, false, 'WEB', $5)`,
|
||||
[cuid(), userId, med2Id, date, takenTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4)`,
|
||||
[cuid(), userId, med2Id, date]
|
||||
);
|
||||
}
|
||||
|
||||
// Magnesium: only last 60 days, 88% compliance
|
||||
if (i >= 30) {
|
||||
const takeMed3 = Math.random() > 0.12;
|
||||
if (takeMed3) {
|
||||
const takenTime = new Date(date);
|
||||
takenTime.setHours(20, Math.floor(Math.random() * 60), 0);
|
||||
await client.query(
|
||||
`INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, false, 'WEB', $5)`,
|
||||
[cuid(), userId, med3Id, date, takenTime]
|
||||
);
|
||||
} else {
|
||||
await client.query(
|
||||
`INSERT INTO medication_intake_events (id, user_id, medication_id, scheduled_for, taken_at, skipped, source, created_at)
|
||||
VALUES ($1, $2, $3, $4, NULL, true, 'WEB', $4)`,
|
||||
[cuid(), userId, med3Id, date]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Mood Entries ─────────────────────────
|
||||
console.log("Creating mood entries (90 days)...");
|
||||
|
||||
const moodOptions = ["SUPER_GUT", "GUT", "OKAY", "SCHLECHT", "LAUSIG"];
|
||||
const tagOptions = [
|
||||
["productive", "exercise", "well-rested"],
|
||||
["focused", "social", "creative"],
|
||||
["tired", "stressed"],
|
||||
["anxious", "low-energy"],
|
||||
["exhausted", "pain"],
|
||||
];
|
||||
|
||||
// Mood trend: starts around 3, improves to ~4 over time
|
||||
const moodTrend = randomWalk(3.0, 4.2, days, 0.8);
|
||||
|
||||
for (let i = 0; i < days; i++) {
|
||||
// Skip ~5% of days (forgot to log)
|
||||
if (Math.random() < 0.05) continue;
|
||||
|
||||
const date = daysAgo(days - i);
|
||||
const rawScore = Math.round(Math.min(5, Math.max(1, moodTrend[i])));
|
||||
const mood = moodOptions[5 - rawScore]; // invert: score 5 = SUPER_GUT
|
||||
const tags = tagOptions[5 - rawScore];
|
||||
const loggedAt = new Date(date);
|
||||
loggedAt.setHours(21, Math.floor(Math.random() * 59), 0);
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO mood_entries (id, user_id, date, mood, score, tags, source, mood_logged_at, synced_at, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'WEB', $7, $7, $7, $7)`,
|
||||
[cuid(), userId, formatDate(date), mood, rawScore, JSON.stringify(tags), loggedAt]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Achievements ─────────────────────────
|
||||
console.log("Creating achievements...");
|
||||
|
||||
const achievements = [
|
||||
{ id: "first-measurement", daysAgo: 89 },
|
||||
{ id: "first-medication", daysAgo: 89 },
|
||||
{ id: "first-mood", daysAgo: 89 },
|
||||
{ id: "week-streak-measurements", daysAgo: 82 },
|
||||
{ id: "week-streak-mood", daysAgo: 82 },
|
||||
{ id: "month-streak-measurements", daysAgo: 59 },
|
||||
{ id: "compliance-90", daysAgo: 45 },
|
||||
{ id: "weight-loss-5", daysAgo: 30 },
|
||||
{ id: "bp-normal", daysAgo: 20 },
|
||||
{ id: "steps-10k", daysAgo: 15 },
|
||||
{ id: "month-streak-mood", daysAgo: 10 },
|
||||
{ id: "three-month-streak", daysAgo: 2 },
|
||||
];
|
||||
|
||||
for (const ach of achievements) {
|
||||
const unlocked = daysAgo(ach.daysAgo);
|
||||
await client.query(
|
||||
`INSERT INTO user_achievements (id, user_id, achievement_id, unlocked_at, created_at)
|
||||
VALUES ($1, $2, $3, $4, $4)`,
|
||||
[cuid(), userId, ach.id, unlocked]
|
||||
);
|
||||
}
|
||||
|
||||
// ── App Settings ─────────────────────────
|
||||
console.log("Creating app settings...");
|
||||
await client.query(
|
||||
`INSERT INTO app_settings (id, registration_enabled, default_locale)
|
||||
VALUES ('singleton', false, 'en')
|
||||
ON CONFLICT (id) DO UPDATE SET registration_enabled = false, default_locale = 'en'`
|
||||
);
|
||||
|
||||
// ── Audit Log (some login entries) ──────
|
||||
console.log("Creating audit log entries...");
|
||||
for (let i = 0; i < 15; i++) {
|
||||
const loginDate = daysAgo(Math.floor(Math.random() * 30));
|
||||
await client.query(
|
||||
`INSERT INTO audit_logs (id, user_id, action, details, ip_address, created_at)
|
||||
VALUES ($1, $2, 'auth.login', '{"method":"password"}', '203.0.113.42', $3)`,
|
||||
[cuid(), userId, loginDate]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
console.log("\nDemo data seeded successfully!");
|
||||
console.log(` User: demo / demo123demo123`);
|
||||
console.log(` Measurements: ${days * 6} entries (7 types, 90 days)`);
|
||||
console.log(` Medications: 3 (with ${days * 2 + 60} intake events)`);
|
||||
console.log(` Mood: ~${Math.round(days * 0.95)} entries`);
|
||||
console.log(` Achievements: ${achievements.length}`);
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
console.error("Seed failed:", err);
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
await pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
seed();
|
||||
@@ -0,0 +1,209 @@
|
||||
/**
|
||||
* 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/timezone";
|
||||
|
||||
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/timezone
|
||||
|
||||
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);
|
||||
});
|
||||
Reference in new issue
Block a user