Commit graph

206 commits

Author SHA1 Message Date
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
db2fc5dea1 fix: busybox crond statt dcron, pg17-Tokens aus Checksum filtern (#87)
- Dockerfile: dcron -> busybox crond (kein setpgid-Fehler im Container)
  Crontab in /var/spool/cron/crontabs/root
- backup.sh: grep-Filter fuer PostgreSQL 17 Security-Tokens
  (\restrict / \unrestrict) die sich bei jedem Dump aendern
2026-05-17 11:14:47 +02:00
Jens Reinemann
d66f0d65c3 feat: tägliches PostgreSQL-Backup mit 1 GB Rotation (#87)
- backup/Dockerfile: Alpine 3.21 + postgresql-client + dcron
- backup/backup.sh: pg_dump -> MD5-Checksum-Vergleich (skip bei unveränderter DB)
  -> gzip-komprimiertes Backup mit Timestamp
  -> Rotation: älteste .sql.gz löschen bis Gesamtgröße < 1 GB
- docker-compose.yml: neuer Service 'backup', Volume 'backup_data'
- Cronjob: täglich 03:00 UTC
2026-05-17 11:06:52 +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
11d2094eef style: QR-Code auf Server-Homepage zentrieren
#qrcode-Container nutzt jetzt display:flex + justify-content:center
statt margin:0 auto, damit das von QRCode.js generierte Canvas
korrekt mittig angezeigt wird.
2026-05-17 10:22:40 +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
994d6b1b07 feat(server): Version-Endpoint, APK-Hosting & Homepage mit QR-Code
Routing.kt: GET /api/version (öffentlich, kein Auth) liefert JSON mit
versionCode, versionName und apkUrl (aus Request-Host abgeleitet).
GET / zeigt HTML-Homepage mit App-Name, Version und QR-Code
(clientseitiges JS via qrcode.js CDN) für direkten APK-Download.
staticFiles /static bedient APK aus server/data/ (Dateisystem).

Neue Dateien:
- VersionInfo.kt: Serializable DTO (versionCode, versionName, apkUrl)
- VersionRoutes.kt: Route-Definitionen für /api/version und /
- VersionEndpointTest.kt: 11 Tests (Endpoint, Homepage, Admin, 404)

Geänderte Dateien:
- application.conf: appVersionCode + appVersionName (mit Env-Override)
- Routing.kt: versionRoutes + staticFiles eingebunden
- TestHelpers.kt: testMapConfig um Version-Felder erweitert
- Dockerfile: data-Verzeichnis für APK-Hosting angelegt

Closes #83
2026-05-17 04:32:28 +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
61ef56425d test(server): Server-Integrationstests vervollständigen
Auth: Expired-Token-Tests (Access + Refresh), fehlende Felder
Message-API: Send, Get Conversation, Blank Body, Receiver Not Found,
  Custom ID, Response-Format (9 Tests)
WebSocket: Connect mit gültigem/ungültigem/fehlendem/abgelaufenem Token,
  inventoryUpdated-Event, new_message-Event, Disconnect, undelivered
  Messages bei Connect (8 Tests)
CI-Pipeline: Auto-Trigger für push/PR auf app/shared/server-Pfade,
  Step-Label verdeutlicht (app + shared + server)

Server-Tests: 130 gesamt, 0 Failures

Closes #80
2026-05-17 04:02:34 +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
7c17f8ea2f feat(server): Rate-Limiting auf alle API-Endpoints
Ktor RateLimit-Plugin mit abgestuften Limits pro Endpoint-Gruppe:
- Auth (/api/auth/*): 10 req/min per IP (Brute-Force-Schutz)
- Messages (/api/messages/*): 30 req/min per IP (Spam-Schutz)
- Inventory (/api/inventory/*): 60 req/min per IP (DoS-Schutz)
- Admin (/api/admin/*): 20 req/min per IP

Neue Dateien:
- RateLimiting.kt: Plugin-Konfiguration mit 4 benannten Limitern
- RateLimitingTest.kt: 5 Tests (Limit-Ueberschreitung, Within-Limit,
  Health-Endpoint ohne Limit, Retry-After-Header)

Geaenderte Dateien:
- Routing.kt: rateLimit()-Wrapper um Route-Gruppen
- Application.kt: configureRateLimiting() in Plugin-Pipeline
- libs.versions.toml + build.gradle.kts: ktor-server-rate-limit Dep

Closes #75
2026-05-17 03:31:57 +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
f792213b1e refactor(server): H2 durch PostgreSQL ersetzen
- DatabaseFactory: HikariCP Connection-Pool fuer PostgreSQL (10 Connections,
  REPEATABLE_READ), H2 weiterhin ohne Pool (fuer Tests)
- Dependencies: postgresql-Treiber + HikariCP hinzugefuegt, H2 nur noch
  testImplementation
- Migration-SQL: uppercase Tabellennamen auf lowercase normalisiert
  (dialect-agnostisch fuer H2 und PostgreSQL)
- docker-compose.yml: PostgreSQL 17 + Krisenvorrat-Server mit DB-Env-Vars
- Env-Var-Konfiguration: KRISENVORRAT_DB_URL, _DB_USER, _DB_PASSWORD,
  _DB_DRIVER (Defaults auf PostgreSQL localhost)
- Alle 554 Tests gruen (H2 in-memory fuer Tests beibehalten)

Closes #70
2026-05-17 02:35:08 +02:00
Jens Reinemann
033b0fae61 feat(server): Statistik-Kacheln auf Admin-Inventarübersicht
- InventoryStatsDto: neues DTO mit totalItems, totalLocations,
  totalCategories, lastUpdated, recentTransactions
- InventoryRepository.getAggregatedStats(): Aggregierte Statistiken
  über alle Inventare (COUNT, MAX, 30-Tage-Filter)
- AdminRoutes: GET /api/admin/stats Endpoint (admin-only)
- Admin-UI: Stats-Grid mit 5 Kacheln (Artikel, Orte, Kategorien,
  Änderungen 30 Tage, letzte Änderung) oben auf der Übersichtsseite
- 7 neue Tests: 4 Endpoint-Tests (InventoryStatsTest),
  3 Repository-Tests (getAggregatedStats)
- Alle 87 Tests grün

Closes #68
2026-05-17 02:19:18 +02:00
Jens Reinemann
549e4c916e feat(server): PATCH /api/inventory/items/{id} auf partielles Update umstellen
- Route empfängt JsonObject statt ItemDto, nur übergebene Felder werden aktualisiert
- validatePartialItem() validiert nur vorhandene Felder (name, unit, expiryDate)
- patchItemPartial() im Repository: Guard für leere Felder, kein SQL-Update wenn nichts zu ändern
- Response liefert das aktualisierte Item aus der DB (loadItem) statt des Inputs
- Bestehende Tests in InputValidationTest angepasst (senden nun partielle JSON-Bodys)
- Neue PatchItemTest-Klasse: 10 Tests (Happy Path, 404, Auth, Validierung, Persistenz)
- Alle 554 Tests grün

Closes #56
2026-05-17 02:13:24 +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
95e262d009 chore: Backlog-Status in Board-Skripten ergaenzen
- set-board-status.ps1: 'Backlog' (4ce6ee37) als gueltigen Status erganzt
- create-next-ticket.ps1: -Status Parameter (Todo|Backlog, Default: Todo)
  Todo: Order = min - 1 (naechstes Ticket)
  Backlog: Order = max + 10 (ans Ende gestellt)
  Explizite Status-Setzung via gh project item-edit nach Board-Add
2026-05-17 01:51:18 +02:00
Jens Reinemann
d354e3b37c security: Server-seitige Input-Validierung & Body-Size-Limit (#67)
- Routing.kt: Content-Length-Intercept (max 1 MB, HTTP 413)
- InventoryRoutes.kt: String-Längen (name ≤255, unit ≤50, id ≤36,
  category/location ≤255, setting key ≤255), expiryDate-Regex
  YYYY-MM-DD, Array-Limits (items ≤10000, categories/locations ≤500)
- AdminRoutes.kt: username-Länge ≤255
- InputValidationTest.kt: 16 neue negative Tests, alle 532 Tests grün
2026-05-17 01:20:49 +02:00
Jens Reinemann
26b50eea36 test: Szenario 6 Bob-Generaltest in run-integration-tests.ps1
- Haupt-Szenario: Login, PUT 10 Items (3 Kat, 2 Lagerorte), PATCH qty,
  PUT 11 Items (Salzcracker), WS inventoryUpdated, WS fullSyncRequired
- T1: PATCH unbekannte ID -> 404
- T2: PUT leer -> 0 Items
- T3: Zwei PUTs -> zweites ueberschreibt
- T4: Bob patcht selbst -> WS-Event
- T5: Bob/Alice getrennte Inventare
- T6: Ungueltiger Token -> 401
- T7: Zwei WS-Sessions -> beide empfangen Events
- T8: PATCH nach Disconnect -> kein Server-Fehler
- Fix: Receive-WsMessages mit return ,\ (PS-Array-Unwrapping)
- Fix: @()-Wrapping fuer Where-Object-Filter (1-Element-Ergebnis)
Ergebnis: 30/30 Tests bestanden
2026-05-17 01:01:29 +02:00
Jens Reinemann
2555a942ec fix: Server-App WebSocket-Kompatibilität und DELETE-Route
- WebSocketManager: inventory_updated -> inventoryUpdated,
  full_sync_required -> fullSyncRequired (camelCase wie App erwartet)
- InventoryRoutes: DELETE /api/inventory/items/{id} hinzugefügt
- InventoryRepository: deleteItem(inventoryId, itemId) implementiert
2026-05-17 00:42:03 +02:00
Jens Reinemann
2d4ebd63b0 fix(server): Code-Review-Korrekturen Inventory Sharing
- UserRow: inventoryId-Feld ergaenzt
- AdminRoutes: Delete nutzt findById() statt listAll().find{}
- AdminRoutes: PUT/POST inventory/new geben 404 wenn User nicht existiert
- index.html: Doppelten alten HTML-Content entfernt (277 Zeilen)
2026-05-17 00:35:15 +02:00
Jens Reinemann
c03475e7e5 feat(server): Inventory Sharing – User:Inventory N:1
- Inventories-Tabelle neu: id, created_at
- Users.inventory_id FK → Inventories
- Items/Categories/Locations/Settings: user_id → inventory_id
- DatabaseFactory: Schema-Migration + Data-Migration (user_id→inventory_id)
- InventoryRepository: getEffectiveInventoryId(), createInventory(),
  assignUserToInventory(), cleanupOrphanedInventory(),
  listInventoriesWithUsers(); alle Ops auf inventoryId umgestellt
- UserRepository.create(): legt automatisch neues Inventory an
- InventoryRoutes: löst inventoryId via getEffectiveInventoryId()
- AdminRoutes: PUT /users/{id}/inventory (zuweisen),
  POST /users/{id}/inventory/new (trennen), GET /admin/inventories
- Admin-UI: Inventar-Spalte, 'Inventar wechseln'-Modal, 'Neues
  Inventar'-Button, Inventar-Übersicht (gruppiert)
- InventorySharingTest: 8 neue Integrationstests (Sharing, Isolation,
  Cleanup, Berechtigungen)
- Alle 48 Server-Tests gruen (inkl. bestehende Tests unveraendert)
2026-05-17 00:30:06 +02:00
Jens Reinemann
8576fffdb7 style: run-integration-tests.ps1 Formatierung angleichen 2026-05-17 00:12:19 +02:00
Jens Reinemann
6bed1214c5 test: Integration-Test-Suite fuer Server (Auth, Sync, Messaging, WS)
run-integration-tests.ps1:
- Szenario 1: Auth-Flow (Alice + Bob Login, Token-Validierung)
- Szenario 2: Inventory Sync (PUT, GET, PATCH/items/{id})
- Szenario 3: Messaging + Offline-Delivery (WS-Push nach Reconnect)
- Szenario 4: JWT Refresh (neuer Access-Token bleibt gueltig)
- Szenario 5: Parallele Sessions (Server erkennt Online-Status korrekt)
Alle 15 Tests bestanden gegen VPS-Server (195.246.231.210:8080)
Closes #60
2026-05-17 00:08:35 +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
14631c7327 feat(server): User-Konzept Auth, JWT, Admin-CRUD, WebSocket-Push, Admin-UI (#57)
- Users-Tabelle mit bcrypt-Passwort-Hash
- JWT-Auth (Access + Refresh Token) ersetzt API-Key
- POST /api/auth/login + /api/auth/refresh
- Admin-Endpoints: GET/POST/DELETE/PUT /api/admin/users
- Seed-Admin beim Start (KRISENVORRAT_ADMIN_PASSWORD)
- Inventar-Endpoints user-spezifisch (userId aus JWT)
- PATCH /api/inventory/items/{id} fuer Einzel-Updates
- WebSocket /ws/sync mit Push-Events (inventoryUpdated, fullSyncRequired)
- Minimale Admin-Web-UI unter /admin/ (XSS-sicher)
- Categories/Locations: Surrogate-PK fuer User-Isolation
- Alle Server-Tests gruen (43 Tests)
2026-05-16 19:28:03 +02:00
Jens Reinemann
db72a8b4ad ci: Android CI deaktivieren (nur noch manuell per workflow_dispatch) 2026-05-16 18:58:49 +02:00
Jens Reinemann
504207376f docs: SSH-Public-Key fuer VPS-Zugang hinterlegen 2026-05-16 18:56:07 +02:00
Jens Reinemann
748140acbd docs: VPS-Deploy Skill hinzufügen (1984 Hosting, Docker, SSH) 2026-05-16 18:47:50 +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