Commit Graph

317 Commits

Author SHA1 Message Date
schalli 42daa87e5e feat(calendar): add domain field and test-connection button to Exchange sources
- Prisma: domain String? added to CalendarSource model (db push applied)
- DTOs: domain in CreateCalendarSourceDto, UpdateCalendarSourceDto, new TestCalendarSourceConfigDto
- Service: domain in SOURCE_SAFE_SELECT, addSource, updateSource; new testConnectionFromConfig method
- Controller: POST /calendar/sources/test-config (before :id routes to avoid collision)
- ExchangeProvider: domain in all source interfaces; passed as 3rd arg to EWS WebCredentials
- Frontend: domain in CalendarSource/CreateSourcePayload/UpdateSourcePayload; testSourceConfig API fn
- Form: domain field (Exchange-only), "Test connection" button with idle/loading/success/error states
- i18n: de+en keys for formFieldDomain, formFieldDomainHint, formTestConnection, formTesting, formTestSuccess, formTestFailed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 13:15:13 +02:00
schalli 2e0e7290ba fix(calendar): remove setState call from render in isFormValid
validateUrl() calls setUrlError() — calling it during render triggers
React error #301 (cannot update component while rendering). Remove it
from the isFormValid computation; onChange/onBlur already keep urlError
in sync so !urlError is sufficient.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 13:01:03 +02:00
schalli f6173bbe35 docs(quick-260701-abc): fix i18n missing keys — summary and state update
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Has been cancelled
Tessera CI/CD / Build & Publish Images (push) Has been cancelled
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 12:06:32 +02:00
schalli e5b76b735a fix(i18n): add missing marketplace.accessDenied and translate calendar form
- Add marketplace.accessDenied to de.json and en.json
- Add 12 calendar form field/action keys to widgets.calendar in both locales
- Replace all hardcoded English strings in calendar-source-form.tsx with t() calls
- Remove locale-detection hack on Cancel button (was comparing t() result to English string)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 12:05:52 +02:00
schalli eebceb298d fix(08): apply code review findings (CR-01, CR-02, WR-01–05, IN-01)
- CR-01: fix SSRF bypass — isPrivateIpv6 now delegates ::ffff:<ipv4> to
  isPrivateIpv4, covering 172.16-31.x and 169.254.x ranges
- CR-02: add ParseUUIDPipe to GET /favorites widgetId param + service guard
  so missing widgetId returns 400 instead of leaking all user favorites
- WR-01: link-widget — replace raw 'link.error' key with t('link.error') (4 sites)
- WR-02: favorites-widget — fix load-path error to use t('favorites.error')
- WR-03: widget-catalog-modal — move aria-hidden from outer wrapper to backdrop
- WR-04: calculator — remove duplicate M button (MR clone); MC/MR/M+/M−/MS remain
- WR-05: schema — add FavoriteLink→WidgetInstance FK with onDelete:Cascade
- IN-01: create-widget.dto.ts — update comment from four to eight supported types

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-01 11:03:16 +02:00
schalli 54e4731b36 docs(08): add code review report 2026-07-01 10:59:02 +02:00
schalli 34df808d83 docs(phase-08): update tracking after wave 4 2026-07-01 10:51:53 +02:00
schalli 71689fc5df merge(08-04): Link widget sharing Favorites backend 2026-07-01 10:51:34 +02:00
schalli dcff93e2ae docs(08-04): complete Link widget plan summary
- 3/3 tasks complete (RED test → GREEN implementation → full suite gate)
- 88/88 web tests passing (18 test files, +7 link-widget tests)
- web + api tsc --noEmit clean
- LinkWidget: single-link D-06 via FavoriteLink backend reuse
2026-07-01 10:51:08 +02:00
schalli e9914f61cb feat(08-04): LinkWidget implementation + page.tsx wiring (GREEN)
- LinkWidget: single-link widget reusing FavoriteLink backend (D-06)
- Single-link enforcement: add form hidden when link exists
- List (row) and tile (grid) view modes, switchable in edit mode
- Inline add/edit/delete forms in edit mode
- Letter fallback span + icon with onError hide (T-08-11)
- Links with target="_blank" rel="noreferrer" (T-08-12)
- i18n keys added to de.json + en.json (link.*)
- wireLinkWidget(LinkWidget) added to portal page.tsx
- All 7 link-widget tests pass (GREEN); tsc --noEmit clean
2026-07-01 10:48:58 +02:00
schalli 4657e50494 test(08-04): add failing tests for LinkWidget single-link contract (RED)
- fetchFavorites called with instanceId on mount
- Link renders as anchor with target="_blank" rel="noreferrer" (T-08-12)
- Empty + edit mode shows add form; createFavorite called on submit
- Single-link enforcement: add form hidden when link exists (D-06)
- Edit mode: updateFavorite called with id and new title
- View toggle: list default, tile container on gridView click
- Letter fallback: iconUrl null shows first uppercase letter
2026-07-01 10:46:50 +02:00
schalli 7b42a3aef9 docs(phase-08): update tracking after wave 3 2026-07-01 10:39:05 +02:00
schalli 423ce90608 merge(08-03): Favorites widget with backend persistence
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Has been cancelled
Tessera CI/CD / Build & Publish Images (push) Has been cancelled
2026-07-01 10:38:47 +02:00
schalli f7d4818c0a docs(08-03): complete Favorites widget plan summary 2026-07-01 10:38:28 +02:00
schalli cc6f5ae893 feat(08-03): FavoritesWidget frontend + API client + page.tsx wiring (GREEN)
- favorites-api.ts: FavoriteLink type + fetchFavorites/createFavorite/updateFavorite/deleteFavorite
  all use credentials: include and API_URL/favorites
