Commit Graph

588 Commits

Author SHA1 Message Date
schalli 8a4bb32847 docs(15): create phase plan — 8 plans, 4 waves, PERM-01..07
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 46s
Tessera CI/CD / Build & Publish Images (push) Successful in 6s
2026-08-04 14:39:46 +02:00
schalli ff22178847 docs(state): record phase 15 UI-SPEC session
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 6s
2026-08-04 13:58:34 +02:00
schalli 4f78999238 docs(15): UI design contract
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
2026-08-04 13:58:33 +02:00
schalli 37ab7c537d docs(15): record owner-approved typography exception, add icon-only aria-labels
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 6s
2026-08-04 13:54:44 +02:00
schalli 2f354cd59c docs(15): UI design contract
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 48s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
2026-08-04 13:39:00 +02:00
schalli 960acb55c5 docs(phase-15): add validation strategy
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 51s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m55s
2026-08-04 13:32:14 +02:00
schalli 32c4ec3fc8 docs(15): research phase domain 2026-08-04 13:30:53 +02:00
schalli d4ae20b300 docs(15): add PERM-01..07 requirements and widget success criterion 2026-08-04 11:59:50 +02:00
schalli ce96e5a5e7 docs(state): record phase 15 context session 2026-08-04 10:52:41 +02:00
schalli 6859c7504e docs(15): capture phase context 2026-08-04 10:52:37 +02:00
schalli 7e1b4a2964 docs(roadmap): add Phase 15 module permissions (groups & user grants)
Two-level module access: tenant activation stays a prerequisite, plus new
per-group and per-user grants. Records the four design decisions taken with
the user: Tessera-owned groups with optional AD binding, closed-by-default
access, ADMIN/SUPER_ADMIN bypass within their tenant, and access on/off only
(no permission levels inside modules). Starts milestone v1.2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 08:42:56 +02:00
schalli bd84a26824 docs(260729-d3k): add plan + verification (passed 6/6) for LDAP multi-base-DN
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 48s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m40s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 09:43:06 +02:00
schalli 97465a6f25 docs(260729-d3k): complete LDAP multi-base-DN quick task 2026-07-29 09:40:29 +02:00
schalli 96be7e168b feat(260729-d3k): multi-line Base-DN textarea + reworked scope i18n
- Base-DN admin field is now a multi-line textarea (one DN per line),
  value stays a single newline-separated string, no schema change
- baseDnHint key added (de/en) explaining the Base-DN(s) sync scope
- groupFilter.description/emptyMeansAll reworded: group filter is an
  optional extra restriction; empty selection means all users under
  the base DN(s) are synced (drops the old "nothing is synced"
  framing)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 09:37:51 +02:00
schalli 5cbd530a87 feat(260729-d3k): multi-base LDAP sync scope + re-keyed no-op guard
- parseBaseDns() splits the newline-separated baseDn field into a list
- syncUsersForTenant no-op guard re-keyed on empty parsed base-DN list
  (was empty groupFilterDns) — the sole condition that skips search +
  the deactivation loop, preventing mass-deactivation on an
  unconfigured config
- collectSearchEntries/listGroups/searchUsers loop every base DN and
  merge/dedupe results by entry dn
- empty groupFilterDns is no longer a no-op: it now performs a normal
  multi-base search with no memberOf restriction
- groupFilterDns ou= entries stay additional search bases; group DNs
  become an optional memberOf constraint applied to every base search
- spec: replaced empty-groupFilterDns no-op test with empty-base-DN
  no-op test, added multi-base merge/dedup test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 09:36:55 +02:00
schalli 5cdd48d864 docs(260728-lih): add plan + verification (passed 6/6) for LDAP selective sync
Tessera CI/CD / Lint & Type Check (push) Successful in 44s
Tessera CI/CD / Tests (push) Successful in 46s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m44s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 15:47:22 +02:00
schalli fd9c1bee34 docs(260728-lih): complete LDAP sync selektiv und auto-sync default plan 2026-07-28 15:43:37 +02:00
schalli 63a07abb47 feat(260728-lih): default LDAP create-form syncIntervalMin to 0 + reword copy
- New-config create form now defaults syncIntervalMin to 0 (matches
  backend default, auto-sync off by default)
- de+en groupFilter.description + emptyMeansAll reworded: empty
  selection now says "nothing is synced" instead of "imports everyone
  under the base DN" (matches the backend semantic change)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 15:42:10 +02:00
