Files
tessera-ctl/.planning/STATE.md
T
schalli 86ad95f74e
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 1m19s
Tessera CI/CD / Desktop-Pakete bauen (push) Successful in 5m30s
Tessera CI/CD / Build & Publish Images (push) Successful in 3m12s
docs: Windows-Test bestanden, Review-Fixes seit 26.09., Uebergabe verbraucht
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-30 03:23:59 +02:00

188 KiB
Raw Blame History

gsd_state_version, milestone, current_phase, current_phase_name, status, stopped_at, last_updated, last_activity, last_activity_desc, state_head, progress, milestone_name
gsd_state_version milestone current_phase current_phase_name status stopped_at last_updated last_activity last_activity_desc state_head progress milestone_name
1.0 v1.3 18 desktop-client-fertigstellen verified 22.09.2026: 1.3.0 freigegeben; danach quick-260922-hk4 — Bilderrahmen-Bilder liegen jetzt im Dateibereich (user-files) statt in der Datenbank, Umzug laeuft automatisch beim Start, Selbstheilung aus der alten data-Spalte eingebaut; im Browser nachgewiesen. NAECHSTER SCHRITT, vom Nutzer noch nicht bestaetigt: (1) einmaliges Aufraeumen, damit ein Modul seine Dashboard-Kachel selbst mitbringt (heute sieben Hartkodierungen je Kachel; Katalog zeigt auch Kacheln gesperrter Module; gesperrte Kachel bleibt leer statt zu erklaeren) — das Geruest WIDGET_MODULE_MAP existiert und ist leer; (2) danach das Proxmox-Modul (PVE/PBS/PMG) und seine Kachel. Offen beim Nutzer: Live-Server auf 1.3.0 ziehen, neuen Client per Browser installieren. 2026-09-23T15:30:00.000Z 2026-09-30 Quick 260928-ujj — Design Mosaik uebernommen, Hintergrund pro Benutzer in der DB; Freigabe 1.5.0 4d485432c0
total_phases completed_phases total_plans completed_plans
18 16 89 88
Plattform-Berechtigungen

Project State

Project Reference

See: .planning/PROJECT.md (updated 2026-07-17)

Core value: Eine zentrale Plattform, in der beliebige Workflow-Tools als Module lizenziert, aktiviert und genutzt werden koennen -- ohne zwischen verschiedenen Anwendungen wechseln zu muessen. Current focus: Kein laufender Meilenstein — v1.2 ist abgeschlossen, ein neuer wurde noch nicht begonnen.

Current Position

Phase: 18 (desktop-client-fertigstellen) — COMPLETE (2026-09-17, Verifikation passed, Windows-Bedienprobe bestanden) Plan: 6 of 6 Status: Alle 18 Phasen abgeschlossen; Version 1.2.0 freigegeben. Kein laufender Meilenstein. Nach 1.2.0 auf main (Beta): Bildmarke in Akzentfarbe, CI-Desktop-Skip, Favoriten-Symbol/-Sortierung, Desktop-Server-Adresse, Update in der App (signiert), Versionszeile auf der Setup-Seite — alles verifiziert und auf VM/CI nachgewiesen Last activity: 2026-09-30 - Windows-Test (Tray-Update + Erinnerungs-Toast) bestanden; Review aller Aenderungen seit 26.09. mit 4 Fix-Commits (be1e003, c2e4467, 0710829, 12214a9)

Progress: [██████████] 99%

Performance Metrics

Velocity:

  • Total plans completed: 2
  • Average duration: 12 min
  • Total execution time: 0.38 hours

By Phase:

Phase Plans Total Avg/Plan
01-foundation-portal-shell 2/3 23 min 12 min

Recent Trend:

  • Last 5 plans: 01-01 (16 min), 01-02 (7 min)
  • Trend: improving

Updated after each plan completion | Phase 02-authentication-multi-tenancy P02 | 8min | 3 tasks | 26 files | | Phase 02-authentication-multi-tenancy P03 | 5min | 2 tasks | 20 files | | Phase 05-dashboard-calendar P01 | 14min | 4 tasks | 28 files | | Phase 05-dashboard-calendar P02 | 4min | 4 tasks | 16 files | | Phase 05-dashboard-calendar P03 | 11min | 4 tasks | 14 files | | Phase 05-dashboard-calendar PP04 | 5min | 2 tasks | 11 files | | Phase 06-desktop-client-ci-cd P01 | 5min | 2 tasks | 13 files | | Phase 06 P03 | 3min | 3 tasks | 3 files | | Phase 07-dkv-fleet-module P03 | 5min | 4 tasks | 8 files | | Phase 07-dkv-fleet-module P04 | 7min | 3 tasks | 8 files | | Phase 07-dkv-fleet-module P05 | 8min | 3 tasks | 11 files | | Phase 07-dkv-fleet-module P06 | 5min | 2 tasks | 7 files | | Phase 09 P03 | 17 | 3 tasks | 6 files | | Phase 09-cert-manager-module P04 | 4 | 3 tasks | 5 files | | Phase 09-cert-manager-module P05 | 8 | 3 tasks | 6 files | | Phase 09 P06 | 7 | 3 tasks | 7 files | | Phase 10 P01 | 15min | 3 tasks | 4 files | | Phase 10 P02 | 20min | 3 tasks | 6 files | | Phase 10 P03 | 35min | 3 tasks | 9 files | | Phase 10 P04 | 25min | 3 tasks | 5 files | | Phase 10 P05 | 20min | 3 tasks | 5 files | | Phase 10 P06 | 3min | 2 tasks | 4 files | Per-Plan Metrics:

Plan Duration Tasks Files
Phase 11 P01 8min 3 tasks 11 files
Phase 11 P02 4min 3 tasks 12 files
Phase 11 P03 9min 4 tasks 12 files
Phase 11 P04 12min 2 tasks 5 files
Phase 11 P05 24min 3 tasks 15 files
Phase 11 P06 35min 3 tasks 12 files
Phase 12 P01 35min 3 tasks 7 files
Phase 12 P02 8min 3 tasks 5 files
Phase 12 P03 15min 2 tasks 3 files
Phase 12 P04 12min 3 tasks 10 files
Phase 13 P01 35min 3 tasks 6 files
Phase 13 P02 20min 2 tasks 4 files
Phase 13 P03 45min 3 tasks 5 files
Phase 13 P06 15min 2 tasks 5 files
Phase 13 P04 55min 3 tasks 6 files
Phase 13 P05 40min 2 tasks 4 files
Phase 14 P01 13min 2 tasks 10 files
Phase 14 P02 30min 3 tasks 21 files
Phase 14 P04 25min 2 tasks 6 files
Phase 14 P05 50min 3 tasks 19 files
Phase 15 P01 24min 3 tasks 10 files
Phase 15-modul-berechtigungen-gruppen-user-grants P02 11min 2 tasks 11 files
Phase 15 P05 9min 2 tasks 5 files
Phase 15 P03 32min 3 tasks 8 files
Phase 15 P06 35min 3 tasks 8 files
Phase 15 P07 30min 3 tasks 7 files
Phase 15 P08 30min 2 tasks 12 files
Phase 16 P01 34min 3 tasks 10 files
Phase 16-ad-gruppen-synchronisation P02 5min 3 tasks 5 files
Phase 16-ad-gruppen-synchronisation P03 12min 2 tasks 3 files
Phase 16-ad-gruppen-synchronisation P04 3min 3 tasks 5 files
Phase 16 P05 6min 2 tasks 4 files
Phase 17 P01 76min 3 tasks 12 files
Phase 17 P02 58min 3 tasks 14 files
Phase 17 P03 62min 3 tasks 13 files
Phase quick-260909-ipc P01 55min 3 tasks 8 files
Phase quick-260910-jab P01 70min 3 tasks 16 files
Phase quick-260910-krx P01 26min 3 tasks 7 files
Phase quick-260911-cwh P01 21min 3 tasks 7 files
Phase quick-260911-nke P01 1 Sitzung 3 tasks 27 files
Phase quick-260914-ebg P01 6min 3 tasks 4 files
Phase quick-260914-eym P01 1 Sitzung 3 tasks 29 files
Phase 18 P01 13min 2 tasks 15 files
Phase 18 P02 8 min 2 tasks 3 files
Phase 18-desktop-client-fertigstellen P03 20 min 2 tasks 12 files
Phase 18 P04 9min 2 tasks 15 files
Phase 18 P05 29 min 3 tasks 1 files
Phase 18 P06 21min 3 tasks 7 files

Accumulated Context

Roadmap Evolution

  • Phase 18 added (2026-09-16): Desktop-Client fertigstellen — Installer aus Tessera und am Gitea-Release herunterladbar (User-Entscheidung), Windows-NSIS per Cross-Bau auf dem Linux-Runner, Linux-AppImage, Client-Version = Freigabe-Tag, Update-Hinweis mit Download-Link, Server-Adresse beim Erststart

  • Phase 17 added (2026-08-12): Eigene Ausschreibungs-Quellen je Nutzer. TenderEmailConfig (heute tenantId @unique) und TenderRssFeedSource (heute url @unique, plattformweit) wandern auf userId; die Rollenpruefung faellt fuer diese beiden Abschnitte weg, das Abrufintervall der oeffentlichen Quelle bleibt Admin-Sache. Ausschreibungsdaten bleiben plattform-global (D-03 aus Phase 10 unangetastet) — geaendert wird nur, wer Quellen einspeist, nicht wer Treffer sieht. Ausloeser: Backlog 2026-08-11-tender-radar-einstellungen-mischen-rollen.md; die urspruengliche Zustimmung zur gemeinsamen Konfiguration beruhte auf einer missverstaendlichen Erklaerung.

  • Phase 15 added (2026-08-04): Modul-Berechtigungen — Gruppen & User-Grants. Zweistufiger Modulzugriff (Mandanten-Aktivierung + Grants pro Gruppe/User), Gruppen mit optionaler AD-Bindung, default geschlossen, ADMIN/SUPER_ADMIN umgehen Grants, nur Zugriff an/aus. Startet Milestone v1.2 Plattform-Berechtigungen.

Decisions

