feat: platform backbone and field capture complete (checkpoint)

Platform backbone (plan 2026-10-13-plattform-rueckgrat), all tasks reviewed:
- webhooks with delivery table, retries and stable delivery id
- calendar handlers; a finished task removes its Outlook event
- tasks, automations, inspections, varroa, treatments, feedings,
  harvests, colonies, wintering, stock and bottling record domain events
  in the same transaction; legacy post-commit calls removed
- row locks through one helper (lockRowForUpdate, lockRowsForShare,
  lockRowsForUpdate)
- job framework with JobRun, cli exit codes, cleanup, task templates,
  daily evaluation, hourly app-backup and the rekey job
- system API and page "Verwaltung > System" with retry
- @nestjs/schedule removed, guard tests against in-process schedulers

Field capture T3 (plan 2026-10-13-t3-erfassung-am-stand), all tasks reviewed:
- inspection photo offline without metadata
- bulk capture for treatments, feedings and varroa (backend + frontend)
- offline stand mode with service worker, offline pages and packs
- offline session, cold start and logout on shared devices

Docs: ARCHITECTURE.md assumptions and sections updated, TASKS.md.
Gates: backend lint, typecheck, 1770 unit, 420 e2e, build, no schema
drift; frontend lint, typecheck, 913 tests, build. Not rolled out.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-14 11:36:38 +02:00
1 parent c01a38835a
commit cdd30cf3ee
288 files changed
+39165 -2853

No files matched your search

@@ -11,6 +11,7 @@ import { WAKE_CHANNELS } from '../src/common/worker-wakeup';
import { PgListener } from '../src/worker/pg-listener';
import { createTestApp } from './utils/create-test-app';
import { login } from './utils/api';
import { drainDomainEvents } from './utils/domain-events';
/**
* The M365 outbox against the real database, with Microsoft Graph replaced
@@ -119,6 +120,9 @@ describe('Calendar sync outbox (e2e)', () => {
await prisma.calendarSyncOutboxEntry.deleteMany({
where: { sourceType: 'TASK', sourceId: { in: taskIds } },
});
await prisma.domainEvent.deleteMany({
where: { aggregateType: 'Task', aggregateId: { in: taskIds } },
});
await prisma.task.deleteMany({ where: { id: { in: taskIds } } });
const admin = await prisma.user.findUnique({
where: { email: adminEmail },
@@ -237,6 +241,35 @@ describe('Calendar sync outbox (e2e)', () => {
expect(entry.lastError).toBeNull();
});
it('removes the Graph event of a task that was completed, and settles the entry SKIPPED', async () => {
const task = await queueTask('Completed with event');
await calendarSync.processOutbox();
const synced = await entryFor(task.id);
expect(synced.status).toBe('SYNCED');
const graphEventId = synced.graphEventId;
expect(graphEventId).toMatch(/^event-/);
const deleteEvent = jest.spyOn(graph, 'deleteEvent');
// The real path: the request records TaskCompleted, the dispatcher runs
// the calendar-sync.task handler, which queues the entry again.
await request(app.getHttpServer())
.post(`/api/v1/tasks/${task.id}/complete`)
.set('Authorization', `Bearer ${token}`)
.expect(201);
expect((await entryFor(task.id)).status).toBe('SYNCED');
await drainDomainEvents(app, [task.id]);
expect((await entryFor(task.id)).status).toBe('PENDING');
await calendarSync.processOutbox();
expect(deleteEvent).toHaveBeenCalledWith('group-e2e', graphEventId);
expect(await entryFor(task.id)).toMatchObject({
status: 'SKIPPED',
graphEventId: null,
claimedAt: null,
});
});
describe('API', () => {
const auth = () => ({ Authorization: `Bearer ${token}` });