diff --git a/docs/projektplan.md b/docs/projektplan.md index cb042f1..49b1bcc 100644 --- a/docs/projektplan.md +++ b/docs/projektplan.md @@ -117,6 +117,8 @@ UI-unabhängige Tests laufen auf der JVM (keine Emulator-Runtime). Importe von ` #### 1a. Pixel Watch App (Wear OS / Kotlin) — primäres Entwicklungsziel +**Ist-Stand (2026-04-23):** Vertikaler PoC end-to-end lauffähig auf Wear-OS-34-AVD. Record-Button → 5 s MediaRecorder (MPEG-4/AAC-LC 16 kHz 64 kbps mono) → Multipart-`POST /api/upload` mit `X-API-Key` → Server schreibt `.m4a`, Whisper transkribiert, Ollama klassifiziert, `document.md` entsteht. Manuell verifiziert mit "Hallo, hallo" → korrekte Transkription + `oneliner.kind = empty` (Silence-Rule). Architektur-Unbekannte aus der vertikalen Scheibe (Audio-Codec-Kompatibilität, Cleartext-HTTP-Policy, Emulator-Mic, Wire-Vertrag) sind **gemessen**, nicht mehr spekulativ. Der PoC hat aber **bewusst** nicht: Fallliste, Marker-Dateien, `unsynced/`-Queue, Foreground Service, Retry-Backoff, Oneliner-Polling, Bluetooth-Headset, Neu/Fortsetzen-Flow — diese Features bleiben laut Plan zu bauen. + **Funktion:** - Audioaufnahme direkt auf der Watch (MediaRecorder, AAC/m4a) - Optionale Aufnahme über gekoppeltes Bluetooth-Headset (automatisch bevorzugt) @@ -1181,32 +1183,32 @@ wiremock = "0.6" **Stand-in, solange keine Hardware:** `scripts/dictate.sh` simuliert den Client-Flow via ffmpeg-PulseAudio-Aufnahme + Upload gegen den laufenden Server (Modi: neuer Fall / aktuellen Fall fortsetzen / refresh). State in `/tmp/doctate-current-case`. So lassen sich Transkription und Oneliner ohne native Clients testen. #### 5a — Gemeinsame Code-Basis (vor jeder UI-Arbeit) -- [ ] Gradle-Multi-Modul-Projekt aufsetzen (`:core-domain`, `:core-audio`, `:core-sync`, `:core-http`, `:core-storage`, `:app-wear`, `:app-mobile` als Platzhalter) +- [ ] Gradle-Multi-Modul-Projekt aufsetzen (`:core-domain`, `:core-audio`, `:core-sync`, `:core-http`, `:core-storage`, `:app-wear`, `:app-mobile` als Platzhalter) — PoC (2026-04-23) läuft vorerst paket-basiert in einem `:app`-Modul; Split wird spätestens beim Start der Handy-App fällig (siehe Abweichungen → Client-Architektur). - [ ] `:core-*`-Module strikt UI-frei halten (kein Import von `android.*`/`androidx.compose.*` erlaubt — Build schlägt sonst fehl) -- [ ] JVM-Unit-Tests für `:core-domain` und `:core-sync` (case_id-Generierung, ACK-Zustandsautomat, Backoff-Kurve) -- [ ] API-Client (`:core-http`): Endpoints typisiert, API-Key im Header, Reqwest-Äquivalent (Ktor oder OkHttp) +- [~] JVM-Unit-Tests für `:core-domain` und `:core-sync` (case_id-Generierung, ACK-Zustandsautomat, Backoff-Kurve) — Teil-abgedeckt im `:app`-Modul: `CaseIdTest` (UUID-v4-Vertrag) und `UiStateReducerTest` (10 Übergänge inkl. Recorder-Failure). Backoff-Kurve fehlt noch, weil keine Retry-Queue existiert. +- [x] API-Client (`:core-http`): Endpoints typisiert, API-Key im Header, Reqwest-Äquivalent (Ktor oder OkHttp) — implementiert als `net/UploadClient` mit **OkHttp 4.12.0**, Wire-Protocol 1:1 zu `doctate-client-core/src/server_sync.rs:344-399`. Siehe Abweichungen. #### 5b — Pixel Watch App (primäres Entwicklungsziel) -- [ ] Wear OS Projekt in Android Studio (`:app-wear`) -- [ ] Jetpack Compose UI (ein Screen pro Eintrag, fullscreen, Swipe-Navigation) +- [x] Wear OS Projekt in Android Studio (`:app-wear`) — `watch/wearos/` Gradle-Projekt (AGP 9.2, Kotlin 2.2.10, Compose BOM 2024.09, minSdk 30, targetSdk 36). **Single `:app` statt `:app-wear` + `:core-*`.** +- [x] Jetpack Compose UI (ein Screen pro Eintrag, fullscreen, Swipe-Navigation) — PoC-UI (`RecordingScreen`) rendert den Record-Flow. Swipe-Navigation und Pro-Eintrag-Screens sind für die Fallliste geplant, aber noch nicht gebaut. - [ ] Vertikale Fallliste (Neu ganz oben, heutige Fälle darunter) - [ ] Nach Stop: Screen bleibt auf aktuellem Fall (→ "Fortsetzen" direkt sichtbar) -- [ ] MediaRecorder-Integration via `:core-audio` +- [x] MediaRecorder-Integration via `:core-audio` — in `audio/AudioRecorder.kt` (MPEG-4/AAC-LC, 16 kHz mono, 64 kbps, `context.cacheDir`). Landet bei künftigem Multi-Modul-Split in `:core-audio`. - [ ] Bluetooth-Headset Erkennung + Indikator -- [ ] case_id (UUIDv4) Generierung: Neu → neue case_id, Fortsetzen → bestehende -- [ ] Persistente Speicherung im lokalen Dateisystem (UTC-Timestamps in Dateinamen) +- [~] case_id (UUIDv4) Generierung: Neu → neue case_id, Fortsetzen → bestehende — PoC erzeugt `CaseId.new()` pro Aufnahme (`domain/CaseId.kt`). Neu/Fortsetzen-Dualität fehlt, weil keine Fallliste-UI existiert. +- [~] Persistente Speicherung im lokalen Dateisystem (UTC-Timestamps in Dateinamen) — PoC speichert in `cacheDir` und **löscht nach Upload**; Datei lebt also nur während der laufenden Pipeline. `unsynced/`-Pattern (überlebt Neustarts) fehlt. - [ ] Marker-Datei pro Fall (`/recordings/cases/{case_id}.json`): minimales JSON bei Erstellung, Oneliner ergänzen nach Empfang - [ ] Fallliste aus Marker-Dateien ableiten (`created_at` von heute + optionaler Oneliner) - [ ] Lazy Cleanup: nur Marker-Dateien (`cases/`) von gestrigen/älteren UTC-Tagen beim Scan löschen — Audiodateien in `unsynced/` werden nie durch Cleanup gelöscht - [ ] Foreground Sync Service (automatischer Upload) — einziger Verantwortlicher für Audio-Löschung (nur nach ACK) -- [ ] ACK-Protokoll: "received" → lokal löschen, "gone" → lokal löschen, kein ACK → Retry +- [~] ACK-Protokoll: "received" → lokal löschen, "gone" → lokal löschen, kein ACK → Retry — PoC parst die Server-ACK-Response (`UploadResult.Success/Transient/Terminal`), löscht die Datei aber **vor** persistenter Queue. Retry/Backoff fehlt. - [ ] Exponentieller Backoff bei Fehlern - [ ] Doze-Modus-Kompatibilität (Netzwerk-Wiederherstellung) - [ ] Sync-Indikator (☁↑ Anzahl ungesyncter Aufnahmen) - [ ] (optional) Speicherplatz-Warnung bei >500 MB ungesyncten Daten - [ ] Stop → direkt in Sync-Queue, ✓-Feedback, Screen bleibt auf aktuellem Fall - [ ] Oneliner-Polling (alle paar Sekunden für den sichtbaren Fall, bis Oneliner empfangen) -- [ ] Emulator-Tests + Pixel Watch Hardware-Test +- [~] Emulator-Tests + Pixel Watch Hardware-Test — vertikaler PoC im Emulator manuell verifiziert (Aufnahme → Upload → Whisper → Ollama → `document.md`). Instrumented MockWebServer-Test (`UploadClientTest`) hängt auf Wear-OS-34-AVD (siehe Abweichungen). Hardware-Test steht aus. #### 5c — Android-Handy-App (nach Watch-Abschluss) - [ ] `:app-mobile`-Modul auf bestehende `:core-*`-Basis aufsetzen @@ -1389,3 +1391,10 @@ Alle Einträge beziehen sich auf den Ist-Stand im Repository. Die ursprüngliche | Desktop-Tech-Stack | Offen — Kandidaten Tauri / Compose Multiplatform / nativ | `eframe`/`egui` (Immediate-Mode-UI) + `tokio` + `reqwest` + `ffmpeg`-Subprozess | Kleinere Binaries als Tauri (keine WebView, kein WebKitGTK auf Linux), keine Android-Verpflichtung wie Compose, pure Rust-Toolchain. ffmpeg-Subprozess statt `libav`-FFI: Lizenz (GPL vs. LGPL), Build-Komplexität, plattformübergreifende Konsistenz. | | Desktop-Audio-Stop | nicht spezifiziert | **Unix:** `SIGINT` via `libc::kill`. **Windows:** `q`-Stdin-Write als Best-Effort. | ffmpeg's `term_init()` prüft `isatty(stdin)`; bei Pipe-stdin wird Keyboard-Polling deaktiviert, `q` landet in der Pipe, wird aber nie gelesen. SIGINT triggert ffmpeg's Signal-Handler unabhängig vom TTY-Status. Exit-Code 255 bei SIGINT-Exit ist normal — wir prüfen Datei-Existenz + Nicht-Leer statt Exit-Code. | | Desktop-Tray-Icon | Geplant als Schritt 4f des Client-Aufbaus | Zurückgestellt, kein Tray im MVP. Close-Button beendet App. | `tray-icon`-Crate zieht drei System-Libs (`libxdo`, `gtk3`, `libappindicator`) für triviale Funktion. `ksni` (pure-Rust via dbus/zbus) als saubere Linux-Alternative für später dokumentiert. Client läuft ohne Tray sinnvoll. | +| Watch-Modul-Struktur | Multi-Modul-Gradle-Projekt (`:core-domain`, `:core-audio`, `:core-sync`, `:core-http`, `:core-storage`, `:app-wear`, `:app-mobile`) — "nicht optional" | Single `:app`-Modul mit paket-basierter Trennung: `presentation/`, `audio/`, `net/`, `settings/`, `domain/`. Split auf Multi-Modul spätestens beim Handy-App-Start (Phase 5c). | Für den vertikalen PoC (2026-04-23) war Multi-Modul reine Build-Ergonomie-Last ohne Mehrwert — alle Schichten sind testbar getrennt, Imports zwischen `presentation` und `audio`/`net` sind manuell diszipliniert. Beim Handy-App-Ausbau zwingt das fehlende Modul zur ersten echten Code-Duplikation — das ist der natürliche Trigger-Punkt für den Split. | +| Watch-HTTP-Client | Im Plan offen als "Reqwest-Äquivalent (Ktor oder OkHttp)" | **OkHttp 4.12.0** mit `kotlinx-coroutines-android`; Multipart-API ist API-Klon zu `reqwest::multipart::Form` | API-Nähe zum bestehenden `doctate-client-core/src/server_sync.rs` (1:1-Portierung der `post_upload`-Klassifikation), kleinere APK als Ktor (keine zusätzliche Engine + Serialization-Transitiv), MockWebServer ist im selben Maven-Artefakt für Wire-Tests. Ktor bleibt Option, falls wir später einen WASM-Browser-Client wollen. | +| Watch-Dependency-Injection | Nicht explizit im Plan (impliziert Hilt, das Android-Studio-Scaffold-Default) | **Service-Locator** in `DoctateApp : Application` mit `by lazy`-Properties | Für vier Singletons (Settings, OkHttpClient, UploadClient, AudioRecorder) rechtfertigen keinen Annotation-Processor + Compile-Time-Graph. `ViewModelProvider.Factory` via `viewModelFactory { initializer { ... } }` gibt Constructor-Injection ohne DI-Framework. Bei deutlich mehr Singletons (~10+) ist Hilt der natürliche Aufrüstpfad, weil `by lazy`-Boilerplate dann skaliert. | +| Watch-Settings-Kanal | Nicht explizit im Plan | `local.properties` (gitignored) speist `BuildConfigField("SERVER_URL", "API_KEY")`; `Settings`-Interface + `BuildConfigSettings`-Impl erlauben späteres `DataStoreSettings` ohne Konsumenten-Refactor | Watch hat kein ergonomisches Text-Input-UI; Build-Time-Konstante via `local.properties` ist die einzige pragmatische Lösung, bis ein Settings-Screen existiert. Default `"MISSING_API_KEY"` bei fehlender Property sorgt für sichtbaren 401-Fehler statt unsichtbarer Silent-Fail. | +| Watch-Case-ID-Strategie im PoC | Plan: "Neu" → neue case_id + Marker, "Fortsetzen" → bestehende | **PoC**: jede Aufnahme = neue `UUID.randomUUID()`; keine Marker-Datei, keine Fortsetzen-Logik | Solange keine Fallliste-UI existiert, gibt es keinen Mechanismus zum Auswählen eines bestehenden Cases. Neu-pro-Aufnahme matcht das Server-Verhalten (unbekannte UUID → neuer Case-Ordner) und hält den PoC-Scope klein. Umstellung auf Neu/Fortsetzen berührt nur das ViewModel (Case-Store hält die "aktuelle" ID), nicht `UploadClient` oder `AudioRecorder`. | +| Watch-Datei-Lifecycle im PoC | Plan: Persistentes `unsynced/`-Verzeichnis, Sync-Service löscht nach ACK | **PoC**: Aufnahme in `cacheDir`, im `finally`-Block des Upload-Flows gelöscht — egal ob Success, Transient oder Terminal | Für den vertikalen Beweis unnötig, Crash-Recovery zu implementieren. Volle Pending-Queue mit Marker + Retry + Backoff kommt in der Arbeitsphase nach dem PoC (entspricht den noch offenen Phase-5b-Tasks). | +| Watch-Instrumented-Test-Hang | Nicht im Plan | `UploadClientTest` (MockWebServer-basiert) **hängt in `@Before setUp()`** auf Wear-OS-34-AVD. JVM-Unit-Tests laufen. Real-Server-E2E validiert denselben Vertrag strenger. | Vermutlich Wear-SELinux-Policy gegen Loopback-Socket-Bind aus der Instrumentation-APK-Prozess. Priorität niedrig, weil der echte Server-Upload funktioniert — Mock-Test ist nur Regressions-Absicherung fürs Refactoring. `SKIP_INSTRUMENTED=1 ./run.sh test` überspringt den on-device-Tier. |