Commit Graph

845 Commits

Author SHA1 Message Date
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 ("Ü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
schalli 9bd93ce8ff docs(14): add D-13..D-15 (per-tenant email visibility, RSS feed-list, poll granularity) 2026-07-23 11:58:10 +02:00
schalli 7f757772f1 docs(14): research RSS/email-alert ingestion and module rollout 2026-07-23 11:45:57 +02:00
schalli d71a781be0 docs(state): record phase 14 context session 2026-07-23 11:16:23 +02:00
schalli b655038835 docs(14): capture phase context 2026-07-23 11:16:13 +02:00
schalli 7de0714b30 docs(13): re-verify after normalizer fix — 3/4, Truth 3 deferred (option C)
Normalizer-Gap (quick 260723-e7i) closed criteria 1/2; re-verification
lifts Phase 13 from 1/4 to 3/4. Truth 3 (cross-source fingerprint dedup)
stays PARTIAL: newly-found cpvDivisions asymmetry (scrapers always [],
~79% of DÖE tenders populated) makes exact-match fingerprint miss most
real overlaps. User decision: accept as dormant deferral — dedup is inert
(both scrapers isActive=false), decide with live data once a 2nd source
is activated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:37:28 +02:00
schalli 7bd65e0077 docs(260723-e7i): pre-dispatch plan for normalizer-gap-phase-13-schliessen-tende
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:21:33 +02:00
schalli d5128d5759 docs(quick-260723-e7i): complete normalizer-gap-phase-13-schliessen-tende quick task 2026-07-23 10:21:18 +02:00
schalli 988100576f test(quick-260723-e7i): cover ai-netserver/cosinex bag normalization + DOE regression
- bagRecord() helper builds inline RawTenderRecords for the flat ocdsPayload
  bag shape (no fixtures exist for NetServer/cosinex-DTVP)
- ai-netserver and cosinex-dtvp full-bag mapping, null/empty-field fallback,
  and contentHash-format assertions
- Existing DOE fixture-based assertions untouched, confirming the refactor
  didn't change DOE-path behavior

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:19:00 +02:00
schalli 82c9a48305 feat(quick-260723-e7i): per-sourceType dispatch in TenderNormalizerService
- Extract shared assemble() tail (status/dedupKey/contentHash/publishedAt)
  so it is computed identically across all sources, not duplicated
- Move existing DOE eForms/OCDS extraction into normalizeDoe() (byte-identical
  behavior, regression guard)
- Add normalizeBag() for the flat ocdsPayload bag shared by the NetServer and
  cosinex/DTVP scraper adapters ({title, buyerName, procedureType,
  legalFramework, deadlineAt}); legalFramework deliberately not mapped
- normalize() dispatches on raw.sourceType, defaulting to the DOE path so the
  additive SourceType union never throws

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 10:18:16 +02:00
schalli 9d5aca0e1b docs(13): phase verification — gaps_found (normalizer stub blocks criteria 1/2/3)
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m36s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 09:58:59 +02:00
schalli 7602795622 docs(13-05): complete cosinex/DTVP adapter plan 2026-07-23 09:51:43 +02:00
schalli 6fe0dd07fe feat(13-05): register CosinexAdapter as tenders module provider
Adds CosinexAdapter to TendersModule's providers and registers it with
SourceRegistry at DI boot, alongside DoeOpenDataAdapter/NetServerAdapter
(cosinex-dtvp is not AGB-denylisted, so registration succeeds). Seeds a
cosinex-dtvp TenderSourcePollConfig row with isActive: false, matching
the ai-netserver "framework ready, activation deferred" stance (D-02).

tsc --noEmit clean; src/tenders slice: 21 files, 221/221 tests pass
(205 pre-existing + 16 new cosinex.adapter.spec.ts).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 09:49:19 +02:00
schalli 12fc5ac02d feat(13-05): add cosinex/DTVP source adapter with fixture tests
Separate HTML adapter for the cosinex Vergabemarktplatz (DTVP) satellite
(sourceType='cosinex-dtvp'), distinct from the NetServer adapter since
cosinex markup differs structurally. Live inspection (2026-07-23) found
the "Aktuelle Bekanntmachungen" results table is fully server-rendered
(not JS-dependent as D-01 anticipated), so selectors are fully populated
rather than falling back to a needs-JS stub — parses publish date,
deadline (or "nv"), title, legal framework/procedure type, buyer name,
and a real per-notice deep link (pid) into RawTenderRecord[].

Rule 1 fix: cosinex serves charset=ISO-8859-1 with raw Latin-1 bytes for
umlauts (not HTML entities); Response.text() always UTF-8-decodes per
the Fetch spec, so the adapter reads arrayBuffer() and decodes explicitly
via TextDecoder('iso-8859-1') to avoid mojibake.

16 spec tests pass against a live-captured fixture (20 rows, transcoded
to UTF-8 on disk): full-fixture parse, deadline/publish date parsing,
nested-<abbr> procedure-type extraction, umlaut decoding, empty/broken
HTML and missing-pid row fallback, fetch-throw/non-2xx fallback, no-axios
and no-input-interpolated-URL guards.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 09:48:21 +02:00
schalli 775ed153b7 docs(13-04): complete NetServer adapter plan
Tessera CI/CD / Lint & Type Check (push) Successful in 48s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m43s
2026-07-23 09:16:04 +02:00
schalli 88572f5694 feat(13-04): NetServerAdapter im Modul registriert (INGEST-02)
- NetServerAdapter als Provider ergaenzt, additiv neben DoeOpenDataAdapter
  in onModuleInit ueber SourceRegistry.register() registriert (Denylist-
  Gate erlaubt tender24/lhs-vpbw/vergabe.landbw, keine auf der Denylist)
- TenderSourcePollConfig-Row fuer 'ai-netserver' geseedet, isActive:false
  (Aktivierung bleibt Admin/Seed-Entscheidung, D-02)
- tsc --noEmit clean, src/tenders-Suite 20 Dateien/205 Tests gruen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 09:14:24 +02:00
schalli 043ada9438 feat(13-04): NetServer-Adapter fuer tender24/lhs-vpbw/vergabe.landbw (INGEST-02)
- Package-Legitimacy-Checkpoint bestaetigt: cheerio (sauberer Gate-Pass,
  27M Downloads/Woche) statt node-html-parser installiert
- EIN config-getriebener NetServerAdapter bedient alle 3 AI-AG-Portale
  via PublicationSearchControllerServlet-Trefferliste, parst <table> mit
  cheerio zu RawTenderRecord[], sourcePortal je Zeile korrekt
- sourceNoticeId = data-oid (stabil, per Zeile eindeutig); sourceUrl =
  Such-URL als dokumentierter Best-Effort-Fallback (kein Deep-Link ohne
  JS-Ausfuehrung ermittelbar, Open Question 2)
- Fehlertoleranz: try/catch pro Zeile + pro Portal, []-Fallback bei
  Totalausfall, AbortController 15s-Timeout, hardcodierte Portal-URLs
  (SSRF-Guard T-13-04-01)
- Live-gecapturte Fixture (tender24.de, 2026-07-23) mit Umlaut-/&amp;-
  Buyer, gefuellter und leerer Deadline; 14 Spec-Tests gruen

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 09:13:01 +02:00
schalli 2c28605397 docs(13-06): complete multi-source read surface plan 2026-07-23 08:57:25 +02:00
schalli bf61316500 feat(13-06): TenderDetail renders all cross-source links
Tender.sources[] added to the API client type (sourcePortal, sourceUrl,
sourceNoticeId). TenderDetail now renders one link per TenderSource
with a German portal label (DÖE/tender24/DTVP/...), falling back to
the existing single sourceUrl block when sources is missing or empty
(older responses, single-source tenders).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:56:06 +02:00
schalli 166194fa04 feat(13-06): getTender include sources[] (SCHEMA-03 read surface)
GET /modules/tender-radar/:id now includes the TenderSource relation
(sourcePortal, sourceUrl, sourceNoticeId) so a cross-source-deduped
tender's detail response carries links to all its source portals, not
just the single primary sourceUrl column. Route order unchanged (:id
stays after all static routes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:55:02 +02:00
schalli 1c523c039c docs(13-03): complete dedup-resolver-fan-out-wiring plan 2026-07-23 08:53:33 +02:00
schalli 1d4f9cf1b3 feat(13-03): wire SourceRegistry + TenderDedupService into TendersModule
Register SourceRegistry and TenderDedupService as providers.
onModuleInit registers DoeOpenDataAdapter with the registry before the
scheduler's first tick — the DI-boot-time enforcement point for the
INGEST-07 denylist gate (D-06). This is Wave 2's sole writer of
tenders.module.ts; 13-04 (NetServer) and 13-05 (cosinex) add their
own registry.register(...) calls additively in later waves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:51:47 +02:00
schalli d453dbbd1f feat(13-03): pollDueSources fan-out over all active sources (SCHEMA-03)
Replace the DÖE-only findUnique with findMany({isActive:true}) fan-out
(poll-once-fan-out-many, D-01). Each active TenderSourcePollConfig is
resolved through SourceRegistry.get(sourceType) and processed inside
its own try/catch (catch-per-source, D-01) — one broken/blocking source
no longer aborts the tick for the others. dedupActive =
activePortalCount >= 2 (D-05) is computed once per tick and passed to
TenderDedupService.resolve(), which now replaces the direct
tender.upsert call. Delta-only matchDelta boundary (D-07) preserved:
only genuinely-created tender IDs across all sources are collected.

Extended tender-ingestion.service.spec.ts: multi-config fan-out,
catch-per-source isolation, dedupActive gate assertion, adapter-missing
skip, plus the existing SCHEMA-02/D-07/retention/day-cursor suites
updated to the new registry+dedup constructor shape (all green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:50:57 +02:00
schalli 1cd2fcfa01 feat(13-03): implement TenderDedupService three-tier resolver (D-04/D-05)
resolve(n, {dedupActive}) matches OCID -> source:noticeId -> fingerprint
(fingerprint tier hard-gated by dedupActive, D-05). On any match the
existing Tender gets an additional TenderSource attached (D-03 merge)
instead of a new Tender row; SCHEMA-02 change-detection is preserved
inline (matched Tender's mutable fields refresh when contentHash
differs, exactly as the old direct tender.upsert UPDATE branch did).
No match -> tender.create (with computed fingerprint) + tenderSource.create.
Plain PrismaService, no forTenant()/RLS (T-10-09).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:48:12 +02:00
schalli 605ea4cf80 test(13-03): add failing spec for TenderDedupService (D-04/D-05)
RED-first: three-tier dedup resolver spec (OCID -> source:noticeId ->
fingerprint), D-05 inert-proof (dedupActive=false skips fingerprint
tier -> two Tender rows despite equal fingerprints), and SCHEMA-02
change-detection preservation (matched contentHash change still
updates mutable Tender fields).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:47:37 +02:00
schalli 83d8eff40d docs(13-02): complete source-registry-denylist plan 2026-07-23 08:44:53 +02:00
schalli 0fa9567571 feat(13-02): implement SourceRegistry with hard denylist gate
GREEN — SourceRegistry.register() throws DeniedPortalError when any
of an adapter's declared portals is in DENYLISTED_PORTALS
(vergabe24, aumass), enforced at DI-registration time (INGEST-07/
D-06), not just documented. get()/activeAdapters() support the
Plan 13-03 poll-once-fan-out-many scheduler. 6/6 tests pass, no
Prisma/scraping import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:43:29 +02:00
schalli 78b17ef28b test(13-02): add failing SourceRegistry denylist-gate spec
RED — proves Erfolgskriterium 4 (INGEST-07): registering an adapter
whose portals include vergabe24 or aumass must throw DeniedPortalError,
including a mixed portals array with one denylisted entry. Also covers
legitimate register/get/activeAdapters happy paths. Fake adapter stub,
no real scraping.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:42:51 +02:00
schalli 1b11ada112 feat(13-02): generalize adapter contract with portals[] array
TenderSourceAdapter gains a readonly portals: readonly string[] field
so one adapter can serve multiple portals (NetServer: 3, Plan 13-04)
and so SourceRegistry can gate registration per-portal (INGEST-07).
DoeOpenDataAdapter declares portals = ['doe-opendata'] additively,
no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:42:23 +02:00
schalli a547a1d31d docs(13-01): complete fingerprint + TenderSource datenkern plan 2026-07-23 08:40:47 +02:00
schalli c2a60212fe feat(13-01): widen SourceType to open union, add NormalizedTenderFields.fingerprint
SourceType now covers 'doe-opendata' | 'ai-netserver' | 'cosinex-dtvp'
(13-RESEARCH Pattern 1) so the Plan 13-04/05 adapters can register
without further type-contract changes. NormalizedTenderFields gains an
optional fingerprint field for the SCHEMA-03 dedup resolver (Plan
13-03) to populate later. tsc --noEmit clean; full API suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:38:59 +02:00
schalli 447fb74e0c feat(13-01): add TenderSource model + Tender.fingerprint, backfill 2851 rows
Additive schema change (SCHEMA-03/D-03/D-04): new model TenderSource
(1:n Tender, @@unique[sourcePortal, sourceNoticeId], onDelete Cascade)
and a nullable Tender.fingerprint column + index. dedupKey stays
unchanged as the SCHEMA-02 upsert target.

Migration 20260723120000_add_tender_source applies in strict order
(Pitfall 5): table+column create, then one TenderSource row per
pre-existing Tender via SQL INSERT/SELECT, then the unique constraint.
Applied locally against the tessera dev DB (container IP, no host
port) — verified via psql: TenderSource count == Tender count == 2851.

backfill-tender-source.ts is a one-time script that computes
Tender.fingerprint via the Task-1 tenderFingerprint() function
(Decimal->number conversion for estimatedValue, T-13-01-03) — run via
the compiled dist/ output (source uses standard extensionless TS
imports for tsc compatibility). Confirmed: 2851/2851 rows backfilled,
idempotent re-run verified.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:38:26 +02:00
schalli c6cac696ff feat(13-01): implement tenderFingerprint pure NULL-tolerant dedup key
GREEN: title+buyer dominant, CPV division (order-independent, dedup'd),
value bucketed by order-of-magnitude, deadline truncated to day-grain.
sha256 hex, deterministic, no I/O — foundation for the Task-2 backfill
and the Plan 13-03 dedup resolver's fingerprint tier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:33:40 +02:00
schalli 063ba5b180 test(13-01): add failing test for tenderFingerprint (SCHEMA-03)
RED: NULL-tolerant fingerprint (title+buyer+cpv dominant, value-bucket,
deadline-day), collision guard, umlaut normalization, deterministic
sha256. Implementation follows in the next commit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:33:17 +02:00
schalli f3cd702197 docs(13): revise plans per checker FLAG — SCHEMA-02 preserve + task order
- 13-03: dedup resolve() created=false branch preserves Phase-10 SCHEMA-02
  change detection (mutable fields + contentHash on changed re-poll); spec
  covers it. Prevents silent regression of DÖE re-poll updates.
- 13-01: reorder so fingerprint fn (Task 1) precedes fingerprint backfill
  (Task 2) — removes forward reference.
- 13-VALIDATION: task-id + coverage rows updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 08:31:27 +02:00
schalli 74c00166e2 docs(13): create phase plan — scraping adapters + cross-source dedup
6 plans (INGEST-02/03/07, SCHEMA-03) + Nyquist validation.
Core (registry, fingerprint, dedup, TenderSource, backfill) lands
first and is green independent of live scraping (D-01).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 08:24:16 +02:00
schalli f2e0fc8cef docs(13): phase research — portals live-verified, SourceRegistry, fingerprint dedup
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:14:25 +02:00
schalli b98d2e5993 docs(13): phase context — scraping adapters, fuzzy cross-source dedup, denylist
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 08:06:41 +02:00
schalli a1cf05404c feat(auth): LDAP login — authenticate imported users against the directory
Tessera CI/CD / Lint & Type Check (push) Successful in 47s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m21s
LDAP-imported users have no local passwordHash, and validateUser only checked
the local password, so they could never log in. Now a passwordless user with
an ldapDn is authenticated by binding as their OWN DN with the entered
password against the tenant's active LDAP config (reusing the ldaps TLS-skip
option). Empty passwords are rejected before binding to avoid AD's
unauthenticated-bind bypass. Local-password users are unchanged.

LdapService.verifyUserCredentials added; LdapModule now exports
LdapConfigService; AuthModule imports LdapModule (no circular dep). 8 new
specs (bind success/fail, empty-password guard, login via bind, wrong pw, no
config, no ldapDn, inactive). API 226 green, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:51:23 +02:00
schalli af9e968c6f feat(ldap): opt-in skip TLS verification for ldaps (internal CA)
Tessera CI/CD / Lint & Type Check (push) Successful in 45s
Tessera CI/CD / Tests (push) Successful in 49s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m45s
Add a per-tenant "Skip TLS certificate verification" toggle to the LDAP
admin page so admins can connect to an AD whose ldaps:// certificate is
signed by an internal/self-signed CA (Node error: "unable to verify the
first certificate"). When enabled, ldapts is given
tlsOptions.rejectUnauthorized=false; the flag is ignored for plain ldap://
(no TLS). Defaults to full verification.

New Boolean column LdapConfig.tlsRejectUnauthorized (@default(true)) +
migration; wired through DTOs, config service, all Client creations
(test/groups/user-search/import/sync) and the test-connection endpoint. UI
checkbox with an insecure-network warning (de/en). 3 new service specs;
API 218 green, web 131 green, both apps tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 14:18:55 +02:00
schalli 38face43b4 feat(ldap): individual user search + selective import with dedup
Tessera CI/CD / Lint & Type Check (push) Successful in 49s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m45s
Add an AD single-user search (by cn/sAMAccountName/displayName/mail) and a
selective import to the LDAP admin page, alongside the existing group/OU
filter. Imported users are deduped against existing ones by (ldapDn, then
username): a manually-imported user carries its ldapDn, so a later
department/group sync matches and updates it in place instead of creating a
duplicate. Search results flag alreadyImported; import skips existing users
and links a missing ldapDn. Extracted shared mapEntry/upsertMappedUser
helpers so sync and manual import resolve identity identically.

Backend: GET /ldap/users/search, POST /ldap/users/import (RFC-4515 escaped
query, ADMIN-guarded). 6 new service specs (search flags, create, skip,
ldapDn-link, denylist). Full API suite 215 green, both apps tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:52:50 +02:00
schalli 0dd104054b docs(12): phase verified — live email UAT passed (digest sent via tenant SMTP, no double-send)
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 3m45s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:57:20 +02:00
schalli f0948bcab1 docs(12): phase verified — 4/4 criteria, 340 tests green, UAT pending rebuild
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:37:09 +02:00
schalli 314e83f5c1 docs(12-04): complete digest-interval + Sofort-Alert UI plan 2026-07-22 09:32:45 +02:00