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

@@ -1,4 +1,4 @@
import { getDb, type StoredVoiceNote } from "@/lib/offline/db";
import { getDb, type QueueError, type StoredVoiceNote } from "@/lib/offline/db";
import { newIdempotencyKey } from "@/lib/offline/idempotency";
export type { StoredVoiceNote } from "@/lib/offline/db";
@@ -13,7 +13,7 @@ export type { StoredVoiceNote } from "@/lib/offline/db";
/** The device had no room left for another recording. */
export class VoiceStorageFullError extends Error {
constructor() {
super("Auf dem Gerät ist kein Platz mehr für die Aufnahme.");
super("Auf dem Gerät ist kein Platz mehr für die Sprachnotiz.");
this.name = "VoiceStorageFullError";
}
}
@@ -98,3 +98,33 @@ export async function discardAllPendingVoice(ownerUserId: string): Promise<numbe
await tx.done;
return ids.length;
}
/** One note by id, or undefined when it is gone (discarded meanwhile). */
export async function getPendingVoice(id: string): Promise<StoredVoiceNote | undefined> {
const db = await getDb();
return db.get("pending-voice", id);
}
/** Books the server's refusal on a note that still exists; the audio stays for the user to see. */
export async function markVoiceFailed(id: string, error: QueueError): Promise<void> {
const db = await getDb();
const tx = db.transaction("pending-voice", "readwrite");
const note = await tx.store.get(id);
if (note) {
const rejected: StoredVoiceNote = { ...note, lastError: error, state: "recorded" };
delete rejected.blockedBy;
await tx.store.put(rejected);
}
await tx.done;
}
/** Notes that the server could take later (too many open): the sheet says so. */
export async function markVoiceBlocked(id: string, blockedBy: "TOO_MANY_OPEN"): Promise<void> {
const db = await getDb();
const tx = db.transaction("pending-voice", "readwrite");
const note = await tx.store.get(id);
if (note) {
await tx.store.put({ ...note, blockedBy });
}
await tx.done;
}