- favorites-widget.tsx: list/grid view, inline add/edit/delete in edit mode,
  icon + letter fallback, rel=noreferrer + target=_blank, no dangerouslySetInnerHTML (T-08-07)
- useEffect deps fixed to [instanceId] only — excludes t() to prevent re-fetch on each render
- page.tsx: wireFavoritesWidget(FavoritesWidget) wired
- Full test suite: 81/81 pass (17 test files)
- Web TypeScript: clean
2026-07-01 10:34:11 +02:00
schalli 758d246e98 feat(08-03): FavoriteLink schema + FavoritesModule (CRUD + SSRF icon discovery)
- Add FavoriteLink Prisma model (userId/tenantId/widgetId scope, iconUrl nullable, position)
- IconDiscoveryService: port SSRF-protected icon discovery with redirect: 'manual',
  private IP / blocked-hostname checks, 4000ms timeout, 200k HTML cap (T-08-05)
- FavoritesService: list/create/update/remove all scoped by userId (T-08-06 / Pitfall 3)
- FavoritesController: GET /favorites?widgetId, POST, PATCH :id, DELETE :id
- FavoritesModule registered in AppModule
- tsc --noEmit passes for @tessera/api
2026-07-01 10:25:52 +02:00
schalli a3bb3f2396 test(08-03): add failing tests for FavoritesWidget CRUD + view toggle + letter fallback (RED)
- fetchFavorites called with instanceId (widgetId scope, Pitfall 3)
- covers add/edit/delete, empty state, list/grid toggle, letter fallback
- tests fail: favorites-widget.tsx and favorites-api.ts do not exist yet
2026-07-01 10:21:49 +02:00
schalli 4645a5f43f docs(phase-08): update tracking after wave 2 2026-07-01 10:12:34 +02:00
schalli 262ce19646 merge(08-02): Stopwatch widget with config persistence 2026-07-01 10:12:15 +02:00
schalli 93e2b94c47 docs(08-02): complete Stopwatch widget plan summary
- 3/3 tasks complete, 7/7 tests pass, tsc clean
- Worktree fast-forward deviation documented
2026-07-01 10:11:46 +02:00
schalli c1c7bff929 feat(08-02): StopwatchWidget with config persistence + reload reconstruction (GREEN)
- stopwatch-widget.tsx: start/stop/reset/lap controls, setInterval tick (100ms)
- Reload reconstruction: Date.now() - startedAt + elapsed (Pitfall 2 fix)
- State persisted via updateWidgetConfig(instanceId, {...}) on each action
- Single interval cleared on unmount and when not running (T-08-04 mitigated)
- Lap times stored newest-first per RESEARCH recommendation
- No CSS modules — Tailwind only (grep -c module.css = 0)
- page.tsx: added wireStopwatchWidget(StopwatchWidget) import + call
- All 7 stopwatch tests pass (GREEN)
2026-07-01 10:10:03 +02:00
schalli d8d008b2c4 test(08-02): add failing tests for Stopwatch behavior and reload reconstruction (RED)
- Tests for start/stop/reset/lap controls
- Reload reconstruction test verifies elapsed from startedAt + stored elapsed
- Tests fail because stopwatch-widget.tsx does not yet exist (expected RED state)
2026-07-01 10:08:44 +02:00
schalli 2fa5193fd0 docs(phase-08): update tracking after wave 1
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Has been cancelled
Tessera CI/CD / Build & Publish Images (push) Has been cancelled
2026-07-01 10:03:21 +02:00
schalli 8574218ae6 merge(08-01): widget registry foundation + Calculator widget 2026-07-01 10:02:43 +02:00
schalli 5e8b2c2c39 docs(08-01): complete widget registry foundation + Calculator plan summary
- 3/3 tasks executed (RED/GREEN/verify)
- 16 tests added and passing (67 total web suite)
- SUMMARY.md created with self-check PASSED
2026-07-01 10:01:15 +02:00
schalli 63ec93bd35 feat(08-01): registry foundation for 4 new widget types + Calculator widget (GREEN)
- widget-registry.tsx: extend WidgetType union with calculator/favorites/link/stopwatch
- widget-registry.tsx: add WIDGET_CONSTRAINTS entries with per-widget grid constraints (DASH-11)
- widget-registry.tsx: add SVG icons (CalculatorIcon, FavoritesIcon, LinkIcon, StopwatchIcon)
- widget-registry.tsx: add WIDGET_REGISTRY entries and wire functions for all 4 new types
- calculator-widget.tsx: full arithmetic implementation ported from personal-dashboard
  (parseDisplay, formatNumber, calculate, keyboard handler with stopPropagation)