Decisions are logged in PROJECT.md Key Decisions table. Recent decisions affecting current work:

  • [Roadmap v1.1]: 5 phases (10-14) derived from 29 v1.1 requirements, standard granularity — DÖE-only MVP (10: ingestion, 11: filter/UI, 12: notifications) ships first as legally-clean demoable slice, then scraping adapters + cross-source dedup (13), then RSS + email-alert long tail (14)
  • [Roadmap v1.1]: Tender data modeled as platform-global (no tenantId on Tender) — only TenderSavedSearch/TenderMatch are tenant+user-scoped; deviates deliberately from the DKV per-tenant template
  • [Roadmap v1.1]: Scheduler must be poll-once-fan-out-many from Phase 10 onward — explicitly NOT the DKV findFirst() single-tenant pattern (documented pitfall)
  • [Roadmap v1.1]: Notification phase (12) requires an explicit matched-vs-notified state with backfill suppression to avoid first-activation email floods and duplicate digest+instant sends
  • [Roadmap v1.1]: INGEST-07 (vergabe24/aumass hard denylist) enforced in the adapter registry in Phase 13, not just documented
  • [Roadmap v1.1]: inbox/ module extraction (ImapProvider/ExchangeInboxProvider out of dkv/) scoped as a one-time prerequisite refactor inside Phase 14, not done earlier
  • [Roadmap]: 6 phases derived from 44 v1.0 requirements, standard granularity
  • [Roadmap]: Research recommends NestJS + Next.js + PostgreSQL RLS + Keycloak + Tauri stack
  • [01-01]: Used Traefik v2.11 instead of v3.4 due to Docker API version incompatibility on host
  • [01-01]: Traefik placed on frontend-net + backend-net for routing to both web and api services
  • [01-01]: API Dockerfile copies full monorepo node_modules structure for pnpm workspace compatibility
  • [01-02]: OKLCH color space for all design tokens (Tailwind v4 native, perceptually uniform)
  • [01-02]: Dark mode uses oklch(0.17 0.01 260) dark gray-blue for comfortable contrast with yellow primary
  • [01-02]: Cookie-based locale (NEXT_LOCALE) instead of URL routing for portal app
  • [01-02]: CSS custom property --current-sidebar-width for responsive main content margin
  • Phase ?: Route groups (auth)/(portal) for layout separation: auth pages standalone, portal pages wrapped in AppShell
  • Phase ?: Controller-level tenant isolation for ADMIN role as defense-in-depth alongside RLS
  • Phase ?: Remember-me controls cookie maxAge (30d session vs browser-session) not separate token type
  • Phase ?: react-grid-layout v2 uses dragConfig/resizeConfig instead of isDraggable/isResizable
  • [05-02]: SearchProvider defaults as constants merged with user DB rows (no seed migration)
  • [05-02]: useRef with explicit undefined initial value for React 19 strict TypeScript
  • [05-03]: DAVClient class constructor instead of createDAVClient factory (tsdav v2 type compatibility)
  • [05-03]: Dynamic imports for ews-javascript-api and @microsoft/microsoft-graph-client (lazy-load)
  • [05-03]: In-memory Map cache with 5-min TTL for calendar events (Redis not needed at current scale)
  • [05-03]: ews-javascript-api imported as any (no TypeScript definitions available)
  • Phase ?: Optimistic UI for calendar visibility toggle — reverts on API error
  • Phase ?: Auto-run testSource after adding new calendar source for immediate feedback
  • Phase ?: URL constructor for client-side https-only validation (T-05-14)
  • Phase ?: StoreExt trait import required for app.store() in Tauri 2.x
  • Phase ?: frontendDist=../src local page, navigate() for runtime URL override
  • Phase ?: CSP connect-src wildcard for configurable server URL (D-02)
  • Phase ?: Plain docker compose build statt build-push-action (Gitea JWT Pitfall 4)
  • Phase ?: Ephemeral runner mode (GITEA_RUNNER_EPHEMERAL=1) fuer Credential-Revokation pro Job
  • Phase ?: Separate docker-compose.ci.yml fuer opt-in CI-Infrastruktur
  • [07-01]: DKV PDF uses two extraction formats: single-tx (tab-separated) vs multi-tx (columnar) — both handled in DkvParserService
  • [07-01]: Research Pattern 4 regex replaced with empirical dual-format tab/columnar parser after testing against real invoice.pdf
  • [07-01]: CalendarCryptoService exported from CalendarModule for DKV credential encryption reuse
  • [07-02]: Max attachment size 25MB enforced in both ImapProvider and ExchangeInboxProvider before buffering (T-07-05)
  • [07-02]: ExchangeInboxProvider uses WellKnownFolderName.Inbox + FindItems (not FindAppointments — email vs calendar EWS API)
  • [07-02]: export type {} required for type-only re-exports under isolatedModules TypeScript setting
  • [07-03]: SettingsService.getStartupSmtpConfig uses findFirst (tenant-agnostic) for MailModule startup transport
  • [07-03]: MailModule forRootAsync factory priority: DB SmtpConfig → MAIL_* env → TESSERA_SMTP_* env → localhost:1025 fallback
  • [07-03]: DkvMailService injects SettingsService (not PrismaService directly) to reuse decryption logic
  • [07-03]: user-files/ path resolved via path.resolve(__dirname, 4 levels up) from apps/api/dist/dkv/ to monorepo root
  • [07-03]: Export prune sorted by mtime ascending (oldest first), delete all beyond last 10
  • [07-04]: DkvScheduler v1 uses findFirst() — single-tenant; multi-tenant scheduling deferred
  • [07-04]: Circular dep DkvService<->DkvScheduler avoided via controller coordination after PUT config
  • [07-04]: CronJob resolved via require() workaround (pnpm strict isolation: transitive dep)
  • [07-04]: rechnungsnummer from email subject regex /d{2}-d{9}-d{3}/; fallback=email-{uid}
  • [07-05]: refreshKey lift: parent increments on checkNow success; InvoiceHistoryTable reruns useEffect
  • [07-05]: onItemsLoaded callback: InvoiceHistoryTable notifies parent; parent passes items to ExportFileList (avoids second fetch)
  • [07-05]: Password blank on load: configToForm() always sets password=''; hasPassword boolean drives UX hint only
  • [07-05]: CsvImportButton replace: two-step inline confirm (not full modal); accept=".csv" client-side guard
  • Phase ?: splitCerts PEM path reuses parsePemChain; P7B sniffs first bytes
  • Phase ?: splitCerts format crt comparison removed — detectFormat returns pem|der|pfx|p7b only
  • [Phase 10]: Tender ist plattform-global (D-03): kein tenantId, keine RLS/forTenant() — Verhindert versehentliches Ausblenden globaler Daten fuer einen zweiten Mandanten
  • [Phase 10-02]: category: 'procurement' fuer Ausschreibungs-Radar im Marketplace gewaehlt (freies kebab-case, keine Enum-Beschraenkung)
  • [Phase 10-02]: Singleton doe-opendata Poll-Config wird direkt in TendersModule.onModuleInit() upserted, isActive:true per Default (D-04)
  • [Phase 10-02]: Platzhalter-Seite tender-radar/page.tsx nutzt hartkodierten deutschen Text statt next-intl (volle i18n ist CONFIG-03, Phase 14)
  • [Phase 10-03]: Real DÖE fixtures live-captured (pubDay=2026-07-19), not synthetic — 8 notices spanning all D-02 tag classes
  • [Phase 10-03]: sourceNoticeId = OCDS release.id (stable, no version suffix), not the zip entry filename
  • [Phase 10-03]: eForms-DE XML primary for deadlineAt/estimatedValue/procedureType; OCDS primary for ocid/buyerName/title/cpvCodes/region/plz
  • [Phase 10-03]: bundesland left null this plan — NUTS-to-Bundesland mapping deferred to Phase 11 filter UI
  • Phase ?: Poll-once-fan-out-many scheduler: single named cron job, no tenant parameter — deliberately drops DKV's activeTenantId/findFirst per-tenant framing (Pitfall D)
  • Phase ?: SCHEMA-02 change detection implemented via prisma.tender.upsert({ where: { dedupKey } }) — identical notice never duplicates, changed contentHash updates in place
  • Phase ?: D-05 retention as two-phase updateMany/deleteMany with deadlineAt:{lt} filters — null-deadline rows structurally excluded, never auto-expired
  • Phase ?: TendersController talks to PrismaService directly (no intermediate service layer) — source-config upsert and global read are simple enough for this plan's scope
  • Phase ?: Comment wording avoids the literal tenantId token in tenders.controller.ts to prevent false-positive grep-gate failures (same pattern as Plan 10-04)
  • Phase ?: TenderQueryDto.status defaults to active at the controller call site, not baked into the DTO, mirroring DkvController's page/limit default-at-usage pattern
  • Phase ?: Admin-Settings-Formular fuer den DOE-Poll (Intervall 5-1440, Aktiv-Toggle) spiegelt InboxConfigForm ohne Credential-Felder, da die DOE-Quelle keine Auth-Oberflaeche hat
  • [Phase 10]: Keine module-loader-Whitelist noetig fuer settings/page.tsx (verschachtelte Route unter bereits whitelisteter tender-radar-Seite)
  • Phase ?: estimatedValue kommt als String (Prisma Decimal) im JSON-Response — formatValue() im Frontend prüft explizit auf null/NaN statt zu koerzieren
  • Phase ?: openOnly/includeNullValue Default-Semantik lebt im Builder, nicht im DTO
  • Phase ?: deadlineFrom/deadlineTo URL-Param-Namen sind 1:1 identisch zu TenderQueryDto-Feldnamen fuer den Saved-Search-Vertrag aus Plan 11-06
  • Phase ?: bundesland-Filter matcht exakt gegen die befüllte, indexierte Spalte (statt region-startsWith); region bleibt als eigenständiger Präfixfilter erhalten.
  • Phase ?: BUNDESLAND_OPTIONS im Web als kleine Konstante gespiegelt (kein Shared-Package) — web nutzt @tessera/shared nicht, 16-Werte-Katalog rechtfertigt keine neue Cross-Package-Abhängigkeit.
  • Phase ?: CPV-Divisions-Kurzkatalog (2-stellig, ~45 Einträge) statt EU-Vollkatalog; hasSome-Match gegen precomputed cpvDivisions-Spalte statt Raw-SQL-Präfix-Match
  • Phase ?: cpv-URL-Param als wiederholter Key (?cpv=45&cpv=71) statt Komma-Join; DTO normalisiert Einzelwert per @Transform zu Array
  • Phase ?: TenderDetail fetcht selbstständig via getTender(tenderId) im useEffect (Muster SourceConfigForm); page.tsx bleibt reiner ?tender-Param-Reader
  • Phase ?: ResultsList.tsx modifiziert (nicht im Plan gelistet) — Rule 3: Zeilen-Klick-Handler war notwendig, um den Plan-eigenen key_link/Done-Kriterium zu erfüllen
  • Phase ?: TenderTriage (neu, per-user, kein forTenant/RLS) + favOnly-Sentinel-ID 'none' für garantierten Zero-Match statt versehentlich ungefilterter Liste
  • Phase ?: TenderSavedSearch hat keinen Tender-FK — speichert nur Filterkriterien, unkritisch bei Retention.
  • Phase ?: page/tender-URL-Params bewusst aus dem Suchprofil-Payload ausgeschlossen (Navigations-/View-State, kein Filter-State).
  • Phase ?: Single notifiedAt field (not two per-channel timestamps) as the matched-vs-notified eligibility gate (12-01, D-06)
  • Phase ?: Delta-only matching (no backfill/suppression table) structurally prevents backfill-flood for new saved-search profiles (12-01, D-07)
  • Phase ?: TenderMailService swallows missing-SmtpConfig and send-failure into a single boolean (never throws) so the digest cron gets one clean success signal for stamping notifiedAt
  • Phase ?: TenderDigestScheduler.runDigest(now) takes an injectable clock parameter for testable Monday-only weekly-digest gating
  • Phase ?: Instant-Dispatch filtert die bereits geladenen savedSearches (kein zweiter Query); notifiedAt/channel='instant' nur nach Erfolg gestempelt — identisches Gate wie Digest (D-06)
  • Phase ?: Digest-interval selector inline in settings/page.tsx (already 'use client'); instantAlert toggle uses plain checkbox for chip-based SavedSearchBar UI
  • Phase ?: SCHEMA-03 fingerprint: title+buyer dominant, CPV division, value-bucket, deadline-day, sha256; dedupKey untouched, fingerprint additive
  • Phase ?: One-time TS backfill scripts run via compiled dist/ output (not raw .ts execution) to keep tsc --noEmit clean
  • Phase ?: SourceRegistry.register() throws DeniedPortalError for any portal in DENYLISTED_PORTALS (vergabe24, aumass), enforced at DI-registration time not just documented (INGEST-07)
  • Phase ?: TenderDedupService tier-2 (source:noticeId) match is unconditional (not gated by dedupActive) — idempotent same-source re-poll must work even with a single active source
  • Phase ?: Fingerprint always computed on tender.create regardless of dedupActive, so DÖE-only tenders become fingerprint-matchable the instant a 2nd source activates without further backfill
  • Phase ?: 13-06: portalLabel()-Map bleibt lokal in TenderDetail.tsx (nicht geteilt) — einziger Consumer bisher; Fallback auf sourceUrl-Block bei fehlendem/leerem sources[].
  • Phase ?: 13-04: cheerio (nicht node-html-parser) fuer NetServer-HTML-Parsing gewaehlt — Legitimacy-Gate flaggte node-html-parser als 'too-new' (Fehlmessung: Latest-Version-Datum statt Package-Alter); cheerio bestand das Gate sauber, Human-Approval eingeholt
  • Phase ?: cosinex/DTVP-Selektoren voll befuellt statt needs-JS-deferred (D-01) — Trefferliste ist server-gerendert, live geprueft 2026-07-23
  • Phase ?: cosinex/DTVP liefert echte Notice-Deep-Links (pid=) — besser als NetServer-Fallback (Such-URL); Rule-1-Fix: arrayBuffer()+TextDecoder('iso-8859-1') statt res.text(), da Portal ISO-8859-1 mit rohen Latin-1-Bytes sendet
  • Phase ?: [quick-260723-e7i]: TenderNormalizerService dispatcht per sourceType (normalizeDoe/normalizeBag) mit geteiltem assemble()-Tail — schliesst den Normalizer-Gap aus 13-VERIFICATION.md fuer NetServer/Cosinex-Bag-Records
  • Phase ?: 14-01: DKV inbox providers moved verbatim into shared apps/api/src/inbox/ module; dkv.types.ts re-exports InboxConfig/InboxAttachment/InboxEmail so no DKV consumer import changed (D-01)
  • Phase ?: 14-01: fetchMessages() added as a sibling method (findBodyParts/getItemBodySoap) on both providers without touching fetchPdfAttachments (D-02); DKV not switched to it
  • Phase ?: 14-01: httpntlm loaded via raw require() bypasses vi.mock — tests seed require.cache with a stub before dynamically importing the provider
  • Phase ?: 14-02: fast-xml-parser does not decode numeric HTML entities (Ü) — added explicit decodeNumericEntities() in RssAdapter so service.bund.de titles render correctly
  • Phase ?: 14-02: TenderRssFeedSource save-time hostname/SSRF guard is a SEPARATE enforcement point from the code-level SourceRegistry denylist (RSS feed URLs are runtime admin input, not covered by the DI-boot-time gate)
  • Phase ?: 14-02: TenderSourcePollConfig.pollGranularity ('day'|'tick') added — 'day' sources keep the byte-unchanged lastIngestedDay gate, 'tick' sources (rss) fetch every active scheduler tick (D-15)
  • Phase ?: 14-02: seeded service.bund.de active-by-default RSS feed; zero subreport-elvis rows (no single canonical URL, admin adds relevant municipality feeds)
  • Phase ?: 14-03: D-13 read filter fails CLOSED for an unresolved requesting tenant (no auth context) — only global tenders visible, never a private-tenant leak
  • Phase ?: 14-03: email-alert TenderSourcePollConfig seeded isActive=false (no safe default mailbox, unlike RSS's service.bund.de) — framework-ready-activation-deferred
  • Phase ?: PORTAL_URLS typed as Record<(typeof DENYLISTED_PORTALS)[number], string> so the compiler enforces a URL for every denylisted portal (no re-declared set, no silent gap)
  • Phase ?: CoverageBanner's denylist block is independent of the onlyDoe coverage-note condition — component renders when either block has content, not gated behind the DOE-only check
  • Phase ?: 14-05: tenderRadar i18n namespace added; Bundesland/CPV filter option values stay canonical German for backend compatibility, only labels translated; portal display slugs left untranslated as proper nouns
  • Phase ?: [260728-lih]: DELIBERATE back-compat break — empty groupFilterDns now means 'sync nothing' (was 'import everyone under baseDn'); early-return guard in syncUsersForTenant runs before search/deactivation so an empty selection never mass-deactivates existing LDAP users
  • Phase ?: [260729-d3k]: syncUsersForTenant no-op guard re-keyed from empty groupFilterDns to empty parsed base-DN list — Base-DN(s) are now the sync scope, groupFilterDns is an optional extra restriction (ou= = extra bases, group DN = memberOf constraint)
  • Phase ?: [15-01]: D-01/D-05/D-06/D-02 wie in 15-CONTEXT.md gesperrt umgesetzt (Nutzer-Checkpoint mit 'proceed' bestaetigt)
  • Phase ?: [15-01]: RLS fuer Group/GroupMembership/ModuleGrant aktiviert (T-15-11) statt sie wie Tender* RLS-frei zu lassen
  • Phase ?: [15-02]: isDefault:true läuft in this.prisma.$transaction([updateMany, update]); partieller Unique-Index aus 15-01 bleibt Sicherheitsnetz
  • Phase ?: [15-02]: remove() fängt zusätzlich P2025 ab (NotFoundException statt unbehandeltem 500) — Rule 2, für Concurrency-Anforderung aus must_haves
  • Phase ?: [15-02]: UserService.create ist die einzige Codestelle für D-11/D-12 — LdapService erbt die Regel ohne eigene Kopie (ldap.service.ts unverändert)
  • Phase ?: [15-05]: WIDGET_MODULE_MAP bleibt am Ende dieser Phase bewusst leer (D-22) — kein neuer Widget-Typ, keine Schemaänderung, nur die Filtermechanik
  • Phase ?: [15-05]: vi.hoisted() für die je-Testfall mutierbare WIDGET_MODULE_MAP-Mock-Referenz — vi.mock wird an den Dateianfang gehoben, ein normaler top-level const wäre zur Factory-Ausführungszeit noch nicht initialisiert
  • Phase ?: [15-03]: assertTargetBelongsToTenant als eigenständige Cross-Tenant-Prüfung eines referenzierten Fremdobjekts vor jedem Grant-Insert (T-15-01), kein Vorbild im Bestandscode
  • Phase ?: [15-03]: Kein Import von ModuleRegistryModule in GroupsModule — ModuleGrantsService injiziert ausschließlich PrismaService
  • Phase ?: [15-03]: getCatalogFlags liefert Map nur für aktive Module, Controller mappt fehlenden Eintrag auf beide Flags false
  • Phase ?: [15-06]: Task 2/3-Split von page.tsx haelt jeden Task-Commit fuer sich buildbar (Task 2 ohne Import der erst in Task 3 entstehenden Komponenten)
  • Phase ?: [15-06]: Gruppen-Erstellung mit sofortiger AD-Bindung laeuft zweistufig (POST /groups, dann PATCH ldapDn), weil CreateGroupDto aus 15-02 nur name entgegennimmt
  • Phase ?: [15-06]: matrixCheckboxLabel/directCheckboxLabel (Wave-4-Schluessel) als next-intl-ICU-select mit granted-Parameter modelliert, ein Schluessel bedient freigeben/entziehen
  • Phase ?: [15-07]: aria-label des Grant-Checkboxes beschreibt die vom Klick ausgeloeste Aktion (granted: String(!isGranted)), nicht den aktuellen Haekchen-Zustand
  • Phase ?: [15-07]: UserAccessModal leitet Gruppenmitgliedschafts-Chips ausschliesslich aus der Vereinigung aller viaGroups-Namen von GET /module-grants/users/:userId ab, kein zweiter Endpoint
  • Phase ?: [15-07]: ActivateModuleDialog ruft onSuccess bereits nach dem erfolgreichen activate-Call auf, unabhaengig vom Ausgang des nachfolgenden module-grants-Calls
  • Phase ?: [15-08]: isAdmin-Gate auf /marketplace und /marketplace/[slug] entfernt (D-08: Katalog bleibt Schaufenster fuer jeden authentifizierten Benutzer) — isAdmin gated jetzt nur noch die Aktivieren/Deaktivieren-Aktion (canManage-Prop)
  • Phase ?: [15-08]: MarketplaceCard-Klick delegiert an getrennte onOpenDetail/onLockedClick-Callback-Props statt eigener Router-Logik in der Karte — bleibt praesentational und ueber vi.fn() testbar
  • Phase ?: [quick-260805-fok]: ensureDefaultGroup-Waechter prueft ausschliesslich group.count === 0, nie die fehlende isDefault-Markierung (D-13); Reparatur laeuft als zweiter sequenzieller await-Schritt in AdminSeedService.onApplicationBootstrap statt als eigener Hook in GroupsModule (Ordering-Falle wie in tender-scheduler.service.ts)
  • Phase ?: Checkpoint 1 (16-01 Task 1): approve-both — Group.internalName + ldapObjectGuid + Unique-Index in einer Migration, freigegeben 2026-08-06
  • Phase ?: Migrationsverfahren angepasst: prisma migrate dev verweigert nicht-interaktive Shell — Ersatz via migrate diff + Handdatei + migrate deploy, inkl. Baseline der 24 Altmigrationen per migrate resolve --applied
  • Phase ?: [16-02]: reassignDefaultBeforeDelete() nutzt bewusst nicht findOwned() — eigenes still-false-Muster fuer Batch-Sync-Laeufe (D-06), kein NotFoundException-Abbruch
  • Phase ?: [16-02]: Namenssperre fuer importierte Gruppen (D-03) ist eine Backend-Invariante in GroupsService.update() (BadRequestException), nicht nur ein UI-Disable
  • Phase ?: [16-02]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] — dieser Plan liefert nur den Backend-Teil, das Requirement schliesst erst mit Plan 16-05
  • Phase ?: [16-03]: syncBoundGroupsForTenant() als eigene, in Task 1 noch unverdrahtete Methode gebaut; Task 2 liefert ausschliesslich die Verdrahtung als Schritt 5a vor 5b samt Call-Order-Test — Reihenfolge ist die zentrale Korrektheitsbedingung der Phase
  • Phase ?: [16-03]: A1/A2-Live-Pruefung gegen ViCoTest nicht durchfuehrbar (kein erreichbares AD in dieser Sandbox) — als WINDOWS.md #4 (unrun-verify) festgehalten, negatives Ergebnis ist Stopp-Grund fuer die D-05-Loeschsemantik
  • Phase ?: [16-03]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] — schliesst erst mit Plan 16-05
  • Phase ?: [16-04]: Group.internalName vorgezogen von Task 2 nach Task 1 (Rule 3) - GroupFormModal.tsx kompiliert sonst nicht
  • Phase ?: [16-04]: PERM-02 bleibt in REQUIREMENTS.md bewusst auf [ ] - schliesst erst mit Plan 16-05
  • Phase ?: [16-05]: PERM-02 in REQUIREMENTS.md auf [x] gesetzt - alle 5 Phase-16-Erfolgskriterien code-vollstaendig ueber 16-01..16-03; dieser Plan liefert die Sichtbarkeitsschicht (Sync-Bericht D-05/D-06) und die dritte D-04-Anzeigestelle (Freigabe-Matrix)
  • Phase ?: [16-05]: A1/A2-Live-Pruefung gegen echtes AD (WINDOWS.md #4) bleibt trotz PERM-02-Abschluss offen - Korrektheitsannahme unter SC-3/SC-4, kein eigenes Erfolgskriterium; negatives Ergebnis waere Stopp-Grund fuer D-05-Loeschsemantik
  • Phase ?: [17-01]: Checkpoint 1 (gate=blocking) 'weiter' — beide Datenbank-Umbauten der Phase freigegeben, gestuetzt auf gemessene 0 Bestandszeilen (lokal + alpha)
  • Phase ?: [17-01]: TenderEmailConfig.userId @unique ersetzt tenantId @unique (D-01); tenantId bleibt denormalisiert, wird auf create UND update mitgeschrieben
  • Phase ?: [17-01]: email-config-Routen von @Roles(ADMIN,SUPER_ADMIN) auf @UseModule('tender-radar') umgestellt — Postfach ist jetzt Nutzereinstellung (D-01, T-17-06 accept)
  • Phase ?: [17-01]: eigene Seite /modules/tender-radar/my-sources statt Erweiterung von /settings/general/account (D-01 offener Punkt 4)
  • Phase ?: [17-02]: Checkpoint-Freigabe aus 17-01 deckte diese Migration bereits ab, kein zweiter Halt (gemessene 1 Bestandszeile blieb plattformweit)
  • Phase ?: [17-02]: Startbestueckung (tenders.module.ts) vorgezogen aus Task 3 nach Task 1 (Rule 3) - find-then-create statt upsert-on-url, Prismas Compound-Unique-Typ verlangt userId als Pflicht-String
  • Phase ?: [17-02]: seedServiceBundRssFeed() aus TendersModule.onModuleInit extrahiert (tenders.seed.ts), damit die Bestueckungs-Idempotenz echten Produktivcode testet
  • Phase ?: [17-02]: DELETE /rss-feeds/:feedId von @Roles auf @UseModule umgestellt - Besitzpruefung im Dienst ersetzt die Rollenpruefung vollstaendig (T-17-07)
  • Phase ?: [17-03]: RssFeedListForm scope-Prop (personal/platform) bedient beide Seiten; Berechtigung entscheidet der Server (T-17-08)
  • Phase ?: [17-03]: createRssFeed-Antwort traegt kein isPlatformWide (nur GET mappt es) — Komponente leitet es lokal aus dem verwendeten scope ab (Rule 1)
  • Phase ?: [17-03]: Anzeige-Rollenpruefung auf settings/page.tsx ueber useAuthStore (unbekannt/erlaubt/verweigert); verbindliche Pruefung bleibt serverseitig
  • Phase ?: [17-03]: REQUIREMENTS.md SRC-01..05 nachtraeglich ergaenzt — Luecke aus 17-01/17-02, dort schon in SUMMARY-Frontmatter gefuehrt
  • [Phase 17]: [260909-cx0]: Benanntes Volume user-files statt Bind-Mount (uid-1001-Eigentuemerschaft aus dem Image)
  • [Phase 17]: [260909-ipc]: resolveEmailForWrite() bleibt dauerhaft ungebunden (Befund A, T-IPC-04) — email/username sind plattformweit @unique
  • [Phase 17]: [260909-ipc]: getAllActiveConfigs()/onApplicationBootstrap() bleiben dauerhaft ungebunden (Befund B) — Klasse von (ldap-config.service.ts, ldapConfig) korrigiert auf beides
  • [Phase 17]: [260909-ipc]: Standardgruppen-Uebergabe (groups.service.ts) bewusst nicht angefasst — Reihenfolgebedingung fuer Etappe 4
  • [Phase 17]: 260910-krx: Bereich dashboard vollstaendig umgestellt — 12 gebunden, 1 begruendet ungebunden (Modulkatalog); getLayout/saveLayout gemeinsam gebunden; saveLayout uebersetzt PrismaClientUnknownRequestError (nicht P2002) in deutsche Konfliktmeldung; WINDOWS #25 fuer die beweisvernichtende Schleife offen angelegt
  • [Phase 17]: [quick-260911-cwh]: Bereich calendar Etappe 2 gebunden — Cache-Schluessel bleibt ohne Mandantenanteil (User.id ist plattformweit eindeutige UUID, Etappe-3-Entscheidung (1) betrifft nur username/email); keine neue Fehleruebersetzung fuer Besitzpruefungen noetig (Wettlauf-Fall wirft P2025, strukturell unerreichbar); refreshCacheInBackground zaehlt nicht als sechster Hintergrunddienst-Fall
  • [Phase 17]: 260911-nke: forTenant(prisma, tenantId, userId?) — optionaler dritter Parameter statt Schwesterhelfer, IS-NULL-OR-Form in den Regeln der zehn persoenlichen Tabellen, sechs Loch-Pruefungen umgedreht
  • [Phase 17]: [quick-260914-ebg]: Zielrollen-Riegel als eigenstaendige Pruefung nach der Mandantengrenze in UserController.update()/remove() eingezogen (Vorlage AuthService.adminResetPassword, T-FH9-04) — WINDOWS #29 geschlossen
  • [Phase 17]: [quick-260914-eym]: forSystem(prisma) als Schwesterhelfer (eigene Detektor-Erkennungsform, Umkehrung der 3b-Begruendung); system_read_policy FOR SELECT auf fuenf Tabellen, SmtpConfig nicht (Mail-Startpfad entfernt, Transport je Versand nach Mandant); DKV-Planer Auftrag je Mandant (promote); Single-Flight-Riegel bleibt prozessweit -> WINDOWS #37
  • [Phase 18]: [18-01]: DesktopController braucht @Inject(DesktopService) explizit, da Vitest ueber esbuild ohne emitDecoratorMetadata transpiliert (sonst desktopService=undefined im echten NestFactory-HTTP-Durchstich).
  • [Phase 18]: 18-02: Cross-Job-Uebergabe per actions/cache (save/restore, Schluessel exakt am gitea.sha) statt upload-/download-artifact, da diese auf der Gitea-Instanz unzuverlaessig sind. — Pitfall 1 aus 18-RESEARCH.md; publish bricht bei Cache-Fehlschlag hart ab (fail-on-cache-miss + explizite Manifest-Pruefung in Workflow und Skript).
  • [Phase 18]: Desktop-Download-Link/Einstellungsseite lesen GET /desktop/latest memoisiert und blenden sich ohne Pakete aus — Wiederverwendung des app-version.ts-Musters (Modul-Ebene-Promise, still bei Fehler)
  • [Phase 18]: 18-04: api_url() als einzige Stelle mit dem /api-proxy-Rewrite-Praefix; Erststart-Seite spricht nur noch ueber window.TAURI.core.invoke statt Modul-Import
  • [Phase 18]: Windows-Werkzeuge-Schritt nach dem Cargo-Zwischenspeicher platziert, nicht davor — Damit die cargo-xwin-Installationspruefung (command -v ...) den per actions/cache wiederhergestellten Stand sieht und das Werkzeug bei warmem Cache nicht bei jedem Lauf neu gebaut wird.
  • [Phase 18]: 18-06: DESK-03/04/05 bleiben Pending bis Windows-Bedienprobe des Nutzers vorliegt; alle Handbuecher/CHANGELOG/REQUIREMENTS nachgezogen, alle Gesamtlaeufe gruen (API 1086, Web 365, Typpruefungen, cargo check).

Pitfalls & Anti-Patterns

Lehren aus abgeschlossenen Sitzungen. Gerettet aus den Checkpoint-Dateien (.continue-here.md), die am 2026-09-07 entfernt wurden.

Muster Beschreibung Schwere Vermeidung
Tautologischer Test Der Test der objectGUID-Suche baute seinen Erwartungswert mit derselben Hilfsfunktion, die der Produktionscode benutzte. Er bestaetigte damit, dass die Funktion zu sich selbst passt — nicht, dass ein Verzeichnis den Filter versteht. Der Fehler ueberlebte Review und 500+ Unit-Tests und haette beim ersten echten Sync alle AD-gebundenen Gruppen geloescht. blocking Tests gegen externe Systeme muessen die Form des Aufrufs pruefen (Buffer statt String, Objekt statt interpoliertem Text), nicht seinen mit Produktionscode erzeugten Inhalt. Siehe ldap.service.spec.ts, die beiden Tests am Ende des syncBoundGroupsForTenant-Blocks.
HTTP 200 als Funktionsbeleg oeffentlichevergabe.de/ui/... ist eine Single-Page-App und antwortet auf JEDE Kennung mit 200 und identischen 1309 Bytes, auch auf NONSENSE123. Ein Statuscode-Test haette "funktioniert" gemeldet. advisory Bei SPA-Zielen den gerenderten Inhalt pruefen (Playwright), nie den Statuscode.
Image-Datum als Aktualitaets-Beleg Das Web-Image trug den 7. August und sah veraltet aus. Tatsaechlich hatten sich die Web-Quellen seither nicht geaendert; Docker-Layer-Caching erzeugt ein bit-identisches Image mit altem Erstellungsdatum. advisory Vor einer Aussage ueber Rueckstand git log -- apps/web pruefen, nicht das Image-Datum.
Server-Compose ist kein Checkout /opt/tessera ist keine Git-Arbeitskopie. Aenderungen an Compose-Dateien im Repository kommen dort nie an; der Deploy holt nur Images. blocking Aenderungen an docker-compose*.yml wirken NICHT auf alpha. Was dort gelten soll, muss zusaetzlich in /opt/tessera/docker-compose.yml eingetragen werden (erlaubt, mit Sicherung). Siehe Backlog 2026-08-11-compose-datei-auf-server-driftet.md.
Stale live container maskiert Fertigstellung Code, Migration und Tests waren gruen, aber der laufende tessera-ctl-api-1 lief auf dem Image von vor der Phase. Niemand hatte je einen echten HTTP-Aufruf gegen die neuen Routen gesehen. Das war kein Code-Defekt, sondern eine unbeobachtete Fertigstellung. advisory Container neu bauen (docker compose up -d --build api web), bevor ein "Tests gruen"-Zustand als nutzersichtbar fertig gilt. Gilt fuer die lokale Entwicklungsmaschine; auf dem Testserver macht der User das selbst.

Infrastruktur-Stand (alpha, Stand 2026-08-11)

Gerettet aus .continue-here.md. Relevant fuer die noch offenen Live-Tests.

  • alpha (192.168.13.12, https://alpha.tessera.ctl.de): API-Image mit allen Fixes; Migrationen 20260811120000_doe_notice_url_backfill und 20260811140000_encrypt_ldap_bind_password angewandt.
  • /opt/tessera/.env: traegt TESSERA_ENCRYPTION_KEY UND den alten CALENDAR_ENCRYPTION_KEY mit gleichem Wert. Sicherung .env.bak.20260811. Die alte Zeile kann weg, sobald kein Rueckfall auf aeltere Images mehr denkbar ist.
  • /opt/tessera/docker-compose.yml: von Hand um die neue Variable ergaenzt, Sicherung docker-compose.yml.bak.20260811. Driftet vom Repository ab.
  • Testdaten auf alpha: 405 Benutzer und die AD-gebundene Gruppe Claude_VT (9 Mitglieder) aus dem Sync vom 2026-08-11. Der User wirft vor dem Go-live ohnehin alles raus.
  • Deploy-Regel: pull/up/restart macht der User. Konfigurationsdateien auf dem Server darf Claude bearbeiten (seit 2026-08-11), vorher sichern.
  • Kein AD-Schreibzugriff (Stand 2026-08-11): Phase-16-UAT 1/3/4 waren deshalb nicht ausfuehrbar und wurden als dokumentierte Annahme geschlossen. Betrifft direkt den offenen Live-Test WINDOWS #4 — dessen Annahme A1 (objectGUID uebersteht Umbenennung) verlangt eine echte Umbenennung im Verzeichnis, also Schreibrechte. Vor dem Live-Test klaeren, ob diese inzwischen vorliegen.

Pending Todos

  • [2026-08-11] [module-registry] Jeder Mandanten-Admin kann sich jedes Modul selbst freischalten — Aktivierung ohne Lizenzpruefung — todo file
  • [2026-09-07] [web/branding] Administrator kann das Aussehen branden — eigenes Logo und eigene Farben je Mandant — todo file
  • [2026-09-14] [module-registry] Lizenzmodell — Betreiber gibt Modul je Server mit Lizenzanzahl frei, Firmenadmin lizenziert an bis zu N … — todo file
  • [2026-09-15] [desktop] Desktop-Client auslieferungsreif machen — Versions-Check, Windows-Installer, Abnahme, CI-Bau — todo file

Blockers/Concerns

  • [Roadmap v1.1]: DÖE OpenData API pagination/rate-limit parameters unverified (Swagger UI is JS-rendered) — resolve via a live API call during Phase 10 planning, not assumed from docs.
  • [Roadmap v1.1]: Whether AI-AG NetServer / cosinex VMP search pages require JS rendering is unverified — needs a Phase 13 start-of-phase spike before committing to playwright.
  • Phase 14 Plan 03 (14-03): Task 4 human-verify OPEN — needs a real portal-alert mailbox (incl. Exchange/EWS live path) from the operator before INGEST-05's Exchange path is production-ready. Tasks 1-3 complete and committed (4d6fbb1, 8983231, 1be6b15, 48e1252); API 387/387, web 144/144 green.
  • Phase 15 Plan 06 (15-06): manueller Browser-Durchklick nicht ausgefuehrt — ERLEDIGT 2026-09-07, Gegenprobe WINDOWS.md unrun-verify #1 nachgeholt und bestanden
  • [260909-cx0] WINDOWS #17 (user-files-Volume) bleibt offen: Repository-Fix committet, aber /opt/tessera/docker-compose.yml auf alpha weicht ab und muss vom Nutzer manuell um dieselben zwei Zeilen ergaenzt werden (vorher sichern), danach Container neu erstellen und Ledger schliessen (gsd-tools windows fixed 17)

Quick Tasks Completed

# Description Date Commit Directory
260630-gbh User Settings: Passwort ändern (nur non-LDAP) + Profilbild setzen 2026-06-30 merge 260630-gbh-user-settings-passwort-ndern-nur-non-lda
260701-abc Fix i18n: marketplace.accessDenied + calendar form hardcoded EN strings 2026-07-01 e5b76b7 260701-abc-i18n-missing-keys
260707-csw LDAP AD Anbindung: Zugangsdaten aus XWiki vorbefuellen und Import-Filter fuer Benutzer/Gruppen 2026-07-07 a5c500d 260707-csw-ldap-ad-anbindung-zugangsdaten-aus-xwiki
260707-lgh Favoriten-Widget: Icon-Proxy fuer Cross-Origin-Resource-Policy-Seiten (claude.ai) 2026-07-07 f06a2ff 260707-lgh-favoriten-widget-icon-proxy-fuer-cross-o
260708-cuc Fix: FavoriteLink-Tabelle fehlt in Prod-DB, nie als Migration committed (500 auf GET /favorites) 2026-07-08 afef9b2 260708-cuc-fix-favoritelink-tabelle-fehlt-in-prod-d
260708-rev LDAP: CTL-spezifisches AD-Prefill entfernt (Multi-Tenant, "das war nie das Ziel") 2026-07-08 8e8305c (direct)
260708-tst LDAP: Verbindung testen vor dem Speichern einer Config moeglich 2026-07-08 39aa4bf (direct)
260709-abd LDAP: anonymous bind (bindDn/bindPassword optional, Schema nullable + Migration) 2026-07-09 010aceb (direct)
260709-ciu Auth: Benutzernamen ueberall case-insensitive (Login, Seed, LDAP-Sync + Daten-Migration) 2026-07-09 baff7ce (direct)
260709-lda LDAP: ldapts empty-array-Attribut-Bug (E-Mail-Kollision auf Unique-Constraint) 2026-07-09 246dc89 (direct)
260709-sbx LDAP: Suchbox fuer die entdeckten Gruppen/OUs-Liste 2026-07-09 aaa2922 (direct)
260714-lex LDAP: Per-User Exclude/Denylist-Filter (Service-Accounts vom Sync ausschliessen) — live verifiziert: deaktiviert 4 Accounts, 2 echte User aktiv 2026-07-14 9d1323f (direct)
13 Normalizer-Gap Phase 13 schliessen: NetServer/Cosinex-Bag-Dispatch (TenderNormalizerService) 2026-07-23 9881005 —
260728-lih LDAP: Sync strikt selektiv (leere Auswahl = No-Op statt Voll-Import) + Auto-Sync-Default aus (syncIntervalMin 60→0) 2026-07-28 c54e424,57bc7f9,63a07ab 260728-lih-ldap-sync-selektiv-und-auto-sync-default
260729-d3k LDAP: Multi-Base-DN und Base-DN als Sync-Scope (statt leerer Gruppenfilter = No-Op) 2026-07-29 5cbd530,96be7e1 260729-d3k-ldap-multi-base-dn-und-base-dn-als-scope
260805-d0r Benutzer-Detail zeigte Gruppenmitgliedschaften aus Freigaben statt aus Mitgliedschaften — GET /module-grants/users/:userId liefert jetzt { groups, modules }, Chips mit Herkunfts-Badge; im Browser gegengeprüft: Mitgliedschaft bleibt sichtbar, auch wenn die Gruppe kein Modul freigibt 2026-08-05 ecadf69,f8ff74b,8ce3748 260805-d0r-benutzer-detail-zeigt-gruppenmitgliedsch
260811-enc LDAP-Bind-Passwort war das einzige Zugangsdatum im Klartext in der DB. Jetzt AES-256-GCM ueber den bestehenden CalendarCryptoService, Spalte umbenannt zu encryptedBindPassword, Entschluesselung zentral in getConfig()/getAllActiveConfigs(), idempotenter Bootstrap-Backfill fuer Altbestand. Falscher Schluessel wirft, statt still "kein Passwort" zu liefern (sonst wuerde aus einem authentifizierten Bind unbemerkt ein anonymer) 2026-08-11 4f687ea (direct)
260811-j04 DOE-Ausschreibungen verlinkten auf die API (rohes JSON) statt auf die Bekanntmachung — betraf Trefferliste, Detailansicht und Alarm-Mails. Adapter baut die URL jetzt aus der Bekanntmachungsnummer, Migration schreibt 2846 bestehende Zeilen in Tender und TenderSource um. Zielseite im Browser fuer beide Kennungsformen verifiziert (numerisch und UUID) — ein HTTP-Statuscheck taugt dort nicht, die Seite antwortet auf jede Kennung mit 200 2026-08-11 ecf7872 260811-j04-doe-tender-links-point-to-the-api-instea
260811-f9i KRITISCH: objectGUID-Existenzpruefung fand nie etwas — der Filter wurde als \xx-escapter String gebaut, ldapts wandelt das nicht in Rohbytes; beide Suchen (Base-DNs und WR-03-Fallback) teilten ihn, also haette der erste echte Sync JEDE AD-gebundene Gruppe samt Mitgliedschaften und Modulfreigaben geloescht. Jetzt EqualityFilter ueber den rohen Buffer, escapeLdapFilterBuffer() entfernt. Read-only am echten AD gemessen (escapter String 0 Treffer, EqualityFilter 1 korrekter Treffer); Regressionstests gegengeprueft (alter Code = 8 rote Tests) 2026-08-11 d2019dc 260811-f9i-fix-objectguid-existence-sweep-to-use-eq
260805-fok Standardgruppe bei Mandanten-Anlage + Startup-Reparatur — GroupsService.ensureDefaultGroup(tenantId) mit D-13-Waechter (null Gruppen, nicht fehlende Markierung), verdrahtet in TenantService.create und AdminSeedService.ensureDefaultGroupsForAllTenants; schliesst die Migrations-Backfill-Luecke auf frischen Installationen (Testserver: tenants=1 users=4 groups=0) 2026-08-05 9d1254c,0d7d8a5 260805-fok-standardgruppe-bei-mandanten-anlage-und-
21 Verschluesselungsschluessel in den Beispiel-Umgebungsdateien dokumentiert: .env.example hatte gar keinen Eintrag, .env.prod.example nannte noch den alten Namen CALENDAR_ENCRYPTION_KEY. Compose-Teil des Backlog-Punkts war bereits mit 7bda56d erledigt (Vorgabewert raus, :?-Abbruch statt Ersatzwert) 2026-08-11 379606e —
260907-let Verbindungstest fuer das Postfach im Ausschreibungs-Radar nachgeruestet (WINDOWS #16): POST /modules/tender-radar/email-config/test plus Knopf "Verbindung testen" im Formular unter Meine Quellen. Nutzt die vorhandene testConnection() beider Inbox-Provider, Muster vom DKV-Modul. userId ausschliesslich aus dem Auth-Kontext (eigener IDOR-Test mit Koeder-userId), leerer Benutzername oder leeres Passwort faellt auf die gespeicherten verschluesselten Zugangsdaten desselben Nutzers zurueck, keine Zugangsdaten in Logs oder Antwort. Verifiziert: 646/646 API- und 228/228 Web-Tests, beide Typpruefungen sauber, Sprachschluessel-Gate von rot auf gruen. Offen: Browser-Abnahme gegen ein echtes Postfach (Ende-der-Phase, braucht Neubau durch den User) 2026-09-07 c4db3b2 260907-let-verbindungstest-fuer-das-postfach-im-aus
260909-ab3 Zwei Befunde aus der Live-Pruefung behoben. #14: Die Suche in der Freigaben-Matrix filterte beide Achsen mit demselben Begriff und leerte dadurch die jeweils andere — jetzt bleibt die nicht getroffene Achse vollstaendig stehen, die Gruppensuche unter internem UND AD-Namen (#6c) ist per Regressionstest gesichert. #15: AD-Konten mit bereits vergebener Mailadresse werden nun angelegt, nur ohne Adresse (Produktentscheidung des Users vom 2026-09-09; der erste Anspruch behaelt die Adresse), auf BEIDEN Wegen — Sync und Einzelimport. Rohe Prisma-Texte gehen nur noch ins Log, der Bericht zeigt drei verstaendliche deutsche Abschnitte. Sicherheitsfund nebenbei geschlossen (T-Q3-01): der Update-Zweig schrieb die Mailadresse bedingungslos um, ein Verzeichniseintrag haette so die Adresse einer echten Person uebernehmen und deren Passwort-Reset empfangen koennen. User.email ist jetzt optional (Migration geschrieben, laeuft beim naechsten API-Start automatisch mit). Verifiziert 8/8: 651/651 API- und 233/233 Web-Tests, beide Typpruefungen sauber; die Sicherheitspruefung wurde durch Rueckbau falsifiziert (ohne Besitzpruefung schlaegt der Test fehl). Am 2026-09-09 im Browser abgenommen, beide Ledger-Punkte geschlossen (Bericht: 260909-ab3-UAT-2026-09-09.md) 2026-09-09 2167046 260909-ab3-matrix-suche-und-sync-meldungen-reparier
260909-cx0 Hochgeladene Dateien (Avatare, DKV-Exporte) ueberlebten kein --force-recreate des api-Containers (WINDOWS #17) — lagen nur in der fluechtigen Container-Schicht, keine Compose-Datei mountete /app/user-files. Jetzt benanntes Docker-Volume user-files in docker-compose.yml und docker-compose.prod.yml (Eigentuemerschaft uid 1001 aus dem Image, kein Bind-Mount), Betriebshandbuch Kapitel 6/7 entsprechend nachgezogen. Zweiter, unabhaengiger Punkt: CLAUDE.md nannte fuer die Technik-Tabelle noch die 2026-06/07-Empfehlung (Next.js 16.2.x, Prisma 7.8.x, Keycloak, Redis, TanStack Query, shadcn/ui, Playwright, Husky, lint-staged) statt des installierten Stands — jetzt korrigiert auf Next.js 15.5.19, Prisma 6.19.3 etc., nie uebernommene Empfehlungen in eigenem Abschnitt "Recommended But Not Adopted", .planning/research/STACK.md nur mit Hinweiszeile ergaenzt. Keine Abhaengigkeit aktualisiert. WINDOWS #17 bleibt offen — die Aenderung erreicht die laufende Installation auf alpha nicht, /opt/tessera/docker-compose.yml weicht vom Repository ab und muss vom Nutzer selbst ergaenzt werden 2026-09-09 dab72eb,c807049 260909-cx0-dateisicherung-nachruesten-und-versionsa
260909-cx0 Dateisicherung nachgeruestet und Versionsangaben geradegezogen. user-files liegt jetzt in einem benannten Volume (docker-compose.yml und .prod.yml) — vorher lag der Ordner nur in der fluechtigen Container-Schicht, hochgeladene Profilbilder und DKV-Exporte waeren bei jedem --force-recreate weg gewesen. Benanntes Volume statt Bind-Mount, weil das Image /app/user-files an uid 1001 uebereignet; ein frisch angelegtes Host-Verzeichnis gehoert root und haette aus dem Datenverlust einen kaputten Upload gemacht. docker-compose.dev.yml blieb bewusst unveraendert (Compose fuehrt Mount-Listen ueber das Ziel zusammen). CLAUDE.md nennt jetzt die installierten Fassungen statt der urspruenglich empfohlenen (Next.js 15.5.19 statt 16, Prisma 6.19.3 statt 7); neu ist ein Abschnitt 'Recommended But Not Adopted', der sechs nie eingebaute Empfehlungen benennt — darunter Keycloak, Redis und shadcn/ui. Der Block ist generiert, deshalb traegt er einen Herkunftsvermerk und die Recherchedatei eine datierte Hinweiszeile; ihre Zahlen blieben unangetastet. Keine Abhaengigkeit angefasst (per git diff gegengeprueft). WINDOWS #17 am 2026-09-09 geschlossen. Beim Nachtragen auf dem Server kam heraus, dass dort gar nicht docker-compose.yml gilt: die .env setzt COMPOSE_FILE=docker-compose.prod.yml. Die drei Zeilen wurden in dieser Datei ergaenzt (Sicherung docker-compose.prod.yml.bak.20260909-0818). Nach dem Neuerstellen durch den User belegt: Mount tessera_user-files -> /app/user-files vorhanden, Ordner gehoert uid 1001 (das benannte Volume hat die Eigentuemerschaft uebernommen), Schreiben als Dienstnutzer funktioniert, und eine Probedatei lag tatsaechlich unter /var/lib/docker/volumes/tessera_user-files/_data auf dem Host — also ausserhalb des Containers 2026-09-09 c807049 260909-cx0-dateisicherung-nachruesten-und-versionsa
260909-dgj Mandantentrennung auf Datenbankebene vorbereitet (WINDOWS #18). Ausloeser war ein gemessener Befund: die Anwendung verbindet als Rolle mit Superuser- und BYPASSRLS-Recht, daher greifen die sieben vorhandenen Policies gar nicht — ohne gesetzten Mandantenkontext lieferte 'SELECT count(*) FROM Group' zwei statt null Zeilen. Gebaut wurden: Rolle tessera_app ohne Umgehungsrecht (wiederholbare Migration, kein Kennwort im SQL), Trennung von Migrations- und Laufzeitverbindung ueber TESSERA_MIGRATE_DATABASE_URL, ein Pruefwerkzeug mit fuenf transaktionssicheren Nachweisen, Policies fuer die 16 fehlenden Tabellen und eine Betriebsanleitung. Der Schalter bleibt bewusst aus, #18 bleibt offen: im Code stehen 182 Datenbankzugriffe ohne Mandantenkontext gegen 19 mit — darunter zwingend der Anmeldeweg, der die Benutzerzeile liest, bevor der Mandant bekannt ist (er kommt erst aus dieser Zeile). Ein Umschalten wuerde die Anmeldung fuer alle sperren. Dabei fiel #19 an: SearchProvider und TenderRssFeedSource haben ein nullable tenantId; die einfache Policy wuerde die plattformweiten Zeilen nach dem Scharfschalten fuer jeden Mandanten unsichtbar machen. 673/673 Tests gruen 2026-09-09 efaabc9 260909-dgj-mandantentrennung-auf-alle-tabellen-mit-
260909-ipc Mandantentrennung Etappe 2, Bereich ldap: alle 21 klassifizierten Zugriffe in ldap-config.service.ts (9) und ldap.service.ts (12) an forTenant() gebunden. Zuerst gemessen, dann gebaut: rls-scratch-check.mjs um runLdapAreaChecks erweitert (13/13 bestanden), Beleg ist die Zeile ldapconfig-ungebunden-null-zeilen gegen die echte, ausgelieferte Policy — die Umkehr der Fehlerrichtung ist damit gemessen, nicht behauptet. Die geforderte Kritikschrift liegt in docs/mandantentrennung-etappe2-fehlerrichtung.md mit Signaltabelle je Pfad und vier namentlich benannten Stellen, die Leere als Abwesenheit deuten. Sicherheitsluecke nebenbei geschlossen (T-IPC-01): DELETE /ldap/config/mappings/:id nahm nur die Kennung — ein Administrator von Mandant A konnte die Feldzuordnung von B loeschen; der Mandant kommt jetzt aus der Sitzung. Drei Stellen bleiben bewusst ungebunden, jede mit Begruendung im Code: getAllActiveConfigs und die Start-Nachverschluesselung lesen zwingend uebergreifend; resolveEmailForWrite darf nicht gebunden werden, weil email/username plattformweit eindeutig sind — gebunden saehe die Kollisionspruefung keinen fremden Halter, meldete 'frei', und aus einer sauber berichteten Kollision wuerde ein P2002-Abbruch (Produktfrage fuer Etappe 3). Befund, der die Testlage aendert: forTenant war in ldap.service.spec.ts als Identitaet gemockt — die Tests haetten den Umbau in keiner Richtung bemerkt; ersetzt durch zwei unterscheidbare Clients. rls-access-inventory.spec.ts um eine Stand-Spalte und Erkennung gebundener Fundstellen erweitert, dabei zwei bisher unbekannte Paare gefunden (auth.service.ts/passwordResetToken, ldap.service.ts/groupMembership), Klassifikationsdokument auf 61 Paare nachgezogen. Schema, Migrationen, Compose und Umgebungsdateien unberuehrt, Schalter weiter aus. Verifiziert 7/7 (unabhaengig nachgemessen: 719/719 Tests, Typpruefung sauber, 13/13 Live-Pruefungen gegen den echten Container) 2026-09-09 a0c9ef0,9a57fa7,e1586a4 260909-ipc-mandantentrennung-etappe-2-bereich-ldap-
260909-jts Mandantentrennung Etappe 2, Bereich groups — die Berechtigungsschicht. Alle 34 echten Zugriffe in groups.service.ts (21) und module-grants.service.ts (13) gebunden, dazu fuenf Zugriffe, die keine Pruefung dieses Projekts je gesehen hatte: sie laufen innerhalb einer Transaktion ueber den Callback-Parameter, den der Detektor der Inventarpruefung nicht kannte — einer davon ist der Schreibvorgang, der Modulfreigaben vergibt. Das Paar (groups.service.ts, tenantModuleActivation) fehlte im Klassifikationsdokument komplett und ist ergaenzt; der Detektor sieht jetzt auch Transaktionsparameter. Kernbefund — neues Hilfsmittel withTenantTransaction(): die Frage, welche Transaktionsform den Mandantenkontext auf derselben Verbindung traegt, wurde an der lebenden Datenbank gemessen statt angenommen. Form (i) faellt durch (zwei verschiedene pg_backend_pid()), Form (ii) besteht die Einzelmessung, bricht aber unter 40 gleichzeitigen Aufrufen mit P2028 ab, weil jeder Aufruf eine verschachtelte Transaktion aus demselben endlichen Verbindungsvorrat oeffnet; Form (iii) besteht beides. Alle weiteren Bereiche bauen darauf auf. Die Lastprobe war zunaechst nur Fliesstext — eine Zahl, die eine Entscheidung trug, ohne nachvollziehbar zu sein; vom Verifizierer beanstandet und als runConcurrencyProbe nachgereicht (604428a), laeuft seither bei jedem Werkzeuglauf mit: 24 Verletzungen von 40 fuer Form (ii), 0 von 40 fuer Form (iii). Zwei Datenbankregeln greifen kuerzer als gedacht und sind bewusst nur gemessen und festgehalten, nicht repariert: die Regel fuer Gruppenmitgliedschaften prueft nur die Gruppen-, nicht die Benutzerseite (T-JTS-02), die fuer Modulfreigaben nur die Mandantenkennung, nicht die referenzierte Gruppe (T-JTS-03) — dort haengt der Schutz allein an assertTargetBelongsToTenant. Umgekehrter Gefahrenfall geschlossen: ensureDefaultGroup deutet Leere als 'Mandant hat noch keine Gruppe' und baut alles neu auf, eine halb umgestellte Fassung haette eine zweite Standardgruppe samt Freigaben erzeugt — Zaehler und Transaktion sind deshalb gemeinsam gebunden. Beide Testdateien hatten gar keine Attrappe fuer den Helfer, waeren nach der Umstellung also aus dem falschen Grund rot gewesen; ersetzt durch den Zwei-Client-Nachweis, vom Verifizierer durch Rueckbau falsifiziert. Schema, Migrationen, Compose und Umgebungsdateien unberuehrt, Schalter aus. Verifiziert 9/9 (743/743 Tests, Typpruefung sauber, 23/23 Live-Pruefungen) 2026-09-09 fd0b9f7,7f08b27,abb6c8b,604428a 260909-jts-mandantentrennung-etappe-2-bereich-group
260909-laa Mandantentrennung Etappe 2, Bereich tenders — anders geschnitten als die bisherigen: von 23 Paaren werden nur 5 umgestellt (die Nutzer-CRUD-Dienste fuer gespeicherte Suchen, Bearbeitungsstand, Benachrichtigungen, Postfach, RSS), 10 bleiben bewusst ungebunden, weil der Ausschreibungskatalog plattformweit ist (D-03), 2 sind Verteiler, die absichtlich ueber alle Mandanten lesen, und 6 sind Mischfaelle, deren uebergreifende Haelfte zu Etappe 3 gehoert — nur die Je-Treffer-Schleifen wurden gebunden. Die gefaehrlichste Grenze lag in tender-rss-feed.service.ts: dort haben plattformweite RSS-Quellen ein leeres Mandantenfeld; drei der vier Zugriffe duerfen deshalb NICHT binden, sonst waeren diese Quellen nach dem Scharfschalten fuer JEDEN unsichtbar statt nur fuer fremde (WINDOWS #19). Die Datei endet bewusst auf Stand gemischt, und das einzelne bedingte Loeschen blieb eine Anweisung — es aufzuteilen haette das Pruef-/Nutzungsfenster geoeffnet, das der Dateikopf vermeidet. Neue Gegenrichtung, die die Bindung selbst erzeugt: drei upsert-Pfade laufen auf Eindeutigkeitsschluesseln ohne Mandantendimension; ist die Zeile unter dem gebundenen Kontext unsichtbar, wird aus stillem Ueberschreiben ein harter Fehler — jetzt als deutsche Meldung statt als 500. Der Verifizierer fand, dass genau eine der drei fehlte (setTriage), obwohl die Zusammenfassung alle drei behauptete; nachgereicht mit 8cbf4c1 samt zwei Tests, deren Rotwerden durch Rueckbau belegt ist. Befund E, festgehalten statt repariert: alle fuenf Policies dieses Bereichs lesen nur tenantId = current_tenant_id() und haben KEINE Benutzerdimension — zwei Nutzer desselben Mandanten sind auf Datenbankebene fuereinander vollstaendig sichtbar; die Trennung haengt allein am Anwendungscode, der stichprobenartig als korrekt belegt wurde. Produktfrage vor dem zweiten Kunden. Befund K, neue Reihenfolgebedingung fuer Etappe 4: der Mailversand holt SMTP aus dem noch nicht umgestellten Bereich settings — nach dem Scharfschalten ginge fuer NIEMANDEN mehr eine Mail raus; settings muss vor Etappe 4 durch sein. Vierte Zaehlkorrektur des Vorhabens: 62 Rohtreffer sind 61 Modellzugriffe, und von zehn vermeintlichen Controller-Stellen brauchten acht die Durchreichung. Verifiziert 8/9, Luecke behoben (772/772 Tests, Typpruefung sauber, 32/32 Live-Pruefungen) 2026-09-09 3498147,3336a6e,df5c5b7,8cbf4c1 260909-laa-mandantentrennung-etappe-2-bereich-tende
260909-mir Mandantentrennung Etappe 2, Bereich dkv — Tankkarten-Modul. Alle 21 klassifizierten Zugriffe in dkv.service.ts gebunden (am Ende 22, weil der neue Besitzriegel einen Lesezugriff hinzufuegt); genau einer bleibt bewusst ungebunden. Bereits bestehende Fremdzugriffsluecke geschlossen (T-MIR-03): getExportFile(tenantId, filename) nahm die Mandantenkennung entgegen und benutzte sie nie — die Datei kam allein ueber ihren Namen aus dem gemeinsamen user-files/-Verzeichnis, ein Administrator eines beliebigen Mandanten konnte die Tankkarten-Auswertung eines anderen herunterladen. Der Riegel leitet die Zugehoerigkeit jetzt aus DkvInvoiceHistory.exportFilename ab; in der Oberflaeche gegengeprueft, dass jeder angebotene Dateiname aus einer Historienzeile stammt, die regulaere Nutzung aendert sich also nicht. Die Luecke ist keine Folge des Umbaus, sie bestand seit jeher. Zerstoerender Fehler in umgekehrter Richtung behoben: saveConfig verschluckte im Zweig, der ein gespeichertes Passwort erhalten soll, Lese- und Entschluesselungsfehler und machte mit leeren Werten weiter — nach dem Scharfschalten haette er ein vorhandenes Passwort durch ein leeres ersetzt und verschluesselt abgelegt, ohne Meldung, nicht rekonstruierbar. Dritte Variante der Testluecke: der Bereich hatte gar keine Testdatei (nicht wie ldap eine, die nichts prueft, nicht wie groups/tenders eine, die abstuerzen wuerde); dkv.service.spec.ts neu angelegt. WINDOWS #21, bewusste Entscheidung: der Planer-Startpfad bleibt ungebunden und wird als benannte Altlast weitergefuehrt — binden ist unmoeglich (onModuleInit hat strukturell keinen Mandanten), Umbau auf einmal-abfragen-viele-bedienen waere die in 07-04 zurueckgestellte Mehrmandanten-Planung. Unsymmetrie zum ldap-Praezedenzfall ausgeschrieben: jener ist heute korrekt und verstummt spaeter, dieser ist HEUTE bereits falsch (bedient einen willkuerlichen Mandanten, bei inaktiver Zeile niemanden) UND verstummt zusaetzlich. Dreifach markiert. Erster Bereich, dessen Kopfzahl beim Hineinsehen NICHT kleiner wurde. Ausfuehrung brach am 2026-09-09 gegen Ende von Aufgabe 3 an einem Sitzungslimit ab — zwei Aufgaben committet, die dritte vollstaendig im Arbeitsbaum; am 2026-09-10 nachgetragen. Aufgefallen durch git status, nicht durch den Bericht. Verifiziert 9/9, zwei Dokumentationsluecken danach behoben (Hintergrunddienst-Abschnitt um den vierten Fall erweitert, Falsifizierungsnachweise nachgetragen). 789/789 Tests, Typpruefung sauber, 41/41 Live-Pruefungen 2026-09-10 761e5e2,222f453,5e8237d 260909-mir-mandantentrennung-etappe-2-bereich-dkv-a
260910-das Mandantentrennung Etappe 2, Bereich user — Benutzerverwaltung, die schwerste Fehlerklasse des Vorhabens (Fremdzugriff hier ist Rechteausweitung ueber Mandantengrenzen, nicht blosse Sichtbarkeit). Alle 17 Zugriffe eingeordnet: Verwaltungswege gebunden, Eindeutigkeits- und Suchwege bewusst ungebunden, jede Entscheidung mit Begruendung am Ort. Startsperre entschaerft — der schwerwiegendste Fund: nach dem Scharfschalten haette eine FRISCHE Installation ihren ersten Administrator nicht anlegen koennen und die Anwendung waere gar nicht erst gestartet. Kette (Glied fuer Glied belegt): die Startpruefung liefert null — nicht weil der Admin fehlt, sondern weil ohne Mandantenkontext keine Zeile sichtbar ist — also wird angelegt, das laeuft in den plattformweit eindeutigen Anmeldenamen, und weil seedAdmin() ungekapselt in onApplicationBootstrap haengt, bricht der Start ab. Betroffen waere jede Installation mit gesetzten Admin-Umgebungswerten gewesen; auf dem bestehenden System nie aufgefallen, weil dort der Admin laengst existiert. Jetzt bindet die Erstanlage an den eine Anweisung zuvor angelegten Mandanten und faengt GENAU den Doppelanlage-Fall ab — jeder andere Fehler bricht den Start weiterhin ab (beide Haelften einzeln nachgewiesen). Riegel repariert, der seit seiner Entstehung wirkungslos war: die Sperre gegen das Loeschen des eigenen Kontos verglich gegen ein Feld, das der Sitzungsnachweis gar nicht traegt (sub; er traegt id, username, role, tenantId) — ein Administrator konnte sein eigenes Konto loeschen. Kein Mandantenproblem, gefunden weil dieser Durchlauf jede Zeile aufschlaegt. Zwei Falschaussagen in eigenen Artefakten berichtigt: der Kopfkommentar von findByUsername behauptete, sie muesse fuer den mandantenuebergreifenden Anmeldeweg ungebunden bleiben — der laeuft seit Etappe 1 ueber die SECURITY-DEFINER-Funktionen, und die Methode hat gemessen NULL Aufrufer; und die Klassifikationszeile der Erstanlage behauptete, es gebe strukturell keinen Mandanten zum Binden, obwohl er eine Anweisung vorher entsteht (Klasse auf beides korrigiert). SUPER_ADMIN-Sicht war nach dem Scharfschalten in JEDER heutigen Form kaputt (ungebunden null Zeilen, gebunden stille Funktionsminderung) — jetzt Schleife ueber alle Mandanten mit gebundenem Rumpf, neues Fundstellenpaar (user.service.ts, tenant). Steuerungsschicht hatte gar keine Tests (7 der 17 Zugriffe plus die gesamte Rollenlogik) — user.controller.spec.ts neu. Plan-Pruefer fand einen Blocker: vier handgepflegte Dokumentstellen benannt, nur zwei abgesichert — also derselbe Fehler, den der Plan verhindern sollte; nachgebessert mit herleitenden statt fest verdrahteten Pruefungen, in vier Einzelmutationen falsifiziert. Verifiziert 10/10 (810/810 Tests, Typpruefung sauber, 53/53 Live-Pruefungen; Selbstloesch-Riegel und Klassenverteilung vom Pruefer eigenhaendig nachgerechnet) 2026-09-10 b848ba6,888f660,3a9391d 260910-das-mandantentrennung-etappe-2-bereich-user-
260910-exd Mandantentrennung Etappe 2, Bereich module-registry — der Berechtigungs-Anfrageweg. module.guard.ts laeuft bei JEDER Modulanfrage und hat null eigene Datenbankzugriffe; seine Richtigkeit ist vollstaendig eine Funktion dessen, was dieser Bereich liefert. Endstand 7 ungebunden / 10 gebunden. Eine Vorgabe des Auftrags war falsch und wurde widerlegt: angeblich wuerde eine Bindung des Modulkatalogs ihn fuer jeden Mandanten unsichtbar machen — gemessen ueber alle 23 ENABLE ROW LEVEL SECURITY-Zeilen steht Module auf keiner, die Tabelle traegt gar keinen Zeilenschutz und kein tenantId. Eine Bindung waere heute WIRKUNGSLOS, nicht katastrophal; katastrophal wird sie erst, wenn Etappe 3 der Tabelle eine Regel gibt. Handlung unveraendert (Katalog bleibt ungebunden), aber Messung und Bedingung sind in Code und Dokument jetzt getrennt — eine richtige Handlung mit falscher Begruendung haelt nur, bis sich jemand auf die Begruendung verlaesst. Die Kernfrage ehrlich beantwortet: es gibt KEIN Signal, das 'wirklich keine Freigabe' von 'die Abfrage hat nichts gefunden' unterscheidet. Nach dem Scharfschalten saehe ein Unterlauf nicht wie ein Fehler aus, sondern wie 'du hast keine Module' — leere Seitenleiste, leerer Marktplatz, jeder Modulaufruf abgewiesen, fuer den Betroffenen nicht von einem absichtlichen Entzug zu unterscheiden. Dreifach festgehalten: im Text, als Testfall in module.guard.spec.ts (zwei Aufrufe, identische Meldung, gegeneinander gehalten) und als WINDOWS #23 mit konkreter Etappe-4-Vorabpruefung. Erstmals eine Entlastung, die strukturell haelt: die Kette unsichtbare Zeile -> falsches 'frei' -> 23505 kann hier nicht auftreten, weil die Eindeutigkeitsschluessel die Mandantenkennung fuehren — erster von sechs Bereichen, in dem sie abwesend statt umgangen ist; entsprechend wurde KEINE Absicherung eingebaut, die nichts absichert. Wieder zwei Kopfkommentare mit Falschaussagen (isModuleActive 'Used by ModuleGuard', findActiveForTenant als Marktplatz-Lieferant), beide Methoden mit null Aufrufern — berichtigt. module-registry.service.ts hatte trotz 11 der 17 Zugriffe und aller Schreibwege GAR KEINE Testdatei. Reihenfolge-Entlastung: der Dashboard-Filter wird mitgebunden, ohne dass eine dashboard-Datei angefasst wird. Zwei Abweichungen, beide vom eigenen Pruefgatter erzwungen und geprueft: eine Identitaets-Attrappe in tender-scheduler.service.spec.ts (zulaessig — jene Datei prueft Planer-Verhalten, die Bindung ist in module-registry.service.spec.ts mit zwei Klienten belegt) und eine Stand-Spalte, die eine Aufgabe frueher nachgezogen werden musste. Verifiziert 9/9 (833/833 Tests, Typpruefung sauber, 66/66 Live-Pruefungen; Klassenverteilung und Summenzeile vom Pruefer eigenhaendig nachgerechnet). Eine Zahl in der Zusammenfassung (7 statt 6 neue Bindungsnachweise) vom Pruefer nachgezaehlt und berichtigt 2026-09-10 7d45e2f,3df7268,9c0eefe 260910-exd-mandantentrennung-etappe-2-bereich-modul
260910-jab Die drei zu kurz greifenden Datenbankregeln geschlossen — auf ausdrueckliche Anweisung des Users VORGEZOGEN, entgegen der geplanten Reihenfolge (urspruenglich nach Etappe 2, damit jeder Bereich gegen einen stabilen Regelstand misst; der User entschied anders, weil offene Loecher vergessen werden). Erster Durchlauf dieser Serie, der die DATENBANK aendert statt nur Anwendungscode — neue Migration 20260910120000_rls_widen_membership_grant_and_platform_read. T-JTS-02: GroupMembership prueft jetzt BEIDE Seiten (Gruppe UND Benutzer gehoeren zum Mandanten) statt nur die Gruppenseite. T-JTS-03: ModuleGrant prueft zusaetzlich, dass die referenzierte Gruppe bzw. der referenzierte Benutzer zum selben Mandanten gehoert; assertTargetBelongsToTenant bleibt als zweite Verteidigungslinie bestehen. WINDOWS #19: TenderRssFeedSource bekommt VIER nach Befehl getrennte Regeln — Lesen schliesst plattformweite Zeilen ein, Einfuegen/Aendern/Loeschen verlangen weiter einen Mandanten (eine einzige lockere Regel haette jedem Mandanten erlaubt, gemeinsame Quellen zu aendern und zu loeschen, weil USING auch UPDATE und DELETE regelt). Halbe Praemisse von #19 widerlegt: bei SearchProvider gibt es gar keinen Codeweg, der eine mandantenlose Zeile erzeugt — Schreibweg verlangt den Mandanten, Vorgaben sind Konstanten (05-02); als widerlegte Annahme geschlossen, nicht als geloestes Problem, strenge Regel bleibt. DREI Pruefungen schrieben die Loecher als erwartetes Verhalten fest (meine eigene Suche fand nur zwei, der Planer die dritte) — alle drei UMGEDREHT statt geloescht, mit Verweis auf den urspruenglichen Befund: der ausfuehrbare Beleg, dass das Loch existierte, bleibt mit umgekehrtem Vorzeichen erhalten. Die Reparatur erzeugte an einer Stelle selbst den Fehler, gegen den sie antritt: listForUser haette nach der Regelaenderung die plattformweiten, aber nicht die persoenlichen Quellen geliefert — aus einer leeren Liste, die schreit, waere eine kurze geworden, die luegt; deshalb mitgebunden. Messfalle abgefangen: extractPolicySql() las nur die alten Migrationsverzeichnisse und haette nach der neuen Migration still die ABGELOESTE Regel weitergemessen. Werkzeugfalle abgefangen: der uebliche Aufrufweg haette beim Einspielen eine neue Prisma-Hauptversion nachgeladen; stattdessen die im Projekt festgelegte Fassung benutzt. Neuer offener Ledger-Eintrag #24: plattformweite Zeilen lassen sich unter der Anwendungsrolle weder anlegen noch entfernen — in alter wie neuer Regel. Verifiziert 11/11 mit vier ZERSTOERENDEN Gegenproben (jede Regel und die neue Bindung einzeln zurueckgedreht, jedes Mal schlug genau die zustaendige Pruefung fehl, danach byte-identisch wiederhergestellt). 839/839 Tests, Typpruefung sauber, 74/74 Live-Pruefungen; Regeltexte vom Orchestrator in der LAUFENDEN Datenbank gegengelesen 2026-09-10 f4f3115,6b23735,03fb3bf 260910-jab-mandantentrennung-die-drei-zu-kurz-greif
260910-krx Mandantentrennung Etappe 2, Bereich dashboard — 12 von 13 Zugriffen gebunden, der Modulkatalog bleibt bewusst ungebunden (Messung und Bedingung getrennt: heute ohne Zeilenschutz, daher wirkungslos, katastrophal erst wenn Etappe 3 eine Regel setzt). Erster Bereich, in dem die Besitzpruefungen von Anfang an richtig waren: dieselbe Bauform, die in ldap und dkv je eine Luecke riss (nachschlagen, dann loeschen), vergleicht hier dazwischen gegen die angemeldete Person — nichts zu reparieren, nur zu bestaetigen und durch Tests festzunageln. Beweisvernichtungs-Schleife belegt, nicht vermutet (WINDOWS #25, offen): nach dem Scharfschalten liefert getLayout bei unsichtbarer Zeile die Vorgabe, die Oberflaeche uebernimmt sie ohne Fehlerzustand, und das Verlassen des Bearbeitungsmodus schreibt AUTOMATISCH zurueck — der Nutzer ueberschreibt seine urspruengliche Anordnung selbst, ohne es zu merken; dazu haeufen sich Widget-Dubletten, weil es keine Eindeutigkeit ueber (userId, widgetType) gibt. Gehoert in die Etappe-4-Vorabpruefung, nicht in diesen Umbau. Suchleiste: der Rueckfallzweig feuert nie leer, weil drei Vorgaben immer vorangestellt sind — die eigenen Suchmaschinen verschwinden schlicht. DashboardLayout.userId ist plattformweit eindeutig ohne Mandantenanteil (Familie WINDOWS #22). Der Verifizierer fand eine Luecke der bekannten Art: die Behauptung, ein gebundener Konfliktschreibvorgang werfe PrismaClientUnknownRequestError (nicht den P2002-Fall von tenders/user), stuetzte sich auf eine NICHT committete Ad-hoc-Messung — Pruefung 5 mass nur Roh-SQL, kein Test uebte den catch-Zweig. Nachgereicht (6e71206): Messung ueber den GENERIERTEN Client (Konstruktorname geprueft), dabei die Wegwerf-Tabelle korrigiert, der Roh-SQL nie aufgefallen war (createdAt/updatedAt fehlten, der echte Client scheiterte sofort mit P2022); zwei Tests fuer den catch-Zweig, durch Rueckbau falsifiziert. Klassifikation: fremde Datei groups.service.ts mit ungenauem Kopfkommentar bewusst NICHT angefasst, Ungenauigkeit in (w5) festgehalten. Verifiziert 10/11, Luecke behoben (860/860 Tests, Typpruefung sauber, 88/88 Live-Pruefungen; alle drei Falsifizierungsnachweise vom Pruefer eigenhaendig reproduziert) 2026-09-11 6744918,e0ce594,67b5024,6e71206 260910-krx-mandantentrennung-etappe-2-bereich-dashb
260911-cwh Mandantentrennung Etappe 2, Bereich calendar — alle 12 Zugriffe gebunden, ein Klient je Methode, sechs Methoden. Bereich hatte KEINE Testdatei (dkv-Form); calendar.service.spec.ts neu mit 23 Tests. Gespeicherte Zugangsdaten zu fremden Kalender-Servern — ein Fremdzugriff waere hier der Schluessel zu einem fremden Exchange/CalDAV. Der dkv-Passwortverlust-Fall existiert hier NICHT, in beiden Haelften belegt: der Dienst schreibt encryptedPassword nur bei dto.password !== undefined, und calendar-source-form.tsx laesst ein leeres Feld WEG statt einen leeren Text zu schicken; als drei Tests festgenagelt, weil ein nicht festgenagelter Freispruch still aufhoeren kann zu gelten. Zwischenspeicher-Schluessel userId:from:to ohne Mandantenanteil ist sicher: User.id ist @default(uuid()), Kette Schema -> auth.service.ts sub: user.id -> JwtStrategy.validate -> extractContext Glied fuer Glied belegt; die Etappe-3-Entscheidung (Anmeldenamen pro Mandant) beruehrt username/email, nicht id. Eigener Gefahrenfall — halb gebundene Aggregationsschleife: fetchAndCacheEvents liest Quellen und schreibt den Synchronstatus auf Erfolgs- UND Fehlerpfad zurueck, innerhalb von Promise.allSettled; gebundene Lesung mit ungebundenem Rueckschreiben haette Ereignisse still fallen lassen — beide Rueckschreibungen gebunden und als Tests festgenagelt, eines davon vom Pruefer eigenhaendig zurueckgebaut (genau 1 von 23 rot, exakte Meldung). Frontend macht aus lauten Fehlern stille: calendar-widget.tsx und calendar-settings-panel.tsx fangen jeden Fehler in denselben leeren Zustand — ein 403 sieht aus wie ein leerer Kalender; NICHT angefasst, als WINDOWS #26 offen festgehalten. Besitzpruefungen in allen drei Pfaden echt (403, nicht 404). Keine Eindeutigkeitskette, daher keine Konfliktuebersetzung, die nichts uebersetzt. Lehre aus dashboard angewandt: 4 der 13 neuen Pruefungen laufen ueber den GENERIERTEN Client, mit Laufzeitvergleich der Wegwerf-Tabelle gegen schema.prisma (17 = 17 Spalten). Verifiziert 12/12 (883/883 Tests, 57 Dateien, Typpruefung sauber, 101/101 Live-Pruefungen; Klassenverteilung 31/17/13/2 = 63 und Ledger-Zaehler vom Pruefer nachgerechnet). Ein Selbstwiderspruch in der Zusammenfassung ('keine Abweichung' vs. 'kein TDD-Zyklus') berichtigt 2026-09-11 bf5fc4d,77cb124,e0e163e 260911-cwh-mandantentrennung-etappe-2-bereich-calen
260911-e2s Mandantentrennung Etappe 2, Bereich tenant — von anderer Art: alle 8 Zugriffe gehen auf die Mandantentabelle SELBST, die per Definition keinen Mandanten hat. 'Nichts zu binden' war trotzdem falsch, und der Grund ist der wichtigste Fund seit dem kaputten Helfer in Etappe 1: drei der acht Zugriffe (findAll, findOne, remove im Controller) zaehlen ueber include: { _count: { select: { users } } } in die GESCHUETZTE Tabelle User hinein — Prisma 6.19 rendert das als LEFT JOIN (SELECT tenantId, COUNT(*) FROM User ...), das unter DEREN Regel laeuft. Nach dem Scharfschalten haette die Mandantenliste des Plattform-Admins fuer jeden Mandanten 0 Benutzer gezeigt, und der Loeschriegel T-02-09 waere vakuum geworden (der Fremdschluessel faengt es noch, aber als 500 statt 400). Behoben per Fan-out je Mandant ueber gebundenen Client, Muster aus UserService.findAllForPlatformAdmin. Die Bestandsaufnahme ist fuer Relationszugriffe strukturell blind — sie sieht nur this.prisma.<Modell>, nicht was ein include: in eine zweite Tabelle hineinrechnet. Alle 19 include:-Stellen und alle _count-Stellen einzeln beurteilt, vom Orchestrator UND vom Verifizierer unabhaengig gegengeprueft (der Plan-Pruefer hatte diesen Punkt als 'plausibel' durchgewinkt statt ihn zu pruefen): nur diese drei waren gefaehrlich. Der MECHANISMUS bleibt offen und ist als WINDOWS #27 festgehalten — der Planer wollte keinen Eintrag, weil die Instanz behoben ist; Orchestrator und Verifizierer sahen das anders, weil eine Luecke im Messwerkzeug, die nachweislich einen echten Defekt verborgen hat, genau dafuer ins Ledger gehoert. Die seit Etappe 1 offene Architekturfrage ist entschieden: req.tenantPrisma wurde bei jeder Anfrage gebaut und NIRGENDS gelesen; neun Bereiche haben die Konvention auf dienst-internes forTenant() festgelegt. Middleware geloescht (sie war nirgends registriert — der Auftrag irrte bei app.module.ts:57, dort ist der Guard verdrahtet), Guard ohne Prisma-Abhaengigkeit, setzt nur noch req.tenantId (22 Leser in 9 Dateien) und den x-tenant-id-Wechsel fuer SUPER_ADMIN (4 Frontend-Stellen) — beides erstmals getestet; Guard und Middleware hatten NIE Tests, 'ihre Tests' in Etappe 1 war eine Annahme. Totes Kabel, das wie eine Sicherung aussieht, ist schlimmer als keins. Ausnahmeliste in rls-access-inventory.spec.ts geleert und mit Wachhund versehen. Drei Kommentare berichtigt, die TenantMiddleware/req.tenantPrisma als lebendig beschrieben. Executor fing einen still fehlgeschlagenen git add (2 von 7 Dateien) selbst an git status und lieferte nach. Verifiziert 10/10 mit vier eigenhaendigen Falsifizierungen (Header-Wechsel zweimal gebrochen, Fan-out gebrochen, Wachhund ausgeloest — je exakt die benannten Tests rot; 911/911 Tests, 59 Dateien, Typpruefung sauber, 110/110 Live-Pruefungen; 64 Paare und Klassenverteilung 32/17/13/2 nachgerechnet) 2026-09-11 652e762,11f5731,17dca0d,c8de72e 260911-e2s-mandantentrennung-etappe-2-bereich-tenan
260911-fh9 Mandantentrennung Etappe 2, Bereich auth — die drei in Etappe 1 bewusst ausgelassenen Wege (getMe, changePassword, adminResetPassword) gebunden, alle drei brauchten neue Signaturen (nahmen nur userId). Der Anmeldeweg ueber die drei SECURITY-DEFINER-Funktionen NICHT angefasst, per pg_proc belegt (weiterhin genau 9 Spalten, auch nachdem die Wegwerf-Tabelle User 5 fehlende Spalten bekam). Verbleibende 3 'ungebundene' Stellen sind die $queryRaw-Anmeldesuchen, keine Modellzugriffe. Falle, die der Auftrag selbst gestellt hatte: Selbstbedienung darf NICHT an req.tenantId binden — der Guard laesst SUPER_ADMIN diese Kennung per x-tenant-id umschalten (Marktplatz), 'mein Profil' haette ihn sich selbst gegenueber unsichtbar gemacht; gebunden wird an den Mandanten aus dem Sitzungsnachweis (@CurrentUser().tenantId), der Controller enthaelt null Verweise auf req.tenantId/x-tenant-id. Zwei Loecher in adminResetPassword geschlossen, keines davon ein Mandantenproblem: der Weg pruefte weder den Mandanten des Ziels noch dessen Rolle — ein ADMIN konnte das Passwort eines SUPER_ADMIN ueberschreiben. Beides jetzt dicht, SUPER_ADMIN-Pfad ueber UserService.findByIdForPlatformAdmin; AuthModule importiert UserModule, zyklusfrei. Der Schwesterweg PATCH /users/:id hat dieselbe Rollenluecke (T-02-08 prueft nur das ZUWEISEN der Rolle, nicht die bestehende Rolle des Ziels) — ausserhalb der Erlaubnisliste, als WINDOWS #29 festgehalten. Umgekehrte Fehlerrichtung ist hier leise, nicht laut: getMe-Leere wird zu 200 mit leerem Rumpf, header.tsx tut bei if (u) nichts — 'nicht angemeldet' und 'Zeile unsichtbar' sind derselbe Wert (WINDOWS #28); changePassword-Leere liest sich als networkError. Identitaets-Attrappe (ldap-Form) durch asymmetrischen Doppel ersetzt: ungebundener Nachbau ohne Modelle, gebundener ohne $queryRaw — beide Grenzen einzeln falsifizierbar. Verifiziert 8/8 (951/951 Tests, 60 Dateien, Typpruefung sauber, 120/120 Live-Pruefungen; zwei Falsifizierungen vom Pruefer eigenhaendig reproduziert — genau 4 bzw. 2 benannte Tests rot) 2026-09-11 9782bea,92aa8c4,f68beb3 260911-fh9-mandantentrennung-etappe-2-bereich-auth-
260911-gwh Mandantentrennung Etappe 2, Bereiche favorites + settings — LETZTER Durchlauf, Etappe 2 abgeschlossen. 7 favoriteLink-Zugriffe und 3 smtpConfig-Anfragepfade gebunden; genau ein smtpConfig-Zugriff bleibt bewusst offen: der Startpfad, umbenannt in loadAnySmtpConfigForStartupTransport() — SECHSTER Fall der Hintergrunddienst-Falle (findFirst() ohne Mandanten beim Hochfahren in mail.module.ts; heute bedient er einen willkuerlichen Mandanten, nach dem Scharfschalten null), beide Zustaende am Ort, WINDOWS #30. Befund K geschlossen: getDecryptedSmtpConfig(tenantId) bindet — die Reihenfolgebedingung fuer Etappe 4 aus dem tenders-Lauf ist erfuellt und in Kritikschrift (t4)/(d4) und Klassifikation als erfuellt vermerkt. Widget-Besitzriegel in favorites.create() eingebaut, weil GEMESSEN noetig: Pruefung 7 zeigt, dass ein gebundenes Anlegen mit fremder widgetId GELINGT — die Fremdschluessel-Pruefung umgeht den Zeilenschutz; vom Verifizierer live reproduziert und der Riegel durch Rueckbau falsifiziert (genau 4 Tests rot). Beide Bereiche hatten keine Testdatei fuer ihren Dienst; favorites.service.spec.ts (23) und settings.service.spec.ts (20) neu, nodemailer gemockt. Ledger #31/#32 fuer die stille Leere (leere Favoritenleiste = 'nie etwas gespeichert'; fehlende SMTP-Konfiguration = 'nicht eingerichtet', obwohl die Zugangsdaten da sind). Der Planer scheiterte am Sitzungslimit NACH dem Schreiben des Plans, VOR der Rueckmeldung — Plan lag vollstaendig auf der Platte (1226 Zeilen, Struktur gueltig), vom Orchestrator committet, vom Pruefer als Erstleser gegen den Baum gehalten. Verifiziert 9/9 (994/994 Tests, 62 Dateien, Typpruefung sauber, 137/137 Live-Pruefungen; Uebersicht 68/178, Klassenverteilung 33+17+13+2=65 und Migrations-Zaehlung 4+3+16=23 vom Pruefer nachgerechnet) 2026-09-11 88896d3,8f2c13a,b5f22e2,1240932 260911-gwh-mandantentrennung-etappe-2-bereiche-favo
260911-mkj WINDOWS #27 geschlossen — die Bestandsaufnahme sieht jetzt Relationszugriffe. Vierte Erkennungsform in rls-access-inventory.spec.ts: include:/select:/_count: werden ueber schema.prisma (zur Testzeit gelesen) auf das Zielmodell aufgeloest und als (Datei, Modell)-Fundstelle gefuehrt, gebunden oder ungebunden je nach umschliessendem Klienten. Zwei Wachhunde, die LAUT werden statt still: Empfaenger ausserhalb der vier Formen (raw vs. matched) und nicht aufloesbare Konstanten — beide vom Verifizierer live gebrochen und rot gesehen. Gemessen mit Prototyp, Vorhersage exakt getroffen: 7 neue Paare, 3 Stand-Aenderungen, eine Klassenaenderung (ldap-config.service.ts/ldapFieldMapping -> beides/gemischt, weil getAllActiveConfigs() ueber include: { fieldMappings } in die geschuetzte Tabelle reicht — genau die #27-Form, bisher unsichtbar, kein neuer Gefahrenfall). Klassifikation 65 -> 72 Paare (35/21/14/2). Acht gepinnte Proben, darunter die beiden #27-Formen (_count.select.users auf this.prisma.tenant -> user ungebunden; auf gebundenem Klienten -> gebunden). Zwei Dateien standen in KEINER Erkennungsform (tenders.seed.ts mit Client als Funktionsparameter, backfill-tender-source.ts mit eigenem new PrismaClient()) — heute harmlos, als WINDOWS #33 eigenstaendig festgehalten statt still in #27 mitgeschlossen. Planer fing einen Fehler im eigenen Prototyp (Lookahead beim Schema-Parsen, ohne den alle Listenrelationen am Zeilenende verloren gingen). Verifiziert 6/6 (1007/1007 Tests, Typpruefung sauber, nur die Spec unter apps/api/src angefasst). Info vom Pruefer: der Lookahead ist nicht durch einen eigenen Regressionstest gedeckt — der raw/matched-Wachhund ist der eigentliche Schutz 2026-09-11 5ad23d0,388690f 260911-mkj-windows-27-schliessen-relations-blindste
260911-nke Etappe 3b — Benutzerdimension in den Datenbankregeln. Migration 20260911120000_rls_user_dimension_personal_tables: current_user_id() (liest app.current_user, NULLIF fuer den Leerstring), forTenant(prisma, tenantId, userId?) mit optionalem drittem Parameter (kein Schwesterhelfer — der Inventar-Detektor haette ihn nicht gesehen), beide set_config in EINER Anweisung, $transaction behaelt zwei Eintraege. Regeln der ZEHN persoenlichen Tabellen in der Form tenantId = current_tenant_id() AND (current_user_id() IS NULL OR userId = current_user_id()) — ein Aufruf ohne Benutzer (Admin, Hintergrunddienst) sieht weiter den ganzen Mandanten. SearchProvider/TenderRssFeedSource mit vier befehlsgetrennten Regeln (jab-Praezedenz), Mandantenhaelften unveraendert; GroupMembership/ModuleGrant/PasswordResetToken/TenderMatch bewusst ohne Benutzerdimension (Verwaltungs-/Anmelde-/Hintergrundobjekte). 34 Nutzer-CRUD-Aufrufstellen in 8 Diensten reichen den Benutzer durch, Scheduler und Verwaltungswege bleiben zweistellig. SECHS loch-behauptende Pruefungen statt drei — und die Umkehrung war nicht trivial: die alten massen OHNE Benutzer, eine naive Umkehrung waere nach der Migration rot geworden, weil der Aufruf ohne Benutzer per Absicht beide sieht; jede wurde zu ZWEI (alte Messung unter neuem Namen als gewollte Eigenschaft, Umkehrung MIT Benutzer). 13 Extraktionsstellen im Werkzeug auf die neue Migration umgeleitet. Wirkungslos mit ausgeschaltetem Schalter (Rolle tessera hat BYPASSRLS, live bestaetigt) — blockiert das Live-Gehen am Dienstag nicht. Angenommene offene Flanke, festgehalten statt verschwiegen: ein Aufrufer, der den Benutzer vergisst, sieht den ganzen Mandanten (heutiger Stand, keine Verschlechterung) — WINDOWS #34; die dreistelligen Spec-Zusicherungen sind je Datei, nicht je Methode, das Gate 'keine zweistellige Form' ist ein Shell-Check, nicht CI — vom Verifizierer als Bewusstseinspunkt vermerkt. Verifiziert 13/13 (1020/1020 Tests, Typpruefung sauber, 203/203 Live-Pruefungen; NULLIF durch Rueckbau falsifiziert, 33 Pruefungen rot; alle zehn Regeln live in pg_policies gelesen) 2026-09-11 f0b531b,07fc653,b62a905 260911-nke-mandantentrennung-etappe-3b-benutzerdime
260909-eor Etappe 1 der Mandantentrennung: Anmeldeweg mandantenfaehig gemacht und alle Zugriffe klassifiziert. Kernfund (#20): forTenant() setzte den Mandantenkontext per set_config auf der Transaktionsverbindung, dispatchte die Abfrage aber ueber den aeusseren Client — empirisch reproduziert (set_config auf Backend-PID 254999, Abfrage auf 255000, Kontext dort NULL). Die Trennung hat damit nie funktioniert, auch nicht an den Stellen, die sie scheinbar nutzten; nach dem Scharfschalten haetten diese Abfragen NULL Zeilen geliefert, was der LDAP-Loeschzweig als 'Gruppe im Verzeichnis verschwunden' gedeutet und geloescht haette. Behoben und live nachgewiesen. Der Anmeldeweg bekam drei SECURITY-DEFINER-Funktionen als schmale Ausnahme (feste Spaltenliste, Gleichheitsbedingung, LIMIT 1) — eine Policy haette nicht gereicht, weil sie zwangslaeufig die ganze Tabelle freigibt. Browser-Gegenprobe lokal bestanden: Anmeldung laedt das Portal, falsches Kennwort verraet weiterhin nicht welches Feld, Kennwort-vergessen laeuft durch (der einzige Protokollfehler war ein lokal fehlender Mailserver, also NACH dem Datenbankzugriff). Klassifikation aller 227 Zugriffe in 59 Einheiten, maschinell gegen Abdriften abgesichert: 31 muessen mandantengebunden werden, 9 teilweise, 16 betreffen keine mandantengebundene Tabelle, 3 bleiben bewusst uebergreifend. 701 Tests gruen 2026-09-09 da0ac04 260909-eor-anmeldeweg-mandantenfaehig-machen-und-al
260910-jab Die drei zu kurz greifenden Datenbankregeln geschlossen — T-JTS-02, T-JTS-03, WINDOWS #19 (bewusste Reihenfolge-Abweichung, vorgezogen auf Nutzerwunsch, statt wie geplant nach Etappe 2). Neue, handgeschriebene, lokal angewandte Migration 20260910120000_rls_widen_membership_grant_and_platform_read: GroupMembership prueft jetzt beide Seiten der Beziehung (Gruppe UND Benutzer), ModuleGrant prueft zusaetzlich beide moeglichen Ziele mit Leer-Zulassung (D-04), TenderRssFeedSource bekommt vier nach Befehl getrennte Regeln (Lesen schliesst plattformweite Zeilen ein, Schreiben verlangt weiterhin einen Mandanten — die Trennung ist noetig, weil ein einzelner USING-Ausdruck sonst auch UPDATE/DELETE mitregelt). SearchProvider bewusst NICHT angefasst: die WINDOWS-#19-Praemisse ist fuer dieses Modell widerlegt (kein Codeweg erzeugt eine mandantenlose Zeile). Drei loch-behauptende Pruefungen im Wegwerf-Werkzeug UMGEKEHRT statt geloescht (66→74 Pruefungen), mit Verweis auf die alten Pruefungsnamen und Befundkennungen im Meldetext. Genau EIN Anwendungspfad musste mitgebunden werden (TenderRssFeedSourceService.listForUser) — sonst haette die Reparatur ihn still von 'liefert nach dem Scharfschalten nichts' auf 'liefert nur die plattformweiten Zeilen, taeuscht Vollstaendigkeit vor' verschlechtert; Falsifizierungsnachweis gefuehrt (Bindung zurueckgenommen, genau ein Test rot, zurueckgesetzt). WINDOWS #19 geschlossen mit Beleg, WINDOWS #24 neu angelegt (Verwaltungsweg fuer plattformweite Zeilen unter der Anwendungsrolle fehlt weiterhin — verschwindet nicht mit #19). Aktenstand kohaerent: Klassifikation, Kritikschrift (neuer Abschnitt "Regelschluss T-JTS-02, T-JTS-03 und WINDOWS #19" mit Signaltabelle beider Fehlerrichtungen je Regel), Betriebsanleitung, WINDOWS.md — fuenf ueberholte Bestandsstellen mit Nachtraegen versehen, alte Messprotokolle bleiben woertlich stehen. Selbst gemessen statt uebernommen: Baseline 833/56 Tests, 66/66 Live-Pruefungen; Endstand 839/56, 74/74; keine zweite Sitzungsvariable fuer den Benutzer gefunden (nur app.current_tenant). Rule-1-Fix: implizites any in tenders.controller.ts nach der Bindung behoben. npx prisma versuchte ungefragt Prisma 8 herunterzuladen — abgebrochen, lokale gepinnte 6.19.3 verwendet 2026-09-10 f4f3115,6b23735,03fb3bf 260910-jab-mandantentrennung-die-drei-zu-kurz-greif
260914-ebg WINDOWS #29 geschlossen — Zielrollen-Riegel in UserController.update()/remove(). Ein ADMIN kann den SUPER_ADMIN seines Mandanten nicht mehr aendern (Kennwort, isActive, Rolle, Anmeldename, E-Mail) oder loeschen; Riegel nach der Mandantengrenze, vor der Rollenzuweisungs-Pruefung (Vorlage AuthService.adminResetPassword, T-FH9-04). Acht neue Spec-Tests (8 -> 16), Baseline 1020 -> 1028 Tests / 62 Dateien, Falsifizierung durch Rueckbau Tests 2 failed / 14 passed (16) (Test 9/13), unabhaengig vom Verifizierer wiederholt. Kopfkommentar adminResetPassword nachgezogen (T-FH9-05 nicht mehr offen). Ledger 16 offen / 1 zurueckgestellt / 19 geschlossen / 36 gesamt: #29 fixed, NEU #35 (Biome-Konfiguration im Bestand nicht lauffaehig, pnpm lint Leerlauf) und #36 (Admin-Frontend verschluckt 403 still). Verifiziert 6/6, gepusht. 2026-09-14 759ea3b,63f9df0,70d007b 260914-ebg-windows-29-schliessen-rechteausweitung-a
260914-eym Etappe 3c — Systemkontext fuer die Hintergrunddienste. Migration 20260914120000_rls_system_context_read: is_system_context(), fuenf permissive system_read_policy ... FOR SELECT (DkvModuleConfig, LdapConfig, LdapFieldMapping, TenderMatch, TenderSavedSearch); forSystem(prisma) in Array-Form mit ausdruecklichem Zuruecksetzen von Mandant/Benutzer, forTenant() setzt app.system_context zurueck (kein Erben, gemessen). Sechs Faelle: DKV-Planer einmal-abfragen-viele-bedienen (Auftrag je Mandant, WINDOWS #21 fixed); Mail-Transport je Versand aus der SmtpConfig des Empfaenger-Mandanten mit unveraenderter Umgebungs-Rueckfallkette, Startpfad und Mailer-Fabrik entfallen (WINDOWS #30 fixed, SmtpConfig ohne Systemregel); ldap getAllActiveConfigs() und Boot-Nachverschluesselung lesen ueber Systemkontext, schreiben je Mandant gebunden; tender-digest Kandidaten und tender-matching Suchprofile ueber Systemkontext, Schleifen gebunden; admin-seed nur dokumentiert (Tenant ohne Regel). Detektor mit fuenfter Erkennungsform forSystem( und exakter Erlaubnisliste (falsifiziert: Fremddatei 1 rot, Zweitaufruf 2 rot). Werkzeug 203 -> 253 (runSystemContextChecks: ungebunden 0 / System beide Mandanten / Schreiben abgewiesen 42501 bzw. count 0 / kein Erben / pg_policies 34, 5x SELECT). Rueckbau (a) 5 rot mit gelungenem Insert, (b) 1 rot, (c1) 253 gruen + (c2) 5 rot, (d) 2/3 rot. Tests 1028 -> 1054 / 62 -> 64 Dateien, tsc 0, 29 Dateien gegen 5e0e408, Schalter AUS (Compose/.env/Schema/Lockfile unveraendert). Klassifikation 61/179/5, 72 Paare, sechs Zeilen system-gebunden; Kritikschrift (y1)-(y5); Auftrag 3c Erledigt. Ledger 15 offen / 1 zurueckgestellt / 21 geschlossen / 37 gesamt; NEU #37 (prozessweiter Single-Flight-Riegel processInbox). Verifiziert 9/9, gepusht. 2026-09-14 3d64567,6e2a641,939c812 260914-eym-mandantentrennung-etappe-3c-systemkontex
260914-ku1 Zwei Auslieferungskanaele und Versionsstempel. main = Beta (Etiketten beta + latest), Tag vX.Y.Z = Live (Etiketten live + vX.Y.Z), Zweig live ohne Tag nur geprueft — Entscheidung in .gitea/scripts/publish-images.sh (--print-plan), CI-Trigger branches: [main, live] + tags: [v*], fetch-depth: 0. Versionsstempel APP_VERSION/APP_CHANNEL/APP_COMMIT/APP_BUILD_TIME als Build-Args in beide Dockerfiles (web zur Bauzeit als NEXT_PUBLIC_APP_*, api als Laufzeit-ENV; Vorgabe dev). GET /health/version liefert name/version/channel/commit/buildTime, Startlog Tessera API vX (channel) commit. Web: app-version.ts, AppVersionBadge in sidebar.tsx (sidebar-footer.tsx ist seit ba02b25 toter Code). docker-compose.prod.yml: image: ...:${IMAGE_TAG:-beta}. Betriebshandbuch Kapitel 9 (Zwei Kanaele, Freigabe, Hotfix ohne Datenbankaenderung, neuer Live-Server), ci-cd-setup.md auf gemessenen Stand. Falsifiziert: Build mit v9.9.9-test live -> Stempel in dist und Web-Bundle, ohne Args dev. Echter CI-Lauf 297 gruen (5:18 min), Abbilder beta/latest tragen ea6aa99 beta. Tests API 1054 -> 1060 / 64 -> 65 Dateien, Web 233 -> 243 / 38 -> 40, tsc 0, 20 Dateien gegen 6c19451. Offen: Zweig live + Tag v1.0.0 nach dem Fehler-melden-Knopf anlegen; Handgriffe fuer den User (IMAGE_TAG je Server) im SUMMARY. Verifiziert 8/8, gepusht. 2026-09-14 cdb571c,9731501,ea6aa99 260914-ku1-zwei-auslieferungskanaele-beta-auf-main-
260914-m97 Fehler-melden-Knopf. Kaefer-Knopf in der Kopfzeile: Bildschirmfoto VOR dem Dialog (html-to-image 1.11.13, laengste Kante 1600 px, computeCaptureSize), Dialog mit Vorschau, Haekchen und "Was ist passiert?"; Fehlerpuffer (Ringpuffer 20: window.onerror, unhandledrejection, console.error, fehlgeschlagene fetch-Antworten — keine Ruempfe/Cookies/Tokens); POST /bug-reports als Multipart (FileInterceptor 4 MiB -> 413, PNG-Signatur -> 400, kein Empfaenger -> 409, Drossel 5/10 min -> 429, Versandfehler -> 502; Mandant/Benutzer nur aus der Sitzung); E-Mail mit PNG-Anhang und Kontext (URL, Web-/API-Version+Kanal+Commit, Browser, Fenster, Zeitpunkt, Benutzer, letzte Fehler) ueber MailService.sendBugReport (Anhaenge; Kennwort-Reset bleibt verschluckend). Empfaenger: neue nullable Spalte SmtpConfig.bugReportRecipient (Migration 20260914170000), Feld "Fehlermeldungen an" unter Administrator -> SMTP, Rueckfall TESSERA_BUGREPORT_TO (docker-compose.prod.yml). Handbuecher Anwender/Administration/Betrieb. Tests API 1060 -> 1076 / 67 Dateien, Web 243 -> 260 / 43 Dateien, tsc 0, --frozen-lockfile 0, 35 Dateien gegen 5c42c55, vier Commits. CI-Lauf 299 gruen (zweiter Versuch, erster scheiterte an Gitea-DB). Browser-Beweis durch den Orchestrator: E-Mail mit 85-KB-PNG (ohne Dialog, OKLCH korrekt) in mailhog, 409-Pfad im Dialog. Ledger #38 (Rule-1-Fix @Expose()) als fixed. Verifiziert 9/9 + Browser, gepusht. 2026-09-14 54121c1,60b0ee8,b41be21,77117de 260914-m97-fehler-melden-knopf-bildschirmfoto-der-a
260916-bwo Dashboard — feineres Raster, skalierende Widget-Inhalte, halbe Abstaende. Raster verdoppelt (COLS 24/20/12/8/2, rowHeight 20, margin 8; WIDGET_CONSTRAINTS x2), gespeicherte Anordnungen einmalig x2 mit Marker __gridVersion: 2 (nur im JSON, migrateGridLayouts/withGridVersion, idempotent). Widget-Rumpf container-type: size; Uhr/Stoppuhr/Rechner skalieren per Container-Queries; Uhr mit timeFontSizePt (leer = automatisch, 8..200 = fest in pt) und Feld in Einstellungen -> Dashboard -> Widgets. Abstaende halbiert: app-shell p-6 -> p-3 (alle Seiten, User-Nachtrag), Dashboard p-2, Grid 8 px, Widget-Innenabstaende; mt-8 bleibt (Umschalter-Hoehe). Anwenderhandbuch. Tests Web 260 -> 286 / 46 Dateien, API 1076 -> 1078, tsc 0, 29 Dateien gegen 5f50c5f, vier Commits, CI-Lauf 351 gruen, Beta-Abbild v1.0.0-10-g1aefaa3. Browser-Beweis durch den Orchestrator: SQL-Probe in alten Einheiten -> DB verdoppelt + Marker; Rand 28 px (vorher 56), Abstand 8 px (vorher 16), main 12 px; Uhr 51 px -> 107 px beim Vergroessern; 36 pt = 48 px fest. Verifiziert 8/8 + Browser, gepusht. 2026-09-16 3f5afb0,2d8efe1,a175c00,1aefaa3 260916-bwo-dashboard-feineres-raster-spalten-und-ze
260916-dyv Dashboard-Nachbesserung nach User-Test. Mindestgroessen inhaltsgetrieben (clock 2/2, search 6/2, calendar 3/3, note 4/4, calculator 3/10 — vom Orchestrator im Browser von 9 auf 10 korrigiert, sechs Tastenreihen —, favorites 3/3, link 3/2, stopwatch 4/3 mit kompakter Bedienleiste), gespeicherte Layout-Eintraege bekommen minW/minH aus den Konstanten und zu kleine w/h werden angehoben (applyConstraintMinima, Test 9/9b). Bearbeiten-Schalter in feste Leiste unten rechts, mt-8 weg: Rand oben 28 px statt 60. Drag & Drop: ganze Kachel als Griff mit Overlay-Kopfleiste, dragConfig.cancel (Eingaben, Knoepfe, .widgetNoDrag, Resize-Griff), preventCollision: true mit noCompactor (Ablegen auf belegtem Raum stoppt am Nachbarn, kein Ueberlappen). Anwenderhandbuch. Tests Web 286 -> 294 / 47 Dateien, API 67/1078, tsc 0, 12 Dateien gegen df16f46 + Fix 8792819; CI-Laeufe 353 und der Fix-Lauf gruen. Browser-Beweis: 28 px, Uhr 126x48, Ziehen an Kachelmitte, Suchfeld ohne Drag, Kollision stoppt, Rechner 272 px ohne Ueberlauf. Ledger #39 fixed. Verifiziert 6/6 + Browser, gepusht. 2026-09-16 dc992c9,dbbd54f,cf97b5b,8792819 260916-dyv-dashboard-nachbesserung-mindestgroessen-
260916-dcz Aenderungsliste. CHANGELOG.md (Keep-a-Changelog, Alltagssprache, echte Umlaute: ## Unveröffentlicht mit Neu/Geändert/Behoben, ## 1.0.0 – 2026-09-15 mit 11 Punkten); Seite "Was ist neu" unter /changelog (Server-Komponente, Text zur Bauzeit ueber env.TESSERA_CHANGELOG_MD in next.config.ts, nur im Server-Bundle; Kanalfilter filterChangelogForChannel: live ohne Unveroeffentlicht, beta/dev markiert; MDEditor.Markdown + rehypeSanitize), Versionsabzeichen als Link; Dockerfile COPY CHANGELOG.md + .dockerignore !CHANGELOG.md; .gitea/scripts/publish-release.sh (awk-Abschnitt, jq, API-Basis aus GITHUB_*, POST/PATCH, --dry-run, Exit 1 ohne Abschnitt) + ci.yml-Schritt nur bei Tag-Refs; Gitea-Release v1.0.0 rueckwirkend angelegt (id 1); Handbuecher (Betrieb Kap. 9, Anwender "Was ist neu", Entwicklung, CI). Tests Web 294 -> 309 / 49 Dateien, API 67/1078, tsc 0, 19 Dateien gegen 963fa36; CI 356/357 gruen. Nachtrag c3d8e16: 21 fehlende Uebersetzungen (Kalenderquellen-Formular, Kalender-Einstellungen, Marktplatz) in de/en, Changelog "Behoben". Browser-Beweis: /changelog mit 20 Punkten, Kalender-Formular ohne Schluesselnamen. Verifiziert 8/8 + Browser, gepusht. 2026-09-16 ba06db9,6940bd0,c5f4ade,c3d8e16 260916-dcz-aenderungsliste-changelog-md-in-alltagss
260916-hiv Kalenderquellen-Formular: URL-Platzhalter je Typ + EWS-Hinweis. Adressfeld zeigt je nach Typ ein Beispiel (Exchange EWS https://mail.firma.de/EWS/Exchange.asmx, Graph, CalDAV, ICS) statt fix https://; bei Exchange EWS grauer Hinweis unter dem Feld (vollstaendige Adresse inkl. /EWS/Exchange.asmx noetig). 5 i18n-Schluessel de/en, neuer Komponententest (6 Faelle), Changelog "Geändert". Ausloeser: User scheiterte mit blossem Hostnamen owa.ctl.de, curl bestaetigte 401 + NTLM auf /EWS/Exchange.asmx. Tests Web 309 -> 315 / 50 Dateien, tsc 0. 2026-09-16 618fbd6,2306a6d,9439c33 260916-hiv-kalenderquellen-formular-url-platzhalter
260916-htc Kalender-Widget nach Vorbild personal-dashboard. Monatsraster (Zurueck/Monat/Weiter, Mo-So, 42 Zellen ab Montag, heute hervorgehoben, Zaehler-Plakette je Tag, Termine beim Ueberfahren als Tooltip per createPortal/position: fixed, weil die Kachel overflow-hidden ist) + Block "Naechste Termine" (Datum/Uhrzeit, Titel, Ort, Farbpunkt). Drei Einstellungen unter Einstellungen -> Dashboard -> Widgets (CalendarConfig, Muster ClockConfig): showMonth (Standard an), maxEvents 0..10 (Standard 3, 0 = ausblenden), lookaheadDays 7/14/30/60/90 (Standard 30). Ein fetchEvents(from,to)-Aufruf je Ladevorgang mit lokalen Tagesgrenzen (Backend-Cache-Schluessel bleibt stabil), Neuladen bei Monatswechsel, 5-Minuten-Intervall bleibt. Neues reines Modul calendar-month.ts (resolveCalendarConfig, buildCalendarDays, groupEventsByDate, computeFetchWindow, selectUpcomingEvents). Mindestgroesse calendar 6x8 (Registry-Test mitgezogen). Keine Quellenauswahl pro Widget (User-Entscheidung: nur Optik). 16 i18n-Schluessel de/en, Changelog "Geändert", Anwenderhandbuch. Tests Web 315 -> 332 / 51 Dateien, tsc 0, 12 Dateien. 2026-09-16 0858102,61996dc,6d8c7c4 260916-htc-kalender-widget-nach-vorbild-personal-da
260916-iex Dashboard-Widgets: Notiz-Haekchen, Favoriten-Titel, Link-Widget entfernt. (1) Notiz-Widget: Aufgabenlisten (- [ ]/- [x]) in der Ansicht direkt abhakbar — previewOptions.components.input ersetzt das von rehypeSanitize erzwungene disabled-Kaestchen durch NoteCheckbox (greift NACH Sanitize, per Spike bestaetigt), delegierter Klick auf dem Vorschau-Container, Index = Reihenfolge der Kaestchen, toggleTaskLine kippt genau diese Zeile (strenge Regex, Code-Zaeune uebersprungen), Sofort-Speichern. (2) Favoriten-Widget: config.title optional — Kopfzeile im Notiz-Look nur bei Titel, im Bearbeitungsmodus Textfeld (widgetNoDrag, 1500 ms entprellt), FavoritesConfig + "— {title}" im Einstellungsfeld, NoteConfig-Beschriftung uebersetzt. (3) Link-Widget restlos entfernt: Registry/Union/Constraints/Katalog/page.tsx, widgets.link de/en, API-DTO, Migration 20260916120000_remove_link_widget (DELETE FROM "WidgetInstance" WHERE "widgetType" = 'link', FavoriteLink kaskadiert, Migrationsrolle tessera = Superuser/BYPASSRLS), widget-wrapper.test.tsx (unbekannter Typ -> grauer Text), Handbuch, Changelog. Tests Web 332 -> 344 / 52 Dateien, API dashboard 31 gruen, tsc Web+API 0. 2026-09-16 684f063,7f1ee3b,39ea147 260916-iex-dashboard-widgets-notiz-haekchen-in-der-
260916-j4f Nachtraege nach Browser-Pruefung. Kalender-Tooltip bricht lange Termintitel um (break-words + min-w-0, Breite/Klemmung aus TOOLTIP_WIDTH_PX = 288); Notiz-Widget nimmt data-color-mode aus useTheme().resolvedTheme mit mounted-Guard (Muster changelog-view) statt auto — Textbereich blieb bei OS-dunkel/Tessera-hell dunkel (User-Meldung); CHANGELOG.md auf kurze Stichpunkte gestrafft (User: "Kein Fliesstext"), alle drei Abschnitte, Ueberschriften unveraendert, publish-release.sh --dry-run --tag v1.1.0 Exit 0. Tests Web 344 -> 347 / 52 Dateien, tsc 0. 2026-09-16 b16e4b8,4c2495b,a6bb7aa 260916-j4f-nachtraege-kalender-tooltip-umbrechen-no
260916-jvj Kalender-Plaketten in Kalenderfarbe + Aufzaehlungspunkte in Markdown-Ansichten. Tages-Plakette nimmt day.events[0].color (groupEventsByDate sortiert jetzt je Tag nach Start) als Inline-Hintergrund mit weisser Schrift, ohne Farbe unveraendert bg-primary; Farbpunkt je Tooltip-Zeile. Tailwind-v4-Preflight entfernt list-style global, markdown.css setzt es nicht zurueck -> Vier-Regeln-Block am Ende von globals.css (.wmde-markdown ul/ol, Abhak-Listen bleiben ohne Punkt) fuer "Was ist neu" und Notiz-Ansicht. Changelog. Tests Web 347 -> 350 / 52 Dateien, tsc 0. 2026-09-16 1e4ec30,c85cf9a 260916-jvj-kalender-plaketten-in-kalenderfarbe-stat
260916-k2z Kalender-Widget: mehrere Kalender am selben Tag als kleine Kreise. Neue reine Helfer groupDayBySource (nach sourceId, Reihenfolge des ersten Auftretens, Farbe = erster Termin der Gruppe) und buildDayBadges(events, max=3): 1 Quelle = bisherige Einzelplakette (unveraendert, Tests 3/3b/3c gruen), 2-3 Quellen = kleine Kreise je Farbe mit eigener Anzahl, >3 = zwei Kreise + grauer Restkreis (bg-muted-foreground text-background, Summe; testid calendar-day-count-rest), Wrapper calendar-day-badges. Changelog-Stichpunkt erweitert. Tests Web 350 -> 354 / 52 Dateien, tsc 0. 2026-09-16 4ddadc6,7429c5b 260916-k2z-kalender-widget-mehrere-kalender-am-selb
260917-fast Desktop-Client: Startseite + Bau-Parallelitaet (Schnellkorrektur nach Bedienprobe). Fenster "main" hatte keine Startseite -> "asset not found: index.html" (Altlast Phase 6); "url": "setup.html" in tauri.conf.json, per strings im Release-Binary bewiesen. CARGO_BUILD_JOBS=4 im CI-Job desktop (8 rustc-Prozesse brachten den gemeinsam genutzten Host mit 15 GB an die Grenze), Betriebshandbuch Kap. 10, Befund in 18-UAT.md. 2026-09-17 b6d9013 —
260917-e15 Desktop-Client-Icon: T statt "1". Die fuenf Icon-Dateien in apps/desktop/src-tauri/icons/ waren mit ImageMagicks internem MSVG-Renderer erzeugt, der transform="rotate(12 51 21)" nicht rendert -> gedrehte gelbe Kachel fehlte, App-Symbol sah aus wie eine "1". Satz mit tauri icon (resvg) aus apps/web/src/app/icon.svg neu erzeugt (nur die fuenf Dateien aus bundle.icon, kein icns/android/ios), Pixel-Gate an der Kachelmitte FFED00FF, icon.ico 16/24/32/48/64/256. Changelog. Nebenbefund: Erststart-Seite (Inline-SVG im WebView) war nie betroffen. 2026-09-17 16564f4,6bb92dc 260917-e15-desktop-client-icon-fehlende-gedrehte-ge
260917-eta Desktop-Client: Tray „Beenden" beendete die App nicht; „Öffnen"/Linksklick holten minimiertes Fenster nicht zurueck. Auf der Windows-Test-VM reproduziert (tasklist: tessera-desktop.exe lief nach „Beenden" weiter; Autostart-Haken im selben Menue funktionierte -> Klick kam an). Ursache: app.run-Handler rief bei jedem RunEvent::ExitRequested api.prevent_exit() — auch fuer app.exit(0) aus dem Tray. Fix: Muster ExitRequested { code: None, api, .. } (Tauri 2.11.3: code None = Nutzer-Interaktion, Some = programmatisch). Dazu w.unminimize() vor show() in „open" und im Linksklick-Handler (nach Win+D bewirkte „Öffnen" nichts). cargo check/clippy 0 Warnungen, rustfmt (9cb9d2e). Changelog 2 Stichpunkte. 2026-09-17 68a69c6,9ba7456,9cb9d2e 260917-eta-desktop-client-tray-eintrag-beenden-been
260917-gsh Akzentfarbe als Hex-Code eingebbar; Bildmarke uebernimmt die Akzentfarbe. normalizeHexColor() in lib/color.ts (optionales #, 3-stellige Kurzform, Kleinschreibung; 9 Tests), Textfeld neben dem Farbwaehler mit Zwei-Wege-Sync, aria-invalid + Fehlertext + gesperrtes Speichern bei ungueltigem Wert (7 Komponententests), i18n settings.account.accentColorHex/-Invalid. Gedrehte Kachel in LogoMark per Inline-Style fill: var(--primary, #ffed00) — folgt applyAccentColor, Anmeldeseite bleibt gelb. Tests Web 365 -> 381 / 57 Dateien, tsc 0. 2026-09-17 795c6a4,1601d97,db478e0 260917-gsh-akzentfarbe-in-einstellungen-konto-zusae
260917-gyd Web-Robustheit: Ruecksprung nach Anmeldung, Sitzungswaechter, Widgets-Seite uebersetzt. Middleware leitet auf /login?next=<Pfad> (Helfer lib/safe-next.ts: nur relative Pfade, kein //, kein \\, kein /login; Tests), Anmeldeseite springt nach Erfolg dorthin. Neue Server Action fetchSessionState() (authenticated/unauthenticated/unavailable): bei 401/403 oder 200 ohne Benutzer wird das Sitzungscookie geloescht und der Header leitet auf /login?next=… — 5xx/Netzwerkfehler bleiben still (kein Redirect bei API-Ausfall). Befund vom Testserver-DB-Reset: /auth/me liefert bei geloeschtem Benutzer 200 mit leerem Body. settings/dashboard: common.loading + settings.widgets.empty statt englischer Hartkodierung. Tests Web gruen, tsc 0. 2026-09-17 4b279ea,474d170,2868ffe 260917-gyd-web-nach-anmeldung-zurueck-zur-ursprueng
260917-h2s Desktop-Client-Erkennung, Beta-Hinweis, deutscher Installer. Rust: with_desktop_marker() haengt desktop=1 an beide Navigationen zur Server-Adresse (Store bleibt sauber); update_labels() — bei gleicher Version nennt Tray/Benachrichtigung „Neuen Beta-Stand {commit}“ statt „Version X“ (5 Rust-Tests). Web: Middleware setzt Cookie tessera_desktop=1 (withDesktopCookie um jede Rueckgabe), lib/desktop-client.ts (isDesktopClient/useIsDesktopClient), DesktopDownloadLinks rendert im Client nichts, DesktopContextMenuGuard im RootLayout blockt Rechtsklick ausser in Eingabefeldern. Installer: bundle.windows.nsis languages German, kein Sprachwahldialog, installerIcon icon.ico, Header/Sidebar-BMP (Markengelb + Tessera-Zeichen, resvg-Quelle), installMode currentUser; Handbuecher ergaenzt. Web-Tests 417 / 63 Dateien, tsc 0. Browser: Cookie, Link-Ausblendung, Kontextmenue, Ruecksprung, 401-Waechter lokal bestaetigt; Installer/Client-Cookie nach CI auf der Windows-VM. 2026-09-17 5bdabf5,d9b94bd,2cd4adc 260917-h2s-desktop-client-web-erkennt-den-client-do
62 Freigabe 1.2.0 (CHANGELOG umbenannt f7f406a, live ff auf main, Tag v1.2.0; Abbilder live/v1.2.0 gebaut). Release-Anhaenge schlugen im CI fehl: publish-release.sh nahm GITHUB_API_URL (git.vicolab.de, Proxy bricht 82-MB-Upload ab, curl 92). Anhaenge vom Host ueber localhost:3002 nachgetragen; Skript nimmt jetzt NIE die oeffentliche Adresse — im CI Host-Gateway aus /proc/net/route:3002, lokal localhost:3002 (507556f, docs/ci-cd-setup.md). 2026-09-17 2956583 —
260917-jdf Bildmarke: ganzes T uebernimmt die Akzentfarbe. Die vier olivfarbenen Kacheln fuellen sich mit color-mix(in oklab, var(--primary, #ffed00) 54%, #363636) (Konstanten BRAND_OLIVE_MIX/BRAND_OLIVE_FILL in brand.ts, Rueckfall-Attribut #9c9440 bleibt); kalibriert auf #ffed00 → #9c9440 exakt (Referenzrechnung 260917-jdf-oklab-kalibrierung.cjs, brand.test.ts rechnet nach). Nebenbefund: --primary ist per globals.css immer oklch(0.91 0.19 102) ≈ #fbe405, der Rueckfall greift nie — Standardkacheln #9a903f statt #9c9440 (unsichtbar). Anmeldeseite/icon.svg unveraendert. Web 424 Tests. Browser: Akzent #0057b8 → Kacheln #284a7b, Zuruecksetzen → #9a903f. Verifikation passed 9/9. 2026-09-17 ecff144,29db4c0 260917-jdf-bildmarke-ganzes-t-uebernimmt-die-akzent
260917-jdh CI: Job desktop ueberspringt den Rust-Bau, wenn der Desktop-Stand unveraendert ist. Neues .gitea/scripts/desktop-stamp.sh (stamp: Version aus desktop-version.sh --print + voller SHA von git log -1 -- apps/desktop desktop-version.sh desktop-collect.sh desktop-stamp.sh ci.yml; check: Manifest/Kanal/Version/Groesse/sha256 des restaurierten desktop-dist). Drei neue Schritte direkt nach dem Checkout (stamp → cache/restore desktop-dist-stamp-<Stempel> nur auf main → check), 13 Bau-Schritte mit if: steps.reuse.outputs.reuse != 'true', nach echtem Bau cache/save unter dem Stempel; Uebergabe an publish und publish unveraendert; Tags bauen immer. Doku: Betriebshandbuch Kap. 10, ci-cd-setup.md 4/6, Entwicklungsanleitung. Verifikation passed 9/9 (lokale Proben). Offen: CI-Beweis nach Push (baut → Docs-Push ueberspringt → Desktop-Push baut neu; cache/save bei belegtem Schluessel beobachten). 2026-09-17 8c4aaa5,e7633e1 260917-jdh-ci-job-desktop-ueberspringen-wenn-apps-d
260917-jdd Favoriten-Widget: Symbol trotz Zertifikatsfehler/interner Adresse, Favoriten sortierbar. API: undici@7.28.0 (exakt, war schon im Lockfile) — LENIENT_TLS_AGENT (rejectUnauthorized: false) als Dispatcher NUR in fetchWithRedirectGuard, SSRF-Schutz (DNS/private IPs/Redirects/Timeouts/Deckel) byteweise unveraendert; PUT /favorites/order {widgetId, ids} VOR den :id-Routen, reorder() in withTenantTransaction mit userId+widgetId je Eintrag, eine 400-Meldung; Icon-Proxy mit nosniff + CSP sandbox. Web: FavoriteIcon Kette Proxy-Bild → bei Fehler Direktbild {origin}/favicon.ico (nur http/https, no-referrer) → Buchstabe; Pfeile „Nach oben/unten“ im Bearbeitungsmodus, optimistisch + Reload bei Fehler; Altbestand position 0 normalisiert sich beim ersten Klick. Befund: discoverFavoriteIconUrl liefert nie null (immer Origin-Rueckfall) — deshalb haengt der Browser-Ersatzweg am Bildfehler. API 1101 / Web 429 Tests. Browser: self-signed.badssl.com → Proxy-Symbol; http://192.168.13.11:3002 → Proxy 502 → Direktbild; Sortierung ueber Reload, DB-Positionen 0..3. Verifikation 15/15 + Browser. 2026-09-17 2a562d0,b18ac25,b023d6f 260917-jdd-favoriten-widget-favicon-ersatzweg-bei-u
260917-jn2 Desktop-Client: Server-Adresse sichtbar und nachtraeglich aenderbar. Rust: TrayIconBuilder::with_id("main"), TrayItems { connected, update } in app.manage, apply_server() setzt Tooltip Tessera – {host} + gesperrte Menuezeile Verbunden mit {host} an einer Stelle; Tray-Eintrag Server-Adresse ändern… navigiert zu setup_page_url() (http://tauri.localhost/setup.html unter Windows, sonst tauri://localhost/setup.html); Commands get_server_url/open_server (kein Capability-Eintrag noetig — Remote-Origin darf keine Commands rufen); parse_server_url (nur http/https) gemeinsam; spawn_version_check herausgezogen, update-Klick liest Adresse per stored_server_url beim Klick. setup.html: Vorbelegung, „Aktuell verbunden mit“, „Abbrechen“. Web: Einstellungen → Desktop-App zeigt im Client „Verbunden mit: {origin}“ + Hinweis (settings.desktop.*). 18 Rust-Tests, Web 431. Browser: Web-Block mit Cookie bestaetigt. Verifikation human_needed: Windows-VM-Probe mit CI-Paket offen (Tooltip, Menuezeile, Adresse aendern/Abbrechen, Wechsel ohne Neustart). 2026-09-17 29c132e,4c79874,4d48543 260917-jn2-desktop-client-aktuelle-server-adresse-s
260917-kgc Desktop-Client: Update in der App (tauri-plugin-updater, signierte Pakete). Client: Plugin 2.11 + semver, plugins.updater.pubkey (minisign; privater Schluessel + Passwort NUR unter ~/.tessera/desktop-updater/ auf dem Dev-Rechner, Gitea-Secrets TAURI_SIGNING_PRIVATE_KEY/_PASSWORD), Endpunkt zur Laufzeit {server}/api-proxy/desktop/update?target&arch&current&base, is_update_newer (hoehere Basis → Update; gleiche Basis nur bei beta.g<sha7> mit anderem Commit; kleinere/gleiche Live → nichts), Pruefung 15 s / Download 600 s (Plugin-Timeout gilt fuer beides), Tray „Auf Version X / Beta-Stand aktualisieren" → Fortschritt → passiver NSIS-Installer startet die App neu (Linux: app.restart()), Fehler → Benachrichtigung + Download-Seite im Browser, http:// → gesperrt „Update nur über https möglich". API: GET /desktop/update (statisch VOR download/:platform, base nur Origin, 204 ohne signature/updateVersion). CI: createUpdaterArtifacts, Secrets nur an den zwei tauri build-Schritten, desktop-collect.sh schreibt signature + updateVersion (X.Y.Z-beta.g<sha7>), desktop-stamp.sh check verlangt beides. 33 Rust-Tests, 23 API-Tests. Nachweise erbracht: CI baut .sig fuer beide Plattformen (Cross-Bau rustls ok); alpha-Endpunkt 200/400; Windows-VM: Client 7479cb4 → Tray-Klick → Neustart als a6d1a64, Adresse erhalten. Bereits installierte Clients (≤ 1.2.0) brauchen einmal den Browser-Installer. 2026-09-17 678ba51,de81c74,7004b5b,7479cb4 260917-kgc-desktop-client-update-in-der-app-herunte
260918-gza Fehlermeldung: Herkunft ausweisen (Browser/Desktop-App, Betriebssystem, App-Version). Betreff traegt direkt nach [Tessera Fehlermeldung] ein Kuerzel [Browser] / [Desktop/Windows] / [Desktop/Linux] ([Desktop] bei altem Client ohne Details); Mailtext bekommt die Zeile Herkunft: — Browser: Browser — <Name> <Hauptversion> auf <OS> aus dem User-Agent (reine Regex-Helfer origin.ts, keine Abhaengigkeit), Desktop: Desktop-App (<OS>), Tessera-App <Version> · Stand <Commit>. Kette: Rust with_client_marker haengt neben desktop=1 die Parameter dv/dc/dos an (drei Aufrufstellen unveraendert, nach In-App-Update automatisch frisch) → Middleware setzt Cookie tessera_desktop_client = ` (musterbereinigt, nur wenn alle drei da) →getDesktopClientInfo()→ vier optionale DTO-FelderclientKind/clientOs/clientVersion/clientCommit(whitelist deklariert, alte Web-Baue/Clients bleiben gueltig) →describeOrigin(). Rohe Zeilen Browser:/Fenster:bleiben; Kuerzel auch in der einen Protokollzeile; nichts in DB,main.tsunangetastet (T-GZA-01..04). Tests: API 1124 (origin 10 neu), Web 447, Rust 37, Typecheck sauber. Plan-Pruefer und Verifier bestanden (9/9 must_haves). **Nachweise lokal (mailhog):** Browser →[Browser] … Herkunft: Browser — Chrome 154 auf Linux; Desktop-Marker wie der Rust-Client (?desktop=1&dv=1.2.0&dc=a6d1a64&dos=windows) → Cookie 1.2.0%7Ca6d1a64%7Cwindows, [Desktop/Windows] … Herkunft: Desktop-App (Windows), Tessera-App 1.2.0 · Stand a6d1a64`. Offen: Windows-VM-Probe mit echtem Client nach CI-Bau und alpha-Deploy durch den User. 2026-09-18
fast Desktop-Client: Setup-Seite zeigt Version und Stand der App („Tessera-App 1.2.0 · Stand a6d1a64"; ohne Stempel nur Version) — Command get_client_info, Helfer client_info_label (2 Tests), <p id="client-info"> in setup.html, CHANGELOG. Diente zugleich als zweiter Desktop-Stand fuer den Update-Nachweis. 35 Rust-Tests. 2026-09-18 a6d1a64 —
260921-9ie Biome lauffaehig machen und das Lint-Tor scharf schalten (WINDOWS #35). biome.json per biome migrate auf Biome 2.5.0 gezogen: organizeImports nach assist.actions.source, linter.rules.recommended → preset: "recommended", javascript.parser.unsafeParameterDecoratorsEnabled (NestJS-Parameter-Dekoratoren: 238 parse-Fehler in 19 Dateien → 0), quoteStyle: single (belegt: 1496 einfach-gequotete Importzeilen gegen null doppelte), vcs.useIgnoreFile, Ausschluss von **/__fixtures__/** (nur html/zip/xml, keine TS-Datei) und globals.css (Tailwind-4-At-Regeln). lint-Skript (biome lint .) in allen fuenf Workspaces; turbo.json bekommt globalDependencies: ["biome.json"], sonst liefert der Cache nach einer Regelaenderung alte Ergebnisse. Zweig (b) gewaehlt, gemessen: biome check . → Exit 1/760 Fehler, biome lint . → Exit 1/275, also kein "nur Warnungen"-Ausweg; Skript ruft lint statt check (haelt 319 Formatierungsbefunde draussen, kein Rundumumbau), Rest gezielt auf warn → 0 Fehler, Exit 0. Sicherheit: Gruppe security bleibt auf error, maschinell geprueft; die 6 noScriptUrl-Treffer lagen ausnahmslos in der ausgeschlossenen HTML-Testvorlage, keiner in echtem Quellcode. Registereintrag #35 war in zwei Punkten falsch: Pfad ist apps/api/src/user/... (Einzahl), und die Wirkung des Parser-Schalters betrug 238 statt 17 Fehler. Nachweise (dreifach unabhaengig — Planer, Orchestrator, Verifier): pnpm lint → "5 successful, 5 total", Exit 0 (vorher "No tasks were executed"); Gegenprobe mit Wegwerfdatei (debugger) → Exit 1 mit noDebugger, danach Baum wieder sauber; repo-weit 0 parse-Fehler, 0 Fehler; Diff nur Konfiguration/Skripte/Doku, keine Quelldatei, pnpm-lock.yaml unveraendert. Verifikation passed (7/7). Offen als eigener Durchlauf: rund 2800 Warnungen (any-Familie, Barrierefreiheit in apps/web), in docs/anleitung-entwicklung.md als bewusster Rueckstand festgehalten. 2026-09-21 6a727e9,00d769b,6f0f05a 260921-9ie-windows-35-biome-json-fuer-biome-2-5-0-r
260921-a1d Benutzerverwaltung: verbotene Aktionen melden sich jetzt (WINDOWS #36). Drei Stellen in apps/web/src/app/(portal)/admin/users/page.tsx verschluckten Server-Antworten still (if (res.ok) ohne else, catch {} mit dem Kommentar // silently fail): Liste laden, Formular speichern, Loeschen. Sichtbare Wirkung vorher: Formular blieb offen, Loeschdialog stand still, beim gescheiterten Laden log die Seite mit "Keine Benutzer gefunden". Jetzt je ein Banner (role="alert") im Listenkopf, im Formulardialog und im Loeschdialog; readApiMessage(res) liest ausschliesslich body.message und zeigt den Servertext in einem deutschen Rahmensatz, sonst eine uebersetzte Ersatzmeldung — auch wenn der fetch selbst wirft. Vier Schluessel admin.users.errors.* in de.json und en.json (Katalog-Paritaet 890/890 geprueft). Dazu canManageRow: einem ADMIN werden Bearbeiten/Loeschen in der SUPER_ADMIN-Zeile gar nicht erst angeboten (seit #29 im Alltag erreichbar), "Details" bleibt ueberall. apps/api blieb unangetastet — der Zielrollen-Riegel im Controller ist und bleibt die wirksame Grenze, der versteckte Knopf ist Ergonomie darueber, kein Ersatz; eigenes Gatter im Plan weist das nach. Gemessen: keine Namen/IDs/Stapelspuren in den 403-Rumpftexten (kein eigener ExceptionFilter in apps/api/src). Nachweise (dreifach unabhaengig): Web-Tests 66 Dateien/459 Tests gruen (vorher 65/447, neue users-page.test.tsx prueft echten DOM-Text via getByText/within, nicht nur State-Setter), type-check Exit 0, pnpm lint 5/5 ohne neue Fehlerrang-Meldung, silently fail im Code 3 → 0, role="alert" 0 → 3, Diff nur vier Dateien unter apps/web. Verifikation passed (7/7). 2026-09-21 38d2586,51bff75,13b70df 260921-a1d-windows-36-benutzerverwaltung-zeigt-bei-
260921-bi2 Lint-Rueckstand abgebaut: 2923 → 465 Warnungen (WINDOWS #35 Folgearbeit). Seit das Lint-Tor wirklich prueft, war der Rueckstand sichtbar. Aufgeteilt nach Risiko statt nach Datei: (1) Konfiguration — zwei begruendete overrides, (2) maschinelle Fixes + toter Code, (3) Barrierefreiheit von Hand. Der wichtigste Befund ist ein Beinahe-Schaden: Biomes style/useImportType-Korrektur ist als safe eingestuft, zerstoert in apps/api aber die NestJS-Abhaengigkeitsspritze — __metadata("design:paramtypes", [PrismaService, …]) kollabiert zu [Function, …], 61 von 65 Dateien betroffen, API startet nicht mehr. Dabei bleibt tsc gruen und alle 1124 API-Tests bleiben gruen, weil kein einziger Test createTestingModule aufruft — das waere durch jedes vorhandene Tor unbemerkt bis auf alpha durchgelaufen. Planer und Plan-Pruefer haben es unabhaengig voneinander reproduziert (Datei kompiliert, Metadatenzeile verglichen). Deshalb zweiter overrides-Eintrag auf apps/api/**. Zweite Falle, ebenfalls gemessen: --only=<regel> schaltet eine in der Konfiguration abgeschaltete Regel wieder AN — ein repo-weites biome lint . --only=useImportType --write haengt die Ausnahme aus (77 API-Dateien veraendert). Nur pfadgebundene Aufrufe. Nachweis, dass sich nichts geaendert hat, ist NICHT die Testsuite, sondern ein sha256 ueber alle 593 erzeugten __metadata-Zeilen: 6e1583f1…, vor und nach dem Umbau identisch, dreifach geprueft. Barrierefreiheit: 155 Handkorrekturen in 53 Dateien (Symbole 71, Knopf-Typen 52, Beschriftungen 22, Rollen/Semantik 10) — je Fundstelle entschieden, ob ein Symbol dekorativ (aria-hidden) oder die einzige Beschriftung ist (<title>/aria-label); in der Seitenleiste erkannt, dass der Text beim Einklappen verschwindet, dort also ein echter Name noetig ist. Alle neuen Texte ueber next-intl in de und en (892/892 Schluessel). Zahlen: gesamt 2923 → 465, echter Quellcode 856 → 386, Testdateien 2067 → 79, Fehler-Rang durchgehend 0. Bewusst NICHT angefasst, benannt statt stillschweigend: 288 noExplicitAny im Quellcode (echte Typarbeit), 20 useExhaustiveDependencies (je ein moeglicher Effekt-Fehler), 30 a11y-Befunde mit Bedienentscheidungsbedarf, noUselessSwitchCase (Fallmarke dokumentiert Absicht), sowie fuenf tote Stellen, die Symptome echter Luecken sind — darunter: Passwortwechsel-Seite leitet nach erzwungenem Wechsel nicht weiter, Loeschknopf in VehicleTable ohne Besetztzustand, force-password-change.interceptor liest das HTTP-Verfahren und fragt es nie ab. Klicktest am laufenden System (echte Abbilder, Playwright): API meldet healthy und Nest application successfully started — Abhaengigkeitsspritze zur Laufzeit bewiesen; Abbrechen legt nichts an, Speichern legt an; als ADMIN bietet die SUPER_ADMIN-Zeile nur noch Details; abgewiesene Server-Antwort erscheint sichtbar als "Der Server hat die Aktion abgelehnt: …". Testbenutzer wieder geloescht. Verifikation passed. 2026-09-21 8d1c8f3,636fe0d,+11 260921-bi2-lint-rueckstand-abbauen-mechanische-fixe
260921-fi3 Erzwungener Passwortwechsel wurde an der API nie durchgesetzt — Sicherheitsfix. Aus dem Lint-Durchlauf 260921-bi2 kamen fuenf gemeldete "Symptome". Alle am laufenden System nachgestellt: eines widerlegt (Passwortwechsel-Seite leitet sehr wohl weiter, siehe bi2-VERIFICATION), drei bestaetigt, eines (ZIP-Dateiname) bewusst nicht angefasst. Der schwere Befund: auth.service.ts:176 legt mustChangePassword in den JWT, jwt.strategy.ts liess das Feld beim Auspacken fallen, also war request.user.mustChangePassword immer undefined und der global registrierte ForcePasswordChangeInterceptor eine Attrappe — er hat seit seiner Einfuehrung nie etwas blockiert. Durchgesetzt wurde der Zwangswechsel allein von der Web-Middleware; jeder Weg daran vorbei (Desktop-App, Skript, curl) umging ihn. Der Kommentar des Interceptors behauptete woertlich "T-02-14: Prevents bypass via direct API access" — das war falsch. Kein Rechteausbau: die eigene Rolle bleibt, aber der Zwang entfaellt. Gemessen vorher: Sitzung mit mustChangePassword=true bekam auf GET /users 200 samt vollstaendiger Benutzerliste. Behoben: Strategie reicht das Feld durch (strikt === true, fehlender Anspruch in alten Sitzungen wird false), Erlaubnisliste von Teilzeichenketten-Vergleich auf exaktes Verfahren+Pfad umgestellt. Sauberer Nachweis (gleicher Nutzer, gleiche Rolle, gleiche Route, nur die Kennzeichnung unterscheidet sich — auf /users haette der Rollen-Riegel das Ergebnis verdeckt): GET /modules/active → 403 {"message":"FORCE_PASSWORD_CHANGE"} mit Zwang, 200 ohne. /auth/me, /auth/change-password und /auth/logout kommen weiterhin durch. Rot-dann-Gruen belegt: neue Spezifikationen gegen den alten Stand 6 von 12 rot, danach 12/12 gruen — vom Verifier unabhaengig nachgestellt (alte Dateien aus 116041b rekonstruiert). Gezielt nach Schlupfloechern gesucht (Schraegstrich am Ende, Abfragezeichen, Gross/Klein, ../): keins. Kein Aussperren: kompletter Browser-Ablauf durchgespielt — Anmeldung leitet auf /change-password, Seite bedienbar, Wechsel gelingt, landet auf /, Kennzeichnung geloescht, freie Navigation. Die Seitenleiste zeigt waehrenddessen "Keine Module" (neuer 403 auf /modules/active, wortlos geschluckt) — sachlich richtig. Dazu Fahrzeugtabelle (dkv-fleet): Loeschknopf war doppelt ausloesbar (isDeleting wurde geschrieben, nie gelesen; Dialog blieb waehrend der Anfrage offen) — beide Dialogknoepfe jetzt gesperrt. Nebenbefund des Verifiers: die Wirkung kommt vom disabled-Attribut, React unterdrueckt Klicks darauf selbst; der Zustandscheck ist redundant, nicht falsch. Ausserdem sieben fest verdrahtete deutsche Texte und sechs Vorlese-Beschriftungen auf next-intl umgestellt (de und en, 87 Schluessel deckungsgleich). Nicht angefasst, begruendet: SplitTab.tsx 'certificates.zip' — ein Downloadname ist ein Dateisystem-Artefakt, kein Bedienelement; uebersetzt braechte er Umlaute in Windows-Dateifreigaben. Balken: api 71 Dateien/1136 Tests, web 66/462, type-check 4/4, pnpm lint 5/5 ohne Fehlerstufe, Warnungen 466. Verifikation passed (9/9). 2026-09-21 f7c02b7,e56cce4 260921-fi3-erzwungener-passwortwechsel-wird-von-der
260921-gof 21 React-Effekt-Abhaengigkeiten einzeln beurteilt — 15 davon waren Fallen, nicht Fehler. Die Klasse war aus 260921-bi2 zurueckgestellt worden, weil jeder Befund einzeln zu beurteilen ist. Ergebnis: nur 2 echte Defekte (A), 15 Fallen (B, das naive Eintragen haette eine Abruf-Schleife erzeugt), 3 Absicht (C, mit begruendetem biome-ignore — erste Verwendung im Projekt), 1 Ballast (D). Die gefaehrlichste Stelle: calendar-widget.tsx:82 — showToday setzt bei jedem Klick ein frisches Date; monthDate naiv in die Liste einzutragen haette jeden Druck auf den Monatstitel einen Termin-Abruf ausloesen lassen, ueber die API bis zum Exchange-Server. Reihenfolge war Pflicht: erst Identitaet stabilisieren, dann die Liste umstellen. Die haeufigste Falle: t aus useTranslations ist in diesem Projekt bei jedem Durchlauf eine frische Funktion (die Test-Attrappen sind nachweislich so gebaut) — 8 Befunde. Griff ohne Ausnahme-Kommentar: den uebersetzten Text vor dem Hook in eine Konstante ziehen und diese eintragen; React vergleicht Zeichenketten per Wert. Nebenbefund: 11 eslint-disable-Zeilen fuer genau diese Regel waren wirkungslos, seit Biome ESLint abgeloest hat — alle entfernt. Laufzeitnachweis vom Orchestrator im Browser (Netzwerkprotokoll, nie fetch aus der Seite; gegen neu gebaute Abbilder): Dashboard 62 s Ruhe → Protokoll byte-identisch, genau 1 calendar/events; Monatstitel 3x gedrueckt → nur der erste Druck (Bereich aendert sich wirklich) loest einen Abruf aus, Druck 2 und 3 null; "Weiter" 3x → 3 Abrufe, korrekt; Stoppuhr 6 s real → Anzeige 00:06, 4 Runden ueber 4,8 s → 16/17/19/20 monoton, kein Ruecksprung. Dazu acht weitere Ansichten je 20-25 s ruhen gelassen (Marktplatz, Modulverwaltung, Gruppenverwaltung, DKV dreimal, Ausschreibungsradar zweimal) — jeder Endpunkt genau einmal. InvoiceHistoryTable hatte als einzige Datei keinen Test und ist damit gemessen statt nur gelesen; ResultsList ist die Stelle, an der die t-Falle in bi2 tatsaechlich zuschnappte. Zahlen: Warnungen 467 → 446 (exakt 21, nichts anderswo gewachsen), useExhaustiveDependencies 0, web-Tests 66/462 → 67/477, api 71/1136 unveraendert, type-check 4/4, pnpm lint 5/5 ohne Fehlerrang. Verifikation passed. Benannt, nicht behoben: zwei Verschwendungen im Kalender-Abruffenster (gleicher Zeitbereich zweimal geholt; calendar/sources bei jedem Monatswechsel) — vorbestehend; und t in vier vorbestehenden Abhaengigkeitslisten ausserhalb des Auftrags, die Biome nie gemeldet hat. 2026-09-21 b3f0e3c,e2c508c,e780b2c 260921-gof-effekt-abhaengigkeiten-in-react-21-befun
260921-i8x Fuenf fehlerverdaechtige Lint-Klassen geprueft — kein einziger echter Fehler darunter. Zwoelf Stellen einzeln beurteilt, Ergebnis: 7x gleichwertig oder Absicht, 2x Haertung, 3x idiomatisch korrekt. Das ist das Ergebnis, keine Ausrede — die Klassen klangen gefaehrlicher als sie waren. Die eine Stelle mit echtem Wert: apps/web/src/lib/safe-next.ts, der Schutz gegen Weiterleitung auf fremde Seiten nach der Anmeldung. Der Kommentar dort behauptete, die Steuerzeichen stuenden als Unicode-Escapes im Muster; die rohen Bytes zeigten das Gegenteil (NUL, 0x1F, 0x7F direkt eingebettet). Funktionierte, war aber zerbrechlich: verschluckt ein Werkzeug das NUL-Byte, wird aus dem Bereich stillschweigend ein anderer und der Schutz loechrig — der Rueckgabewert landet in login/page.tsx direkt in window.location.href. Jetzt echte Escapes, Datei ohne ein einziges Steuerbyte. Beweis der Gleichwertigkeit, nicht Behauptung: ueber alle 65536 Codepunkte dieselbe Menge abgelehnter Zeichen — 54 Stueck (32 Steuerzeichen 0x00-0x1F, dazu 0x7F, Backslash und die 20 Leerraum-Zeichen von \s), null Abweichung; vom Orchestrator unabhaengig gegen ein selbst gebautes Referenzmuster nachgerechnet. Bewusst nicht angefasst: die NUL-Maskierung in ldap.service.ts (RFC 4515) — genau dieses Zeichen zu treffen ist ihr Zweck, wer sie "repariert", oeffnet LDAP-Filter-Injection. Ebenso die drei while ((m = re.exec(s)))-Schleifen (idiomatisch, kein verrutschtes Gleichheitszeichen) und noUselessSwitchCase aus bi2. Zwei Korrekturen an frueheren Annahmen: sanitizeNextPath laeuft NICHT in der Edge-Middleware (die importiert nur buildNextParam), und das blosse Umschreiben auf Escapes senkt die Warnzahl nicht — Biome beanstandet die Escape-Schreibweise genauso, es braucht zusaetzlich einen einzeiligen Unterdrueckungskommentar. Werkzeugfalle, dreimal zugeschnappt: das Schreibwerkzeug wandelt \uXXXX still in das echte Zeichen um — der Planer erzeugte so zehn rohe Steuerbytes in seiner ersten Planfassung, der Executor zweimal in Commit-Text und Akte (git verweigerte den Commit wegen eines NUL-Bytes), und der Orchestrator beim Nachrechnen. Umgehung ueber python3/chr(92) ist im Plan hinterlegt. Zahlen: 446 → 434, noControlCharactersInRegex/useIterableCallbackReturn/noGlobalIsNan je 0, suppressions/unused 0, web-Tests 67/477 → 68/481, api 71/1136 → 72/1137, type-check 4/4, lint 5/5. 2026-09-21 f85c91b,076ca4b,b92dd5d 260921-i8x-fehlerverdaechtige-lint-klassen-steuerze
260921-iwr Listenschluessel und Ausrufezeichen-Zusicherungen: 30 Stellen geprueft, wieder kein echter Fehler. Damit ist der fehlerverdaechtige Rueckstand abgearbeitet. Zwei Vorannahmen des Orchestrators widerlegt, beide durch Messung statt Argument: (1) Die LDAP-Seite galt als heisser Kandidat, weil dort Zuordnungsregeln hinzugefuegt und geloescht werden — die Liste, die tatsaechlich waechst und schrumpft (config.fieldMappings), benutzt jedoch laengst key={mapping.id}; die sechs Meldungen betreffen zustandslose Textlisten. (2) Im Cert-Manager galt eine Zusicherung auf hochgeladenen Dateiinhalt als moeglicher Absturz — der Planer hat eine 83-Byte-Schrottdatei gebaut, die node-forge bag.cert = null setzen laesst, und gegen den echten Dienst laufen lassen: alle vier Pfade enden mit 400, nie 500, und certificateToPem(null) wirft nachweislich, statt still ein falsches Zertifikat zu bauen. Also weder Verfuegbarkeits- noch Integritaetsluecke, sondern eine irrefuehrende Fehlermeldung. Ein Fund dreht die Richtung um: bei admin/modules/grants/page.tsx:246 waere die Korrektur schaedlich — die Gruppierung fasst nur aufeinanderfolgende Kategorien zusammen, die Positionsnummer ist dort fuer die Eindeutigkeit noetig, ohne sie entstuenden doppelte Schluessel. Geaendert: 5 Stellen (drei Waechter im Cert-Manager, die den Meldungstext praezisieren — Status bleibt 400, rot-dann-gruen belegt; zwei ueberfluessige Zusicherungen in imap.provider.ts, die imapflow ohnehin als Pflichtfeld typisiert). 25 Stellen bleiben bewusst stehen und bleiben in der Zaehlung sichtbar — mit Begruendung je Stelle in der Akte, damit der naechste Durchgang sie nicht erneut aufrollt; kein Unterdrueckungskommentar, um die Zahl zu schoenen. Das Tor hat sich selbst bewaehrt: der erste Entwurf eines Waechters erzeugte einen neuen Lint-Fund (430 statt 429) und wurde von der Verifikation des Plans gefangen; die Reparatur brach tsc, weil @types/node-forge Bag.cert als `Certificate undefineddeklariert, waehrend die Bibliothek zur Laufzeitnullzuweist — Endfassung prueft beides. **Zahlen:** 434 → 429,noArrayIndexKeyunveraendert 19 (alle geprueft, alle harmlos),noNonNullAssertion` 11 → 6, web-Tests 68/481 → 69/484, api 72/1137 → 72/1143, type-check 4/4, lint 5/5. 2026-09-21 8716fa5,b4aaed4,27909e4,de69863
260921-jt4 Barrierefreiheit von 30 auf 1 Befund, plus die vier zurueckgestellten Restposten. Die 30 a11y-Befunde galten seit bi2 als "braucht Bedienentscheidungen"; die hat der Orchestrator getroffen, und der Planer hat zwei davon widerlegt: (1) Der vorgesehene Rueckfallweg (role + tabIndex + Tastaturhandler, wo kein echter Knopf geht) tauscht gemessen drei Befunde gegen einen neuen useSemanticElements — eine Regel, die bi2 gerade erst auf 0 gebracht hatte; wird nirgends benutzt, fuer den Verschachtelungsfall (Marktplatz-Karte) tritt eine deckende Geschwister-Schaltflaeche an seine Stelle. (2) Vier der elf "Klick"-Befunde sind gar keine Klicks, sondern onError-Handler an <img> — da gibt es keinen Tastaturweg zu schaffen, sie bekommen aria-hidden. Ein Fund darueber hinaus: alle fuenf ARIA-Befunde sind aria-label auf rollenlosen Elementen — die werden von Vorleseprogrammen still verworfen, die Beschriftungen kamen also bei niemandem an; jetzt mit korrekter Rolle. Sechs Stellen wurden zu echten <button> (Aussehen unveraendert), vier autoFocus auf Seiten entfernt (auf Seiten reisst er beim Laden den Fokus an sich — im Dialog waere er richtig gewesen, alle vier waren Seiten). Ein Befund bleibt bewusst stehen und bleibt gezaehlt (calculator-widget.tsx:323), samt ausdruecklich verworfener Umgehung. Restposten: ZIP-Name uebersetzt mit getesteter Schutzfunktion zip-filename.ts (der frueher genannte Umlaut-Einwand trifft fuer "Zertifikate.zip" nicht zu, die Schutzfunktion sichert kuenftige Uebersetzungen ab); die ueberfluessige case-Marke im Normalisierer aufgeloest, Absicht in den Kommentar gewandert; Kalender-Verschwendung abgestellt. Zur t-Frage eine Korrektur an gof: use-intl 4.13 erzeugt t in einem useMemo, es ist also in der Bibliothek stabil — instabil ist es nur in den Test-Attrappen, und daher kam der Beleg von damals. Die acht Korrekturen aus gof bleiben richtig und schaedlich sind sie nicht, aber die Begruendung war zu breit; ein Test an der Wurzel misst es jetzt. Laufzeitnachweis vom Orchestrator (Browser, 90 Tage Vorschau — bei der Voreinstellung 30 tritt der Doppelabruf gar nicht auf, die Messung haette also nichts gezeigt): drei Monatswechsel holen calendar/sources nur noch 1x statt 4x, und der Termin-Abruf mit identischem Zeitraum ist weg (3 Klicks → 2 Abrufe statt 3). Der 5-Minuten-Auffrischer bleibt unangetastet — belegt nicht durch Warten im Browser (zwei Messversuche waren ungueltig, weil das Werkzeug die Seite zwischendurch neu laedt: nach 330 s Wartezeit war das Dokument 37 s alt), sondern durch Test 19 mit gestellter Uhr: nach advanceTimersByTime(300_000) werden beide Abrufe erneut ausgefuehrt. Zahlen: 429 → 399, a11y 30 → 1, web-Tests 69/484 → 73/529, api 72/1143 unveraendert, type-check 4/4, lint 5/5, keine neuen Unterdrueckungen. 2026-09-21 a8531d4,3d0bc0b,0c89c13,b601141,e651c24,+7 260921-jt4-barrierefreiheit-mit-bedienentscheidunge
260921-ldf Der Wackeltest war ein echter Produktfehler — nachgewiesen, nicht vermutet. CI-Lauf 395 war rot; durchgefallen war ein Test aus quick-260914-m97, rund einmal in 17 vollen Laeufen, isoliert nie. Symptom: Vorschaubild da, Haekchen "Bildschirmfoto anhaengen" aus. Ursache: der Fehler-melden-Dialog war dauerhaft eingehaengt, sein useState(screenshot !== null) lief damit genau einmal — beim allerersten Laden der Seite, als noch kein Bild existierte — und der richtige Wert wurde erst von einem useEffect nachgezogen, der bauartbedingt nach dem Commit laeuft. Beleg, deterministisch statt statistisch: ein MutationObserver ueber jeden einzelnen DOM-Commit zeigt gegen den alten Stand, ohne jede kuenstliche Verzoegerung: COMMIT dialog=true img=ja box=AUS gefolgt von COMMIT dialog=true img=ja box=AN. Der falsche Zustand entsteht bei JEDEM Oeffnen, nicht nur unter Last, und haelt zwei Makrotask-Runden — dazwischen darf der Browser zeichnen, ein Nutzer kann es also sehen. Ehrliche Einordnung der Tragweite: die Korrektur kommt binnen Millisekunden, lange bevor jemand "Senden" treffen kann. Der befuerchtete Fall (Bild gesehen, abgeschickt, Bild fehlt) ist NICHT erreichbar; es bleibt ein kurzes Flackern. Repariert wurde trotzdem der Produktcode, nicht der Test — wer einen wirklich vorhandenen falschen Zustand im Test wegberuhigt, laesst ihn stehen. Zwei Teilursachen, einzeln reicht keine: der Dialog wird nur noch eingehaengt, solange er offen ist (frischer Mount je Oeffnen, der zuruecksetzende Effekt entfaellt), und das Haekchen wird beim Rendern abgeleitet statt nachgezogen. Dieselbe Ursache lag an einer zweiten Stelle: nach einem Versand stand beim erneuten Oeffnen zwei Runden lang der alte Danke-Bildschirm im DOM. Zur Statistik, weil es der Kern der Sache ist: 20 volle Laeufe ohne Fehlschlag gelten ausdruecklich NICHT als Beweis — bei der Ausgangsrate 1:17 waeren sie auch ohne Reparatur zu rund 30 Prozent zu erwarten. Tragend ist, dass der falsche Zwischenzustand nicht mehr existiert und die neuen Tests gegen den alten Stand 5 von 5 rot sind. Kein retry, kein hoeheres Zeitlimit — die Ursache war nie blosse Zeit. Zwei Konstruktionsfehler des Tests mitbehoben: das expect innerhalb der Attrappe (wirft es, landet der Fehler mitten im await von captureScreenshot, dessen catch still null liefert — der Test waere viel spaeter mit "kein Vorschaubild" durchgefallen, also in die falsche Richtung zeigend) und die per Object.defineProperty gesetzte document.body-Groesse, die cleanup() ueberlebte und alle zwoelf folgenden Tests derselben Datei 3200x1000 sehen liess. Widerlegt unterwegs: der Verdacht auf den dynamischen Import von html-to-image — er loest auf, bevor ein zuvor gesetzter setTimeout(0) feuert, ueberschreitet also keine Makrotask-Grenze. Zahlen: Warnungen 399 unveraendert, web-Tests 529 → 531, api 72/1143 unveraendert, type-check 4/4, lint 5/5. 2026-09-21 c0ab5b5,de7fdb7,9f02fcc,a6181e2 260921-ldf-wackeltest-fehler-melden-haekchen-bildsc
260921-m34 288 any im Backend beurteilt: 15 bleiben, mit Urteil je Stelle. Drei Durchgaenge. Der groesste Posten war ein einziges Missverstaendnis: 105 Stellen trugen forTenant(...) as any, obwohl prisma.$extends() laengst einen getypten Klienten liefert — die Zusicherung war nie noetig. Entfernen ergab genau EINEN Folgefehler, und der war selbst ein Befund (eine Handannotation, die nur existierte, um unter dem ungetypten Klienten eine Meldung zu umgehen, und falsch geworden war). Aufgabe 2 war die sicherheitsrelevante: ein gemeinsamer Typ AuthUser fuer die Aufrufer-Identitaet. Die tenantId-Frage wurde HERGELEITET, nicht nach Bequemlichkeit entschieden — `string undefinederzeugt 8 Fehler,stringkeinen, und das war ausdruecklich kein Argument. Belege: Pflichtspalte inschema.prisma:38, Bestandstyp SessionUser, und der Super-Admin-Zweig in TenantGuard. Der dritte Beleg widerlegt stringNICHT, weil der Waechter sein Anfrageobjekt ungetypt holt undAuthUsergar nicht liest — der Zweig kann also nicht zu totem Code werden. Dass es ihn gibt, steht trotzdem im Typsystem:AuthenticatedRequest.tenantIdiststring null undefined, das nullstammt nur von dort, mit Warnkommentar.tenant.guard.tsueber den ganzen Lauf 0 geaenderte Zeilen (Tor). **Aufgabe 3 ist zugleich das Urteilsregister:** typisiert 252, aufunknownumgestellt 21, bleibt 15 — jede der 15 mit Begruendung im Code (6 node-forge, wo die mitgelieferten Typen die Bibliothek nachweislich falsch beschreiben; 3 Cron; 4withTenantTransaction, wo der genaue Typ eine bewusst unvollstaendige Test-Attrappe braeche; 2 imapflow). Null war ausdruecklich NICHT das Ziel. **Vier Befunde gemeldet statt still repariert** — zwei davon brauchen eine Entscheidung des Nutzers: (B-06, sicherheitsrelevant) imap.provider.ts:402setztrequireTLS, das es in imapflow 1.4.3 NIRGENDS gibt (vom Orchestrator unabhaengig nachgeprueft: kein Treffer im ganzen Paket). Die Option wird still verworfen, die Einstellung "STARTTLS" erzwingt also nichts; die Bibliothek faellt dann auf ihr Standardverhalten zurueck und setzt laut eigener Dokumentation unverschluesselt fort, wenn der Server kein STARTTLS anbietet — sie nennt das selbst eine Downgrade-Angriffsflaeche. Richtig waere doSTARTTLS: true. Die as any-Zusicherung hatte das verdeckt. (B-05) imap.provider.ts:78liest.parametersvon einer Zeichenkette (imapflow deklariertdisposition: string, die Parameter liegen in dispositionParameters) — zur Laufzeit immer undefined, Outlook-Anhaenge als application/octet-streamwerden ueber Content-Disposition nicht erkannt; betrifft den DKV-Rechnungseinzug. Dazu (B-04) eine Falle im RLS-Erkenner (er zaehlt jedeselect:-Angabe ausserhalb eines Modellaufrufs als Verstoss) — Erkenner NICHT aufgeweicht, Typ anders hergeleitet; und (B-07) httpntlm liefert den Rumpf als Zeichenkette, nicht als Buffer. **Zahlen:** Diagnosen 399 → 125, anyim Quellcode 288 → 15,apps/web 1 → 0, Disziplin-Zaehler unveraendert (as unknown as33,noNonNullAssertion56, Unterdrueckungen 1,ts-expect-error` 0), api 72/1143, web 73/531, type-check 4/4, lint 5/5, RLS-Waechter 30/30.
260921-oxm IMAP: STARTTLS erzwingt jetzt wirklich, Outlook-Anhaenge werden erkannt. Die zwei Befunde aus m34, beide mit Entscheidung des Nutzers behoben. (B-06, Sicherheit) imap.provider.ts setzte requireTLS — eine Option, die es in imapflow 1.4.3 NIRGENDS gibt (Orchestrator: kein Treffer im ganzen Paket). Sie wurde still verworfen, die Bibliothek fiel auf ihr Standardverhalten zurueck und setzte laut eigener Dokumentation unverschluesselt fort, wenn der Server kein STARTTLS anbietet — Benutzername und Kennwort gingen dann im Klartext. Ersetzt durch doSTARTTLS, nachgeprueft in imap-flow.d.ts:81 und imap-flow.js:1183. Bei implizitem TLS wird ausdruecklich false gesetzt, nicht weggelassen: die Bibliothek wirft bei secure=true zusammen mit doSTARTTLS=true. Gewollte Verhaltensaenderung: ein auf STARTTLS eingestelltes Postfach, dessen Server das nicht anbietet, meldet ab jetzt einen Verbindungsfehler statt still im Klartext zu verbinden. (B-05) imap.provider.ts:78 las .parameters von einer Zeichenkette — imapflow fuehrt die Parameter in dispositionParameters (imap-flow.d.ts:450), der Ausdruck war zur Laufzeit immer leer. Anhaenge als application/octet-stream (typisch Outlook) wurden ueber die Content-Disposition nicht erkannt; betraf den DKV-Rechnungseinzug. Nachgeprueft: imapflow schreibt die Schluessel klein und setzt RFC-2231-Fortsetzungen selbst zusammen — dafuer war nichts zu tun. Rot-dann-gruen belegt: gegen den Stand mit Tests aber ohne Reparatur scheiterten genau 3 von 12 Faellen, danach 12/12. Zwei der fuenf neuen Faelle sind absichtlich von Anfang an gruen — sie sichern ab, dass B-05 nicht zu viel einsammelt. Die as any-Zusicherung konnte ersatzlos entfallen (sie existierte nur wegen der erfundenen Option); alle sechs uebergebenen Felder sind jetzt deklariert. Zahlen: any im Backend 15 → 13, as unknown as 33 → 27 (Testdoppel-Einhaengung in einen Helfer gezogen statt fuenf neue Umdeutungen), kein Zaehler gestiegen, api-Tests 1143 → 1148, web 73/531, type-check 4/4, lint 5/5. 2026-09-21 7691d1f,d0266bf,6def539 260921-oxm-imap-starttls-wirklich-erzwingen-und-anh
260921-pi9 Dashboard-Widget „Bilderrahmen“: eigene Bilder oder https-Adressen als Diashow. Erstes der zwei vom Nutzer bestellten Widgets. API: neues Prisma-Modell DashboardImage (Bytes in der Datenbank — kein neues Docker-Volume, Sicherung ueber den DB-Dump), handgeschriebene Migration 20260921120000_dashboard_image mit RLS-Regel inklusive Benutzerdimension; Routen GET/POST /dashboard/images, GET/DELETE /dashboard/images/:id; Bildtyp ausschliesslich ueber Magic Bytes (PNG/JPEG/GIF/WebP), nicht ueber den behaupteten MIME-Typ; 5 MiB je Datei (multer-Grenze, 413), 30 Bilder je Benutzer; fremde Kennung → 404, nie 403; Binaerantwort mit Cache-Control: private, nosniff, Content-Disposition: inline ohne Dateinamen, CSP default-src 'none'; sandbox. Web: Widget picture-frame mit einer geordneten Liste images aus Eintraegen mit kind-Unterscheider (upload oder url), Bildausschnitt contain/cover, Intervall 0/5…3600 s, Reihenfolge oder Zufall (nie dasselbe zweimal), Unterschrift-Streifen, Grossansicht per Klick (nicht im Bearbeitungsmodus), kaputte Bilder fallen aus dem Umlauf; Bildverwaltung im WidgetSettingsPanel (Upload, https-Adresse, Unterschrift, Pfeile, Entfernen loescht den Upload auch serverseitig). Fremdbilder laedt AUSSCHLIESSLICH der Browser (<img referrerPolicy="no-referrer">) — die API ruft nie eine Adresse ab, keine SSRF-Flaeche; https-Pflicht web-seitig zweifach (Formular + Render-Resolver), weil die API Widget-Configs nicht inhaltlich prueft. Browser-Rundgang (Orchestrator, zehn Punkte) fand drei Dinge, behoben in 8bf3601: die Grossansicht war auf die Kachelflaeche beschraenkt (ein react-grid-item mit CSS-transform wird fuer position: fixed zum Bezugsrahmen → createPortal in document.body wie der Kalender-Tooltip), „1 Minuten“ → ICU-Plural, Standardkachel 8x8 zu flach → 8x12. curl-Rundgang gegen die lebende API belegt 201/400/413/404/401 und fremder Benutzer → 404. Zahlen: api 1148 → 1175, web 531 → 569, type-check 4/4, lint 5/5, RLS-Waechter 78/78, Zaehler unveraendert (as unknown as 27/6, noNonNullAssertion 56, noExplicitAny 13). Anwenderhandbuch und CHANGELOG ergaenzt. 2026-09-21 737974b,c080580,c3b4597,8bf3601 260921-pi9-dashboard-widget-bilderrahmen-bilder-hoc
260921-qd3 Dashboard-Widget „XFrame“: eine Webseite per https-Adresse als Rahmen in der Kachel. Zweites der zwei vom Nutzer bestellten Widgets, vom Nutzer so benannt. Config url (https-Pflicht ueber dieselbe isHttpsUrl-Regel wie der Bilderrahmen, web-seitig doppelt: Formular + Render-Resolver), title (max. 100 Zeichen, Kopfleiste), reloadSeconds (0/60/300/600/1800/3600; Timer haengt den Rahmen per key neu ein, nicht im Bearbeitungsmodus). <iframe sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"> — bewusst OHNE allow-top-navigation* (die eingebettete Seite kann den Tessera-Tab nicht umleiten) und OHNE allow-modals; allow="" (keine Kamera/Mikro/Standort-Delegation), referrerPolicy="no-referrer", loading="lazy". Die API ruft die Adresse nie ab (nur 'xframe' im @IsIn des DTO; kein CSP/Frame-Header in apps/web noetig, per grep belegt). Im Bearbeitungsmodus liegt eine unsichtbare Flaeche ueber dem Rahmen, sonst schluckt der iframe die Zeigerereignisse und die Kachel liesse sich nicht ziehen. Ob eine Seite das Einbetten verweigert, entscheidet die fremde Seite (X-Frame-Options/frame-ancestors, cross-origin nicht erkennbar) — deshalb dauerhafter Hinweis im Formular und immer ein Link „In neuem Tab öffnen“ (rel="noopener noreferrer", in der Kopfleiste oder als Ecksymbol). Formular als eigenes Modul xframe-config-form.tsx wie beim Bilderrahmen; Kachel-Vorgabe 12x12. Browser-Rundgang (Orchestrator, neun Punkte + Tests) ohne Befund: example.com im Rahmen, google.com verweigert mit X-Frame-Options: sameorigin und der Link fuehrt trotzdem hin, Neuladen nach 60 s mit genau einem zweiten Dokumentabruf, Ziehen und Groesse aendern ueber dem Rahmen, API-Log ohne Fremdabruf. Zahlen: web 569 → 603, api 1175 unveraendert, type-check 4/4, lint 5/5, Zaehler unveraendert (as unknown as 27/6, noNonNullAssertion 56, noExplicitAny 13). Biome useAnchorContent wertet aria-label nicht als Linkinhalt → sr-only-Text statt biome-ignore. 2026-09-21 d63d9f5,20a9eb2 260921-qd3-dashboard-widget-xframe-eine-webseite-pe
fast-260922 Kosmetik nach dem Browser-Rundgang (fast, ohne Akte). Bilderrahmen: das laengste Wechselintervall (3600 s) hiess „60 Minuten“, beim XFrame dieselbe Stufe „Jede Stunde“ → neuer Schluessel pictureFrame.intervalHours (ICU-Plural, de + en); Kopfzeile „Bilderrahmen #N“ unter Einstellungen → Dashboard nennt jetzt „— 1 Bild“ / „— N Bilder“ (zwei Schluessel statt ICU, weil die Panel-Tests eine einfache Uebersetzungs-Attrappe nutzen). Web-Tests 603 → 604. Hinweis fuer spaeter: gsd-tools quick-tasks-append scheitert an dieser Tabelle, weil aeltere Zeilen (260918-gza, 260921-iwr, 260921-m34) unmaskierte | im Text tragen — Zeilen daher von Hand anfuegen. 2026-09-22 8b45a28 —
260922-frg Desktop-Client: Update-Eintrag im Tray nie mehr stumm ausgegraut. Befund des Nutzers: „Update installieren“ bleibt grau, obwohl alpha 1.2.0-beta.gc001a08 anbietet und der Client auf a6d1a64 steht — auch nach App-Neustart. Nachgemessen: Tessera-seitig antwortet /desktop/update auf dem alpha-Server selbst (am Proxy vorbei) mit 200 und gueltigem Manifest; DAVOR antwortet der Nginx Proxy Manager auf jede Anfrage an alpha mit 401 Basic (vom Dev-Host und vom Testserver ueber 217.7.63.32 gemessen). Die Webansicht der App merkt sich das Proxy-Passwort, der Updater (tauri-plugin-updater, eigener reqwest) nicht. Produktfehler: das Plugin verschluckt Nicht-2xx-Status (updater.rs 529-559: last_error bleibt leer → Err(ReleaseNotFound)), unser Err(_) => {} machte daraus stumm denselben grauen Eintrag wie „kein Update“; geprueft wurde nur beim Start. Fix (d73aad1, nur lib.rs + CHANGELOG): drei Endzustaende, alle anklickbar — „Auf Beta-Stand … aktualisieren“ (installiert), „Kein Update verfügbar – erneut prüfen“, „Update-Prüfung fehlgeschlagen (HTTP 401) – erneut prüfen“ (Statuscode per eigener Diagnose-Anfrage nachgeliefert, nur Status gelesen); Benachrichtigung mit Erklaerung (Passwortschutz/Zugriffsliste am Proxy), entprellt ueber LastCheckNotice; Wiederhol-Thread alle 4 h (std::thread, ueberspringt bei abgelegtem Update); http-Server weiterhin „Update nur über https möglich“. Proxy-Zugangsdaten NICHT in den Client (T-FRG-03). cargo fmt/clippy/test/build gruen, 37 → 44 Tests, Rot-Nachweis 9x E0425. Behebung beim Nutzer: Passwortschutz vor alpha im Proxy Manager entfernen oder /api-proxy/desktop/* durchlassen; neuen Client einmal ueber den Browser installieren. 2026-09-22 d73aad1 260922-frg-desktop-client-update-eintrag-im-tray-ni
fast-260922-b Desktop-App: Download-Knoepfe in der App ohne Funktion (fast, 747a4d4). Befund des Nutzers: „Herunterladen“ unter Einstellungen → Desktop-App tut in der App nichts (Windows und Linux). Ursache: die Webansicht hatte keinen Download-Handler — webkit2gtk verwirft Downloads dann still, WebView2 zeigte ebenfalls nichts. Fix: Hauptfenster entsteht im Code (app.windows in tauri.conf.json leer), weil nur WebviewWindowBuilder on_download annimmt; der Handler bricht den Download in der App ab und oeffnet die Adresse per Opener im System-Browser (Fortschritt, Speicherort, Passwortfenster fuer den Proxy). Capability main unveraendert. cargo fmt/clippy/test gruen. Nicht am laufenden Client geprueft (kein Display auf dem Dev-Host) — CI baut, Nachweis beim Nutzer oder auf der Windows-VM. 2026-09-22 747a4d4 —
260922-ge2 XFrame: Ausschnitt der Seite waehlen und einpassen, Zoom, „Nur anzeigen“. Wunsch des Nutzers: nur einen bestimmten Ausschnitt der eingebetteten Seite zeigen, und die Groesse soll skalieren. Config: crop {x,y,w,h} in Seitenpixeln bei fester Layoutbreite 1280 (XFRAME_PAGE_WIDTH, keine UI), Klemmung ueber EINE Funktion clampXframeCrop (x+w ≤ 1280 verschiebt x; w ≥ 100, h ≥ 60, y+h ≤ 4000); zoom (50…150 %, nur Ganzseiten-Modus); readOnly (transparente Flaeche ueber dem Rahmen im Ansichtsmodus). Kachel: computeCropLayout (contain + Zentrierung, Massstab darf > 1 sein), der <iframe> wird selbst verschoben und skaliert (cross-origin — die Seite laesst sich von aussen nicht scrollen), Kachelmass per ResizeObserver. Einstellungen: Vorschau der Seite bei 1280 px (Stage 3000 Seitenpixel hoch, eigener Bildlauf), Rahmen als <fieldset> (Biome useSemanticElements) mit vier Eckgriffen, Ziehen per Pointer-Events mit lokalem Entwurf und genau einem PATCH beim Loslassen, Zahlenfelder als Tastaturweg; Zoom-Auswahl nur ohne Ausschnitt; Aktivieren setzt readOnly mit. Befund im Browser-Rundgang, behoben (cf70a19): Kachel und Vorschau hatten verschiedene Rahmenhoehen (max(y+h,720) vs. 3000) — bei vh-relativen Seiten (example.com margin: 15vh) lag derselbe Inhalt an verschiedenen Stellen, der gewaehlte Ausschnitt haette in der Kachel daneben gelegen; jetzt dieselbe Layouthoehe. Neun Pruefpunkte bestanden (Verschieben, Ecken mit fester Gegenecke und Mindestbreite, Klemmung der Zahlenfelder, Einpassen und Mitskalieren bei Kachelgroesse, Nur-anzeigen, Zoom 60 %, verweigernde Seite). Playwright kann in einem per transform skalierten iframe nicht selbst klicken — per elementFromPoint + mouse.click umgangen, ist eine Werkzeuggrenze. Test-Helfer src/test/fake-resize-observer.ts. Zahlen: web 604 → 640, api 1175, type-check 4/4, lint 5/5 (web 53 Warnungen unveraendert), as unknown as 27/6, Umlaut-Allowlist + „Ausschnitt“. 2026-09-22 445b1d3,30fdd99,cf70a19 260922-ge2-xframe-widget-ausschnitt-der-eingebettet
260922-hk4 Bilderrahmen-Bilder liegen jetzt im Dateibereich statt in der Datenbank. Frage des Nutzers nach der Freigabe 1.3.0, ob bytea auf Dauer sinnvoll ist. Befund: Geschwindigkeit ist NICHT das Argument (ein Bild wird je Browser einmal taeglich geladen), die SICHERUNG ist es — gesichert wird von Hand per pg_dump, und 30 Bilder à 5 MiB je Benutzer waeren im Extremfall 150 MB pro Benutzer in jedem Abzug (alpha-DB heute 18 MB). Dazu Einheitlichkeit: Profilbilder (user-files/avatars, User.avatarPath) und DKV-Exporte liegen laengst im Volume. Umsetzung: Spalte storagePath, Ablage user-files/dashboard-images/<userId>/<uuid>.<ext> — Dateiname IMMER vom Server (UUID + Endung aus dem erkannten Mime-Typ), originalName nie im Pfad; ein eigener Ordner je Benutzer ist ausdruecklich KEIN Schutz, es entscheidet weiterhin die Besitzpruefung im Dienst. Umzug laeuft automatisch beim Start (onApplicationBootstrap ueber forSystem()), idempotent; die Spalte data bleibt bewusst vorerst stehen (Todo fuer den DROP, erst wenn alpha und live einmal gelaufen sind). Befund im Rundgang, eigener Commit: eine Zeile zeigte auf eine fehlende Datei (lokal Host vs. Container-Volume; im Betrieb: alter pg_dump + leeres Volume) — getBytes stellt die Datei jetzt aus der noch vorhandenen Spalte data wieder her, statt 404 zu melden. Zahlen: api 1175 → 1188, web 640, type-check 4/4, lint 5/5, RLS-Waechter 78/78. 2026-09-22 9039cea,8cbfb8b,82472ee 260922-hk4-bilderrahmen-bilder-auf-die-festplatte
260922-m1h Ein Modul bringt seine Dashboard-Kachel jetzt selbst mit (Vorarbeit fuer Proxmox). Bestandsaufnahme (lesend) hatte ergeben: ein neuer Widget-Typ war an SIEBEN Stellen hartkodiert (Union-Typ, Constraints, Registry, eigene wireXWidget() je Typ, Aufruf in page.tsx, zweite Liste im Katalogfenster, @IsIn im API-DTO); die Verbindung Kachel↔Modul existierte als WIDGET_MODULE_MAP in dashboard.service.ts (filtert fail-closed), war aber nie befuellt; der Katalog zeigte jedem alle Kacheln, auch die gesperrter Module. Umbau: WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS in packages/shared als EINE Quelle (API validiert per @IsIn gegen genau sie), ein generisches registerWidget() statt neun Funktionen, Katalog leitet seine Liste aus der Registry ab und filtert ueber /modules/active (fail-closed bei Fehler, reine Funktion visibleWidgetTypes), nicht verfuegbare Kachel zeigt widgets.unavailable statt leer zu bleiben. Deckungsgleichheits-Test faengt kuenftig jede vergessene Stelle. Befund des Executors, geprueft statt vermutet: apps/web hatte KEINE Abhaengigkeit auf @tessera/shared (frueher bewusst) — vor der Umsetzung nachgemessen, dass Bau und Produktions-Abbild das tragen (node:24-alpine strippt die Typen nativ); Folgeregel „nur loeschbare Syntax in shared“ steht als Warnung in der Datei. Verhalten der neun Kacheln unveraendert, im Browser bestaetigt (Reihenfolge, Anlegen, Entfernen, keine rohen Schluessel). Bewusst offen: der Einstellungs-Zweig je Typ in widget-settings-panel.tsx und die Live-Aktualisierung des Katalogs. Zahlen: api 1188 → 1202, web 640 → 659, type-check 4/4, lint 5/5 (74/53 wie Basis). 2026-09-22 56c07c3,8be0725 260922-m1h-dashboard-widgets-ein-modul-bringt-seine
260922-vdk Dashboard-Raster misst seine Breite auch aus dem Leerzustand heraus. Meldung des Nutzers aus dem Linux-Client: rechts neben dem Kalender freie Flaeche, in die sich keine Kachel schieben laesst — „als ob es keinen Anker gibt“. Aus dem Bildschirmfoto zurueckgerechnet (Spaltenbreite 51,5 px, Platzhalter auf Spalte 13 = letzte moegliche, Rasterende bei x=1459 bei ~1660 px Inhaltsbreite): das Raster rechnete mit 1200 px statt mit der echten Breite, rechts blieben ~460 px totes Feld. Ursache: die Breitenmessung hing in useEffect(..., []) mit if (!containerRef.current) return — haengt DashboardGrid mit NULL Kacheln ein, rendert der fruehe Ruecksprung in den Leerzustand den gemessenen <div> gar nicht, der Effekt bricht ab und laeuft nie wieder, auch nicht wenn spaeter die erste Kachel entsteht. width blieb die ganze Sitzung auf dem Startwert 1200; react-grid-layout vergleicht strikt (width > breakpoint), 1200 ist damit md (20 Spalten, 51,6 px) statt lg. Fix: Ref-Rueckruf measureRef statt Einmal-Effekt — folgt dem Knoten ueber den Wechsel Leerzustand ↔ gefuellt, misst synchron in der Commit-Phase, haengt den ResizeObserver dort an; Fenster-Horcher als zusaetzliches Netz; applyWidth verwirft 0 und nicht endliche Werte. Verhalten sonst unveraendert — belegte Plaetze bleiben gesperrt, nichts weicht aus (Ansage des Nutzers). Geprueft im echten Client, nicht im Browser: Tessera-1.3.0.AppImage auf DISPLAY=:10 ueber den WebKit-Remote-Inspektor gesteuert. Gleicher Fehlerfall vorher/nachher: Kachel 469 px → 389 px bei 1000 px Bereich, Ziehen endet jetzt bei 603 px = 1000 − 8 − 389, exakt der rechte Rand. Messfalle notiert: im Client gegen style.width/style.transform messen, nie gegen getBoundingClientRect() — bei Fenster im Hintergrund friert WebKitGTK die Animationsuhr ein und der width-Uebergang bleibt auf dem alten Wert stehen. Zahlen: web 659 → 661 Tests, type-check 4/4, lint 5/5, Biome web 53 Warnungen unveraendert. 2026-09-22 d9f2af3,cf67c8a 260922-vdk-dashboard-raster-misst-seine-breite-nich
260923-ad9 Dashboard-Reiter: mehrere Dashboards je Benutzer. Wunsch des Nutzers (23.09.): mehrere Dashboards als Reiter, per Ziehen sortierbar, der erste ist der Standard und wird beim Oeffnen geladen; „als Favorit festlegen“ = nach vorn ziehen, kein zusaetzliches Kennzeichen. Umsetzung in 5 Schritten: neues Modell Dashboard (userId, tenantId, name, position) mit RLS wie die Nachbartabellen; WidgetInstance.dashboardId und DashboardLayout.dashboardId @unique — Kacheln und Anordnung haengen jetzt am Reiter statt am Benutzer. Handgeschriebene Migration 20260923120000_dashboard_tabs haengt den Bestand um: Bestandsuebernahme VOR NOT NULL/Fremdschluessel, danach 0 verwaiste Kacheln, 0 verwaiste Anordnungen, je Benutzer genau ein Reiter auf Position 0. Fuenf Endpunkte unter /dashboard/tabs; assertOwnedDashboard laeuft als erstes in JEDEM Lese- und Schreibweg und antwortet fuer „gibt es nicht“, „Kollege“ und „fremder Mandant“ identisch (kein Orakel) — acht eigene Tests dafuer. Umsortieren und Loeschen je EINE Transaktion nach dem Muster FavoritesService.reorder. Riegel: 20 Reiter, 40 Zeichen, 20 Kennungen je Anfrage. Ziehen per Pointer-Ereignissen ohne neue Abhaengigkeit (Muster xframe-Ausschnitt), ausserhalb des Bearbeitungsmodus moeglich, weil „nach vorn ziehen“ das Festlegen des Standards IST; Umbenennen und Loeschen bleiben im Bearbeitungsmodus, Loeschen mit alertdialog-Rueckfrage. Raster unangetastet (FREE_PLACEMENT_COMPACTOR/preventCollision und die Breitenmessung aus 260922-vdk) — vom Verifizierer per git diff nachgewiesen. Rundgang mit zwoelf Punkten bestanden (Bestand 5 Kacheln erhalten, Reiter leer angelegt, Kacheln je Reiter getrennt, Ziehen ordnet um, nach Neuladen kommt der erste Reiter, Umbenennen, Loeschen mit Rueckfrage, letzter Reiter ohne Loeschknopf, Kachelbreite 531 px bei 1625 px Bereich). Kleiner Befund, offen: die Knopf-Beschriftungen nennen den betroffenen Reiter nicht (nur das Bestaetigungsfenster tut es). Zahlen: api 1202 → 1240 Tests, web 661 → 693, type-check 4/4, lint 5/5 mit 53 Warnungen unveraendert, migrate diff ohne Unterschied. 2026-09-23 9c51823,df7a5e7,d34f682,05feaa3,58ce88e 260923-ad9-dashboard-reiter-mehrere-dashboards-je-b
260923-dhh Proxmox-Modul (PVE, PBS, PMG) — nur beobachten. Sieben Aufgaben: Tabellen ProxmoxServer/ProxmoxServerStatus mit RLS, Zugang verschluesselt per CryptoService, undici-Klient mit Dispatcher nur fuer die eingetragene Adresse, Zwischenlager statt Live-Abfrage, Hintergrunddienst je Mandant (onApplicationBootstrap, Tender-Muster), Einstellungsseite mit Verbindungstest, Modulseite, Doku. Zugang wahlweise API-Token oder Benutzer/Passwort; PMG nur Passwort (Recherche A1: PMG kennt offenbar keine Token). Kopfzeilen-Formate unterscheiden sich je Produkt (PVEAPIToken=…=… vs. PBSAPIToken=…:…) und liegen an EINER Stelle. Riegel „nur lesen“ maschinell erzwungen: proxmox-nur-lesen.spec.ts zaehlt die nicht-lesenden Aufrufe gegen eine benannte Konstante — einzige Ausnahme ist die Ticket-Anmeldung. Keine SSRF-Adresssperre (Proxmox steht per Definition im internen Netz, eine Sperre wuerde jede echte Adresse blockieren) — Schutz ist, dass nur ein Administrator Adressen eintraegt. Rundgang gegen einen selbst gebauten Proxmox-Nachbau (HTTPS, selbstsigniert, echte Antwortformen): Modul im Marktplatz freigeben, Server anlegen, Zertifikatsfehler korrekt benannt, nach gesetzter Ausnahme „Verbindung erfolgreich“, Zahlen der Modulseite exakt wie im Nachbau (18/42 % Last, 3 laufend / 1 gestoppt), unerreichbarer Server meldet „Der Server ist nicht erreichbar“. Drei Befunde daraus in 260923-ku6 behoben. Ein Befund der Abnahme OFFEN: sumOrNull in normalizePmg liefert bei EINEM fehlenden Teilwert die halbe Summe statt null — stiller Falschwert genau dort, wo die Feldnamen am schlechtesten belegt sind. Zahlen: api 1240 → 1311 Tests, web 693 → 708, type-check 4/4, lint 5/5, 53 Warnungen unveraendert. 2026-09-23 3a1bfd9,4f8a368,998aba9,fccaf8d,723cf68,06fcdc0,3091b04 260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n
260923-ku6 Drei Befunde aus dem Proxmox-Rundgang behoben. (1) „Verbindung testen“ pruefte den GESPEICHERTEN Stand statt der Eingabe — wer den Zugang tippt und vor dem Speichern testet, bekam die Antwort zum alten Wert; jetzt eigene Route POST servers/test mit Merge-Regel: normale Felder folgen dem Formular (auch geleert), Geheimnisfelder folgen „leer → gespeicherten Wert behalten“, weil das Formular Geheimnisse nie vorbefuellt. (2) Ein frisch angelegter Server zeigte „Ein unerwarteter Fehler ist aufgetreten“, obwohl nur noch nichts abgefragt war — jetzt eigener ruhiger Zustand mit Verweis auf „Jetzt aktualisieren“. (3) Die Klasse uppercase faerbte die ganze Zeile und zeigte die Adresse als „HTTPS://…“ — jetzt nur noch das Produktkuerzel. Zahlen: api 1311 → 1316, web 708 → 712, 53 Warnungen gehalten (eine neu ausgeloeste useOptionalChain-Warnung gleich mit aufgeloest). 2026-09-23 710034c,f1bb7f7 260923-ku6-drei-nachbesserungen-aus-dem-browser-run
260923-ku6 Drei Nachbesserungen aus dem Browser-Rundgang zu 260923-dhh (Proxmox-Modul). Befund 1 (wichtig): „Verbindung testen" pruefte den gespeicherten Server statt des Formulars — im Formular abgeschaltete Zertifikatspruefung oder ein neu eingetipptes Geheimnis griffen erst nach dem Speichern. Fix: neues TestProxmoxServerDto + Merge-Baustein resolveEffectiveTestServer in ProxmoxService, neue Route POST servers/test fuer die Neuanlage (noch kein gespeicherter Server), Geheimnisfelder behalten die bestehende „leer gelassen -> gespeicherten Wert weiterverwenden"-Regel. Befund 2 (wichtig): ein frisch angelegter, nie abgefragter Server zeigte faelschlich „Ein unerwarteter Fehler ist aufgetreten" statt eines ruhigen Hinweises — behoben ueber status.lastPolledAt === null. Befund 3 (kosmetisch): uppercase faerbte die ganze Statuszeile inkl. Adresse gross — jetzt nur noch das Produktkuerzel. Zahlen: api 1311 → 1316, web 708 → 712, type-check 4/4, lint 5/5, Biome web 53 Warnungen unveraendert. 2026-09-23 710034c,f1bb7f7 260923-ku6-drei-nachbesserungen-aus-dem-browser-run
260923-le6 Zwei Abnahmebefunde zum Proxmox-Modul behoben. (1) sumOrNull in normalizePmg liefert jetzt null, sobald EIN Teilwert (Spam/Viren je Richtung) fehlt — vorher stille Teilsumme als vollstaendige Zahl (Blocker aus 260923-dhh-VERIFICATION, Wahrheit 7). (2) „Jetzt aktualisieren“ nur noch fuer ADMIN/SUPER_ADMIN sichtbar (Endpunkt verlangte das schon); ServerCard bekommt isAdmin, Nicht-Admins lesen bei nie abgefragtem Server „Die Werte erscheinen nach der naechsten automatischen Abfrage“ statt eines Verweises auf den Knopf. Neuer Seitentest proxmox-page-roles.test.tsx (5 Rollenfaelle). Offener Randfall: inaktiver, nie abgefragter Server — Text passt dort nicht ganz, Nutzerentscheidung. Proxmox-Tests api 82, web 26 gruen; Typpruefung beider Seiten fehlerfrei; Biome ohne neue Befunde. 2026-09-23 c13d657,2eb86e1,2f8dd14,e1b191b 260923-le6-proxmox-abnahmebefunde-sumornull-null-be
260923-fst Favoriten-Kachel bis auf eine Spalte schmal ziehbar (fast): WIDGET_CONSTRAINTS.favorites.minW 3 -> 1; Titel kuerzt, Symbol bleibt. Im Browser gezogen: 321 -> 47 px. 2026-09-23 b03ffb5 —
260923-bug Fehler melden: Bildschirmfoto scheiterte an einem fremden Bild (fast): html-to-image bricht die ganze Aufnahme ab, sobald ein <img> ohne CORS nicht nachladbar ist -> Haekchen gesperrt. Jetzt imagePlaceholder + onImageErrorHandler, zweiter Versuch ohne Bilder/Rahmen. Im echten Linux-Client 1.3.1 nachgestellt (Probe-Bild google favicon) und nach dem Fix gegengeprueft. 2026-09-23 bf4384a —
260923-lrr Favoriten: eigenes Symbol hochladen, Symbol sofort aktualisiert, Cloudflare-Meldung. Versionszaehler iconVersion an der Symboladresse (?v=) statt 24-h-Zwischenspeicher mit fester Adresse (Ursache „neue Logo-Adresse, nichts passiert“); Cache-Control: private. Upload PNG/JPEG/GIF/WebP/ICO/SVG bis 512 KB nach Dateiinhalt, Ablage user-files/favorite-icons/<userId>/<id>.<ext>, Vorrang vor Logo-Adresse, Entfernen-Knopf. Neue Logo-Adresse wird beim Speichern einmal zur Probe abgerufen; scheitert es (Cloudflare-Pruefung, 403), bleibt das Formular offen mit deutscher Meldung und Hinweis aufs Hochladen. Aufraeumen der Dateien auch beim Loeschen einer Kachel/eines Reiters (T-LRR-07 geschlossen). Browser-Nachweis: rot hochgeladen -> sofort rot (v=1), blau -> sofort blau (v=2), Entfernen -> altes Logo (v=3), httpbin 403 -> Meldung, google favicon -> sofort (v=4). Nachtrag Orchestrator: Zeile dashboard.service.ts/favoriteLink in der Zugriffsklassifikation. api 1368, web 742 gruen. 2026-09-23 7704372,61f95c8 260923-lrr-favoriten-eigenes-symbol-hochladen-und-s
260924-h7x Proxmox-Seite neu gestaltet (Status bestimmt das Bild) und Dashboard-Reiter in die Kopfzeile. Nutzer hob am 24.09. die Umbausperre vom 23.09. selbst auf. Design-Plan aus dem frontend-design-Skill: Statusfarben als OKLCH-Tokens (--status-ok/warn/down/idle/orphan, dazu -fg-Textvarianten fuer 4,5:1), Gesundheitsbalken mit Legende, Karten mit Statusleiste links und im Statuston getoentem Schatten, eingelassene Messfelder, Knoten als Einschuebe mit Balken nach Schwellen (80/92 %, Sicherung > 26 h), PMG-Zahlfelder. Deaktivierter Server = „Offline & verwaist“ (Vorrang vor allem, keine alten Werte, gestrichelt). Sortierung down/warn/ok/idle/orphan, Spaltenfluss statt Raster. Reiter als eingelassener Umschalter per Portal in der Kopfzeilenmitte (header-center-slot), eigene Zeile entfallen, Pfeiltasten, weiche Randausblendung bei Ueberlauf; unter 640 px Logo nur Bildmarke. Browser: hell/dunkel 1400 px, 390 px ohne Ueberlauf. web 789 gruen. 2026-09-24 0fa7ce0,57c338f,7416a92,0b659d6,57a4196 260924-h7x-proxmox-seite-status-design-und-dashboar
260924-i8v Proxmox-Kachel fuers Dashboard. Modul-Kachel ueber den Weg aus 260922-m1h (Typ proxmox in packages/shared + Modulbindung, API-Freigabeliste, Registry, Katalog), nur fuer Benutzer mit Modulzugriff. Kompakter Gesundheitsbalken + Zusammenfassung in Worten, Serverliste nach Dringlichkeit mit je einer Kennzahl (Gaeste/Auslastung, aelteste Sicherung, eingehende Mails, unbekannt nie 0), Links auf /modules/proxmox (nicht im Bearbeitungsmodus), liest jede Minute den Zwischenstand (pausiert bei verborgenem Tab, loest NIE eine Abfrage aus), Titel + Serverauswahl an der Kachel und unter Einstellungen > Dashboard, Groessenstufen per Container-Query. Gemeinsame Teile nach components/proxmox/ verschoben. Browser: Katalog, Kachel hell/dunkel, schmale Stufe (nur Punkte+Namen). web 864, api 1370 gruen. 2026-09-24 a906c67,92bf130,a217d60,377b6e3,586da44,602a45c 260924-i8v-proxmox-kachel-fuers-dashboard
260924-m4n Flackernden Test entschaerft, alte Bildspalte entfernt. (1) tenant-selector.test.tsx: Ursache war das Laden der Bausteine INNERHALB des ersten Tests (zaehlte in dessen 5-s-Grenze) -> Import vorab, Doppelfall getrennt, dasselbe in zwei weiteren Marktplatz-Tests; langsamster Web-Test jetzt < 2 s (mit 2 Kernen 1,3 s); act()-Warnungen der Proxmox-Kachel weg. (2) DashboardImage Stufe 2: Migration 20260924120000_dashboard_image_drop_data mit Schutz (bricht ab, wenn noch Zeilen ohne storagePath; Zeilenschutz fuer die Pruefung abgeschaltet, sonst saehe sie still 0), storagePath NOT NULL, data weg, system_read_policy weg, Bootstrap-Umzug + forSystem() entfernt, Upload legt Zeile gleich mit Pfad an. Vorbedingung alpha geprueft (0 von 3 ohne Pfad); Live nicht pruefbar. Rueckweg bei Abbruch in docs/anleitung-betrieb.md Kap. 4. Browser/API: Bilder laden, Upload+Anzeige+Loeschen ok. api 1364, web 865 gruen. 2026-09-24 b10734f,dd54ec5 260924-m4n-flackernden-test-entschaerfen-und-dashbo
260925-bow Was-ist-neu-Fenster nach Versionswechsel. Spalte User.lastSeenReleaseVersion (Migration 20260925120000), Versionsnummer allein aus der API (GET /users/me/release-notice, Semver-Funktionen in packages/shared), Fenster im Portal-Rahmen einmal nach Versionswechsel, gemerkt erst beim Schliessen (POST), nur freigegebene Versionen (dev nie), hoechstens 3 Versionen + Hinweis auf aeltere + Link /changelog; neue Konten bekommen die laufende Version eingetragen; vorhandene ohne Stand sehen nur die aktuelle. Changelog-Text bleibt serverseitig. Browser: 1.3.0 -> Fenster 1.4.0, Verstanden merkt 1.4.0, kein zweites Mal; 1.0.0 -> 1.4.0/1.3.1/1.3.0 + „2 aelteren Versionen“; Link-Kontrast nachgebessert. api 1435, web 924 gruen. 2026-09-25 59db32a,187fb76,5ae9aaa,b3b7b5d 260925-bow-was-ist-neu-fenster-beim-ersten-anmelden
260928-ujj Design Mosaik uebernommen + Hintergrund pro Benutzer. Merge design/mosaik (76d17fe, inkl. RESIZE_AXIS_FALLBACK), Spalte User.dashboardBackground JSONB (Migration 20260928120000), PATCH /users/me/dashboard-background mit parseDashboardBackground aus packages/shared (Preset-Liste, imageId nur UUID), Web liest aus Sitzung, alte localStorage-Wahl einmalig uebernommen. Browser: Duenen gewaehlt, DB-Zeile gesetzt, nach localStorage-Loeschen weiter sichtbar. api 1462, web 952 gruen; Freigabe als 1.5.0. 2026-09-28 9fa0a3f,0aaa152,cb45d26 260928-ujj-design-mosaik-uebernehmen-und-als-1-5-0-
260929-9wc Eigene Module (nur lokal, nicht gepusht). Modell CustomModule + Migration 20260929120000 mit RLS (Muster ProxmoxServer), /custom-modules (GET alle Angemeldeten, POST/PATCH/DELETE Admin, nur https ohne Zugangsdaten), MODULE_CATEGORIES in packages/shared, Seitenleisten-Eintrag unter gewaehlter Kategorie, Rahmen-Seite /modules/custom/[id] mit XFRAME_SANDBOX + no-referrer + „In neuem Tab öffnen“, Verwaltung /admin/custom-modules, Zugriffsklassifikation 61/224/6. Gruppen-Beschraenkung zurueckgestellt (ModuleGrant haengt an Module). api 1495, web 992 gruen; Browser dunkel 9 Schritte bestanden. 2026-09-29 b9d87be,e7fc4de,e48c0de 260929-9wc-eigene-module-admin-legt-seitenleisten-e
260929-d37 Desktop-App nur einmal starten. User-Meldung Windows 11: beim Systemstart zwei Instanzen/zwei Tray-Symbole. tauri-plugin-single-instance 2.4.5 als erstes Plugin, zweiter Start ruft show_main_window (neuer Helper, ersetzt 3 Kopien) und beendet sich. cargo build/test (44)/clippy gruen. Windows-Pruefung offen (VM 8233 oder User-PC nach naechster Desktop-Version). 2026-09-29 c0b145a,0751198 260929-d37-desktop-client-nur-einmal-starten-single
260929-dmx Widget-Raster horizontal feiner + Kalender schmaler. COLS lg 48/md 40/sm 24/xs 16/xxs 4, GRID_VERSION 3 (v2->v3 nur x/w/minW/maxW x2), alle minW/defaultW x2, Kalender minW 8 (~250 px). Browser: Anordnung pixelgleich, Kalender bis 252 px, Schritt 33 px. Auch: Hover-Anheben der Widgets entfernt (acd3c7a, Nutzerwunsch). 2026-09-29 97744b5,9c9e142,46ebb4e 260929-dmx-widget-raster-horizontal-feiner-48-spalt
260929-dzu Eigene Module fuer jeden Benutzer (persoenlich). CustomModule.ownerUserId (null = gemeinsam), RLS-Muster SearchProvider, Einstellungen > Eigene Module (nur eigene), Verwaltung nur gemeinsame; Browser: Sichtbarkeit/Rechte wie verlangt. Nebenbei ohne eigenen Quick: Zentrierung entfernt (bc4c011), Desktop neue Fenster -> System-Browser (76a9234, Windows-VM bestaetigt), Single-Instance auf VM bestaetigt. 2026-09-29 c703d87,ee97b4e,8f41bd2 260929-dzu-eigene-module-fuer-jeden-benutzer-persoe
260929-if2 Erinnerungen-Widget (Reminder). Modell Reminder + RLS, API /reminders (anlegen/listen/bearbeiten/loeschen/erledigt/snooze, 409/404-Regeln), E-Mail-Scheduler alle 30 s mit Claim-once + max. 3 Versuche, globaler ReminderNotifier (Browser-Notification, Desktop via Tauri-Notification mit Laufzeit-Capability nur fuer die Server-Origin, Pattern escaped + vorab geprueft). Verifier human_needed (Windows-Toast offen); Browser dunkel bestanden inkl. echter Mail ueber MailHog. api 1570, web 1069, cargo 57. Nebenbei: eigene Module ohne Kopfzeile (cd1f8f6), Update-Klick prueft frisch (41d00a3). 2026-09-29 325c5dd,709b41a,6879c75 260929-if2-reminder-widget-mit-benachrichtigung
260929-lh3 Favoriten: eigene Symbol-Adresse wirkt. Neue iconUrl ersetzt Upload + bumpt iconVersion; iconUrl wird auch gespeichert, wenn nur der Browser sie laden kann (kein 422 mehr, nur Formpruefung); Kachel: Proxy -> iconUrl direkt -> origin/favicon -> Buchstabe; Discovery liest auch aus Nicht-2xx-Seiten (docuvita 400). 2026-09-29 7188c5b,b15c746,0e72ad4 260929-lh3-favoriten-eigenes-symbol-wirkt-nicht

Deferred Items

Items acknowledged and carried forward from previous milestone close:

Category Item Status Deferred At
Live-Test AD WINDOWS #4 am 2026-09-07 geschlossen. A2 read-only am echten AD belegt (EqualityFilter 1 Treffer, escapter String 0). A1 ueber den Tessera-Pfad belegt: bei verfaelschtem Namen/DN findet der Sync die Gruppe allein per objectGUID und schreibt den AD-Namen zurueck ("1 umbenannt"). Nicht gemessen, weil dafuer das Verzeichnis geaendert werden muesste: dass AD den objectGUID bei Umbenennung stabil haelt — zugesicherte AD-Eigenschaft, kein Tessera-Code erledigt 2026-09-07
Live-Test AD WINDOWS #6 am 2026-09-07 geschlossen. (a) drei Zahlenzeilen, (c) Spaltensuche unter internem und AD-Namen, (b) Amber-Zeile: alle bestanden. (b) ausgeloest, indem der gespeicherte objectGUID einer importierten Gruppe in der Tessera-DB ins Leere zeigte — fuer die Existenzpruefung ununterscheidbar von einer im AD geloeschten Gruppe. Markierung wanderte vor der Loeschung zurueck (D-06 haelt) erledigt 2026-09-07
Live-Test E-Mail WINDOWS #12 am 2026-09-09 zurueckgestellt (Ledger: waived). Es gibt intern derzeit kein Postfach, in das Ausschreibungs-Alarme hereinkommen — die Voraussetzung existiert nicht; ein frueheres Missverstaendnis hatte eines angenommen. Nachzuholen, sobald ein Alarm-Postfach eingerichtet ist. Teilentlastung: der handgeschriebene NTLM/EWS-Weg wurde am 2026-09-09 beim Test von #16 erstmals gegen den echten Exchange owa.ctl.de gemessen — Anmeldung, FindFolder und Ordneraufloesung funktionieren (echte ServerVersionInfo 15.2 im Protokoll). Offen bleibt allein das Einlesen einer echten Alarm-Mail zurueckgestellt, Voraussetzung fehlt 2026-09-09

Entscheidung des Users vom 2026-09-07 zum AD-Zugriff: An der AD-Struktur wird nichts veraendert — nicht von Claude, nicht vom User. Keine Testgruppen, keine Umbenennungen, keine Loeschungen. Das Dienstkonto svc_tessera hat nur Lesezugriff, und das ist gewollt. Nie Schreibrechte vorschlagen und nie eine Aenderung im Verzeichnis erbitten.

Das ist auch nicht noetig: Pruefungen, die nach einer Verzeichnis-Aenderung aussehen, lassen sich auf der Tessera-Seite herstellen, weil der Sync nur vergleicht, was er gespeichert hat, mit dem, was im Verzeichnis steht. Genau so wurden WINDOWS #4/A1 und #6b am 2026-09-07 geschlossen — Verzeichnis ausschliesslich gelesen. Siehe 16-LIVETEST-2026-09-07.md.

Entscheidung des Users vom 2026-09-14 zur Mandantenfaehigkeit (ERSETZT die Lesart vom 2026-09-07): Der User will "vorerst von der Mandantenfaehigkeit nichts mehr wissen" — das Thema hat ihn viel Zeit gekostet und er ist darueber veraergert. Gebaut und gepusht sind Etappe 1, 2, 3b und 3c; Tessera laeuft als Ein-Firmen-System vollstaendig (alpha, BYPASSRLS, Schalter AUS), und das reicht ihm. Etappe 3a (Anmeldenamen pro Mandant) und Etappe 4 (Scharfschalten) RUHEN auf unbestimmte Zeit — nicht vorschlagen, nicht als "naechsten Schritt" auflisten, nicht in Zusammenfassungen als offen fuehren; die zugehoerigen Ledger-Eintraege (#18, #22, #23, #25, #26, #28, #31, #32, #33, #34, #37) bleiben stehen, werden aber nicht vorgelegt. Der Schalter bleibt AUS. Neue Funktionen werden weiterhin mandantensicher gebaut (forTenant(), wie bisher), aber ohne das Thema zu benennen. Der User erwaegt, die Mandantenfaehigkeit ganz zu streichen und stattdessen je Kunde einen eigenen Docker-Container zu betreiben, in dem er als Betreiber Module mit Lizenzanzahl freigibt — festgehalten in .planning/todos/pending/2026-09-14-lizenzmodell-freigabe-je-server-mit-lizenzanzahl.md. Diese Entscheidung faellt der User, wenn er sie faellen will; wir stossen sie nicht an.

Entscheidung des Users vom 2026-09-07 zur Mandantenfaehigkeit: Tessera wird zunaechst nur intern eingesetzt. Die Mandantentrennung ist damit vorerst zweitrangig — sie bleibt in der Architektur verankert und wird nicht zurueckgebaut, aber ihre Abnahme hat keine Dringlichkeit. Konkret betrifft das den offenen Abnahmeplan 02-05 (Phase 2), dessen Erfolgskriterium 4 die Datentrennung zweier Mandanten im Browser nachweisen soll: bewusst zurueckgestellt, nicht vergessen. Vor einem Einsatz bei externen Kunden ist er nachzuholen.

Entscheidung des Users vom 2026-09-07: Diese drei Punkte werden spaeter am Live-System geprueft, nicht in der Entwicklungsumgebung. Sie bleiben im Ledger ausdruecklich open und sind NICHT auf waived gesetzt — sie sollen nachgeholt werden, nur eben dort, wo ein echtes Verzeichnis und ein echtes Postfach erreichbar sind. Kein Anlass, sie vorher erneut vorzulegen.

Session Continuity

Last session: 2026-09-22T13:40:00Z Resumed: 2026-09-21 (abends) ueber /gsd-resume-work; seitdem Bilderrahmen, XFrame (inkl. Ausschnitt), Desktop-Korrekturen, Freigabe 1.3.0, Bilder in den Dateibereich. Stopped at: hk4 fertig und nachgewiesen. Dem Nutzer vorgelegt: erst das Aufraeumen (Modul bringt seine Kachel selbst mit), dann Proxmox-Modul + Kachel — Antwort steht aus. Resume file: None Last activity: 2026-09-29 - Quick 260929-if2 Erinnerungen-Widget (lokal, nicht gepusht); v1.7.0 auf alpha+live