Commit Graph

514 Commits

Author SHA1 Message Date
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
schalli 9a652ea440 chore(web): remove debug logging from changePasswordAction
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 53s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m50s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:30:39 +02:00
schalli 40c4876a19 fix(web): move redirect() outside try/catch in changePasswordAction
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 40s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m27s
redirect() throws NEXT_REDIRECT internally — inside catch it was swallowed
and returned networkError. Extract cookie data in try/catch, then set
cookie and redirect() after the block so the throw propagates correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:25:57 +02:00
schalli f4ece4890d fix(web): redirect from server action after password change
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 40s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m19s
client-side router.push races with Set-Cookie processing. redirect() in the
server action sends cookie + redirect in one response — browser applies the
new JWT before navigating, so middleware sees mustChangePassword=false.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:20:14 +02:00
schalli 195354cd7a debug(web): log set-cookie header value from API change-password response
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m18s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-30 07:12:45 +02:00
schalli f96a0db769 fix(web): forward new session cookie after password change
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 35s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m21s
After changePassword the API issues a new JWT with mustChangePassword=false.
The server action now reads Set-Cookie from the API response and sets it
in the browser so the middleware sees the updated flag and allows /dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:47:55 +02:00
schalli c8210a2abc debug(web): add logging to changePasswordAction to diagnose Verbindungsfehler
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Successful in 36s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m22s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:38:48 +02:00
schalli 5779f0f6c9 fix(api): reissue JWT with mustChangePassword=false after password change
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 36s
Tessera CI/CD / Build & Publish Images (push) Successful in 23s
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>
2026-06-29 16:09:36 +02:00
schalli b28ee472c5 fix(api): use user.id instead of user.sub in changePassword controller
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 21s
JWT strategy maps payload.sub to user.id — user.sub was always undefined,
causing Prisma findUnique to fail with id: undefined validation error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 16:02:38 +02:00
schalli 46ba8868c7 fix(web): change password via server action instead of client-side fetch
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Successful in 36s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m23s
Client-side fetch to NEXT_PUBLIC_API_URL was unreachable in production.
Replace with a server action that uses API_INTERNAL_URL (http://api:3001)
server-to-server — no browser connectivity required.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:54:29 +02:00
schalli 6020021370 fix(web): use http://api:3001 as default rewrite destination
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 37s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m24s
next.config rewrites() runs at build time — API_INTERNAL_URL is not set
in CI, so the previous localhost:3001 fallback was baked into the bundle.
Default to http://api:3001 which is always correct in Docker network.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:42:44 +02:00
schalli c42ab5dc6f fix(web): proxy client API calls through Next.js to fix production connectivity
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 36s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m33s
NEXT_PUBLIC_API_URL was undefined at build time, causing client bundles to
fall back to http://localhost:3001 — unreachable from the browser in prod.

- Add /api-proxy rewrite in next.config.ts (forwards to API_INTERNAL_URL at runtime)
- Bake NEXT_PUBLIC_API_URL=/api-proxy at build time in Dockerfile
- Fix api.ts to prefer API_INTERNAL_URL for server-side calls
- Fix docker-compose.prod.yml: set NEXT_PUBLIC_API_URL=http://api:3001 for runtime server-side code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:36:06 +02:00
schalli 24868ec1b8 chore(db): add migration for all tables missing from history
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 28s
Captures DashboardLayout, WidgetInstance, SearchProvider, CalendarSource,
DkvModuleConfig, DkvVehicleMaster, DkvInvoiceHistory, SmtpConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:17:27 +02:00
schalli 27c3abf3df fix(deploy): correct prisma binary path in API startup
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m40s
pnpm puts package binaries in apps/api/node_modules/.bin/, not root.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 15:08:07 +02:00
schalli e26fbd720e fix(deploy): embed prisma migrate deploy in API startup
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 42s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m52s
- 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>
2026-06-29 14:50:42 +02:00
schalli 28d4fa5c82 fix(ci): fix sidebar tests + add registry publish + prod compose
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 38s
Tessera CI/CD / Build & Publish Images (push) Failing after 8m32s
- sidebar.test: expand category before asserting on module names
  (categories are collapsed by default since UI-Umbau)
- ci.yml: replace build-deploy with publish job that pushes images
  to git.vicolab.de container registry
- docker-compose.prod.yml: pull-only compose for server deployments
  using registry images

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 11:14:34 +02:00
schalli a4e03830c1 fix(07): NTLM support for Exchange EWS + crypto key init timing fix
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- 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>
2026-06-29 10:02:48 +02:00
schalli 3b2a36cd61 feat(07): add CSV format help text and Exchange 401 diagnostic hints
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- CSV import dialog shows format line + example before mode selection
- Exchange connection test: on 401, inline hint lists common causes
  (wrong credentials, domain format, username prefix, O365 not supported)
- Both de/en translations updated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-29 09:42:08 +02:00
schalli 9b453e3ed3 fix(07): propagate Exchange connection error, add folder selection for Exchange
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- 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>
2026-06-29 09:33:13 +02:00
schalli 01ff137f43 fix(07): UAT fixes — SMTP test email, DKV routing, Exchange domain field
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- SMTP: add test-to field, send real email via sendMail() instead of verify()
- SMTP: fix no_auth warning shown as error for open-relay servers
- DKV: register dkv-fleet in MODULE_REGISTRY (fixes "Modul nicht gefunden")
- DKV: add dynamic [category]/[moduleSlug]/settings + vehicles sub-routes
- DKV: settings/vehicles links use useParams for consistent URLs
- DKV: add gear icon settings link to module main page
- DKV: rename module to "DKV-Rechnung" in seed + translations
- DKV: add optional domain field for Exchange (WebCredentials 3rd arg)
- DKV: hide IMAP-only fields (Port/Verschlüsselung/Ordner) when Exchange selected
- DKV: hide Exchange-only field (Domain) when IMAP selected
- Prisma: add domain column to DkvModuleConfig

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-28 21:58:26 +02:00
schalli 4de87a8ea2 fix(07): SMTP test warns when connection passes but no auth configured
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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>
2026-06-28 01:29:53 +02:00
schalli 853095faef fix(07): SMTP test falls back to stored username and adds 10s timeouts
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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>
2026-06-27 21:04:58 +02:00
schalli 8320a34035 fix(07): replace TenantMiddleware with TenantGuard to fix tenant context
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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>
2026-06-27 20:57:03 +02:00
schalli dd5bc90395 fix(07): DKV/Settings module wiring and missing cron dep
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
- 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>
2026-06-27 20:28:06 +02:00
schalli d2d224cdd5 fix(07): WR-05 add 5 MB file size limit to CSV vehicle import endpoint
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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.
2026-06-27 17:22:27 +02:00
schalli 9de16babe4 fix(07): WR-04 sanitise Exchange UniqueId in invoice number fallback
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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.
2026-06-27 17:22:09 +02:00
schalli 338655c57b fix(07): WR-03 replace dead try/catch in formatDateTime with isNaN guard
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
new Date() never throws so the catch was unreachable. Invalid dates rendered
as NaN.NaN.NaN, NaN:NaN Uhr. Use isNaN(d.getTime()) guard to fall back to
the raw string instead.
2026-06-27 17:21:51 +02:00
schalli 49eab55abe fix(07): WR-02 add @Type(Number) coercion to pagination DTO fields
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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.
2026-06-27 17:21:28 +02:00
schalli ded652382d fix(07): WR-01 close nodemailer transport in finally to prevent pool leak
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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.
2026-06-27 17:21:01 +02:00
schalli cb0d378ded fix(07): CR-03 close IMAP connection when getMailboxLock throws
Tessera CI/CD / Lint & Type Check (push) Waiting to run
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
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.
2026-06-27 17:20:39 +02:00
schalli 955a94638c fix(07): CR-02 correct cron expression for pollIntervalMin >= 60
Tessera CI/CD / Tests (push) Blocked by required conditions
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Has started running
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.
2026-06-27 17:20:11 +02:00
schalli f3f610f9e1 fix(07): CR-01 rename import return field imported→count to match frontend
Tessera CI/CD / Lint & Type Check (push) Failing after 37s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Deploy (push) Has been skipped
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.
2026-06-27 17:19:44 +02:00
schalli 75aec38dae feat(07-06): extend SettingsSidebar with Allgemein > SMTP category
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Waiting to run
- Add 'Allgemein' category section ABOVE existing Dashboard category
- Single SMTP link to /settings/general/smtp with identical active/inactive styling and aria-current
- isActive('/settings/general/smtp') works via pathname.startsWith branch
- Existing Dashboard/Widgets/Calendar items unchanged
2026-06-27 17:04:25 +02:00
schalli fd2dda69cb feat(07-06): settings-api client + SmtpSettingsForm + SMTP page (DKV-05)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Waiting to run
- settings-api.ts: fetchSmtp/saveSmtp/testSmtp with credentials:'include'; SmtpConfig exposes only hasPassword (T-07-17)
- smtp-settings-form.tsx: Surface C form with show/hide password toggle, test-feedback auto-clears 6s
- settings/general/smtp/page.tsx: SmtpSettingsPage heading + SmtpSettingsForm
- de.json + en.json: add smtp.showPassword/hidePassword/testTesting/testSuccess/testFailed keys
- TDD GREEN: 5/5 tests pass; aria-hidden* on required markers removed for correct getByLabelText matching
2026-06-27 17:03:57 +02:00
schalli a96d79ad52 test(07-06): add failing SMTP settings form tests (RED — DKV-05)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
- 5 Vitest tests for SmtpSettingsForm: load config, save, test success/failure, password toggle
- Mocks @/lib/settings-api (fetchSmtp/saveSmtp/testSmtp) and next-intl
- Mirrors calendar-settings.test.tsx mocking pattern
- RED: SmtpSettingsForm does not yet exist
2026-06-27 17:00:27 +02:00
schalli 4d81b8b2d5 feat(07-05): VehicleTable + CsvImportButton + vehicles page — GREEN (DKV-03)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Waiting to run
- VehicleTable: list, inline edit, create row, delete with confirm dialog
- All 4 UI-SPEC aria-labels on icon-only buttons (Rule 2: accessibility correctness)
- CsvImportButton: merge/replace modes, destructive confirm, importVehiclesCsv
- vehicles/page.tsx: heading + back link to settings + VehicleTable
- VehicleTable.test.tsx: fix mock sequencing bug (mockResolvedValueOnce for init load)
- All 5 Vitest tests pass (GREEN)
2026-06-27 00:41:03 +02:00
schalli c739d2788b test(07-05): add failing VehicleTable tests (RED — DKV-03)
Tessera CI/CD / Lint & Type Check (push) Failing after 39s
Tessera CI/CD / Tests (push) Has been skipped
Tessera CI/CD / Build & Deploy (push) Has been skipped
- Covers: render list, empty state, delete with confirm, inline edit + save
- Mocks @/lib/dkv-api + next-intl (mirrors calendar-settings.test.tsx pattern)
- Verifies all 4 UI-SPEC aria-labels on icon-only action buttons
2026-06-27 00:38:38 +02:00
schalli 45e9a3591f feat(07-05): settings page + InboxConfigForm — Surface B inbox tab (DKV-01)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 37s
Tessera CI/CD / Tests (push) Waiting to run
- settings/page.tsx: tab bar (Posteingang / Fahrzeuge), Vehicles tab links to /vehicles
- InboxConfigForm: all UI-SPEC Tab-1 fields in order, show/hide password (T-07-12)
- Active toggle: role=switch, pill shape, primary/muted colors
- testConnection + saveConfig wired; inline green/destructive feedback
- Password never pre-filled from server (hasPassword only) per T-07-12
2026-06-27 00:37:50 +02:00
schalli 2fe7bca6e8 feat(07-05): dkv-api client + Surface A — history table + export list + check-now
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Waiting to run
- dkv-api.ts: typed fetch client for all /dkv/* routes (credentials: include)
- StatusBadge: OKLCH inline styles per status (T-07-14: React text nodes only)
- InvoiceHistoryTable: 6-column table, 5 skeleton rows, pagination >25, refreshKey
- ExportFileList: up to 10 unique export filenames derived from history (T-07-13)
- page.tsx: Jetzt prüfen trigger, error banner, refreshKey lift, DKV-04/DKV-01
2026-06-27 00:36:28 +02:00
schalli 2a3d1c1e85 feat(07-04): DkvModule + registry seed + AppModule registration + i18n keys
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 40s
Tessera CI/CD / Tests (push) Waiting to run
- dkv.seed.ts: seedDkvModule with slug=dkv-fleet, category=fleet, isSystem=true
- dkv.module.ts: imports ModuleRegistryModule + CalendarModule (CalendarCryptoService)
  provides all 7 DKV services + 2 providers + controller; OnModuleInit seeds registry
- app.module.ts: DkvModule added to imports (ScheduleModule + SettingsModule from Plans 01/03)
- de.json + en.json: complete dkvFleet namespace (50+ keys incl. status, col, form, errors)
- settings namespace extended with categoryGeneral, categorySmtp, smtp sub-object
2026-06-27 00:30:08 +02:00
schalli c22d36758c feat(07-04): DkvSchedulerService + DkvController — dynamic cron + REST surface
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 36s
Tessera CI/CD / Tests (push) Waiting to run
- 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)
2026-06-27 00:28:18 +02:00
schalli c40a023321 feat(07-04): DkvService — pipeline orchestration + vehicle/config/history logic
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
- Single-flight guard (processing flag) prevents concurrent inbox processing
- processInbox: poll → 3-retry parse → driver-map → xlsx → 3-retry SMTP send → history
- Parse failure (D-10): records Fehler history row with errorMessage
- SMTP failure (D-16): exponential backoff 2s/4s, records Versand fehlgeschlagen
- CONFIG_SAFE_SELECT excludes encryptedInboxCreds (T-07-12)
- getExportFile rejects filenames with path separators or outside DKV_*.xlsx pattern (T-07-09)
- CSV import: merge (upsert by tenantId+kennzeichen) and replace (deleteMany then createMany) modes
- Invoice number extracted from email subject via regex; falls back to email-{uid}
- Vehicle format string resolved via DkvExportService.resolveFahrzeug (D-19)
2026-06-27 00:24:47 +02:00
schalli de48e35c74 feat(07-03): Migrate MailModule to DB-sourced SMTP transport with env fallback (D-06)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
- MailerModule.forRootAsync factory now async; injects SettingsService + ConfigService
- Priority 1: getStartupSmtpConfig() reads first SmtpConfig DB row (single-tenant default)
  — T-07-11: decrypted password used only to build transport, never logged
- Priority 2: env vars MAIL_HOST/MAIL_PORT/MAIL_USER/MAIL_PASS
- Priority 3: legacy TESSERA_SMTP_* env vars (backward compat)
- Priority 4: localhost:1025 hardcoded final fallback (Mailhog dev default)
- imports SettingsModule; no circular import (MailModule → SettingsModule → CalendarModule)
- mail.service.ts unchanged — still injects @nestjs-modules/mailer MailerService
2026-06-27 00:12:43 +02:00
schalli 4deefb52de feat(07-03): DkvMailService — runtime nodemailer transport with xlsx attachment (DKV-04)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 38s
Tessera CI/CD / Tests (push) Waiting to run
- 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
2026-06-27 00:11:59 +02:00
schalli 6b76ca9633 feat(07-03): DkvExportService — xlsx generation + user-files/ prune (DKV-04)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Waiting to run
- 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)
2026-06-27 00:10:56 +02:00
schalli 1bec0e76ee feat(07-03): SettingsModule — SMTP config backend + connection test (DKV-05)
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 37s
Tessera CI/CD / Tests (push) Waiting to run
- SmtpConfigDto: host/port/encryption/username/password/fromAddress with class-validator
- SettingsService: getSmtpConfig (SMTP_SAFE_SELECT, no password), saveSmtpConfig (AES-256-GCM
  encryption via CalendarCryptoService, preserve existing password on empty), getDecryptedSmtpConfig
  (internal, used by DkvMailService), testSmtpConfig (nodemailer.verify(), returns boolean, T-07-16),
  getStartupSmtpConfig (tenant-agnostic, used by MailModule factory, D-06)
- SettingsController: GET/PUT /settings/smtp + POST /settings/smtp/test, all @Roles(ADMIN, SUPER_ADMIN)
- SettingsModule: imports CalendarModule, exports SettingsService
- AppModule: imports SettingsModule
2026-06-27 00:09:47 +02:00
schalli 924de76f93 feat(07-02): ExchangeInboxProvider — EWS email inbox access
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Waiting to run
- Implements InboxProvider contract (fetchPdfAttachments + testConnection)
- Uses WellKnownFolderName.Inbox + FindItems + EmailMessage.Bind — NOT FindAppointments (Pitfall 6)
- Dynamic import('ews-javascript-api') following ExchangeProvider calendar pattern
- Server-side sender filter via SearchFilter.ContainsSubstring with client-side verification
- 25MB attachment size guard in extractPdfAttachments — PDF-bomb mitigation (T-07-05)
- Generic error messages only in all catch blocks — no credential values (T-07-03)
- Returns [] on error (consistent with calendar ExchangeProvider error path)
2026-06-27 00:03:56 +02:00
schalli b3f21a8747 feat(07-02): ImapProvider — IMAP inbox access via imapflow
Tessera CI/CD / Build & Deploy (push) Blocked by required conditions
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Waiting to run
- Implements InboxProvider contract (fetchPdfAttachments + testConnection)
- fetchAll() called before any download() — avoids IMAP connection deadlock (Pitfall 1)
- ImapFlow constructed with logger:false — credential safety (T-07-03)
- 25MB attachment size guard in streamToBuffer — PDF-bomb mitigation (T-07-05)
- collectPdfParts() recursively traverses MIME tree for application/pdf parts
- Generic error messages only — no credential values in logs (T-07-03)
- secure/requireTLS flags derived from encryption field (ssl-tls vs starttls)
2026-06-27 00:02:00 +02:00