Commit graph

78 commits

Author SHA1 Message Date
Jens Reinemann
575c0ad709 feat: automatic forced logout on expired session
When both access and refresh token are invalid (401 on /auth/refresh),
the app now automatically logs out and navigates to Settings (login form).
No data loss - only auth tokens are cleared, local inventory data is intact.

- AuthEventBus: singleton SharedFlow that signals session expiry
- MainViewModel: observes bus, calls logout + disconnect, navigates to Settings
- MainScreen: LaunchedEffect collects navigateToSettings event
- MessageRepositoryImpl: emits session expired when refresh fails
- SyncServiceImpl: emits session expired when refresh fails
2026-05-18 00:55:25 +02:00
Jens Reinemann
a14c40d756 fix: 401 token refresh in MessageRepositoryImpl
fetchUsers() and attemptSendToServer() had no retry logic on 401.
SyncServiceImpl already had this pattern (auto-refresh on 401).

Add private refreshAccessToken(serverUrl) and retry once on 401
in both methods.
2026-05-18 00:44:58 +02:00
Jens Reinemann
ea02029dbe fix: SettingsKey circular init crash on app start
SENSITIVE_KEYS and SENSITIVE_KEY_STRINGS used eager initialization in the
companion object. When E2EEKeyManager.hasKeyPair() was the first access to
SettingsKey, it triggered SettingsKey.<clinit> which tried to resolve
StringKey.E2EEPrivateKeyset - but that class was already 'in initialization
by the current thread' (JVM spec). The JVM returned null, causing NPE in
SENSITIVE_KEY_STRINGS.map { it.key }.

Fix: use by lazy for both properties to defer initialization past <clinit>.
2026-05-18 00:39:32 +02:00
Jens Reinemann
d02a38455b chore: version bump 1.5 (6) -> 1.6 (7) 2026-05-18 00:32:53 +02:00
Jens Reinemann
8c0db56223 feat: E2EE Messaging mit Tink HPKE (X25519 + ChaCha20-Poly1305)
Closes #96

## App

- E2EEKeyManager: Tink HPKE Schlüsselpaar generieren, privaten Key
  via EncryptedSharedPreferences sichern, Nachrichten verschlüsseln
  und entschlüsseln (X25519 + ChaCha20-Poly1305)
- EnsureKeyPairUseCase: Keypair-Initialisierung beim App-Start;
  Public Key via HTTP PUT an Server übermitteln
- MainActivity: EnsureKeyPairUseCase.execute() in onCreate
- SettingsKey: E2EEPrivateKeyset + E2EEPublicKeyBase64 als SENSITIVE_KEYS
- MessageRepositoryImpl: sendMessage verschlüsselt Body mit Empfänger-
  Public-Key; eingehende Nachrichten werden lokal entschlüsselt und
  als Klartext in Room gespeichert; Public-Key-Cache (in-memory) +
  key_updated Handler
- WebSocketClient: KeyUpdated Event hinzugefügt
- WebSocketClientImpl: key_updated Frame parsen; Exception-Logging
- Tink 1.15.0 als neue Dependency

## Server

- V4 Flyway Migration: ALTER TABLE users ADD COLUMN public_key TEXT
- Tables.kt: publicKey Feld in Users-Objekt
- UserRepository: getPublicKey() / setPublicKey()
- UserRoutes: PUT /api/users/{id}/public-key (Auth + Owner-Check +
  Längenvalidierung ≤ 10.000 Zeichen) und
  GET /api/users/{id}/public-key
- WebSocketManager: notifyKeyUpdated() Broadcast an alle anderen
  verbundenen Clients
- MessageRepository: EncryptionService für message body bypassed –
  Server speichert E2EE-Ciphertext direkt (Zero-Knowledge)

## Tests

- E2EEKeyManagerTest: 5 Tests (Roundtrip, Nonce-Uniqueness,
  Wrong-Key, hasKeyPair)
- EnsureKeyPairUseCaseTest: 4 Tests (generate+upload, skip wenn
  vorhanden, kein Upload ohne UserId, kein Crash bei Server-Fehler)
- MessageRepositoryImplTest: 5 neue E2EE-Tests

## Docs

- docs/migration-guide.md: E2EE-Einschränkungen dokumentiert
  (Pending-Message Klartext in SQLite)

## Follow-up

- #105: E2EE Private Key – AndroidKeysetManager statt
  CleartextKeysetHandle (Security Hardening)