- widget-catalog-modal.tsx: extend WIDGET_TYPES to include all 8 types
- create-widget.dto.ts: extend @IsIn to accept 8 widget types (T-08-01 mitigated)
- page.tsx: import CalculatorWidget and call wireCalculatorWidget()
- de.json / en.json: add i18n keys for calculator, favorites, link, stopwatch
- All 16 tests passing (GREEN)
2026-07-01 09:57:44 +02:00
schalli b751ae7453 test(08-01): add failing tests for Calculator widget and registry constraints (RED)
- calculator-widget.test.tsx: 5 behaviour tests (render, arithmetic, div/0, keyboard, decimal)
- widget-registry.test.tsx: DASH-11 structure check for all 8 widget types including new Phase-8 types
- Both suites fail (RED baseline) — implementation does not exist yet
2026-07-01 09:54:22 +02:00
schalli b885c767fb docs(08): create phase 8 plan — 4 widget slices (Calculator, Stoppuhr, Favorites, Link) 2026-07-01 09:10:19 +02:00
schalli 29636bd2b1 docs(08): create phase plan (4 widget vertical slices) 2026-07-01 09:03:54 +02:00
schalli dc7b291d28 docs(08): research phase dashboard-widgets-vollimplementierung 2026-07-01 08:53:53 +02:00
schalli 3b0ac41f59 docs(state): record phase 8 context session 2026-07-01 08:33:00 +02:00
schalli 53fb106e39 docs(08): capture phase context 2026-07-01 08:32:54 +02:00
schalli 88db54fa7d fix(account-settings): clear stale pw error on input + live header avatar update
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m34s
- Password form errors (wrong pw, mismatch) now clear on first keystroke
  in any password field instead of persisting until next submit.
- Avatar upload now bumps avatarVersion in auth store and sets hasAvatar=true,
  so the header avatar switches to the uploaded image immediately without reload.
- Header img src uses ?v={avatarVersion} as cache-buster to force browser to
  fetch the new avatar when version increments.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 15:29:21 +02:00
