feat: sleep chart, bottle-from-stock, growth alert cron
- SleepTimeline component in stats: 7-night bar chart, 19h–11h window, NIGHT_SLEEP (indigo) and NAP (violet) blocks, pure CSS positioning - Bottle modal: fetch available maternal milk lots, chip picker, auto-selects oldest, marks selected lot as used after save - /api/cron/growth-alert: Bearer-auth POST, configurable threshold via growth_alert_days SystemConfig key (default 30 days), Pushover alert to all family users per baby with no recent weight log - Admin config ALLOWED_KEYS: add cron_secret + growth_alert_days Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ const ALLOWED_KEYS = [
|
||||
"pushover_app_token",
|
||||
"allow_registration",
|
||||
"maintenance_mode",
|
||||
"cron_secret",
|
||||
"growth_alert_days",
|
||||
];
|
||||
|
||||
async function requireSuperAdmin() {
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { sendPushover } from "@/lib/push-notify";
|
||||
|
||||
// Call daily with:
|
||||
// POST /api/cron/growth-alert
|
||||
// Authorization: Bearer <CRON_SECRET>
|
||||
|
||||
async function getCronSecret(): Promise<string> {
|
||||
const row = await prisma.systemConfig.findUnique({ where: { key: "cron_secret" } });
|
||||
return row?.value || process.env.CRON_SECRET || "";
|
||||
}
|
||||
|
||||
async function getAlertDays(): Promise<number> {
|
||||
const row = await prisma.systemConfig.findUnique({ where: { key: "growth_alert_days" } });
|
||||
const n = parseInt(row?.value ?? "");
|
||||
return isNaN(n) || n <= 0 ? 30 : n;
|
||||
}
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const secret = await getCronSecret();
|
||||
if (secret) {
|
||||
const authHeader = req.headers.get("authorization") ?? "";
|
||||
if (authHeader !== `Bearer ${secret}`) {
|
||||
return NextResponse.json({ error: "Non autorisé" }, { status: 401 });
|
||||
}
|
||||
}
|
||||
|
||||
const alertDays = await getAlertDays();
|
||||
const threshold = new Date(Date.now() - alertDays * 24 * 3600 * 1000);
|
||||
|
||||
const families = await prisma.family.findMany({
|
||||
include: {
|
||||
babies: {
|
||||
include: {
|
||||
growthLogs: {
|
||||
orderBy: { date: "desc" },
|
||||
take: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
users: {
|
||||
where: { pushoverUserKey: { not: null } },
|
||||
select: { pushoverUserKey: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const alerts: { familyId: string; babyName: string }[] = [];
|
||||
|
||||
for (const family of families) {
|
||||
if (family.users.length === 0) continue;
|
||||
|
||||
for (const baby of family.babies) {
|
||||
const lastLog = baby.growthLogs[0];
|
||||
const lastDate = lastLog ? new Date(lastLog.date) : null;
|
||||
|
||||
if (!lastDate || lastDate < threshold) {
|
||||
alerts.push({ familyId: family.id, babyName: baby.name });
|
||||
|
||||
const daysSince = lastDate
|
||||
? Math.floor((Date.now() - lastDate.getTime()) / 86400000)
|
||||
: null;
|
||||
|
||||
const message = daysSince !== null
|
||||
? `Pas de mesure de croissance depuis ${daysSince} jours pour ${baby.name}. Pensez à peser votre bébé !`
|
||||
: `Aucune mesure de croissance enregistrée pour ${baby.name}. Pensez à peser votre bébé !`;
|
||||
|
||||
for (const user of family.users) {
|
||||
if (user.pushoverUserKey) {
|
||||
await sendPushover(user.pushoverUserKey, message, `📏 Rappel croissance — ${baby.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NextResponse.json({ ok: true, alertsSent: alerts.length, alerts, alertDays });
|
||||
}
|
||||
Reference in New Issue
Block a user