schalli 57bc7f96b3 feat(260728-lih): make LDAP sync strictly selective (empty selection = no-op)
- collectSearchEntries() returns [] on empty/undefined groupFilterDns
  instead of scanning the whole baseDn subtree
- syncUsersForTenant() early-returns an empty successful result before
  any LDAP search or the deactivation loop when groupFilterDns is empty,
  so an empty selection can never mass-deactivate existing LDAP users
- Updated exclude-list tests to use a non-empty groupFilterDns; added a
  dedicated no-op test proving empty selection performs zero search/
  create/update/deactivate operations

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 15:41:33 +02:00
schalli c54e424c05 feat(260728-lih): default LDAP syncIntervalMin to 0 (auto-sync off)
- LdapConfig.syncIntervalMin default changed 60 -> 0
- New migration sets column DEFAULT only, no data rewrite
- isActive @default(true) left unchanged (gates LDAP login only)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 15:40:28 +02:00
schalli 7e5a6a6e01 docs(planning): add v1.1 milestone audit and 260723-lvg quick plan
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-28 14:55:27 +02:00
schalli ae85b91468 fix(admin): refresh sidebar after module toggle on admin page
Tessera CI/CD / Lint & Type Check (push) Successful in 52s
Tessera CI/CD / Tests (push) Successful in 54s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m25s
Activating/deactivating a module from the admin modules page updated only
the page's local state — the sidebar (which refetches its active-module
list on the shared marketplace-store sidebarRefreshKey signal) was never
bumped, so the module's nav link only appeared/disappeared after a manual
full page reload. The marketplace pages already call bumpSidebarRefresh()
after a toggle; mirror that here.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:10:39 +02:00
schalli 51501aa7ee docs(260723-lvg): complete "Jetzt abrufen" quick task summary
Tessera CI/CD / Lint & Type Check (push) Successful in 1m13s
Tessera CI/CD / Tests (push) Successful in 1m5s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m52s
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:55:23 +02:00
schalli bdaf154f66 feat(260723-lvg): add "Jetzt abrufen" manual poll button to tender radar
PollNowButton sits next to the settings gear in the tender-radar header,
mirrors the DKV spinner/disabled UX, and bumps a refreshKey on success to
refetch ResultsList without touching any filter. Failure surfaces an
i18n error (de/en parity). pollNow() client hits POST
/modules/tender-radar/poll-now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:54:43 +02:00
schalli 7502f97e85 feat(260723-lvg): add admin-gated POST /poll-now endpoint for tender radar
Manual "Jetzt abrufen" trigger delegates to
TenderIngestionService.pollDueSources() — the same fan-out tick the
scheduler cron runs. Gated to ADMIN/SUPER_ADMIN (T-lvg-01, DoS) and
declared before @Get(':id') per the established route-order convention.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:52:44 +02:00
schalli e1358d70fd fix(tenders): register poll cron in onApplicationBootstrap (fresh-DB bootstrap)
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 48s
Tessera CI/CD / Build & Publish Images (push) Successful in 26s
On a fresh database the DÖE poll cron was never registered: TenderScheduler
read the doe-opendata poll config in its onModuleInit, which raced ahead of
TendersModule.onModuleInit seeding that config. The scheduler saw the config
absent → skipped registering the single global cron that drives pollDueSources
(DÖE + RSS + email-alert) → the platform ingested NOTHING until a second restart.
Observed live on a fresh prod DB (0 tenders, 'doe-opendata config inactive —
cron job not registered', lastIngestedDay null despite isActive=true).

Move the scheduler to onApplicationBootstrap, which runs after every module's
onModuleInit, so the seed is guaranteed complete before the config is read.
Adds a regression test asserting the lifecycle choice.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:30:04 +02:00
schalli cc57de7313 feat(tender-radar): add settings gear link on module page
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m26s
The tender-radar settings page (with the E-Mail-Alerts form) was only
reachable by manually typing the literal URL /modules/tender-radar/settings
— no on-screen link existed, and appending /settings to the dynamic
[category]/[moduleSlug] URL returns 'Modul nicht gefunden'. Add a gear-icon
Link (mirroring dkv-fleet's pattern) pointing at the literal settings route,
plus a tenderRadar.page.settingsTitle key in de/en.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:10:46 +02:00
schalli c8bf229524 docs(14): phase verification — 4/5 verified, criterion 2 (EWS live) human_needed
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 48s
Tessera CI/CD / Build & Publish Images (push) Successful in 7s
Alle Code-Pläne (14-01..14-05) fertig + getestet (389/389 API, 151/151 web,
tsc clean). D-13 Mandanten-Isolation (write ownerTenantId create-only +
read OR-filter + getTender 404-guard) im Code bestätigt, Migration nullable
ohne Backfill. Einziger offener Punkt: 14-03 Task 4 = Live-EWS-Test an echtem
Postfach (human_verification, kein Code-Gap).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:28:01 +02:00
schalli eff5d26413 docs(14-05): complete i18n rollout for tender-radar plan
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 55s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m51s
2026-07-23 14:23:23 +02:00
schalli 849aa9b0a9 feat(14-05): convert tender-radar settings + config forms to useTranslations
Converts settings/page.tsx, SourceConfigForm, RssFeedListForm and
EmailAlertConfigForm from hardcoded German strings to
useTranslations('tenderRadar'). Interval bound and RSS-feed removal
validation/error messages use next-intl interpolation ({min}/{max},
{label}). Updates the three affected settings component tests with a
next-intl useTranslations mock mirroring the marketplace test convention.
The entire tender-radar module UI (results, filters, saved searches,
detail, coverage, settings, RSS/email forms) now honors the selected
locale (CONFIG-03, D-10) with no language switcher added (D-11).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:20:52 +02:00
schalli 2ea02a2671 feat(14-05): convert tender-radar results-side components to useTranslations
Converts page.tsx, ResultsList, FilterPanel, SavedSearchBar, TenderDetail
and CoverageBanner from hardcoded German strings to
useTranslations('tenderRadar'). FilterPanel's Bundesland/CPV division
option labels are now looked up by stable code (NUTS-1 prefix / CPV
division code) while the underlying filter *value* sent to the backend
stays the canonical German string the API already matches against.
Portal display slugs (DÖE, DTVP, tender24, ...) in TenderDetail's
portalLabel() are left untranslated as proper-noun identifiers, not UI
copy. Updates the four affected component tests with a next-intl
useTranslations mock mirroring the marketplace test convention. Also
fixes an unrelated `t` parameter shadowing the translations function
inside ResultsList's triage batch-fetch (Rule 1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:15:33 +02:00
schalli d73171b052 feat(14-05): add tenderRadar i18n namespace (DE + EN) with key-parity guard
Introduces the tenderRadar top-level namespace in de.json/en.json covering
page, results, filter (incl. Bundesland/CPV division labels), savedSearch,
detail, coverage, settings, sourceConfig, rssFeeds and emailAlerts groups.
EN translations authored with consistent Vergabe-domain terminology
(Ausschreibung->tender, Vergabestelle->contracting authority, Frist->
deadline, Auftragswert->estimated value). tenderRadar-parity.spec.ts
enforces recursively-flattened de/en key-set equality so no follow-up edit
can silently add a string to only one locale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 14:07:41 +02:00
schalli a4889f8399 docs(14-04): complete denylist transparency plan 2026-07-23 14:01:12 +02:00
schalli 947325f6ad feat(14-04): CoverageBanner "manuell beobachten" denylist block
- Add DenylistedPortal type + fetchDenylistedPortals() to
  tender-radar-api.ts, following the existing credentials:'include' fetch
  convention
- CoverageBanner fetches the denylisted-portals endpoint on mount and
  renders vergabe24/aumass with direct links (rel="noopener noreferrer",
  target="_blank"); block renders independently of the onlyDoe coverage
  note and fails silently on fetch error
- Add CoverageBanner.test.tsx asserting both portal hrefs, independence
  from the coverage note, and fail-silent behavior

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:59:44 +02:00
schalli 28c6c7fee1 feat(14-04): denylisted-portals read endpoint sourced from DENYLISTED_PORTALS
- Add PORTAL_URLS map (vergabe24, aumass) in source-registry.ts, keyed off
  the existing DENYLISTED_PORTALS constant so the portal set is never
  re-declared
- Add GET /modules/tender-radar/denylisted-portals, declared before
  @Get(':id') (route-order pitfall), mapping over DENYLISTED_PORTALS
- Extend tenders.controller.spec.ts: response shape + route-order guard

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:58:25 +02:00
schalli c140d1186a docs(14-03): pause after Tasks 1-3 — Task 4 (live EWS human-verify) OPEN 2026-07-23 13:56:00 +02:00
schalli 48e12523f3 feat(14-03): add email-config admin routes + D-13 read-side visibility filter + EmailAlertConfigForm
buildTenderWhere gains an optional ownerTenantId param: a resolved
requesting tenant sees global tenders (null) plus its own private ones
(OR[global, mine]); an unresolved requester fails CLOSED to global-only —
never an accidental cross-tenant leak.

TendersController: listTenders/getTender resolve the requesting tenant
leniently from the auth context (resolveRequestingTenantId, never throws)
and apply the D-13 filter; getTender 404s (not a distinct "forbidden") when
a tender's non-null ownerTenantId doesn't match the requester, so no
cross-tenant detail leak. New GET/PUT /modules/tender-radar/email-config
routes (Roles ADMIN/SUPER_ADMIN, tenantId from auth context, never the
body) delegate to TenderEmailConfigService — declared before @Get(':id')
per the project's NestJS route-order convention.

Web: EmailAlertConfig type + fetchEmailConfig/saveEmailConfig client
functions; EmailAlertConfigForm mirrors the DKV InboxConfigForm (password
blank on load, only sent when typed — T-07-12), added as a new
"E-Mail-Alerts" section on the existing tender-radar settings page.
Hardcoded German strings — i18n is Plan 14-05.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:53:17 +02:00
schalli 1be6b15249 feat(14-03): add per-tenant encrypted TenderEmailConfig + ownerTenantId write-side (D-13)
Prisma: new TenderEmailConfig model (per-tenant, tenantId @unique, mirrors
DkvModuleConfig) + Tender.ownerTenantId nullable column + index (D-13:
null = global/platform-wide, unchanged for all existing rows and every
public source; set = visible only to that tenant). Migration
20260723113917_tender_email_config_owner_tenant_id applied locally.

TenderEmailConfigService: safe-select admin CRUD (GET never returns the
password, only hasPassword — T-07-12) with DkvService's encrypt-preserve-
empty semantics, via CalendarCryptoService (AES-256-GCM).

RawTenderRecord/NormalizedTenderFields gain optional ownerTenantId,
threaded through TenderNormalizerService.assemble() unchanged.
TenderDedupService's CREATE branch writes ownerTenantId (defaulting to
null); the UPDATE branch deliberately never references it, so a tender
later also seen on a public source is never retroactively hidden.

EmailAlertAdapter.fetchTenders() now does the real per-tenant fan-out:
findMany({isActive:true}) across ALL tenants (deliberate, documented
cross-tenant platform-scheduler read, never forTenant()/RLS), decrypts
each tenant's credentials, picks imap/exchange provider, and tags every
extracted candidate with ownerTenantId — catch-per-tenant so one broken
mailbox never blocks the others.

tenders.module.ts: imports CalendarModule/InboxModule, registers
EmailAlertAdapter + TenderEmailConfigService, seeds an 'email-alert'
TenderSourcePollConfig row (pollGranularity='tick', isActive=false —
no default mailbox to activate yet, D-02 framework-ready stance).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:45:11 +02:00
schalli 8983231196 feat(14-03): add EmailAlertAdapter generic extraction + 'email-alert' normalizer dispatch
GREEN phase (TDD) for Task 1: extractCandidateLinks (cheerio a[href] +
footer-noise filter + MAX_LINKS_PER_EMAIL cap, plaintext regex fallback),
titleFromEmail (subject -> first body line -> fallback), and
sourceNoticeIdFor (sha256 link hash) implement D-04's generic, no-portal-
specific-parser evaluation of alert emails.

SourceType gains 'email-alert'; TenderNormalizerService routes it through
the existing normalizeBag() path (same as ai-netserver/cosinex-dtvp/rss).
EmailAlertAdapter.fetchTenders() is a Task-1 placeholder — Task 2 wires the
real per-tenant fan-out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:38:36 +02:00
schalli 4d6fbb136e test(14-03): add failing spec for email-alert generic link/subject extraction
RED phase (TDD) for Task 1: pure-function tests for extractCandidateLinks,
titleFromEmail, sourceNoticeIdFor — covers HTML + plaintext bodies,
footer-noise removal, link cap, and D-04 no-portal-specific-parser restraint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:37:37 +02:00
schalli 6e3789a0b0 docs(14-02): complete RSS ingestion slice plan
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m15s
2026-07-23 13:33:01 +02:00
schalli 48a2dc1026 feat(14-02): add RSS feed admin routes, API client, and settings UI
Adds GET/POST/DELETE /modules/tender-radar/rss-feeds (Roles-guarded
ADMIN/SUPER_ADMIN), declared before the existing @Get(':id') handler to
avoid NestJS route-order shadowing. Delegates to
TenderRssFeedSourceService; the denylist/SSRF rejection (D-14) surfaces
as a 400 unchanged.

Web: tender-radar-api.ts gains listRssFeeds/createRssFeed/deleteRssFeed
(relaying the backend's specific rejection message via
extractErrorMessage), and a new RssFeedListForm client component renders
an "RSS-Feeds" section on the tender-radar settings page (D-09) — list,
add (with inline denylist error), and remove global feed URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:29:07 +02:00
schalli e812738c3a feat(14-02): add TenderRssFeedSource CRUD, tick poll gate, and wire RssAdapter
Global admin-managed RSS feed list (TenderRssFeedSource, D-08/D-14) with
a save-time hostname/SSRF guard (TenderRssFeedSourceService) — RSS feed
URLs are runtime admin input, so the code-level SourceRegistry denylist
gate does not cover them; a separate check rejects DENYLISTED_PORTALS
hostnames, non-http(s) schemes, and private/loopback hosts.

Adds TenderSourcePollConfig.pollGranularity ('day' | 'tick', D-15):
pollDueSources() branches per source — 'day' sources keep the existing
lastIngestedDay gate byte-unchanged, 'tick' sources (rss) fetch on every
active scheduler tick regardless of lastIngestedDay, since the day-cursor
gate was built for a genuine daily batch-export API and would otherwise
silently cap RSS to one fetch per calendar day.

Wires RssAdapter.fetchTenders() to fan out over active feed rows (native
fetch + AbortController 15s + response-size ceiling, catch-per-feed),
registers it in tenders.module.ts, and seeds the 'rss' poll config
active with pollGranularity='tick' plus a default-active service.bund.de
feed row (subreport-elvis has no single canonical URL — zero rows seeded,
admin adds relevant municipality feeds).

Migration applied locally per project convention (host -> container IP).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:24:36 +02:00
schalli 3a96cbbbe6 feat(14-02): add RssAdapter.parseFeed + 'rss' normalizer dispatch
Fixture-first RSS parsing (INGEST-04): parses live-captured
service.bund.de (pubDate present, numeric-HTML-entity titles) and
subreport-elvis (pubDate absent, CDATA titles) feed shapes into
RawTenderRecord[] via fast-xml-parser, mirroring the DoeOpenDataAdapter
config. SourceType extended with 'rss'; normalize() dispatches 'rss'
through the existing normalizeBag() path unchanged (D-04/D-05).

Rule 1 fix: fast-xml-parser only decodes the 5 predefined XML entities,
not numeric character references — added an explicit decode step so
service.bund.de titles ("&#220;bermittlung...") render correctly
instead of leaking raw entity syntax.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:17:02 +02:00
schalli a47c0c57ee docs(14-01): complete inbox-module extraction plan 2026-07-23 13:11:21 +02:00
schalli c404954bee feat(14-01): add fetchMessages() to InboxProvider + both implementations
- Add InboxMessage type (subject + html/text body) and fetchMessages() to
  the InboxProvider interface, ImapProvider, and ExchangeInboxProvider
- IMAP: findBodyParts() walks the MIME tree for first text/html + text/plain
  parts, reusing the connect/lock/search/fetchAll skeleton; marks \Seen
- EWS: new getItemBodySoap() requests item:Body, extracts BodyType via the
  existing extractAttr/extractAll helpers, marks IsRead via markReadSoap
- Net-new spec coverage (imap.provider.spec.ts, exchange-inbox.provider.spec.ts)
  mocking ImapFlow and httpntlm.post (via require.cache stub, since httpntlm
  is loaded with a raw require() that vi.mock cannot intercept)
- fetchPdfAttachments untouched in both providers (D-02); full API suite
  (301 tests) + tsc --noEmit stay green

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:09:45 +02:00
schalli eb668fd5c8 refactor(14-01): extract DKV inbox providers into shared inbox/ module
- Move ImapProvider, ExchangeInboxProvider, InboxProvider into apps/api/src/inbox/
- Move InboxConfig/InboxAttachment/InboxEmail into new inbox.types.ts
- dkv.types.ts re-exports the moved types so existing DKV imports keep compiling
- DKV switches import paths to ../inbox/... and imports InboxModule
- Pure move + import-path swap: fetchPdfAttachments and all DKV logic unchanged (D-01/D-02)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:02:15 +02:00
schalli 9dd5038575 docs(state): record phase 14 planning session
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 26s
2026-07-23 12:17:58 +02:00
schalli b2c52843aa docs(14): mark RESEARCH open questions resolved (plan-checker warning 2) 2026-07-23 12:17:57 +02:00
schalli cf105f0884 docs(14): create phase plan (5 plans, 4 waves) 2026-07-23 12:14:26 +02:00