2026-05-18 00:22:28 +02:00
Jens Reinemann
9631ec9a92 chore: ungestagede Aenderungen und neue Docs committen 2026-05-17 22:51:07 +02:00
Jens Reinemann
d00b5b245a chore: version bump 5 -> 6 (1.4 -> 1.5) 2026-05-17 22:47:13 +02:00
Jens Reinemann
045a4b7674 feat: Migration-Safety – Room v7, AutoMigration, Flyway, kein fallbackToDestructiveMigration (#99)
- fallbackToDestructiveMigration() aus DatabaseModule entfernt
- BollwerkDatabase auf Version 7 gebumpt
- AutoMigration(from=5, to=6) und (from=6, to=7) definiert
- MigrationTestHelper-Test migrate6To7_preservesData implementiert
- 7.json Schema-Export generiert
- Server: Flyway 9.22.3 integriert (baselineOnMigrate=true)
- V1__initial_schema.sql + V2__cleanup_user_id.sql angelegt
- Skill android-db-migration erstellt
- versionCode 5 / versionName 1.4
2026-05-17 21:17:24 +02:00
Jens Reinemann
3d7c01cef5 feat(update): AlertDialog bei verfuegbarem Update anzeigen
- AlertDialog in MainScreen zeigt verfuegbare Version mit Bestaetigung
- UpdateBanner blendet bei UpdateStatus.Available aus (Dialog uebernimmt)
- FEATURE_CAMERA_ENABLED temporaer deaktiviert

fix(server): Logo-Pfad und statische Ressourcen bereinigen

- /res-Route fuer classpath-Assets (logo.png etc.) hinzugefuegt
- Logo-Pfad von /static/logo.png auf /res/logo.png korrigiert
- Build-Nummer aus Versionsanzeige auf der Homepage entfernt

ci: GitHub Actions Workflow fuer Swift-Tests hinzugefuegt

style: Tabellenformatierung im Code-Reviewer-Agenten bereinigt
2026-05-17 20:52:47 +02:00
Jens Reinemann
9ff21cbc4b ui: Server-Synchronisierung als ElevatedCard neu gestaltet
- Status-Dot + Verbindungstext links, Sync-Button (Refresh-Icon) rechts
  in einer kompakten Zeile statt verstreuter Einzelelemente
- Bei Disconnect: Haupttext 'Keine Verbindung', Countdown als
  zweite Zeile darunter (statt alles in einer langen Zeile)
- Sync-Spinner ersetzt den Button solange Sync läuft
- Aktivitätsmeldung animiert unter der Statuszeile
- Letzte-Sync-Zeit mit Divider am Card-Boden, klar abgetrennt
- 'Synchronisierung erfolgt automatisch.' entfernt (redundant)
2026-05-17 20:35:15 +02:00
Jens Reinemann
fdc016c786 fix: Differenzierte Sync-Aktivitätsmeldungen
- Neues SyncActivityMessage.CheckingForChanges 'Prüfe auf Änderungen…'
  wird beim initialen Catch-up nach Connect angezeigt (statt dem
  irreführenden 'Empfange Inventar-Update…')
- Neues SyncActivityMessage.NoChanges 'Keine Änderungen'
  wird angezeigt wenn der initiale Sync keine Daten zurückliefert
- Echter Server-Push (InventoryUpdated/FullSyncRequired) zeigt weiterhin
  'Empfange Inventar-Update…' → jede echte Kommunikation bleibt sichtbar
2026-05-17 20:27:49 +02:00
Jens Reinemann
aafb9ddd64 fix: Verbindungs-Noise unterdrücken
- loadSettings() ruft connect() nur noch auf wenn ConnectionState
  NotConfigured oder Disconnected ist – verhindert den self-triggering
  Loop (pullSync → loadSettings → connect → Connecting → Connected → …)
- pullSync() erhält Parameter silent=true für den initialen Connect-Sync;
  zeigt kein 'Empfange Inventar-Update…' mehr beim bloßen Verbindungsaufbau
- Version 1.2 (3) → 1.3 (4)
2026-05-17 19:05:37 +02:00
Jens Reinemann
e73d3a11a0 ci: disable automatic CI triggers on push/PR
Both android-ci.yml and ci.yml now only run via workflow_dispatch
(manual trigger). Automatic builds on push/PR are disabled to stop
failing pipeline notifications.
2026-05-17 18:23:27 +02:00
Jens Reinemann
e0130910af chore: migrate server URLs from IP to bollwerk.online domain
Replace all HTTP references to 195.246.231.210 with bollwerk.online
across skills, prompts, scripts, and app default settings:

- Dockerfile: rename KRISENVORRAT_JWT_SECRET to BOLLWERK_JWT_SECRET
- SettingsKey.kt: default server URL now http://bollwerk.online:8080
- publish SKILL/prompt/script: HTTP URLs updated to bollwerk.online
- vps-deploy SKILL: Admin-UI and health-check URLs updated
- run-integration-tests.ps1: default BaseUrl updated

SSH commands (ssh/scp) intentionally kept on IP, as DNS is not
used for SSH access.
2026-05-17 18:22:17 +02:00
Jens Reinemann
a5f89e6a69 rename: Krisenvorrat -> Bollwerk
- Package: de.krisenvorrat.* -> de.bollwerk.*
- Klassen: KrisenvorratApp/Database/Theme -> Bollwerk*
- ApplicationId: de.bollwerk.app
- Server: BOLLWERK_* Env-Vars, bollwerk HOCON-Config
- Docker: bollwerk-server/db/backup Container-Namen
- Room DB: bollwerk.db, SharedPrefs: bollwerk_secure_prefs
- Export-Dateien: bollwerk_export/inventar
- UI-Strings, HTML, Admin-UI: alle auf Bollwerk
- Docs, Skills, README angepasst
- Alle Tests gruen, Build erfolgreich
2026-05-17 17:44:02 +02:00
Jens Reinemann
46bfaa0367 chore: Logo überarbeiten – neue Ratte mit Plättchenpanzer & Patronengurt
- Altes Vektor-Foreground (IKEA Gosig-Style) durch PNG-basiertes Logo ersetzt
- Neue Ratte: grau, illustrativ, bronzener Plättchenpanzer, Patronengurt
- Adaptive-Icon-Foreground als PNG in allen Density-Stufen (mdpi–xxxhdpi)
- Legacy-Launcher-Icons mit dunklem Hintergrund (#16140F)
- Hintergrund von Olivgrün (#3D5229) auf App-Theme-Dunkel (#16140F) geändert
- Web-Logo auf Download-Seite eingebunden (/static/logo.png)
- Quelldatei unter docs/ratti.png archiviert

Closes #93
2026-05-17 17:14:11 +02:00
Jens Reinemann
0fb1ebbdca style: App-UI an Admin-Bereich angleichen (#92)
- Color.kt: Farbpalette von Olivgrün auf Admin-Palette umgestellt
  (burnt orange #C1440E primary, warm beige #E8D5B0 text,
   dark brown #16140F background, tan #A89070 labels)
- Type.kt: Neue Typography mit Monospace für Headings/Titles/Labels
  (analog Admin Share Tech Mono), system-ui für Body
- Theme.kt: KrisenvorratTypography eingebunden
- Dark Mode vollständig spezifiziert, Admin-konsistent
- Alle bestehenden Screens profitieren automatisch via MaterialTheme
- Build , alle Tests grün (70 tasks)
2026-05-17 16:51:46 +02:00
Jens Reinemann
47a2865b34 feat: Sync-Statusanzeige mit Live-Verbindungsstatus und Aktivitaets-Feed (#94)
- ConnectionState sealed interface (Connected/Connecting/Disconnected/NotConfigured)
- WebSocketClientImpl: connectionState StateFlow mit Reconnect-Countdown-Timer
- SyncActivityMessage: Aktivitaets-Feed-Modell mit 3-Sek-Auto-Dismiss
- PendingSyncOpDao: getCount() Flow fuer Queue-Groesse
- SettingsUiState: SyncStatus durch ConnectionState + SyncActivity ersetzt
- SettingsViewModel: observeConnectionState, observePendingQueueCount,
  showActivity mit Job-basiertem Timer-Reset
- SettingsScreen: Farbiger Punkt + Statustext, AnimatedVisibility
  fuer Aktivitaets-Feed, Countdown bei Disconnected
- Alle 306 Tests gruen
2026-05-17 16:02:55 +02:00
Jens Reinemann
5434c00f20 feat: automatischer Sync nach Login/Reconnect, manuelle Push/Pull-Buttons entfernen
- WebSocket Connected-Event löst jetzt automatisch pullSync() aus
  (nach Login = Full Sync, nach Reconnect = inkrementell)
- Push/Pull-Buttons ersetzt durch Hinweis 'Synchronisierung erfolgt
  automatisch' + Fallback-Button 'Jetzt synchronisieren'
- ServerUrl: Default-Wert (VPS-IP) als Konstante in SettingsKey,
  Reset-Button neben dem URL-Feld
- SettingsUiState: serverUrl Default = DEFAULT_SERVER_URL
- Tests angepasst (SettingsRepositoryImplTest)
2026-05-17 15:36:11 +02:00
Jens Reinemann
1df2d1cff5 refactor: manuelle DB-Migrationen durch Room AutoMigration ersetzen
- DB-Version auf 6 hochgezaehlt (Clean-Slate, keine Rueckwaertskompatibilitaet)
- Alle manuellen Migrationen (v1-v5) aus Migrations.kt entfernt
- DatabaseModule: addMigrations() durch fallbackToDestructiveMigration() ersetzt
- migration-guide.md: AutoMigration-Workflow dokumentiert
- Instrumentierte Tests: alte Migrationstests durch frische DB-Tests ersetzt
- Schema 6.json exportiert

Closes #89
2026-05-17 11:43:27 +02:00
Jens Reinemann
5e9c072b51 refactor: kcalPerKg -> kcalPerUnit (kcal pro Einheit)
- ItemEntity, ItemDto: kcalPerKg -> kcalPerUnit (kcal_per_unit)
- Room DB: version 4 -> 5, MIGRATION_4_5 hinzugefuegt
- CalculateSupplyRangeUseCase: Berechnung vereinfacht zu
  quantity * kcalPerUnit (keine Einheitenumrechnung mehr noetig,
  alle Einheiten unterstuetzt)
- ItemFormScreen: Label 'kcal / kg' -> 'kcal / Einheit'
- CsvExporter: Header 'kcal/kg' -> 'kcal/Einheit'
- OpenAiVisionService: Prompt-JSON-Feld angepasst
- Server Tables + InventoryRepository: kcal_per_unit
- Alle Tests aktualisiert und gruen
2026-05-17 11:29:39 +02:00
Jens Reinemann
28b7e83297 refactor: Bottom-Navigation auf custom Surface+Row+IconButton umstellen
Material NavigationBar/NavigationBarItem hat hardcoded interne Paddings
(16dp oben/unten + 32dp Indicator), die bei height-Constraints die Icons
proportional schrumpfen lassen. Custom-Lösung mit:
- Surface + NavigationBarDefaults.containerColor/Elevation
- windowInsetsPadding(NavigationBarDefaults.windowInsets) für System-Insets
- Row mit IconButtons (standard 48dp Touch-Target, 24dp Icons)
- padding(vertical = 4.dp) statt 24dp Material-Default
Icons behalten volle Größe, Bar ist deutlich kompakter.
2026-05-17 11:05:05 +02:00
Jens Reinemann
4397159d62 fix: NavigationBar-Höhe auf 56dp fixieren 2026-05-17 10:59:48 +02:00
Jens Reinemann
32ed321df2 feat: Admin-Statistiken pro Inventar & Inventar-Tabelle mit Paging/Sortierung/Filter/Suche
- Neues DTO: InventoryStatsPerInventoryDto (inventoryId, inventoryName,
  totalItems, totalLocations, totalCategories, recentTransactions,
  lastUpdated, userCount)
- InventoryRepository.getStatsPerInventory(): Stats pro Inventar
- Neuer Endpoint: GET /api/admin/stats/inventories (Admin-only)
- Admin-UI: aufklappbarer Bereich Statistiken pro Inventar (sortierbar)
- Admin-UI: Inventar-Karten durch Tabelle ersetzt mit
  - Paging (10/25/50 Eintraege pro Seite)
  - Sortierung per Klick auf Spaltenheader
  - Filter (alle / mit Benutzern / ohne Benutzer)
  - Freitextsuche nach Name oder Inventar-ID
- Tests: 3 neue InventoryRepositoryTests, 3 neue InventoryStatsTests
  (401/403/Inhalt fuer neuen Endpoint), setUp bereinigt alle Tabellen
- Alle 148 Tests gruen
2026-05-17 10:56:22 +02:00
Jens Reinemann
dfa4b37eda feat(update): Update-Dialog, Installation & App-Start-Integration (#85)
- AndroidManifest: REQUEST_INSTALL_PACKAGES Permission hinzugefuegt
- file_paths.xml: cache/update/ Pfad fuer FileProvider ergaenzt
- UpdateUiState: Sealed UI-State (Hidden, Checking, Available, Downloading,
  ReadyToInstall, Error)
- UpdateViewModel: State-Management fuer Update-Check beim App-Start,
  APK-Download mit Fortschrittsanzeige, Installation via FileProvider
- UpdateBanner: Nicht-blockierender animierter Banner mit Versionsanzeige,
  Download-Button, LinearProgressIndicator, Dismiss-Moeglichkeit
- MainScreen: UpdateBanner ueber dem NavGraph integriert
- ApkInstaller Interface + Impl: Testbare Abstraktion fuer APK-Installation
- RepositoryModule: ApkInstaller DI-Binding registriert
- 7 Unit Tests fuer UpdateViewModel (Update verfuegbar, kein Update,
  nicht konfiguriert, Fehler, Download-Fortschritt, Download-Fehler, Dismiss)
- Alle 441 Tests gruen

Closes #85
2026-05-17 05:13:11 +02:00
Jens Reinemann
3ce8ec28e9 feat(update): Update-Check & APK-Download Data/Domain-Layer
VersionInfo-Datenmodell, UpdateRepository (checkForUpdate + downloadApk
mit Progress-Callback) und CheckForUpdateUseCase implementiert.

Neue Dateien:
- domain/model/VersionInfo.kt: @Serializable Datenmodell (versionCode,
  versionName, apkUrl)
- domain/model/UpdateCheckResult.kt: Sealed interface (UpdateAvailable,
  UpToDate, Error, NotConfigured)
- domain/repository/UpdateRepository.kt: Interface
- data/repository/UpdateRepositoryImpl.kt: Ktor HttpClient mit
  Streaming-Download und Progress-Reporting
- domain/usecase/CheckForUpdateUseCase.kt: Vergleicht Server-versionCode
  mit BuildConfig.VERSION_CODE, prueft Server-URL via SettingsRepository

Geaenderte Dateien:
- di/RepositoryModule.kt: UpdateRepository Hilt-Binding ergaenzt

Tests (12 neue):
- UpdateRepositoryImplTest: checkForUpdate (Erfolg, 500, trailing slash),
  downloadApk (Erfolg+Progress, 404, Parent-Dir-Erstellung)
- CheckForUpdateUseCaseTest: neuer/gleicher/aelterer versionCode,
  leere/blanke Server-URL, Netzwerkfehler

Closes #84
2026-05-17 04:38:34 +02:00
Jens Reinemann
ec41a64b5e refactor(settings): type-safe Settings-Keys mit SettingsKey sealed class
Neue SettingsKey<T> sealed class mit typisierten StringKey-Objekten,
Default-Werten und Compile-Time-Checks auf gueltige Keys. Alte String-
basierte API bleibt fuer Import/Export-Filter (SENSITIVE_KEYS) erhalten.

SettingsRepository: Neue typisierte Methoden getString(), getStringOrNull(),
setString(), observeString() mit SettingsKey.StringKey-Parameter.

Alle Caller migriert:
- SettingsViewModel: KEY_*-Companion entfernt, StringKey.* direkt genutzt
- CameraViewModel, DashboardViewModel, InventoryPickerViewModel: analog
- SyncServiceImpl: KEY_*-Companion entfernt, getString()/setString()
- ItemRepositoryImpl, MessageRepositoryImpl: getString()/getStringOrNull()

Tests: Alle FakeSettingsRepository-Klassen um typisierte Methoden
erweitert, Mock-Calls in CameraViewModelTest und SyncServiceImplTest
angepasst, 5 neue Tests fuer typisierte API in SettingsRepositoryImplTest.
72 Tests gruen.

Closes #82
2026-05-17 04:26:27 +02:00
Jens Reinemann
d81acfbb4f feat(export): CSV- und PDF-Export mit Share-Intent
- CsvExporter: UTF-8 BOM, Semikolon-Separator, DE-Locale, aufgelöste
  Kategorie-/Lagerort-Namen, CSV-Escaping für Sonderzeichen
- PdfExporter: Android PdfDocument API, A4-Format, nach Kategorie
  gruppiert, Tabellenheader, Gesamtwert, automatischer Seitenumbruch
- ImportExportRepository: +exportToCsv(), +exportToPdf(File)
- SettingsViewModel: +exportCsv(), +exportPdf() mit FileProvider-URI
- ShareContent: +Csv(fileUri), +Pdf(fileUri) Varianten
- SettingsScreen: CSV- und PDF-Buttons, Share-Intent für text/csv
  und application/pdf
- 15 neue Tests: CsvExporterTest (7), CSV-Repository-Tests (5),
  ViewModel-Tests für CSV/PDF (4 = 2 success + 2 failure)
- Alle 282 Tests grün

Closes #81
2026-05-17 04:13:14 +02:00
Jens Reinemann
eb9ab6aa54 feat: Multi-Inventar auf Client-Seite (#79)
Server:
- Inventories-Tabelle um name-Spalte erweitert
- Neue User-facing REST-Routes: GET/POST /api/inventories,
  POST /api/inventories/{id}/switch
- LoginResponse enthält inventoryId + inventoryName
- InventoryRepository: createInventory(name), getUserInventories(),
  getInventoryName()
- AuthRoutes: inventoryRepository injiziert für Login-Response

Shared:
- InventoryInfoDto (id, name, isActive)
- CreateInventoryRequest (name)

Client:
- SettingsKeys: ACTIVE_INVENTORY_ID, ACTIVE_INVENTORY_NAME
- SyncService: listInventories(), createInventory(), switchInventory()
- SyncServiceImpl: Implementierung der drei Endpunkte + Login
  speichert inventoryId/Name
- InventoryPickerViewModel: Laden, Erstellen, Wechseln von Inventaren
  mit automatischem Re-Sync
- InventoryPickerSheet: ModalBottomSheet mit Inventarliste, Auswahl
  und Erstellen-Dialog
- MainScreen: TopAppBar zeigt aktiven Inventar-Namen mit Wechsel-Button

Tests:
- 8 neue Server-Tests (InventoryManagementTest)
- 8 neue Client-Tests (InventoryPickerViewModelTest)
- Bestehende FakeSyncService in 2 Test-Dateien aktualisiert

Closes #79
2026-05-17 03:55:08 +02:00
Jens Reinemann
6711a0e056 feat(item-list): Suche und Filter auf Item-Liste (#76)
ItemListScreen: Suchleiste (OutlinedTextField) mit Search/Clear-Icons,
FilterChipRow mit Dropdown-Menüs für Kategorie, Lagerort und
Ablaufstatus (Abgelaufen/Bald ablaufend/OK). Leere Ergebnisse
zeigen 'Keine Items gefunden'. Suche durchsucht Name und Notizen
(case-insensitive), alle Filter sind kombinierbar.

ItemListViewModel: FilterState (MutableStateFlow) mit searchQuery,
categoryId, locationId, expiryFilter. combine() mit 4 Flows,
lokale Filterung auf gemappten ItemUiModel-Instanzen.

ItemUiModel: locationId und notes mit Defaults hinzugefügt.

13 neue Unit-Tests für Suche, Filter, Kombinationen und
Leerzustände. Alle 257 Tests grün.

Closes #76
2026-05-17 03:42:06 +02:00
Jens Reinemann
0f25c180ed feat(sync): Full-Inventory-Sync durch Delta-Sync ersetzen
Server + Client: Timestamp-basierter Delta-Sync als Alternative zum
Full-Sync. GET /api/inventory akzeptiert jetzt optionalen ?since=<ts>
Query-Parameter und liefert nur Items mit lastUpdated > since.

Shared: InventoryDto um deletedItemIds-Feld erweitert (Default: leer,
backward-compatible mit bestehenden Clients).

Server:
- DeletedItems-Tabelle trackt geloeschte Item-IDs pro Inventory
- InventoryRepository.loadInventorySince(): Delta-Query mit Items +
  deletedItemIds seit Timestamp
- saveInventory()/deleteItem(): Loeschungen werden in DeletedItems
  protokolliert
- DatabaseFactory: DeletedItems-Tabelle registriert

Client (App):
- SyncService.downloadInventory(since: Long?): optionaler since-Param
- SyncServiceImpl: haengt ?since= an GET-Request
- ItemDao.deleteByIds(): Batch-Loeschung fuer Delta-Sync
- ImportExportRepositoryImpl: verarbeitet deletedItemIds aus DTO
- SettingsViewModel.pullSync(fullSync): Delta-Sync mit letztem
  Sync-Timestamp; fullSyncRequired-Event loest weiterhin Full-Sync aus

Entscheidungen:
- Timestamp-basiert (nutzt bestehendes lastUpdated-Feld)
- Full-Sync bleibt Fallback (fullSyncRequired, erster Sync)
- Categories/Locations/Settings immer vollstaendig (klein)

6 neue DeltaSyncTests + 3 Repository-Tests + 2 SyncService-Tests

Closes #74
2026-05-17 03:15:49 +02:00
Jens Reinemann
75cfc41924 fix(websocket): Reconnect-Strategie robuster machen
WebSocketClientImpl:
- Jitter (±25%) zum exponentiellen Backoff hinzugefügt, um
  Thundering-Herd-Effekt bei Server-Neustart zu vermeiden
- Retry-Zähler mit MAX_RETRIES=5: nach 5 konsekutiven Fehlschlägen
  wird ConnectionFailed-Event emittiert (nach ~62s)
- Client versucht weiterhin im MAX_BACKOFF-Intervall (60s) zu
  reconnecten, gibt nicht vollständig auf
- Zähler wird bei erfolgreicher Verbindung zurückgesetzt

WebSocketEvent: neues ConnectionFailed(message) Event hinzugefügt

SettingsViewModel: ConnectionFailed -> SyncStatus.Error,
Connected -> SyncStatus.Idle (Fehler wird beim Reconnect gelöscht)

Closes #73
2026-05-17 03:00:51 +02:00
Jens Reinemann
eb5bdd4b7b feat(security): JWT-Tokens in EncryptedSharedPreferences speichern
Sensitive Keys (auth_access_token, auth_refresh_token, auth_username,
auth_user_id, openai_api_key) werden jetzt ueber EncryptedSharedPreferences
gespeichert statt als Klartext in der Room-Settings-Tabelle.

Neue Dateien:
- SecureTokenStorage: Interface fuer sichere Key-Value-Speicherung
- EncryptedPrefsTokenStorage: Implementierung mit AndroidX Security Crypto
- SecurityModule: Hilt-Provider fuer SecureTokenStorage

Aenderungen:
- SettingsKeys: SENSITIVE_KEYS Set definiert welche Keys verschluesselt werden
- SettingsRepositoryImpl: Routet sensitive Keys an SecureTokenStorage,
  nicht-sensitive weiterhin an Room DAO
- ImportExportRepositoryImpl: Filtert sensitive Keys bei Export und Import
- SettingsRepositoryImplTest: 4 neue Tests fuer Secure-Storage-Routing

Closes #72
2026-05-17 02:55:43 +02:00
Jens Reinemann
90580ecb3e refactor(room): fallbackToDestructiveMigration entfernen und Migrationstests vervollständigen
Migrations.kt: KDoc für MIGRATION_2_3 und MIGRATION_3_4 ergänzt.
KrisenvorratDatabaseMigrationTest: MIGRATION_3_4 in alle Testhelfer
aufgenommen, createV3Database() + openMigratedDbV4() hinzugefügt,
Tests für v3→v4 (messages-Tabelle) und v1→v4 Full-Path-Migration
ergänzt. freshInstall-Test registriert jetzt alle Migrationen.
docs/migration-guide.md: Entwickler-Leitfaden mit Checkliste,
SQLite-Einschränkungen und Testanleitung.

fallbackToDestructiveMigration() war bereits entfernt; dieses Ticket
stellt sicher, dass alle Migrationspfade getestet und dokumentiert sind.

Closes #71
2026-05-17 02:40:20 +02:00
Jens Reinemann
4b1a5818f2 feat(chat): UTF-8-Unterstützung für Umlaute und Emoji-Eingabe
Closes #66

Server (Serialization.kt):
- ContentNegotiation explizit mit charset=UTF-8 konfiguriert, damit
  Response-Header immer 'application/json; charset=utf-8' enthält

App (ChatScreen.kt):
- Emoji-Button (Face-Icon) zur MessageInputBar hinzugefügt, der bei Klick
  den Fokus auf das Eingabefeld setzt und die Soft-Keyboard öffnet (System-
  IME mit Emoji-Panel-Zugang)
- FocusRequester + LocalSoftwareKeyboardController integriert

Tests:
- Utf8MessagingTest (Server): 6 Tests für Umlaute, Emojis, Multi-Codepoint-
  Emojis, gemischte Nachrichten, Konversationsabruf, charset-Header
- ChatViewModelTest (App): 4 neue Tests für Umlaut-, Emoji- und gemischte
  Nachrichten
- run-integration-tests.ps1: Szenario 6a mit 5 Testfällen (Umlaute, Emojis,
  gemischt, Konversation, WebSocket-Delivery mit UTF-8)
2026-05-17 01:58:27 +02:00
Jens Reinemann
56ac9b1425 feat: Messaging-System mit Offline-First und WebSocket-Push (#58)
## Server
- Messages-Tabelle (id, sender_id, receiver_id, body, sent_at, delivered_at)
- MessageRepository: save/getUndelivered/getConversation/markDelivered (JOIN statt N+1)
- POST /api/messages, GET /api/messages/{userId}: Nachrichten senden/abrufen
- GET /api/users: User-Liste fuer authentifizierte User (ohne eigenen Account)
- WebSocketManager: notifyNewMessage() + isOnline()
- WebSocketRoutes: unzugestellte Nachrichten bei Reconnect pushen
- LoginResponse: userId + username ergaenzt
- Server-Dependency: kotlinx.serialization fuer shared

## App
- MessageEntity + MessageDao (Room, Migration 3->4)
- KrisenvorratDatabase v4, Migrations.MIGRATION_3_4
- MessageRepositoryImpl: Offline-First (isPending), drain bei WebSocket-Connect
- WebSocketEvent.NewMessage -> MessageDto aus shared
- WebSocketClientImpl: new_message-Event parsen
- AUTH_USER_ID in SettingsKeys, SyncServiceImpl speichert userId bei Login
- UserListScreen + UserListViewModel: User-Liste anzeigen
- ChatScreen + ChatViewModel: WhatsApp-Style Chat (links/rechts, Zeitstempel)
- Navigation: Screen.UserList, Screen.Chat, MESSAGES in Bottom-Nav
- RepositoryModule: MessageRepository gebunden

## Tests
- 234 Tests, 0 Fehler
2026-05-16 23:35:25 +02:00
Jens Reinemann
1d7a62448a feat: Offline-Queue, Sofort-Sync & Last-Write-Wins (#61)
- PendingSyncOpEntity + PendingSyncOpDao: Room-Queue fuer ausstehende PATCH/DELETE-Ops
- MIGRATION_2_3: neue Tabelle pending_sync_ops (Version 2 -> 3)
- SyncService.patchItem() + deleteItem(): PATCH/DELETE /api/inventory/items/{id}
- ItemRepositoryImpl: nach insert/update/delete sofortiger PATCH-Versuch (fire-and-forget),
  bei Netzwerkfehler (Timeout/Connection/Unknown) -> Queue, AuthError/NotConfigured -> silent
- drainQueue() bei WebSocketEvent.Connected: Queue abarbeiten, korrupte Ops loeschen
- ImportExportRepositoryImpl.applyInventoryDto(): Last-Write-Wins per lastUpdated-Timestamp
- KrisenvorratDatabaseMigrationTest: V2->V3-Test ergaenzt
- 223 Unit Tests gruen
2026-05-16 21:40:10 +02:00
Jens Reinemann
4c2f5f08a4 feat(app): User-Konzept App-Phase - JWT-Auth, Login, WebSocket-Client (#57)
- SettingsKeys: API_KEY entfernt, AUTH_ACCESS_TOKEN/REFRESH_TOKEN/USERNAME hinzugefügt
- SyncService: login() und logout() Interface-Methoden
- SyncServiceImpl: Bearer-Token statt X-API-Key, Auto-Refresh bei 401
- AuthModels: LoginRequest, LoginResponse, RefreshRequest
- WebSocketClient: Interface + Impl mit exponentiellem Backoff
- SettingsViewModel: Login/Logout, WebSocket-Connect, FullSyncRequired auto-pullSync
- SettingsScreen: Login-Formular (Username + Passwort) statt API-Key-Feld
- NetworkModule: WebSocketClient als Singleton gebunden
- Alle Tests gruen (70 Tasks up-to-date)
2026-05-16 19:45:11 +02:00
Jens Reinemann
809e6aa069 feat: FEATURE_CAMERA_ENABLED compile flag für KI-Kamera
- build.gradle.kts: buildConfigField FEATURE_CAMERA_ENABLED (default: true)
- ItemListScreen: Camera-Icon nur wenn Flag gesetzt, onCameraClick optional
- KrisenvorratNavGraph: CameraCapture-Route und Lambda nur wenn Flag gesetzt
- SettingsScreen: KI-Erkennung-Section nur wenn Flag gesetzt
2026-05-16 18:06:12 +02:00
Jens Reinemann
dd571f46fc feat: KI-Kameraerkennung via OpenAI Vision (Issue #48)
- CameraCapture-Screen: Foto aufnehmen, analysieren, Artikel auswählen
- OpenAiVisionService: gpt-4o Vision API via Ktor (buildJsonObject)
- CameraViewModel: @ApplicationContext, Bitmap-Laden, Nav-Event via UiState
- ItemFormViewModel: prefillJson-Route-Parameter, _pendingCategoryName-Matching
- Settings: OpenAI API-Key (OPENAI_API_KEY) speichern/laden
- Screen.CameraCapture + Screen.ItemForm(prefillJson) in NavGraph
- ItemListScreen: PhotoCamera-Icon in TopAppBar
- AndroidManifest: TakePicture braucht keine CAMERA-Permission (Intent-basiert)
- 8 neue CameraViewModel-Tests, 1 neuer ItemFormViewModel-Test (226 Tests grün)
2026-05-16 17:58:08 +02:00
Jens Reinemann
f4b5197b06 infra: DB-Migration-Infrastruktur einrichten (#49)
- fallbackToDestructiveMigration() entfernt (war inakzeptabel)
- addMigrations(MIGRATION_1_2) in DatabaseModule eingetragen
- Migrations.kt: Migration(1,2) mit Tabellen-Neubau fuer SQLite < 3.25
  (kcal_per_100g -> kcal_per_kg, min_stock entfernt)
- exportSchema = true + KSP-Argument room.schemaLocation = app/schemas/
- 2.json Schema-Snapshot eingecheckt (Basis fuer kuenftige Migrationen)
- androidTest-Assets zeigen auf app/schemas/ (fuer MigrationTestHelper)
- KrisenvorratDatabaseMigrationTest: 4 instrumentierte Tests
  - Datenerhalt nach Migration
  - Korrekte Spalten nach Migration
  - Indices nach Migration
  - Fresh-Install ohne Migration
2026-05-16 14:52:06 +02:00
Jens Reinemann
018d8dc7da feat: Monat/Jahr-Picker statt Tages-Datepicker fuer Ablaufdatum (#52)
- ExpiryDateField: DatePicker entfernt, neuer MonthYearPickerDialog
  mit zwei Dropdowns (Monat + Jahr)
- Gespeicherter Wert: letzter Tag des gewaehlten Monats (atEndOfMonth())
- Anzeigeformat: MM/yyyy statt dd.MM.yyyy (ItemFormScreen + ItemListScreen)
- TextField ganzflaechig klickbar (nicht nur Icon)
- Dialog: OK / Entfernen / Abbrechen
- remember fuer DateTimeFormatter und Listenliterale
- Test auf Monatsende-Datum ausgerichtet
2026-05-16 14:39:39 +02:00
Jens Reinemann
b7f27b6f81 feat(item): DatePicker durch Monat/Jahr-Picker ersetzen
- Ablaufdatum wird jetzt als MM/yyyy gespeichert (letzter Tag des Monats)
- DatePickerDialog -> AlertDialog mit zwei Dropdowns (Monat, Jahr)
- Anzeige in ItemListScreen ebenfalls auf MM/yyyy umgestellt
2026-05-16 14:37:54 +02:00
Jens Reinemann
fcc7142ea1 fix: Room DB-Version auf 2 erhöhen nach Schema-Änderung
kcalPerKg-Umbenennung und minStock-Entfernung haben den Identity-Hash
geändert. fallbackToDestructiveMigration() greift nur bei Version-
wechsel – daher version=1→2 notwendig.
2026-05-16 14:33:05 +02:00
Jens Reinemann
8280a9daf9 refactor: kcal/100g -> kcal/kg umbenennen und Mindestbestand entfernen
- ItemEntity, ItemDto: kcalPer100g -> kcalPerKg (kcal_per_kg),
  minStock-Spalte komplett entfernt
- CalculateSupplyRangeUseCase: Formel angepasst (/ 1000.0 * kcalPerKg)
- GetMinStockWarningsUseCase + MinStockWarning: gelöscht
- UI (ItemFormScreen, WarningsScreen, DashboardScreen): Mindestbestand-
  Felder und Warnungsabschnitte entfernt
- ViewModels, UiState, Repository: alle Referenzen bereinigt
- Server (Tables, InventoryRepository): Schema angepasst
- Room: fallbackToDestructiveMigration() hinzugefügt (keine Produktivdaten)
- Alle 434 Tests gruen
2026-05-16 14:19:10 +02:00
Jens Reinemann
395939a4ec feat(categories): Umbenennen, Löschen mit Umzuweisungs-Dialog, letzter Lagerort vorauswählen (#51)
- Kategorien und Lagerorte umbenennen (Edit-Dialog)
- Löschen mit Umzuweisungs-Dialog wenn Artikel referenzieren
- Letzten verwendeten Lagerort im Artikelformular vorausauswählen
- stale lastLocationId-Validierung gegen aktuelle Location-Liste
- !! durch ?.let ersetzt in beiden Screens
- Irreführenden Delete-Dialog-Text korrigiert
- Tests: 20 neue Unit-Tests (Rename, Reassign, dismissReassign, staleLocation)
2026-05-16 14:05:45 +02:00
Jens Reinemann
caf7002406 fix: Seed-Daten beim App-Start prüfen und ggf. einfügen
RoomDatabase.Callback.onCreate() wird bei adb install -r nicht erneut
ausgeführt, da die DB bereits existiert. Kategorien und Orte blieben
deshalb leer.

Lösung: SeedDatabaseUseCase prüft beim App-Start ob Kategorien/Orte
vorhanden sind und befüllt die Tabellen nur wenn sie leer sind.
2026-05-16 13:57:04 +02:00
Jens Reinemann
9cc15ffaad feat(settings): Kinder-Altersgruppen für Kalorienverbrauch (#47)
domain/model/AgeGroup.kt:
- Neues Enum AgeGroup (6 Gruppen: Kleinkind bis Erwachsener) mit Richtwert-kcal
- AgeGroupEntry-Datenklasse (count + kcalPerDay, anpassbar)
- JSON-Serialisierung via kotlinx.serialization
- Migrationspfad: householdSize + kcalPerPerson → ERWACHSENER-Gruppe

domain/model/SettingsKeys.kt:
- Neue Konstanten-Klasse für Settings-Keys (löst SettingsViewModel-Kopplung im DashboardViewModel auf)

CalculateSupplyRangeUseCase: Signatur auf totalDailyKcal vereinfacht
SettingsViewModel/UiState: ageGroups statt householdSize + dailyKcalPerPerson
DashboardViewModel: combine(3 flows), AGE_GROUPS statt 4 separate Flows
SettingsScreen: Altersgruppen-Tabelle statt zwei TextFields
ImportExportRepositoryImpl: Markdown-Export zeigt Altersgruppen-Gesamtkcal

Tests: +8 neue Tests (Migration, Validierung, Fallbacks), alle bestehenden Tests aktualisiert

Closes #47
2026-05-16 13:35:27 +02:00
Jens Reinemann
cb576349e0 feat(server): add LAN dev-server integration & end-to-end sync tests
Start scripts (PS1 + Shell), Dockerfile, E2E sync tests, and README
documentation for Phase 2 LAN server deployment.

New files:
- start-server.ps1 / start-server.sh: one-command server startup with
  auto-build, LAN-IP detection, and configurable API key
- Dockerfile: multi-stage build (Gradle → JRE Alpine) for container
  deployment with volume mount for persistent data
- .dockerignore: excludes app/, .git, build artifacts from Docker context
- EndToEndSyncTest.kt: 7 E2E tests covering full push/pull sync cycle,
  multi-client overwrite, empty DB pull, multiple round-trips, and auth
  rejection for unauthenticated requests
- README.md: project overview, build instructions, and complete Phase 2
  server setup docs (4 start options, LAN setup, API reference, security)

Changed files:
- AndroidManifest.xml: added usesCleartextTraffic=true for HTTP in LAN

Closes #46
2026-05-14 21:45:33 +02:00
Jens Reinemann
a972ce34ca feat(settings): add sync UI with server configuration and push/pull actions
Closes #45

SettingsScreen:
- Server-URL and API-Key input fields (API-Key masked as password)
- Push (upload) and Pull (download) sync buttons
- Sync status indicator (running/success/error)
- Last sync timestamp display (persisted via Room settings)

SettingsViewModel:
- Inject SyncService for server communication
- pushSync(): exports local inventory to InventoryDto, uploads via SyncService
- pullSync(): downloads InventoryDto from server, imports into local DB
- Persists server_url, api_key, sync_last_timestamp in Room settings table

ImportExportRepository:
- New methods exportToInventoryDto() and importFromInventoryDto()
- Refactored to eliminate code duplication via buildInventoryDto() and
  applyInventoryDto() helper methods

Tests:
- 8 new ViewModel tests covering sync settings loading, push/pull success,
  push/pull error scenarios, and sync status dismissal
- FakeSyncService and extended FakeImportExportRepository for testing
2026-05-14 21:30:55 +02:00