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:
1 parent
c01a38835a
commit
cdd30cf3ee
288 files changed
+39165
-2853
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user