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

+148
View File
@@ -227,3 +227,151 @@ describe("offline queue database", () => {
raw.close();
});
});
describe("bulk entries in the offline queue", () => {
const BULK_TREATMENT = {
date: "2026-10-14T00:00:00.000Z",
product: "Oxuvar",
batchNumber: "Ch-123",
colonyIds: ["c-1", "c-2", "c-3"],
};
const BULK_VARROA = {
date: "2026-10-14T00:00:00.000Z",
method: "NATURAL_MITE_FALL",
entries: [
{ colonyId: "c-1", miteCount: 4 },
{ colonyId: "c-3", miteCount: 9 },
],
};
const INVALID_COLONIES = {
status: 400,
message: "Einige Völker sind nicht (mehr) erfassbar",
details: { invalidColonyIds: ["c-3"] },
at: "2026-10-14T09:00:00.000Z",
};
beforeEach(() => {
globalThis.indexedDB = new IDBFactory();
});
async function onlyBulkEntry() {
const raw = await openDB(DB_NAME);
const [entry] = await raw.getAll("pending-bulk");
raw.close();
return entry;
}
it("queueBulk stores exactly one entry with kind, input, key and owner", async () => {
const db = await loadDb();
await db.queueBulk("treatments", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
expect(await onlyBulkEntry()).toEqual({
id: 1,
kind: "treatments",
input: BULK_TREATMENT,
idempotencyKey: "bulk-key-0000000001",
ownerUserId: "user-1",
queuedAt: expect.any(String),
});
expect(await db.summarizeQueue("user-1")).toEqual({ own: 1, failed: 0, foreign: 0 });
});
it("lists a bulk entry under its own name", async () => {
const db = await loadDb();
await db.queueBulk("varroa", BULK_VARROA, "bulk-key-0000000001", "user-1");
const [item] = await db.listQueue("user-1");
expect(item).toMatchObject({
store: "pending-bulk",
label: "Varroamessung für mehrere Völker",
});
expect(item.resendWithout).toBeUndefined();
});
it("names the rejected colonies by code from the loaded packs", async () => {
const db = await loadDb();
await db.queueBulk("treatments", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
const raw = await openDB(DB_NAME);
await raw.put("offline-packs", {
apiaryId: "apiary-1",
ownerUserId: "user-1",
savedAt: "2026-10-14T08:00:00.000Z",
pack: {
apiary: { id: "apiary-1", name: "Waldrand" },
colonies: [
{ id: "c-1", code: "V-1", apiaryId: "apiary-1", status: "ACTIVE" },
{ id: "c-3", code: "V-3", apiaryId: "apiary-1", status: "DISSOLVED" },
],
},
});
raw.close();
const entry = await onlyBulkEntry();
await db.markFailed("pending-bulk", entry.id, INVALID_COLONIES);
const [item] = await db.listQueue("user-1");
expect(item.resendWithout).toEqual({ colonyIds: ["c-3"], codes: ["V-3"] });
});
it("offers no resend when every colony of the entry was rejected", async () => {
const db = await loadDb();
await db.queueBulk("treatments", { ...BULK_TREATMENT, colonyIds: ["c-3"] }, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.markFailed("pending-bulk", entry.id, INVALID_COLONIES);
const [item] = await db.listQueue("user-1");
expect(item.resendWithout).toBeUndefined();
});
it("retryBulkWithout resends the remaining colonies with the same key and clears the rejection", async () => {
const db = await loadDb();
await db.queueBulk("treatments", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.markFailed("pending-bulk", entry.id, INVALID_COLONIES);
await db.retryBulkWithout(entry.id, ["c-3"]);
expect(await onlyBulkEntry()).toEqual({
...entry,
input: { ...BULK_TREATMENT, colonyIds: ["c-1", "c-2"] },
lastError: undefined,
});
expect((await onlyBulkEntry()).idempotencyKey).toBe("bulk-key-0000000001");
});
it("retryBulkWithout drops a varroa colony with its own count", async () => {
const db = await loadDb();
await db.queueBulk("varroa", BULK_VARROA, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.retryBulkWithout(entry.id, ["c-3"]);
expect((await onlyBulkEntry()).input).toEqual({
...BULK_VARROA,
entries: [{ colonyId: "c-1", miteCount: 4 }],
});
});
it("retryBulkWithout removes an entry that would be left without colonies", async () => {
const db = await loadDb();
await db.queueBulk("treatments", { ...BULK_TREATMENT, colonyIds: ["c-3"] }, "bulk-key-0000000001", "user-1");
const entry = await onlyBulkEntry();
await db.retryBulkWithout(entry.id, ["c-3"]);
expect(await onlyBulkEntry()).toBeUndefined();
});
it("discardQueueItem deletes a bulk entry", async () => {
const db = await loadDb();
await db.queueBulk("feedings", BULK_TREATMENT, "bulk-key-0000000001", "user-1");
const [item] = await db.listQueue("user-1");
await db.discardQueueItem(item);
expect(await onlyBulkEntry()).toBeUndefined();
});
});