feat: Sprachnotizen in Synchronisierung, Warteschlange und Abmelden (T7 L5 Task 18)
Backend Image / build-and-push (push) Successful in 7m6s
Frontend Image / build-and-push (push) Successful in 15m27s
Manifests / validate (push) Successful in 16s

Wartende Sprachnotizen werden nach Einträgen und Fotos hochgeladen, je
Notiz ein fester Idempotency-Key; endgültige 4xx markieren die Notiz
mit festem deutschem Text, 401/408/429/5xx/Netz warten auf den nächsten
Lauf, „wird bearbeitet“ wird übersprungen. Warteschlangen-Sheet mit
Abschnitt „Sprachnotizen“ (Verwerfen je Notiz nur für den Besitzer,
alle verwerfen mit Rückfrage, Hinweis bei zu vielen offenen Notizen),
Offline-Band zählt nur wartende Notizen, Abmelden fragt bei wartenden
Notizen nach. Zählen liest keine Audiodaten.

Gates: Frontend lint, typecheck, 2886 Tests.

Refs #27

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
Nick SzaunigandClaude Opus 5.5 committed 2026-10-16 11:10:14 +02:00
1 parent 0c7147f626
commit e614ded967
18 files changed
+1201 -30

No files matched your search

@@ -9,6 +9,7 @@ vi.mock("@/lib/treatments/api", () => ({ createTreatment: vi.fn() }));
vi.mock("@/lib/documents/api", () => ({ uploadDocument: vi.fn() }));
vi.mock("@/lib/phenology/api", () => ({ createPhenologyReport: vi.fn() }));
vi.mock("@/lib/hive/api", () => ({ postHiveChange: vi.fn() }));
vi.mock("@/lib/voice/api", () => ({ uploadVoiceCapture: vi.fn() }));
vi.mock("@/lib/apiaries/offline-pack", () => ({ refreshPacksAfterSync: vi.fn() }));
// What the inspection drain hands over to the photos, seen at that moment.
@@ -131,6 +132,66 @@ describe("syncPendingEntries", () => {
expect(await db.getPendingEntries("pending-inspections")).toEqual([]);
});
it("uploads recorded voice notes after the records, inside the run", async () => {
const order: string[] = [];
const { uploadVoiceCapture } = await import("@/lib/voice/api");
vi.mocked(uploadVoiceCapture).mockReset().mockImplementation(async () => {
order.push("voice");
return { id: "cap-1", status: "QUEUED" as const };
});
createInspection.mockImplementation(async () => {
order.push("inspection");
return { id: "x" } as never;
});
await db.queueInspection(inspection(1), "key-1", "user-1");
const raw = await openDB("apicore-offline");
await raw.put("pending-voice", {
id: "v1",
ownerUserId: "user-1",
blob: new Blob(["audio"]),
mime: "audio/webm",
durationSec: 20,
colonyId: "colony-1",
inspectionDate: "2026-10-16",
createdAt: "2026-10-16T08:00:00.000Z",
uploadIdempotencyKey: "upload-v1",
state: "recorded",
});
raw.close();
const result = await sync.syncPendingEntries("user-1");
expect(order).toEqual(["inspection", "voice"]);
expect(result).toEqual({ synced: 2, failed: 0, skippedForeign: 0 });
});
it("a cancelled run sends no voice note", async () => {
const { uploadVoiceCapture } = await import("@/lib/voice/api");
vi.mocked(uploadVoiceCapture).mockReset();
await db.getPendingEntries("pending-inspections");
const raw = await openDB("apicore-offline");
await raw.put("pending-voice", {
id: "v1",
ownerUserId: "user-1",
blob: new Blob(["audio"]),
mime: "audio/webm",
durationSec: 20,
colonyId: "colony-1",
inspectionDate: "2026-10-16",
createdAt: "2026-10-16T08:00:00.000Z",
uploadIdempotencyKey: "upload-v1",
state: "recorded",
});
raw.close();
const run = sync.syncPendingEntries("user-1");
await sync.cancelSync();
await run;
sync.resumeSync();
expect(uploadVoiceCapture).not.toHaveBeenCalled();
});
it("offline the plant report is queued and synced later with its key", async () => {
const report = {
phase: "SALWEIDE",