- actions.ts: mergeCertsAction(files, outputFormat, password?) builds FormData with
multiple file fields; delegates to postForm('merge', ...) (T-09-02/T-09-04)
- MergeTab.tsx: multi-file state (local), file input (multiple), output selector
(pem|pfx), Zusammenfuehren button disabled when < 2 files (data-testid for tests),
onOutputFormatChange callback to page.tsx for shared PasswordField visibility
- ConvertTab.tsx: gains pfx option + onTargetFormatChange callback (same pattern)
- page.tsx: lifts mergeOutputFormat + convertOutputFormat state; showPassword now
also true when active tab's output format is 'pfx'; passes callbacks to tabs
- cert-manager.test.tsx: 5 new tests — MergeTab disabled/enabled by file count,
shared PasswordField appears on pfx output, downloadBase64 called on success;
ConvertTab pfx option present; all 19/19 web tests green
- All production cert-manager files type-clean (pre-existing test type issues unchanged)
- Add FileResponse interface to actions.ts
- Add convertCertAction(input, targetFormat): builds FormData with
file/pemText/password + targetFormat, calls postForm convert endpoint
- Implement ConvertTab: native select for pem/der/p7b targetFormat,
Konvertieren button with loading swap, error classification, empty state
- On success: calls downloadBase64(filename, content, mimeType)
- 3 new ConvertTab tests: format selector options, downloadBase64 invoked
on success, text-destructive error on format rejection
- All 14 web cert-manager tests green
- Add FileResponse interface and FORMAT_MIME map to service
- Implement convertCert: parses any input format (PEM/DER/PFX/P7B) via
same logic as parseCert; serializes to pem/der/p7b targetFormat
- DER output uses bytesToHex→Buffer.from(hex,'hex') to avoid utf-8
corruption (Pitfall 1 / T-09-06)
- P7B output: pkcs7.createSignedData + pem.encode (PEM-wrapped PKCS7)
- Wrap all forge ops in try/catch → BadRequestException (T-09-01)
- Controller: add @Body('pemText') + reject when neither file nor pemText
- Fix: re-add NotImplementedException import for mergeCerts stub
- All 23 API cert-manager tests green (including 4 new convertCert)
- splitCerts fullchain PEM: expects count 2, two single-PEM-block certs with correct CN
- splitCerts P7B PEM bundle: expects at least one cert in result
- splitCerts malformed input: expects BadRequestException
- All three tests FAIL against NotImplementedException stub (RED confirmed)
- All 16 prior tests still pass
- Added parseCert describe block with 5 failing tests
- Covers PEM input, DER input, PFX+correct-password, PFX+wrong-password (BadRequestException), malformed input (BadRequestException)
- Existing 11 helper tests still pass
- Fixtures built via node-forge (RSA-1024, DER from asn1.toDer, PFX via toPkcs12Asn1)
- Added certManager namespace to de.json with full key set (tabs, dropZone, paste, password, or, actions, emptyState, error)
- Added certManager namespace to en.json with matching key structure
- German copy matches UI-SPEC Copywriting Contract exactly
- Both files share identical key paths under certManager
- SSRF check skipped for Exchange type (internal EWS servers are common)
- testConnectionFromConfig catches SSRF/validation errors, returns {success:false,error} instead of throwing 403
- updateSource reads existing.type to determine effective type for SSRF check
- Panel shows saveError/editSaveError on failed add/update
- Edit form initialValues now includes domain field
- i18n: calendar.saveError key added (de+en)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
header.tsx: accentColor was missing from setUser call on mount —
applyAccentColor(undefined) fired on every reload, removing --primary.
auth-store: also set --sidebar-accent (15% opacity) and
--sidebar-accent-foreground so active sidebar items match accent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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>
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>
- 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>
- 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>
- 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
- 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
- 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
- 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)
- 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)
- 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>
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>
- 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)
- 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()
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>
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>
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>
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>
- 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>
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>
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>