feat: sync calendar entries to a Microsoft 365 group calendar
Adds a one-way push (ApiCore -> M365) of Aufgaben/Durchsichten/ Behandlungen/Fütterungen/Ernten/Königinnen-Zuordnungen into the calendar of a Microsoft 365 group, via the Graph API. - CalendarSyncModule (app-only client-credentials Graph auth, separate from the interactive SSO app registration; new env vars M365_CALENDAR_TENANT_ID/_CLIENT_ID/_CLIENT_SECRET). - Outbox + minute-interval cron worker (CalendarSyncOutboxEntry) instead of a synchronous push, since Graph calls are too slow/flaky to block a domain create request on; failed syncs retry up to 5 times. - Per-source-type toggle plus a group-ID field, configurable in Verwaltung > Automatisierung > Kalender-Sync (Verwaltung > Backup, an env var still wins on those fields where one applies to related config). - Hooked into Tasks/Inspections/Treatments/Feedings/Harvests/Queens at their existing create (and Task update/complete) points; entries render as all-day Graph events to avoid timezone ambiguity. - Documents the design decision in ARCHITECTURE.md (Annahme 9), since this is a net-new capability the lastenheft doesn't scope. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
28ae0a2723
commit
e7b157ec35
35 files changed
+1760
-7
No files matched your search
@@ -1064,3 +1064,65 @@ model BackupSettings {
|
||||
retentionCount Int?
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Kalender-Synchronisation – einseitige Übertragung (ApiCore → M365) von
|
||||
// Kalendereinträgen in den Kalender einer Microsoft-365-Gruppe via Graph API.
|
||||
// Zugangsdaten (Tenant/Client-ID/-Secret) sind bewusst NICHT Teil dieses
|
||||
// Modells - wie bei OIDC_CLIENT_SECRET nur über Env-Variablen (Secret), nie
|
||||
// über die GUI editierbar. groupId ist keine Zugangsdaten-Information und
|
||||
// daher hier GUI-konfigurierbar.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
enum CalendarSyncSourceType {
|
||||
TASK
|
||||
INSPECTION
|
||||
TREATMENT
|
||||
FEEDING
|
||||
HARVEST
|
||||
QUEEN_ASSIGNMENT
|
||||
}
|
||||
|
||||
enum CalendarSyncStatus {
|
||||
PENDING
|
||||
SYNCED
|
||||
FAILED
|
||||
SKIPPED
|
||||
}
|
||||
|
||||
model CalendarSyncSettings {
|
||||
id String @id @default("singleton")
|
||||
enabled Boolean @default(false)
|
||||
groupId String?
|
||||
syncTask Boolean @default(true)
|
||||
syncInspection Boolean @default(true)
|
||||
syncTreatment Boolean @default(true)
|
||||
syncFeeding Boolean @default(true)
|
||||
syncHarvest Boolean @default(true)
|
||||
syncQueenAssignment Boolean @default(true)
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
// Outbox statt synchronem Push direkt im Request-Pfad (anders als das
|
||||
// Webhooks-Modul): Graph-API-Aufrufe sind zu langsam/fehleranfällig
|
||||
// (Token-Refresh, Drosselung, transiente 5xx), um eine Durchsicht-/
|
||||
// Aufgaben-Erstellung darauf warten zu lassen. Ein periodischer Worker
|
||||
// verarbeitet PENDING-Zeilen und wiederholt fehlgeschlagene Versuche.
|
||||
// @@unique auf [sourceType, sourceId] macht enqueue() ein Upsert: ein
|
||||
// erneutes Enqueue (z. B. nach einer Aufgaben-Änderung) aktualisiert das
|
||||
// bestehende Graph-Event über die gespeicherte graphEventId, statt ein
|
||||
// Duplikat anzulegen.
|
||||
model CalendarSyncOutboxEntry {
|
||||
id String @id @default(cuid())
|
||||
sourceType CalendarSyncSourceType
|
||||
sourceId String
|
||||
status CalendarSyncStatus @default(PENDING)
|
||||
graphEventId String?
|
||||
attempts Int @default(0)
|
||||
lastError String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([sourceType, sourceId])
|
||||
@@index([status])
|
||||
}
|
||||
Reference in new issue
Block a user