schalli 85cd17452c refactor(admin): move SMTP settings from user settings to admin area
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m28s
SMTP configuration is an admin concern, not a per-user setting. Removed
it from the settings sidebar and relocated to /admin/smtp with a link in
the admin sidebar.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 14:47:44 +02:00
schalli fe4e64a0ad docs(quick-260630-gbh): User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 42s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m42s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 12:04:20 +02:00
schalli 86654a466b merge(quick-260630-gbh): User Settings — Passwort ändern (non-LDAP) + Profilbild 2026-06-30 12:03:37 +02:00
schalli 5ae498fd88 feat(quick-260630-gbh-01): header avatar display with initial fallback
- AuthUser store: add optional hasAvatar? field
- Header: populate hasAvatar from fetchCurrentUser() result
- Header avatar button: shows <img src='/api-proxy/users/me/avatar'> when hasAvatar=true with onError fallback to initial span
- Plain <img> tag used (same-origin /api-proxy rewrite, no next/image remote config needed)
2026-06-30 12:02:02 +02:00
schalli 4482a8ce78 feat(quick-260630-gbh-01): account settings page — avatar upload + conditional password form
- AuthUser interface: add isLocalUser? + hasAvatar? fields
- uploadAvatarAction: server action forwarding file to POST /users/me/avatar
- AccountSettingsForm: avatar preview with initial fallback + upload; password form only for local users; LDAP notice
- /settings/general/account page mirroring smtp page structure
- SettingsSidebar: Konto link above SMTP link
- de.json + en.json: categoryAccount + account.* keys
2026-06-30 12:00:40 +02:00
schalli 0fba45d2c2 feat(quick-260630-gbh-01): avatar storage endpoints + enriched /auth/me
- Add avatarPath String? column to User model (migration: add_user_avatar)
- POST /users/me/avatar: 2MB limit, image/png/jpeg/webp allowlist, writes to user-files/avatars/{userId}.{ext}
- GET /users/me/avatar: streams avatar with Cache-Control: no-store
- AuthService.getMe(): returns isLocalUser + hasAvatar without leaking passwordHash/ldapDn
- AuthController GET /auth/me: now returns enriched profile via getMe()
2026-06-30 11:57:33 +02:00
schalli 04b37f54f6 docs(260630-gbh): pre-dispatch plan for User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen 2026-06-30 11:49:12 +02:00
schalli dcba4b9977 fix(dkv): search msgfolderroot for EWS subfolder resolution
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 24s
FindFolder was searching only under inbox DistinguishedFolderId, missing
folders at mailbox root level. Now searches msgfolderroot (full mailbox)
so custom folders like DKV are found regardless of placement.

Also adds HTTP status check and debug logging for FindFolder responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 10:23:30 +02:00
schalli 30eb40c184 feat(dkv): Exchange subfolder support via EWS FindFolder
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 22s
Custom folder names (e.g. "DKV" or "INBOX/DKV") now resolved by calling
EWS FindFolder deep-search under inbox. Well-known names still map to
DistinguishedFolderId directly. Falls back to inbox with a warning log
when the subfolder cannot be found.

IMAP already supported subfolder paths natively via ImapFlow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 10:04:24 +02:00
schalli b5bf3ed8c0 fix(dkv): return username in GET /dkv/config response
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 22s
loadConfig used CONFIG_SAFE_SELECT which excludes encryptedInboxCreds entirely,
so username was never returned to the frontend — form always showed empty username.

