feat: Aufnahme und lokaler Speicher für Sprachnotizen (T7 L5 Task 17)

Recorder (webm/opus oder mp4, 24 kbit/s, Warnung bei 150 s, Stopp bei
180 s oder im Hintergrund; Mikrofon und Wake-Lock werden in jedem Fall
freigegeben, auch bei Fehlern und ausgestecktem Mikrofon) und
IndexedDB v6 mit Store pending-voice (Index ownerUserId); volle
Speicherquote ergibt VoiceStorageFullError ohne Datenverlust; Abmelden
löscht nur die eigenen Notizen.

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 10:47:45 +02:00
1 parent 1dc39f7658
commit ce472164b5
6 files changed
+829 -11

No files matched your search

+32 -5
View File
@@ -95,6 +95,21 @@ export interface StoredPhoto {
lastError?: QueueError;
}
/** A voice note recorded on this device, waiting to be uploaded (Sprachnotiz). */
export interface StoredVoiceNote {
id: string;
ownerUserId: string;
blob: Blob;
mime: string;
durationSec: number;
colonyId: string;
inspectionDate: string;
createdAt: string;
uploadIdempotencyKey: string;
state: "recorded" | "uploading";
lastError?: QueueError;
}
export interface StoredPack {
apiaryId: string;
ownerUserId: string;
@@ -113,6 +128,7 @@ export interface OfflineDB extends DBSchema, RecordStores {
value: StoredPhoto;
indexes: { sessionId: string; inspectionIdempotencyKey: string; ownerUserId: string };
};
"pending-voice": { key: string; value: StoredVoiceNote; indexes: { ownerUserId: string } };
"offline-packs": { key: string; value: StoredPack };
meta: { key: string; value: unknown };
}
@@ -120,11 +136,12 @@ export interface OfflineDB extends DBSchema, RecordStores {
const DB_NAME = "apicore-offline";
// v1 only had "pending-inspections", v2 added the other five quick-capture
// types, v3 adds bulk entries, photos, offline packs and meta, v4 adds plant
// reports (Pflanzenentwicklung), v5 adds hive changes (Beute). Entries already
// reports (Pflanzenentwicklung), v5 adds hive changes (Beute), v6 adds voice
// notes (Sprache). Entries already
// queued on a phone must survive every upgrade, so each store is only created
// if it doesn't exist yet (guarded by oldVersion, per idb's upgrade()
// convention), never recreated.
const DB_VERSION = 5;
const DB_VERSION = 6;
const RECORD_STORES: readonly RecordStoreName[] = [
"pending-inspections",
@@ -198,6 +215,10 @@ export function getDb(): Promise<IDBPDatabase<OfflineDB>> {
if (oldVersion < 5) {
db.createObjectStore("pending-hive-changes", { keyPath: "id", autoIncrement: true });
}
if (oldVersion < 6) {
const voice = db.createObjectStore("pending-voice", { keyPath: "id" });
voice.createIndex("ownerUserId", "ownerUserId");
}
},
// A newer app version in another tab wants to upgrade: step aside
// instead of blocking it. The next call here opens the new version.
@@ -620,10 +641,10 @@ export async function discardQueueItem(item: QueueItem): Promise<void> {
await tx.done;
}
/** Deletes everything one account queued on this device, photos included. */
/** Deletes everything one account queued on this device, photos and voice notes included. */
export async function clearQueueOf(ownerUserId: string): Promise<void> {
const db = await getDb();
const tx = db.transaction([...QUEUE_STORES, "pending-photos"], "readwrite");
const tx = db.transaction([...QUEUE_STORES, "pending-photos", "pending-voice"], "readwrite");
const queues = QUEUE_STORES.map(async (store) => {
const objectStore = tx.objectStore(store);
const entries: QueuedValue[] = await objectStore.getAll();
@@ -635,6 +656,12 @@ export async function clearQueueOf(ownerUserId: string): Promise<void> {
});
const photos = tx.objectStore("pending-photos");
const photoIds = await photos.index("ownerUserId").getAllKeys(ownerUserId);
await Promise.all([...queues, ...photoIds.map((photoId) => photos.delete(photoId))]);
const voice = tx.objectStore("pending-voice");
const voiceIds = await voice.index("ownerUserId").getAllKeys(ownerUserId);
await Promise.all([
...queues,
...photoIds.map((photoId) => photos.delete(photoId)),
...voiceIds.map((voiceId) => voice.delete(voiceId)),
]);
await tx.done;
}