Commit graph

118 commits

Author SHA1 Message Date
Jens Reinemann
16576045a0 fix(ui): change resources nav icon from LibraryBooks to MenuBook 2026-05-19 21:08:36 +02:00
Jens Reinemann
a84d130495 fix(resources): grant explicit URI permissions to all resolved activities before startActivity
Resolves 'Keine App gefunden' for ePub and PDF opening failures (e.g. Firefox).
FLAG_GRANT_READ_URI_PERMISSION alone is not sufficient when Android shows
the Chooser – each potential app must receive grantUriPermission() before
startActivity() is called.

- Extract openResourceFile() into FileOpenHelper.kt (eliminates DRY violation)
- Use PackageManager.ResolveInfoFlags (API 33+) with DEPRECATION suppress fallback
- Remove duplicate inline intent code from ResourceDetailScreen

Closes #129
2026-05-19 00:24:58 +02:00
Jens Reinemann
557a4bcaf8 feat(auth): show delete-local-data dialog on logout for logged-in users 2026-05-19 00:10:17 +02:00
Jens Reinemann
fd2eae227b feat(resources): compact cards, BottomSheet filter/sort (tags+format+language)
- Cards: title + author + overflow row (format, size, tags)
- Description removed from card (detail-only)
- SearchBar + FilterList icon (like Inventar)
- BottomSheet: Tags, Format, Sprache filter + Sortierung
- Crash-fix: catch exceptions in refresh()
2026-05-18 23:53:47 +02:00
Jens Reinemann
c24a32b033 release: v1.7.24 – ResourceDetailScreen with markdown rendering 2026-05-18 23:42:09 +02:00
Jens Reinemann
26117ac23f feat(app): add ResourceDetailScreen with markdown rendering and clickable cards
- ResourceDetailScreen: full metadata display, download button, markdown description
- ResourceListScreen: cards now clickable, navigates to detail view
- Navigation: added ResourceDetail route with guid parameter
- Dependencies: compose-markdown 0.5.4 (JitPack), added toRoute import
- settings.gradle.kts: added JitPack repository
2026-05-18 23:37:00 +02:00
Jens Reinemann
2b33f930d0 feat(app): implement resource download + 'open with' dialog
Closes #123
2026-05-18 22:19:23 +02:00
Jens Reinemann
44476b21e6 feat(app): add ResourceListScreen + BottomBar navigation
Closes #122
2026-05-18 22:15:40 +02:00
Jens Reinemann
542fbb0941 feat(app): add ResourceEntity, Dao, Repository + DB migration 8→9
Closes #121
2026-05-18 22:10:51 +02:00
Jens Reinemann
6fc37ee203 fix(notification): wire up updateBadgeCount to launcher badge via setNumber() 2026-05-18 22:10:04 +02:00
Jens Reinemann
ab2cbff8ba fix(notification): set badge count via setNumber() for multi-sender notifications 2026-05-18 22:05:28 +02:00
Jens Reinemann
ae3cf3e660 fix(notification): minimize FGS notification visibility (bump channel to v2) 2026-05-18 21:59:58 +02:00
Jens Reinemann
afbec03ebb fix(item-form): make kcal label dynamic based on selected unit
Analogous to the price label, kcal now shows 'kcal pro [Einheit]'
based on the dropdown selection from #114.

Closes #115
2026-05-18 21:41:04 +02:00
Jens Reinemann
645578b66e feat(item-form): add unit dropdown with predefined list and custom option
Provides 8 predefined units (Stück, g, kg, ml, l, Packung, Dose, Flasche)
plus a custom input option. Normalizes legacy unit strings on app start.
Dynamic price label shows selected unit.

Closes #114
2026-05-18 21:39:24 +02:00
Jens Reinemann
975976fd06 feat(chat): add message pagination with cursor-based loading
Load initial 200 messages, auto-load older messages on scroll to top.
Uses cursor-based DAO queries for stable scroll position.
Also fixes pre-existing ackMessage compile errors in test fakes.

Closes #113
2026-05-18 21:32:47 +02:00
Jens Reinemann
7b21394cc6 fix(chat): increase message text size from bodyMedium to bodyLarge
Closes #112
2026-05-18 21:25:27 +02:00
Jens Reinemann
9eefa79c64 fix(notifications): merge FG-service and message notifications into one
Use a single notification ID (9999) for both idle foreground-service state
and incoming-message alerts. Shows sender name for 1 message, summary for
multiple senders. Cancelling resets to idle instead of removing.

