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

@@ -628,3 +628,68 @@ describe("bulk entries in the offline queue", () => {
expect(await onlyBulkEntry()).toBeUndefined();
});
});
describe("voice notes in the offline queue", () => {
beforeEach(() => {
globalThis.indexedDB = new IDBFactory();
});
function note(id: string, ownerUserId: string, lastError?: typeof REJECTION) {
return {
id,
ownerUserId,
blob: new Blob(["audio"]),
mime: "audio/webm;codecs=opus",
durationSec: 30,
colonyId: "colony-1",
inspectionDate: "2026-10-16",
createdAt: "2026-10-16T08:00:00.000Z",
uploadIdempotencyKey: `upload-${id}`,
state: "recorded" as const,
...(lastError ? { lastError } : {}),
};
}
async function storeNotes() {
const db = await loadDb();
await db.queueInspection(INSPECTION, "key-1", "user-1");
const raw = await openDB(DB_NAME);
await raw.put("pending-voice", note("v1", "user-1"));
await raw.put("pending-voice", note("v2", "user-1", REJECTION));
await raw.put("pending-voice", note("v3", "user-2"));
raw.close();
return db;
}
it("summarizeQueue counts waiting, rejected and foreign voice notes", async () => {
const db = await storeNotes();
expect(await db.summarizeQueue("user-1")).toEqual({
own: 2,
failed: 1,
foreign: 1,
voice: 2,
voiceWaiting: 1,
});
expect(await db.summarizeQueue("user-2")).toEqual({
own: 1,
failed: 0,
foreign: 3,
voice: 1,
voiceWaiting: 1,
});
});
it("listQueue lists the user's voice notes and never another account's", async () => {
const db = await storeNotes();
const items = (await db.listQueue("user-1")).filter((item) => item.store === "pending-voice");
expect(items.map((item) => item.id)).toEqual(["v1", "v2"]);
expect(items[0]).toMatchObject({
label: "Sprachnotiz",
voice: { colonyId: "colony-1", inspectionDate: "2026-10-16", durationSec: 30 },
});
expect(items[1].lastError).toMatchObject({ status: 400 });
});
});