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>
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>
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>
- 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>
After a successful password change the old cookie still contained
mustChangePassword=true, causing the middleware to redirect back to
/change-password. Now changePassword issues a fresh session cookie.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move prisma to runtime dependencies so it's available in prod image
- API runs migrate deploy before starting (handles fresh installs + updates)
- Remove separate migrate service from prod compose
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ExchangeInboxProvider rewritten to use httpntlm + raw EWS SOAP:
FindItem / GetItem / GetAttachment via NTLM challenge-response.
No longer requires Basic Auth on Exchange EWS virtual directory.
Folder name mapped to EWS DistinguishedFolderId (Inbox/SentItems/etc).
- CalendarCryptoService: move key init from onModuleInit to constructor
so MailModule.forRootAsync() factory can call decrypt() before NestJS
lifecycle hooks execute (startup crash when SmtpConfig row has password).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- testConnection now returns { success, message? } instead of boolean so
admins see the actual EWS/IMAP error in the UI rather than "Unbekannter Fehler"
- Exchange provider: resolveFolder() maps folder string to WellKnownFolderName
(Inbox, SentItems, DeletedItems, Drafts, JunkEmail + German aliases)
- InboxConfigForm: folder field now shown for both IMAP and Exchange protocols
with Exchange-specific help text listing valid well-known names
- Controller returns testConnection result directly (no more redundant wrapping)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Server reachable without credentials (port 25 open relay) returns
{ success: true, warning: 'no_auth' } instead of green success.
Frontend shows red warning: server reachable but emails will fail.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously only the password fell back to stored value; username could be
missing if form field was cleared. Now both credentials fall back to the
stored config, ensuring auth is always tested when credentials exist.
Also adds explicit 10s timeouts to prevent indefinite hangs on unreachable
SMTP servers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Middleware runs before guards in NestJS — req.user was always undefined
when TenantMiddleware executed, so req.tenantId was never set.
Convert to TenantGuard (APP_GUARD, registered after JwtAuthGuard) so it
runs after JWT validation and can read req.user.tenantId correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `cron@4.4.0` as direct dep (pnpm strict isolation blocks transitive access)
- Import SettingsModule in DkvModule so DkvMailService can inject SettingsService
- Fix dkv.service.ts return key: `count` → `imported` to match declared return type
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FileInterceptor used multer's default memory storage with no size limit.
An oversized file could exhaust Node.js heap before parsing begins.
Add fileSize: 5*1024*1024 (5 MB) — sufficient for any realistic vehicle list.
Exchange EWS UniqueIds are base64-encoded and can contain +, /, = characters.
When used as the fallback rechnungsnummer (email-{uid}), a slash would cause
path.join() to resolve into a subdirectory, making writeFileSync fail silently.
Sanitise uid to [a-zA-Z0-9-] before it reaches the filesystem write path.
HTTP query params arrive as strings. Without @Type(() => Number),
class-transformer never coerces page/limit before @IsInt() runs,
causing HTTP 400 for any request that explicitly passes ?page or ?limit.
Also adds @Max(100) on limit to bound result-set size.
Each sendExportEmail call created a new nodemailer transport which was never
closed, leaving the internal SMTP connection pool alive. With 3-retry backoff,
up to 3 leaked transports per invoice accumulate over time and can exhaust OS
socket limits. Add transport.close() in a finally block.
If getMailboxLock() failed (e.g. folder not found) the try/finally cleanup
block was never entered, leaving the ImapFlow connection open and leaking.
Move the lock acquisition inside the try block and use lock?.release() in
finally so client.logout() is always called regardless of lock success.
Values >=60 in the cron minute field silently misbehave (*/60 fires once per
hour, */90 fires once per hour, etc.). Use the hours field for intervals >=60:
<60 min → */N * * * *
>=60 min → 0 */H * * * (H = floor(N/60))
Also adds @Max(1440) to DkvConfigDto to bound the field at 24 h.
Backend dkv.service.ts returned { imported } but frontend read result.count,
causing the success toast to always display "undefined Fahrzeuge importiert".
Align backend field name to count and update the dkv-api.ts return type to
include mode for completeness.
- DkvSchedulerService: SchedulerRegistry.addCronJob (dynamic interval, not static @Cron)
- onModuleInit loads first active config (v1 single-tenant, documented in SUMMARY)
- setInterval() replaces existing job and registers new one with */ cron expression
- stopJob() removes job when config.isActive=false
- cron package resolved via require() workaround (pnpm strict isolation: transitive dep)
- DkvController: 12 handlers all carrying @Roles(Role.ADMIN, Role.SUPER_ADMIN)
- Routes: GET/PUT config, POST check-now, POST test-connection, GET history,
GET exports/:filename, GET/POST/PUT/DELETE vehicles, POST vehicles/import
- vehicles/import uses FileInterceptor('file') for CSV multipart upload
- exports/:filename streams file as attachment; traversal guard in DkvService
- Controller coordinates scheduler after PUT /dkv/config (no circular dep)
- createTransport() called per-send from DB SmtpConfig (Pitfall 3 mitigation — not at startup)
- Injects SettingsService to load decrypted SMTP config per tenant
- secure/requireTLS mapped from encryption field (ssl-tls / starttls / none)
- Auth omitted when username absent (anonymous relay support)
- Attachment contentType: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
- Error path rethrows after generic log (T-07-10) so DkvService can run 3-retry backoff (D-16)
- Does not import @nestjs-modules/mailer abstractions
- buildExcelBuffer: 5-column xlsx per D-13 (Lieferdatum as string, never Date), SheetJS aoa_to_sheet
- resolveFahrzeug: replaces {Marke}/{Modell}/{Kennzeichen}/{Fahrer} tokens in format string (D-19)
- writeAndPrune: server-side filename DKV_YYYY-MM_<nr>.xlsx (T-07-09 path-traversal prevention),
writes to user-files/ (resolved from monorepo root, not request input), prunes to last 10 DKV_*.xlsx
files sorted by mtime ascending (D-15, Pitfall 7 atomicity)
- dkv-parser.validate.ts: empirical validation script against user-files/invoice.pdf
- 27 vehicle blocks, 66 transactions extracted (matches expected count)
- Handles two PDF extraction formats: single-tx (tab-separated) + multi-tx (columnar)
- German number parsing: replace(/\./g,'').replace(',','.') applied to km and menge
- Exits 1 with full raw text dump if zero vehicle blocks parsed (assertion guard)
- dkv-parser.service.ts: @Injectable() NestJS service wrapping validated logic
- parsePdf(buffer: Buffer): Promise<DkvVehicleBlock[]>
- Uses pdf-parse v2 class API: new PDFParse({data:buffer}) — NOT v1 pdfParse()
- Calls destroy() after extraction (T-07-01 memory safety)
- Generic error messages only on parse failure (T-07-02 info disclosure)
Regex adjustment vs Research Pattern 4: space-based regex replaced with
tab-split (single-tx) + columnar transpose (multi-tx) after empirical analysis
of actual invoice.pdf text extraction output.
- Accept full domains (e.g. "example.xyz") not just labels
- Check the entered TLD as primary result
- Show .de, .com, .net, .org as alternative suggestions below
- Primary result highlighted with accent border
- Input without TLD still works (shows all 4 suggestions)
- Updated i18n placeholders and added "suggestions" label
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GET /health/version public endpoint to API
- Extend Tauri with 4 plugins: notification, autostart, window-state, store
- Implement close-to-tray with prevent_close + prevent_exit (Pitfall 2)
- Tray menu with Oeffnen/Beenden (German labels)
- Async startup version check against server /health/version
- Generate Tessera-branded icons (yellow T on dark bg, OKLCH palette)
- Update capabilities for notification, autostart, window-state permissions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
In Docker deps stage only package.json files are copied (no schema),
causing prisma generate to fail. Builder stage already runs explicit
prisma generate with full source.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Implement aggregateEvents with Promise.allSettled across visible sources
- Dispatch to ICS/CalDAV/Exchange providers by source.type with credential decryption
- In-memory per-user event cache with 5-minute TTL (Pitfall 4)
- Background cache refresh when close to expiry
- Implement testConnection with lastSyncAt/lastSyncError updates
- Default window: now to now+30 days
- Events sorted by start ascending with source color included
- Prisma model SearchProvider with userId/tenantId scoping
- Three default providers (Google/Bing/DuckDuckGo) as constants, always returned without DB seed
- GET/POST/DELETE search-providers endpoints on DashboardController
- Ownership verification on delete (T-05-07), default providers cannot be deleted
- CreateSearchProviderDto with class-validator: urlTemplate must contain {query} (T-05-08)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ModuleGuard now reads tenantId from req.user?.tenantId as fallback
(same pattern as module-registry controller), and throws 403 instead
of silently allowing access when no tenant context exists.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Controller now falls back to user.tenantId from JWT when
req.tenantId is null (SUPER_ADMIN without x-tenant-id header).
Also added error display to admin modules page.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CheckDomainDto with regex validation (T-03-05) and max 10 TLDs (T-03-06)
- DomaincheckService using node:dns/promises with 5s timeout per lookup
- POST /modules/domaincheck/check protected by UseModule guard (T-03-08)
- DomaincheckModule seeds itself into registry on startup via OnModuleInit
- Default TLDs: de, com, net, org (D-03)
- ModuleRegistryService with findAll, findBySlug, findActiveForTenant, activate/deactivate, seedModule
- ModuleRegistryController with GET /modules, GET /modules/active, POST activate/deactivate
- ModuleGuard + @UseModule() decorator for tenant-scoped module access control
- ActivateModuleDto with UUID validation
- Registered ModuleRegistryModule in AppModule imports