feat: sync calendar entries to a Microsoft 365 group calendar
Backend Image / build-and-push (push) Successful in 8m34s
Frontend Image / build-and-push (push) Successful in 8m41s

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:
Nick SzaunigandClaude Sonnet 5 committed 2026-09-04 09:23:35 +02:00
1 parent 28ae0a2723
commit e7b157ec35
35 files changed
+1760 -7

No files matched your search

+5 -2
View File
@@ -6,6 +6,7 @@ import {
import { PrismaService } from '../prisma/prisma.service';
import { AuditLogService } from '../audit/audit-log.service';
import { WebhooksService } from '../webhooks/webhooks.service';
import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
import { Queen, QueenStatus } from '../../generated/prisma';
import { CreateQueenDto } from './dto/create-queen.dto';
import { UpdateQueenDto } from './dto/update-queen.dto';
@@ -45,6 +46,7 @@ export class QueensService {
private readonly prisma: PrismaService,
private readonly auditLog: AuditLogService,
private readonly webhooks: WebhooksService,
private readonly calendarSync: CalendarSyncService,
) {}
async findAll(filter: QueenFilter = {}): Promise<Queen[]> {
@@ -118,7 +120,7 @@ export class QueensService {
throw new NotFoundException('Colony not found');
}
await this.prisma.$transaction(async (tx) => {
const history = await this.prisma.$transaction(async (tx) => {
if (colony.currentQueenId && colony.currentQueenId !== queen.id) {
await tx.queenColonyHistory.updateMany({
where: {
@@ -137,7 +139,7 @@ export class QueensService {
where: { id: colony.id },
data: { currentQueenId: queen.id },
});
await tx.queenColonyHistory.create({
return tx.queenColonyHistory.create({
data: {
queenId: queen.id,
colonyId: colony.id,
@@ -158,6 +160,7 @@ export class QueensService {
const updated = await this.findById(id);
await this.webhooks.dispatch('queen.replaced', updated);
await this.calendarSync.enqueue('QUEEN_ASSIGNMENT', history.id);
return updated;
}