Commit Graph

248 Commits

Author SHA1 Message Date
schalli 73122b5676 test(260805-d0r): add failing test for MANUAL/LDAP origin badge on chips 2026-08-05 09:36:45 +02:00
schalli f8ff74bd3f feat(260805-d0r): UserAccessModal chips render from data.groups (D-16)
- Chips come from the response's groups (actual GroupMembership rows),
  not the union of row.viaGroups -- closes the reproduced defect where
  revoking a group's last grant hid an otherwise-unchanged membership
- React key is the group id, not the name
- Test file: moved the LDAP/MANUAL badge assertion out of this commit,
  it belongs to Task 2 which reuses admin.groups.members
2026-08-05 09:35:54 +02:00
schalli 771f680034 test(260805-d0r): rewrite modal test fixtures to { groups, modules } shape
- Regression: group without any module grant stays visible as a chip
- New: mixed empty-modules + populated-groups case, LDAP/MANUAL badge case
2026-08-05 09:34:42 +02:00
schalli 2e7daa481e feat(15-08): Marketplace-Karte mit drittem Zustand "Kein Zugriff"
- marketplace/page.tsx und marketplace/[slug]/page.tsx auf GET
  /modules/catalog umgestellt (ein Aufruf statt zwei), beide Statusflags
  (isActiveForTenant, hasAccess) kommen in einer Antwort -> kein
  Zwischenzustand, in dem eine Karte kurzzeitig ohne Sperr-Badge
  anklickbar erscheint
- MarketplaceCard bekommt hasAccess-Prop: drittes Badge (Bernstein,
  "Kein Zugriff") bei isActive && !hasAccess, Karte opacity-60/
  cursor-not-allowed, Klick loest Toast statt Navigation aus; bei
  Zugriff navigiert der Klick zu /marketplace/[slug]; Badge-Reihe
  bekommt flex-wrap gegen Overflow bei langen Namen