Closes #111
2026-05-18 21:23:58 +02:00
Jens Reinemann
37fd66a417 fix(messaging): keyboard layout, message delivery ACK, background foreground service 2026-05-18 19:26:27 +02:00
Jens Reinemann
a6cc4ca4bf fix(ui): remove duplicate versionCode from update button label 2026-05-18 19:07:42 +02:00
Jens Reinemann
512829dd49 feat(messaging): ungelesene Nachrichten als Badges anzeigen (#110)
- DB-Migration 7→8: is_read-Spalte in messages (default 1 für bestehende Rows)
- DAO: getUnreadCountsBySender, getTotalUnreadCount, markConversationAsRead
- Repository: totalUnreadCount Flow + getUnreadCountsBySender() + markConversationAsRead()
- ChatViewModel: markConversationAsRead beim Öffnen/Empfangen
- UserListViewModel: unreadCounts StateFlow
- UserListScreen: rote Badge-Anzeige pro Chat
- MainViewModel: totalUnreadCount StateFlow
- MainScreen: BadgedBox am Nachrichten-Icon in Bottom Nav
- NotificationHelper: updateBadgeCount() für Launcher-Badge
- Tests: 3 neue Fälle, FakeDao+FakeRepo aktualisiert (328 Tests grün)
2026-05-18 18:28:49 +02:00
Jens Reinemann
06fa017c04 chore: update generaltest_admin_message.cpython-313.pyc, receive_admin_messages.cpython-313.pyc, send_admin_messages.cpython-313.pyc 2026-05-18 18:04:50 +02:00
Jens Reinemann
33c7ddb9ab fix(notification): suppress only when active chat is visible 2026-05-18 17:59:54 +02:00
Jens Reinemann
6fd8528577 feat(admin-message): simplify scripts, add inbox route, fix notification chat switch 2026-05-18 17:45:19 +02:00
Jens Reinemann
e3bcddac70 chore: update publish tooling and Android messaging integration 2026-05-18 15:13:49 +02:00
Jens Reinemann
64ebb737d1 chore: add ADMIN_MESSAGE_TOKEN to VPS docker-compose config 2026-05-18 14:13:52 +02:00
Jens Reinemann
ca6cfbfad9 feat: add plaintext admin message endpoint for testing
- New POST /api/admin/send-message endpoint (admin-only)
- Messages prefixed with [PLAINTEXT] bypass E2EE decryption
- App recognizes [PLAINTEXT] marker and strips it before display
- Allows easy chat testing without E2EE key management
2026-05-18 13:56:15 +02:00
Jens Reinemann
30e86bb7e0 feat(notifications): request POST_NOTIFICATIONS permission on app start 2026-05-18 13:54:03 +02:00
Jens Reinemann
c39bc5e485 feat: foreground service for background message notifications 2026-05-18 13:45:06 +02:00
Jens Reinemann
e43c0ebbb5 feat(update): UpToDate-Status mit temporärem Feedback anzeigen
- Neuer UpdateStatus.UpToDate State
- Button zeigt 'Keine Updates gefunden' für 5 Sekunden
- Mindestens 1s Checking-Anzeige (vermeidet Flicker bei schneller Antwort)
- UpdateBanner blendet UpToDate-Status aus
2026-05-18 13:04:12 +02:00
Jens Reinemann
1492fa879b refactor(ui): Update-Status-Anzeige in Settings überarbeiten
- Einzelne Buttons pro Status statt gemeinsamer 'Auf Updates prüfen'-Button
- Available-Status als prominenter Button mit Icon
- Checking/Downloading als disabled OutlinedButton
- Error-Status mit 'Erneut prüfen'-Button
- Hidden-Status zeigt den Check-Button
2026-05-18 12:48:04 +02:00
Jens Reinemann
292c538d45 fix(ui): close BottomSheet when 'Alle zurücksetzen' is tapped 2026-05-18 12:29:49 +02:00
Jens Reinemann
ad0945ec3c feat(ui): replace filter chips with BottomSheet + sort options
- Filter button with badge next to search bar
- BottomSheet with filter dropdowns (Kategorie, Lagerort, Ablauf)
- Sort options: Name, Ablaufdatum, Menge (asc/desc)
- 'Alle zurücksetzen' button to clear filters + sort
- docs(genome): Konzept nach .github/genome/ verschoben
2026-05-18 12:27:28 +02:00
Jens Reinemann
6a3009569b fix(ui): filter chip label too long – show only selected value 2026-05-18 12:16:35 +02:00
Jens Reinemann
7ea7729f96 fix: version display format -> three-number (e.g. 1.7.10) without v prefix 2026-05-18 12:16:01 +02:00
Jens Reinemann
09e01dff00 style: Beton & Stahl Theme - höhere Kontraste, stahlblaue Surfaces 2026-05-18 12:08:06 +02:00
Jens Reinemann
ea3bd6dc97 fix(sync): robuste WebSocket-Verbindung und Token-Refresh
- Backoff nur nach stabiler Verbindung (>30s) zurücksetzen
  → verhindert rapiden 2s-4s-2s-Reconnect-Oscillation
- VIOLATED_POLICY Close-Reason erkennen → AuthRejected-Event
  → kein endloser Retry mit abgelaufenem Token
- Token-Refresh bei AuthRejected: MainViewModel refresht Access-Token
  und reconnectet WS automatisch; bei Fehlschlag Session-Expired
- executeItemRequest: fehlende 401-Retry-Logik ergänzt (Bug 4)
- SyncService.refreshAccessToken() als neue Interface-Methode
2026-05-18 10:48:46 +02:00
Jens Reinemann
887cdbd3f7 feat(settings): server-sync UI aufräumen (#108)
- Login-Status + Logout auf eigene ElevatedCard
- Logout-Button als OutlinedButton (Material-Button statt TextLink)
- Letzte Sync direkt unter Verbindungsstatus ohne Divider
- Refresh-IconButton neben Serverstatus entfernt
- Server-URL Reset nur sichtbar wenn nicht-default Adresse
- Manuelle Sync-Buttons entfernt (vollautomatisch)
2026-05-18 10:09:58 +02:00
Jens Reinemann
bdd8cb4b11 feat(#106): category tap on dashboard navigates to inventory with filter
- Change Screen.ItemList from data object to data class with optional categoryId
- DashboardScreen: make CategoryCard clickable with onCategoryClick callback
- ItemListViewModel: read initial categoryId from SavedStateHandle
- BollwerkNavGraph: wire category click to navigate with categoryId
- Add test for initial category filter from navigation args
2026-05-18 10:01:14 +02:00
Jens Reinemann
8e7352dcc4 feat(security): replace CleartextKeysetHandle with AndroidKeysetManager (#105)
- Extract PrivateKeysetStore interface for testability
- Add AndroidKeystorePrivateKeysetStore (Android Keystore-backed AEAD)
- Refactor E2EEKeyManager to use PrivateKeysetStore
- Add legacy migration: old cleartext key is removed, forcing re-generation
- Update DI module to provide AndroidKeystorePrivateKeysetStore
- Adapt unit tests with FakePrivateKeysetStore + migration test

Private key material no longer appears as cleartext JSON on the JVM heap.
Existing devices with legacy keys will re-generate and re-upload via
EnsureKeyPairUseCase on next app launch.
2026-05-18 09:51:24 +02:00
Jens Reinemann
24c6fac0f8 feat(messaging): push notifications for incoming messages (#104)
- Add NotificationHelper with channel creation, grouped notifications,
  and deep-link PendingIntent into chat
- Trigger notification from MessageRepositoryImpl on WebSocket NewMessage
- Active-chat suppression in ChatViewModel (no notification for current chat)
- Deep-link from notification tap: MainActivity handles intent extras,
  MainScreen navigates to correct Chat screen
- Add POST_NOTIFICATIONS permission to AndroidManifest
- Add notification icon drawable (ic_notification_message)
- Add unit tests for notification suppression logic
- Fix pre-existing test compilation (SyncServiceImplTest missing authEventBus)
2026-05-18 09:39:39 +02:00
Jens Reinemann
c771aa9547 feat(messaging): enforce 10 MB mailbox limit per receiver with FIFO eviction (#103)
- Add getUndeliveredStorageBytes() and evictOldestUndelivered() to MessageRepository
- Check mailbox size before saving; evict oldest undelivered messages if over 10 MB
- Return systemMessage in SendMessageResponse when eviction occurs
- App parses systemMessage and displays it in the sender's conversation
- Add SendMessageResponse to shared module for server/app interop
- Update existing tests to use new response format
- Add 3 new tests for eviction behavior
2026-05-18 09:17:15 +02:00
Jens Reinemann
6a8ffa17be feat(messaging): remove non-functional emoji button (#102) 2026-05-18 08:45:34 +02:00
Jens Reinemann
dad15b9e94 security: WebSocket Auth-Token aus Query-Parameter in Authorization-Header verschieben
- Client: Token als 'Authorization: Bearer' Header statt ?token= Query-Parameter senden
- Server: Token aus Authorization-Header statt Query-Parameter lesen
- Tests: Alle 8 WebSocket-Tests auf Header-Auth umgestellt
- Integration-Tests: WebSocket-Verbindung mit Header aktualisiert

Closes #97
2026-05-18 08:23:10 +02:00
Jens Reinemann
dad2907481 feat: WebSocket-Lifecycle und Sync ab App-Start unabhaengig von Settings-Screen
- MainViewModel: verbindet WebSocket beim App-Start (connectOnStartup) und
  nach Login (via AuthEventBus.loginSuccess). Behandelt alle WebSocket-Events
  (Connected/FullSyncRequired/InventoryUpdated) -> pullSync/pushSync.
  Auto-pushSync wenn Server leer ist und lokale Daten vorhanden (Daten-Recovery).
- AuthEventBus: loginSuccess-Signal ergaenzt (serverUrl + token)
- SyncServiceImpl: emittiert loginSuccess nach erfolgreichem Login
- SettingsViewModel: WebSocket-Lifecycle entfernt (nur noch ConnectionFailed
  fuer UI-Fehlermeldung). Manueller Sync-Button bleibt erhalten.
- WebSocketClientImpl: vollstaendiges Logging, wiederholende User-Benachrichtigung
  bei Verbindungsfehlern (alle MAX_RETRIES Versuche statt nur einmalig)
2026-05-18 01:17:47 +02:00
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
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
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