diff --git a/docker-compose.yml b/docker-compose.yml
index c9660e3..6818ddb 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -33,6 +33,7 @@ services:
VAPID_PUBLIC_KEY: ${VAPID_PUBLIC_KEY:-}
VAPID_PRIVATE_KEY: ${VAPID_PRIVATE_KEY:-}
VAPID_SUBJECT: ${VAPID_SUBJECT:-}
+ CRON_SECRET: ${CRON_SECRET:-}
migrate:
build:
@@ -45,6 +46,22 @@ services:
DATABASE_URL: "postgresql://grow:${POSTGRES_PASSWORD}@db:5432/grow"
restart: "no"
+ cron:
+ image: alpine:3.19
+ restart: unless-stopped
+ depends_on:
+ - app
+ environment:
+ APP_URL: "http://app:3000"
+ CRON_SECRET: ${CRON_SECRET:-}
+ command: >
+ sh -c "
+ echo '*/15 * * * * wget -qO- --header=\"Authorization: Bearer $$CRON_SECRET\" --post-data= $$APP_URL/api/reminders/check > /dev/null 2>&1' > /etc/crontabs/root &&
+ echo '0 8 * * * wget -qO- --header=\"Authorization: Bearer $$CRON_SECRET\" --post-data= $$APP_URL/api/cron/milk-expiry > /dev/null 2>&1' >> /etc/crontabs/root &&
+ echo '0 7 * * * wget -qO- --header=\"Authorization: Bearer $$CRON_SECRET\" --post-data= $$APP_URL/api/cron/daily-digest > /dev/null 2>&1' >> /etc/crontabs/root &&
+ crond -f -l 8
+ "
+
volumes:
postgres_data:
uploads_data:
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 5434472..5783b99 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -92,12 +92,36 @@ model Family {
babies Baby[]
eventTemplates EventTemplate[]
apiKeys ApiKey[]
+ webhooks Webhook[]
+ pendingInvites PendingInvite[]
pinnedNote String?
pinnedNoteUpdatedAt DateTime?
pinnedNoteAuthor String?
createdAt DateTime @default(now())
}
+model PendingInvite {
+ id String @id @default(cuid())
+ familyId String
+ family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
+ email String
+ role UserRole @default(PARENT)
+ token String @unique @default(cuid())
+ expiresAt DateTime
+ sentAt DateTime @default(now())
+}
+
+model Webhook {
+ id String @id @default(cuid())
+ familyId String
+ family Family @relation(fields: [familyId], references: [id], onDelete: Cascade)
+ url String
+ secret String
+ events String[]
+ active Boolean @default(true)
+ createdAt DateTime @default(now())
+}
+
model ApiKey {
id String @id @default(cuid())
familyId String
diff --git a/src/app/(app)/growth/page.tsx b/src/app/(app)/growth/page.tsx
index 4a6bba9..df03f9d 100644
--- a/src/app/(app)/growth/page.tsx
+++ b/src/app/(app)/growth/page.tsx
@@ -471,7 +471,6 @@ export default function GrowthPage() {
OMS P3–P97
- ⓘ
@@ -487,6 +486,9 @@ export default function GrowthPage() {
+ P50 = médiane OMS (50% des bébés en dessous). P3–P97 = plage normale. Hors de cette plage ne signifie pas un problème — consultez votre pédiatre. +
)} diff --git a/src/app/(app)/photos/page.tsx b/src/app/(app)/photos/page.tsx index 813a3fc..b056e5e 100644 --- a/src/app/(app)/photos/page.tsx +++ b/src/app/(app)/photos/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect, useCallback } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useBaby } from "@/contexts/baby-context"; import { EVENT_CONFIG, EventType } from "@/lib/event-config"; @@ -68,6 +68,18 @@ export default function PhotosPage() { const current = lightbox ? lightbox.events[lightbox.index] : null; + const handleKey = useCallback((e: KeyboardEvent) => { + if (!lightbox) return; + if (e.key === "ArrowLeft") { e.preventDefault(); prev(); } + else if (e.key === "ArrowRight") { e.preventDefault(); next(); } + else if (e.key === "Escape") setLightbox(null); + }, [lightbox]); + + useEffect(() => { + window.addEventListener("keydown", handleKey); + return () => window.removeEventListener("keydown", handleKey); + }, [handleKey]); + async function deletePhoto(eventId: string) { const ev = allEvents.find((e) => e.id === eventId); const existingMeta: RecordInvitations en attente
+{inv.email}
++ {ROLE_LABELS[inv.role] ?? inv.role} · {expiresLabel} +
++ Notifications HTTP vers vos services (n8n, Home Assistant, Zapier…) à chaque événement. +
+ + {revealedSecret && ( +Secret — copiez-le maintenant, il ne sera plus affiché.
+
+ {revealedSecret.secret}
+
+
+ Vérifiez la signature : X-Grow-Signature: sha256=HMAC(secret, body)
+
{hook.url}
+{hook.events.join(", ")}
+Le lien n'expire pas — réinitialisez-le si compromis.
+Le lien n'expire pas — réinitialisez-le si compromis.
+Inviter par email
@@ -584,6 +822,7 @@ export default function SettingsPage() { )} +