Added getConfigForApi() which loads the safe config + decrypts encryptedInboxCreds
to extract username (never password) and adds hasPassword boolean. Controller
getConfig now calls getConfigForApi instead of loadConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:58:59 +02:00
schalli a44e40f101 fix(dkv): correct invoice date extraction; add Exchange IsRead filter + mark-as-read
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 44s
Tessera CI/CD / Build & Publish Images (push) Successful in 21s
Date fix: previous regex matched payment-due date ("10 Tage nach Rechnungsdatum...
10.04.2026") instead of actual Rechnungsdatum. New approach anchors on the
invoice number line (DD/DDDDDDDDD/DDD) and takes the date on the next line,
which is always the actual Rechnungsdatum in DKV PDFs.

Exchange dedup: FindItem now filters IsRead=false (combined with sender filter
via <t:And>), so already-processed emails are skipped automatically.
After downloading attachments, UpdateItem marks the message as read
(using ItemId + ChangeKey from GetItem response), mirroring IMAP \Seen behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:47:17 +02:00
schalli 66ffad149e fix(dkv): extract invoice number/date from PDF, rename export files to RG-DKV format
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s
- Parser now extracts Rechnungsnummer (DD/DDDDDDDDD/DDD) and Rechnungsdatum
  from PDF text, so filename doesn't rely on email subject
- Export filename changed from DKV_YYYY-MM_... to RG-DKV-{nr}-{YYMMDD}.xlsx
  e.g. RG-DKV-26-650869002-002-260331.xlsx
- Subject fallback now also matches slash-separated invoice numbers (26/NNN/NNN)
- writeAndPrune simplified to accept baseName instead of separate fields
- Validation regex and prune prefix updated to match new RG-DKV- pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:39:51 +02:00
schalli 5c1aa03270 fix(dkv): handle EV charging rows and service rows in single-tx tab parser
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 21s
Analyzed Invoice-4302486921-26_650869002_000.pdf text structure. Three cases:

1. FUEL (fields[4] = numeric tx-nr): km+product merged in fields[5], unit in
   fields[6]. Already working; no change.

2. EV CHARGING (fields contains "DDDD KWH" or "DDDD MIN" unit): column layout
   shifts — no km field, station+ort sometimes merged in fields[1]. Detected by
   regex on unit field; kwhIdx drives relative offset for menge/netto/brutto.
   Ort extracted from fields[2] (kwhIdx>=5) or fields[1] (kwhIdx=4, compact).
   Kilometerstand = 0 (EV chargers don't record odometer).

3. SERVICE ROWS (e.g. "DKV Analytics Premiu"): appear inside a VEHICLE: block but
   fields[4] is non-numeric (product description, not a transaction number). These
   were being parsed as fake vehicle transactions producing wrong ort/km values.
   Now filtered out (return null).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:20:12 +02:00
schalli a759e816a0 fix(dkv): fix Kennzeichen matching and NaN/invalid km values in Excel export
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s
Three issues fixed:

1. Kennzeichen normalization: DKV PDF extracts plates without hyphens
   ("GP JL 740E" vs CSV-imported "GP-JL 740E"). Added _normalizeKennzeichen()
   which strips hyphens, spaces, and dots before lookup — resolves vehicle
   master match failure that caused Marke/Modell/Fahrer to appear empty.

2. Empty-string NaN: parser used ?? '0' which doesn't catch empty strings,
   causing parseDE('') = NaN. Changed to || '0' for km, menge, and totals.

3. Invalid km values: EV charging rows from DKV have misaligned columns —
   km position contains a decimal price (e.g. 18.64 EUR or kWh). Added
   sanity check: non-integer km values are written as null (empty cell)
   instead of a misleading decimal. ExportRow.kilometerstand is now number|null.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 09:12:13 +02:00
schalli c9328f60b7 fix(api): create /app/user-files with nestjs ownership in Dockerfile
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m36s
Directory must exist before nestjs user takes over — otherwise DkvExportService
cannot write xlsx export files and throws EACCES on first inbox processing run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:44:21 +02:00
schalli ccfd3f21cf fix(dkv): fix EWS attachment ID extraction — FileAttachment has no Id attribute
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
extractAttrs(block, 't:FileAttachment', 'Id') always returned empty array
because the attachment Id lives in a child <t:AttachmentId Id="..."/>, not
on the <t:FileAttachment> tag itself. This caused all Exchange inbox checks
to silently find zero PDF attachments and report "no matching emails".

Fixed by iterating FileAttachment blocks individually and extracting
t:AttachmentId/@Id from within each block. Also added filename (.pdf)
as fallback when ContentType is application/octet-stream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:41:17 +02:00
schalli 9efa3bab1d fix(dkv): fix inbox processing pipeline — orphan tenant, MIME detection, UNSEEN filter
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m37s
- Fix orphaned DkvModuleConfig: tenantId pointed to deleted tenant, updated to Default tenant
- DKV controller: return 404 instead of HTTP 200 null when no config exists
- IMAP provider: also detect PDFs sent as application/octet-stream (check filename extension)
- IMAP provider: add seen:false filter so already-processed emails are skipped on re-poll
- IMAP provider: mark email as \Seen after successful PDF download to prevent reprocessing
- Frontend dkv-api: handle 404 from fetchConfig as "not yet configured" (returns null)
- InboxConfigForm: show warning banner when config not yet saved in DB
- InboxConfigForm: add "Jetzt prüfen" button to manually trigger POST /dkv/check-now

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 08:31:36 +02:00