- [Rule 2] Marketplace-Ansicht war zuvor komplett isAdmin-gated
  (Zugriff verweigert fuer USER) - das widersprach D-08 ("Katalog
  bleibt Schaufenster fuer jeden authentifizierten Benutzer") und
  haette das neue Sperr-Badge fuer USER nie sichtbar gemacht. isAdmin
  gated jetzt nur noch die Aktivieren/Deaktivieren-Aktion (canManage),
  nicht mehr die gesamte Seite
- bestehende Marketplace-Tests auf einaufrufiges Catalog-Mock
  umgestellt, "access-denied fuer non-admin"-Test durch "Karten
  sichtbar, aber ohne Manage-Button" ersetzt
2026-08-04 19:49:46 +02:00
schalli 43c7fa200b feat(15-08): serverseitige Modulsperre mit 403-Seite
- checkModuleAccess (module-access-actions.ts) fragt GET /modules/active
  serverseitig ab, Cookie-Weiterleitung nach fetchCurrentUser-Muster,
  schliesst im Zweifel (fehlendes Cookie, nicht-ok, Fehler -> false)
- page.tsx zur async Server Component umgebaut, rendert bei fehlender
  Freigabe das 403-Markup direkt (kein notFound(), kein Redirect, D-07)
- bisheriger Client-Inhalt (Whitelist-Pruefung, Nicht-gefunden-Zustand,
  Ruecknavigation) unveraendert nach module-shell.tsx ausgelagert
- 5 Tests in module-access.test.tsx: 403-Zustand, Shell-Rendering,
  fehlendes Cookie, nicht-ok-Antwort, unregistrierter Slug trotz Zugriff
2026-08-04 19:41:25 +02:00
schalli 050d070340 feat(15-07): Benutzer-Detail mit geerbten Rechten und Direkt-Freigaben
- Neue UserAccessModal.tsx: laedt einmal GET /module-grants/users/:userId
  und rendert daraus zwei Abschnitte -- Gruppenmitgliedschaften (read-only
  Chip-Liste, dedupliziert aus allen viaGroups-Namen; Bearbeitung bleibt
  ausschliesslich unter /admin/groups, D-16) und Modul-Zugriff (Modul |
  erbende Gruppen als Chips oder "–" | Direkt-Checkbox)
- Direkt-Checkbox verhaelt sich identisch zur Matrix-Zelle: optimistisches
  Toggle via POST/DELETE /module-grants mit moduleId+userId, Rollback samt
  sichtbarer Fehlermeldung bei Fehlschlag (T-15-25), aria-label pro Zeile
  aus admin.users.grants.directCheckboxLabel
- admin/users/page.tsx: vierter Aktionsbutton "Details" je Zeile oeffnet
  das Modal
- user-access-modal.test.tsx: 5 Tests (Chip-Liste + Leerzustand, Modultabelle
  mit geerbtem/nicht-geerbtem Modul, Rollback bei Fehler, Hinweistext ohne
  aktive Module, aria-label je Checkbox)
2026-08-04 19:25:15 +02:00
schalli 3cd6d997cf feat(15-07): Aktivierungsdialog mit drei Aktionen in /admin/modules
- Neue ActivateModuleDialog.tsx: Abbrechen / "Spaeter konfigurieren" (nur
  POST /modules/:id/activate) / "Sofort freigeben" (POST .../activate
  gefolgt von POST /module-grants fuer die als Standard markierte Gruppe) --
  zwei getrennte Aufrufe, kein neuer kombinierter Endpoint (D-10)
- Laedt GET /groups beim Oeffnen; ohne markierte Standardgruppe ist "Sofort
  freigeben" disabled mit Hinweistext (D-13)
- admin/modules/page.tsx: toggleModule-Klick verzweigt -- Deaktivierung
  bleibt direkt, Aktivierung oeffnet den Dialog statt sofort zu aktivieren;
  Erfolg aktualisiert Aktivierungs-Map + Sidebar-Refresh wie bisher
- grants-matrix.test.tsx erweitert um 3 Tests fuer den Dialog (alle drei
  Buttons vorhanden, Sofort-freigeben deaktiviert ohne Standardgruppe,
  Aufrufreihenfolge activate->grant)
2026-08-04 19:21:25 +02:00
schalli 6b2a6f1ce4 feat(15-07): Freigabe-Matrix Module x Gruppen unter /admin/modules/grants
- Neue Client-Komponente admin/modules/grants/page.tsx: laedt GET /module-grants/matrix
  einmal, rendert Module x Gruppen mit sticky erster Spalte/Kopfzeile, Kategorie-
  Gruppierung, Suchfeld und Admin-Bypass-Fussnote (D-03/D-15, PERM-03)
- Jede Zelle togglet sofort optimistisch (POST/DELETE /module-grants); Fehlschlag
  springt die Checkbox zurueck und zeigt die Fehlermeldung im bestehenden error-Div
  (T-15-25) -- identisches Muster zu AdminModulesPage.toggleModule
- aria-label pro Checkbox aus admin.groups.grants.matrixCheckboxLabel beschreibt die
  bevorstehende Aktion (freigeben/entziehen), nicht den aktuellen Zustand
- admin/modules/page.tsx: neuer Header-Button "Freigaben-Matrix" verlinkt auf die
  Unterseite, kein siebter Sidebar-Eintrag
- grants-matrix.test.tsx: 5 Tests (befuellte Matrix, leerer Zustand, Rollback bei
  Fehler, Suchfilter, aria-label je Checkbox)
2026-08-04 19:18:30 +02:00
schalli 4985e43412 feat(15-06): group member management + delete dialog with concrete impact numbers
- GroupMembersModal.tsx: chip list of current members with source badge
  (MANUAL/LDAP); LDAP-sourced chips carry a disabled remove button with a
  "managed via AD sync" tooltip (D-19) instead of an active one; second
  section adds manual members via GET /users + POST /groups/:id/members,
  already-member candidates shown disabled (upsert on the API is
  folgenlos, no special-case needed)
- DeleteGroupDialog.tsx: loads GET /groups/:id/impact and interpolates
  memberCount/grantCount into the confirmation text (D-17); deliberately
  breaks from the project's silent-delete-failure precedent -- stays open
  and shows a visible error on a failed DELETE, since a silent failure
  here would leave an admin believing a group (and its grants) is gone
  while it still grants access (T-15-24)
- page.tsx: wires both dialogs in, refetches the group list after any
  member/delete mutation so member counts and badges stay current
- groups-page.test.tsx: disabled-vs-active remove button by membership
  source, delete text shows both numbers, visible error + dialog stays
  open on failed delete
2026-08-04 19:02:11 +02:00
schalli c3ba1f74ea feat(15-06): /admin/groups table + create/rename modal with AD radio-select binding
- page.tsx: sixth admin route, table (Name/AD-Bindung/Standardgruppe/
  Mitglieder/Aktionen), empty state matching AdminUsersPage's noUsers
  pattern, optimistic default-group star toggle (PATCH /groups/:id
  isDefault) with rollback + visible error div on failure, full refetch
  on success since setting one group default unsets all others server-side
  (D-13 transaction)
- GroupFormModal.tsx: create/rename dialog; AD binding section reuses
  GET /ldap/groups (D-18) with a radio list (D-05: exactly one AD group
  per Tessera group) instead of the LDAP page's checkbox multi-select;
  visible discoverError/noResults states instead of a silent-empty list
  (UI-SPEC backstop); create-with-binding does POST then a second PATCH
  since CreateGroupDto only accepts `name`
- groups-page.test.tsx: empty state, populated table, star-toggle
  optimistic PATCH + rollback-on-failure
2026-08-04 18:57:43 +02:00
schalli 90dc3981d5 feat(15-06): phase-wide i18n keys + sixth admin nav entry for groups
- de.json/en.json: admin.groups.* (incl. ldapBind, members, deleteConfirm,
  grants sub-namespaces), admin.users.grants.*, adminModules.grantsLink +
  grants.* + activationDialog.*, modules.accessDenied.*, marketplace
  statusNoAccess/toastNoAccess, header.admin.groups -- covers this plan's
  /admin/groups surface plus the Wave 4 surfaces (permission matrix,
  user-detail grants, activation dialog, 403 page, marketplace badge) so
  15-07/15-08 can run in parallel without touching the translation files
- admin-sidebar.tsx: sixth nav entry "Gruppen" -> /admin/groups with a
  roster/list icon (Lucide list glyph, distinct from the users icon)
2026-08-04 18:50:28 +02:00
schalli 96be7e168b feat(260729-d3k): multi-line Base-DN textarea + reworked scope i18n
- Base-DN admin field is now a multi-line textarea (one DN per line),
  value stays a single newline-separated string, no schema change
- baseDnHint key added (de/en) explaining the Base-DN(s) sync scope
- groupFilter.description/emptyMeansAll reworded: group filter is an
  optional extra restriction; empty selection means all users under
  the base DN(s) are synced (drops the old "nothing is synced"
  framing)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-29 09:37:51 +02:00
schalli 63a07abb47 feat(260728-lih): default LDAP create-form syncIntervalMin to 0 + reword copy
- New-config create form now defaults syncIntervalMin to 0 (matches
  backend default, auto-sync off by default)
- de+en groupFilter.description + emptyMeansAll reworded: empty
  selection now says "nothing is synced" instead of "imports everyone
  under the base DN" (matches the backend semantic change)

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 12:10:39 +02:00
schalli bdaf154f66 feat(260723-lvg): add "Jetzt abrufen" manual poll button to tender radar
PollNowButton sits next to the settings gear in the tender-radar header,
mirrors the DKV spinner/disabled UX, and bumps a refreshKey on success to
refetch ResultsList without touching any filter. Failure surfaces an
i18n error (de/en parity). pollNow() client hits POST
/modules/tender-radar/poll-now.

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:10:46 +02:00
schalli 849aa9b0a9 feat(14-05): convert tender-radar settings + config forms to useTranslations
Converts settings/page.tsx, SourceConfigForm, RssFeedListForm and
EmailAlertConfigForm from hardcoded German strings to
useTranslations('tenderRadar'). Interval bound and RSS-feed removal
validation/error messages use next-intl interpolation ({min}/{max},
{label}). Updates the three affected settings component tests with a
next-intl useTranslations mock mirroring the marketplace test convention.
The entire tender-radar module UI (results, filters, saved searches,
detail, coverage, settings, RSS/email forms) now honors the selected
locale (CONFIG-03, D-10) with no language switcher added (D-11).

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 13:53:17 +02:00
schalli 48a2dc1026 feat(14-02): add RSS feed admin routes, API client, and settings UI
Adds GET/POST/DELETE /modules/tender-radar/rss-feeds (Roles-guarded
ADMIN/SUPER_ADMIN), declared before the existing @Get(':id') handler to
avoid NestJS route-order shadowing. Delegates to
TenderRssFeedSourceService; the denylist/SSRF rejection (D-14) surfaces
as a 400 unchanged.

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

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

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

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

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

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 13:52:50 +02:00
schalli d60080ef20 feat(12-04): tender-radar digest-interval selector + Sofort-Alert toggle UI
- Settings page: Benachrichtigungen section with Täglich/Wöchentlich/Aus
  selector, loads via fetchNotificationPref, saves via saveNotificationPref
  (NOTIFY-01)
- SavedSearchBar: per-profile Sofort-Alert checkbox reflecting
  instantAlert, calls updateSavedSearch({ instantAlert }) + reloads
  (NOTIFY-02, D-04)
- SavedSearchBar.test.tsx: checkbox state + toggle-calls-updateSavedSearch
  coverage

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:30:54 +02:00
schalli 73a7e49f85 feat(12-04): tender-radar-api client — notification-pref + instantAlert
- fetchNotificationPref/saveNotificationPref for GET/PUT
  /modules/tender-radar/notification-pref (NOTIFY-01)
- SavedSearch/Create/UpdateSavedSearchPayload now carry instantAlert
  (NOTIFY-02, D-04)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:28:53 +02:00
schalli dd3ff9ea30 feat(11-06): SavedSearchBar UI — save/load/rename/delete profiles (FILTER-06)
GREEN phase — extends tender-radar-api.ts with listSavedSearches/
createSavedSearch/updateSavedSearch/deleteSavedSearch (plain fetch,
credentials: include), adds SavedSearchBar with the
serializeFiltersFromSearchParams/filtersToSearchParams round-trip helpers
(URL searchParams <-> filters JSON, deliberately excluding page/tender —
navigation state, not filter state), and mounts it above FilterPanel in
page.tsx. Hardcoded German UI per phase convention.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:50:55 +02:00
schalli ca843ab134 test(11-06): add failing spec for SavedSearchBar (FILTER-06)
RED phase — serialization round-trip contract (URL searchParams <-> filters
JSON, page/tender excluded), save/load/rename/delete flows. Component does
not exist yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:50:48 +02:00
schalli 1eb9e4f567 feat(11-05): read/favorite triage toggles + Merklisten-Filter in the UI
Adds fetchTriage()/setTriage() to tender-radar-api.ts (plain fetch,
consistent with the existing client). ResultsList batch-fetches the
current user's triage state for the visible ids and merges it into a
local per-tenderId map; a failed triage fetch never blocks rendering the
list itself. Each row gets a Gelesen/Ungelesen and a Favorit toggle
(optimistic update with revert-on-failure, event.stopPropagation() so the
row's own click-to-open-detail doesn't fire); read rows render dimmed.
FilterPanel gains a "Nur Favoriten/Merkliste" checkbox writing favOnly
into the URL, which ResultsList already forwards generically to the
backend. ResultsList.test.tsx extended (Rule 3 — required to keep the
component test green with the new triage batch call) with coverage for
batch-merge, both toggles, optimistic revert, and graceful degradation
when the triage fetch fails.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:37:30 +02:00
schalli 653c63b072 feat(11-04): TenderDetail component with ?tender=<id> wiring
Adds getTender() to the api client and a self-fetching TenderDetail
overlay (pattern: SourceConfigForm) rendering all Tender fields plus a
safe (rel=noopener noreferrer, target=_blank) sourceUrl link. No local
mirroring of Vergabeunterlagen — rawPayload is 100% NULL in the live DB
(research finding), so only the source link exists; NULL value/deadline
render graceful German placeholders (D-05 applies to the detail view too).

page.tsx reads ?tender=<id> via useSearchParams and renders TenderDetail
as an overlay; closing removes the param. ResultsList row clicks set the
param (deviation: ResultsList.tsx was not listed in the plan's
files_modified but is required by the plan's own done-criteria/key_link
"ResultsList-Zeile setzt ?tender=<id>" — Rule 3 auto-fix, blocking).

3/3 TenderDetail tests pass; full web suite (117 tests) and tsc --noEmit
both clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:21:54 +02:00
schalli 3cc7194d9e test(11-04): add failing test for TenderDetail component
Covers the three must-have truths: source link with safe target/rel,
graceful NULL placeholders for value/deadline, and the mandatory
no-local-mirroring notice for Vergabeunterlagen (rawPayload is 100%
NULL in the live DB per research - no document URLs exist to mirror).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:20:28 +02:00
schalli d60bfd1966 feat(11-03): CPV-Filter + Wert-min/max-UI in Builder, DTO, FilterPanel
TenderQueryDto gains a validated cpv[] field (single-or-repeated query
param, normalized via @Transform); buildTenderWhere adds a cpvDivisions
hasSome branch (FILTER-03, Pitfall 2 — never an exact match against raw
cpvCodes). FilterPanel gets a CPV-Division autocomplete (search-by-label,
multi-select chips, repeated ?cpv= params) plus the previously
backend-only value filter's UI: valueMin/valueMax number inputs and an
"ohne Wertangabe einschließen" toggle (default on, matches the builder's
includeNullValue default from Plan 11-01) so the 91.6% NULL-value rows
stay visible by default. Verified against the live DB: cpv=45 matches all
three raw formats ("45", "45000000", "45000000-7") via the backfilled
cpvDivisions column.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:14:51 +02:00
schalli 69e25298c9 feat(11-02): wire Region/PLZ/Bundesland filter into query builder + FilterPanel
TenderQueryDto gains validated plz (@MaxLength(5)), region, and
bundesland fields. buildTenderWhere adds three conditional AND-branches:
plz startsWith, bundesland exact-match against the now-backfilled indexed
column (Pitfall 1), and region startsWith (usable independent of the
bundesland column). FilterPanel gets a PLZ input and a 16-Land Bundesland
dropdown (mirrors NUTS1_BUNDESLAND — web/api are separate packages) that
write plz/bundesland into the URL searchParams; ResultsList already
forwards the full URLSearchParams to listTenders(), so no additional
fetch wiring was needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 16:03:21 +02:00
schalli ee2c40863e feat(11-01): replace tender-radar placeholder with real results UI (GREEN)
Replaces the Phase 10 module stub with a Master-Container page.tsx
(Suspense-wrapped for useSearchParams, Next 16 App Router) rendering
CoverageBanner + FilterPanel + ResultsList.

- ResultsList: URL-param-driven fetch via listTenders(), sortable
  Frist/Wert/Veröffentlicht column headers, "keine Wertangabe" for
  estimatedValue=null rows (D-05), pagination.
- FilterPanel: Freitext (q), Sortierung, "nur noch offene" toggle
  (openOnly, default on), Abgabefrist von/bis date inputs — param names
  match the TenderQueryDto field names 1:1 for the Plan 11-06
  Saved-Search serialization contract.
- CoverageBanner: German coverage hint shown while GET /coverage
  reports only the doe-opendata source (D-12, UI-05).

All strings hardcoded German (i18n = Phase 14). Plain fetch throughout,
no TanStack Query (not installed, per RESEARCH Open Question 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:54:51 +02:00
schalli b55bb55c0e test(11-01): add failing ResultsList spec + extend tender-radar-api client (RED)
Extends tender-radar-api.ts with the Tender type, listTenders(params)
and fetchCoverage() (plain fetch, credentials:'include', matching the
established fetchSourceConfig pattern — TanStack Query is not installed).

Adds the RED-first ResultsList.test.tsx: render items with
title/buyerName/deadline/value, "keine Wertangabe" for null estimatedValue
(D-05), and an empty-state message. ResultsList.tsx does not exist yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:53:04 +02:00
schalli 8e3dfda64d test(10-06): SourceConfigForm component test — fetch, save, bounds
- fetch-on-mount populates pollIntervalMin input
- Speichern calls saveSourceConfig with edited interval + isActive
- interval below 5 or above 1440 rejected client-side, no save call
- automated proof for the INGEST-06 admin-UI slice
2026-07-21 11:25:43 +02:00
schalli 4360bc0c6b feat(10-06): tender-radar-api client + admin source-config settings form
- tender-radar-api.ts: fetchSourceConfig/saveSourceConfig hitting GET/PUT
  /modules/tender-radar/source-config (Plan 05 endpoint)
- SourceConfigForm: load-on-mount, numeric interval (5-1440 min, mirrors
  backend SourceConfigDto bounds) + isActive toggle, read-only sourceType/
  lastIngestedDay display
- settings/page.tsx: standard App Router route rendering the form,
  no module-loader whitelist change needed
2026-07-21 11:24:32 +02:00
schalli 3292afb913 feat(10-02): add tender-radar module-loader whitelist entry + placeholder page
- MODULE_REGISTRY['tender-radar'] entry (mirrors cert-manager/dkv-fleet shape)
- minimal client-component placeholder page proving activation -> page-load path
- hardcoded German string is an intentional MVP stub; full i18n is CONFIG-03 (Phase 14)
2026-07-21 10:43:28 +02:00
schalli 9d1323fe97 feat(ldap): per-user exclude/denylist filter for sync
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 43s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m46s
Add a per-username denylist so individual accounts (service accounts like
administrator, krbtgt, guest, ldap$) can be excluded from LDAP sync,
independent of the group/OU include-filter which only scopes the search.

- schema: LdapConfig.userExcludeList String[] (+ migration)
- sync: skip excluded usernames (case-insensitive) before recording the DN,
  so an already-imported user added to the list gets deactivated next sync
- DTO / config service / controller / scheduler: thread userExcludeList through
- web: exclude-list admin UI section (add/remove/save) + de/en translations
- tests: 3 specs covering empty list, case-insensitive skip, deactivation

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 09:34:01 +02:00
schalli aaa29226c9 feat(ldap): search box for the discovered groups/OUs list
Tessera CI/CD / Lint & Type Check (push) Successful in 39s
Tessera CI/CD / Tests (push) Successful in 40s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m25s
Real directories can return many groups/OUs, making the checkbox list
tedious to scroll through. Adds a client-side search input above the
list that filters by name or DN substring (case-insensitive) as you
type, so picking the right groups for the import filter is faster.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 16:06:10 +02:00
schalli 010aceb1ac feat(ldap): support anonymous bind (no bind DN/password required)
Tessera CI/CD / Lint & Type Check (push) Successful in 41s
Tessera CI/CD / Tests (push) Successful in 39s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m40s
bindDn and bindPassword are now optional on LdapConfig (nullable
migration) and throughout the DTOs/service/client -- an admin can
leave both blank to connect to directories that permit anonymous
read access. LdapService.bind() falls back to an RFC 4513 anonymous
bind (empty DN + empty password) whenever either field is missing,
shared across testConnection, listGroups, and syncUsersForTenant.

Frontend: removed the required attribute from Bind-DN/Bind-Passwort,
added a placeholder hint ("leer = anonymous bind"), and the
"Verbindung testen" button now only needs a Server-URL to enable
(not bindDn+bindPassword). Config responses now return bindPassword
as null (not a misleading "********") when no password is set.

Verified locally: submitted only a Server-URL with both bind fields
empty and confirmed the request reached the anonymous-bind code path
(DNS failure for the unreachable test host, not a validation error).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 12:57:26 +02:00
schalli 39aa4bff2a feat(ldap): allow testing connection before saving a config
Tessera CI/CD / Lint & Type Check (push) Successful in 43s
Tessera CI/CD / Tests (push) Successful in 42s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m35s
"Verbindung testen" only appeared after a config was already saved,
so a fresh/never-configured tenant had no way to validate connection
details at all. Now the button is always visible; POST /ldap/test-connection
accepts optional ad-hoc serverUrl/bindDn/bindPassword and falls back
field-by-field to the saved config for anything omitted (bindPassword
in particular, since the form never re-sends the masked saved password).

Verified locally: tested with typed-but-unsaved values (base DN left
blank, nothing saved) and got a real connection attempt/error back,
not a 404 "no config" response.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 10:55:46 +02:00
schalli 8e8305ce18 revert(ldap): remove CTL-specific AD connection prefill
Tessera CI/CD / Lint & Type Check (push) Successful in 42s
Tessera CI/CD / Tests (push) Successful in 46s
Tessera CI/CD / Build & Publish Images (push) Successful in 1m27s
The prior quick task (260707-csw) pre-filled the LDAP connection form
with one customer's specific Active Directory values (balios.ctl.local,
dc=ctl,dc=local) and a matching bind-DN hint. User clarified this was
never wanted -- Tessera is a generic multi-tenant product, and baking
one customer's infrastructure into the shared admin UI is wrong,
especially since a `dcdown -v` reinstall surfaced it unexpectedly as
seemingly-baked-in defaults on a fresh system.

Reverted to blank fields with generic example placeholders
(ldap.example.com / dc=example,dc=com / cn=admin,dc=example,dc=com),
matching the form's state before that change. Removed the now-unused
bindDnHint i18n key.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 09:33:34 +02:00
schalli 8fb92a4e2a fix(favorites): route icon img through same-origin proxy, not hotlink
<img src={fav.iconUrl}> hotlinked the external favicon directly; sites
that send Cross-Origin-Resource-Policy: same-origin (e.g. claude.ai)
get blocked by the browser (net::ERR_BLOCKED_BY_RESPONSE.NotSameOrigin),
leaving only the letter fallback. Points src at the new same-origin
/api-proxy/favorites/:id/icon route instead (same pattern already used
for the user avatar image). Render guard and onError fallback unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 15:34:56 +02:00
schalli 6d2f82d018 fix(i18n): backfill missing admin.ldap translation keys
Tessera CI/CD / Lint & Type Check (push) Successful in 50s
Tessera CI/CD / Tests (push) Successful in 45s
Tessera CI/CD / Build & Publish Images (push) Successful in 2m12s
The admin.ldap message block referenced throughout the LDAP admin
page (t('title'), t('connectionTitle'), t('serverUrl'), field-mapping
and sync labels, etc.) didn't exist in de.json/en.json at all, so the
whole page rendered raw translation keys instead of text -- a
pre-existing gap surfaced while testing the new AD-prefill/group-filter
feature on this same page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 09:59:33 +02:00
schalli 75b58491e9 feat(ldap): AD connection prefill + group/OU import filter UI
New-config form now defaults to the CTL Active Directory connection
values (balios.ctl.local:3268, dc=ctl,dc=local, AD person filter,
down-level bind-DN hint) with the password left blank; editing an
existing config still shows its real saved values.

Adds a group/OU import filter section: discover AD groups/OUs via
GET /ldap/groups, toggle selection or add DNs manually, persist via
PATCH /ldap/config. Empty selection keeps today's "import everyone
under base DN" behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 09:39:58 +02:00
schalli b03441da59 fix(dashboard): sidebar active-item text unreadable in light mode with custom accent
applyAccentColor() forced --sidebar-accent-foreground to the raw accent
color regardless of theme. Works in dark mode (bright text on dark-tinted
bg) but in light mode the tinted bg is near-white, so full-saturation
yellow text on pale-yellow bg was nearly invisible. Now only overrides
the foreground in dark mode; light mode keeps the theme's default
high-contrast foreground token.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 08:58:32 +02:00
schalli e07af71f9e fix(i18n): add missing note.editMode/viewMode translation keys
Note widget toggle button rendered raw key "widgets.note.editMode"
instead of translated label — keys were never added to either locale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 08:45:22 +02:00
schalli 819d50a222 feat(cert-manager): cert role badges + ZIP download in split view
- API: detectCertRole() classifies certs as root/intermediate/end-entity
  via basicConstraints.cA + self-signed check (subject.hash === issuer.hash)
- API: SplitEntry gains certRole field; filenames now reflect role
  (root-ca.pem, intermediate-1.pem, cert.pem)
- Web: SplitTab shows colour-coded role badge per cert
  (red=Root-CA, amber=Zwischen-CA, blue=Zertifikat)
- Web: "Alle als ZIP herunterladen" button via fflate (client-side)
- i18n: add certRole labels + downloadZip action key (de + en)
- i18n: add missing accentColor* and deleteAvatar* keys (de + en)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 10:40:39 +02:00