wip: session paused between milestones, v1.2 complete
Tessera CI/CD / Lint & Type Check (push) Successful in 46s
Tessera CI/CD / Tests (push) Successful in 47s
Tessera CI/CD / Build & Publish Images (push) Successful in 6s

No phase work in flight: Phase 15 (8/8) and Phase 16 (5/5) are both closed and
the roadmap reflects it. The handoff therefore sits at project level rather
than in a phase directory.

Records four anti-patterns discovered through actual failure this session, two
of them marked blocking: the tautological test that let the objectGUID defect
through review, and the fact that /opt/tessera is not a checkout, so compose
changes in this repository never reach the test server.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:01:00 +02:00
parent f38b9f203f
commit 96432a6a7b
2 changed files with 113 additions and 80 deletions
+80 -55
View File
@@ -1,96 +1,121 @@
---
context: default
phase: null
phase: 16-ad-gruppen-synchronisation
task: null
total_tasks: null
status: idle
last_updated: 2026-07-14T08:16:00.000Z
total_tasks: 0
status: between-milestones
last_updated: 2026-08-11T12:59:01.215Z
---
# BLOCKING CONSTRAINTS — Read Before Anything Else
> These are not suggestions. Each constraint below was discovered through failure.
> Acknowledge each one explicitly before proceeding.
- [ ] CONSTRAINT: no-docker-on-testserver — User explicitly forbids running `docker compose pull/up/down/restart/rebuild` on the test server (root@192.168.13.12, "ViCoTest", app at https://alpha.tessera.ctl.de). Only read-only debugging (`docker compose logs`, `psql` queries, `\dt`) is allowed there. Deployment (pull/rebuild/restart) is the user's own action — they do it and tell you when done, then you test via Playwright in the browser.
**Do not proceed until all boxes are checked.**
## Critical Anti-Patterns
| Pattern | Description | Severity | Prevention Mechanism |
|---------|-------------|----------|---------------------|
| ldapts missing-attribute shape | `ldapts` represents an absent LDAP attribute as an empty array `[]`, not `undefined`. Naive `Array.isArray(value) ? String(value[0]) : String(value)` turns a missing attribute into the literal string `"undefined"` — identical across every entry lacking that attribute, which then collides on DB unique constraints (this broke LDAP sync's email field against real AD). | advisory | Already fixed in `ldap.service.ts` (resolve to first array element / raw value, treat `undefined`/`null`/`''` as absent). Apply the same resolve-then-check pattern to any new LDAP attribute mapping code. |
| Customer-specific values in shared UI | Hardcoding one tenant's/customer's actual infra values (server hostnames, domain, example text) into Tessera's generic admin UI as defaults, even when that exact customer asked for it — Tessera is a shared multi-tenant product that gets sold to other customers too. | advisory | Ask *where* a customer-specific value should live (env var, per-deployment seed, docs) before hardcoding it into shared component code. |
_Remove rows that do not apply. The discuss-phase and execute-phase workflows parse this table and enforce a mandatory understanding check for any `blocking` rows._
| 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 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`. |
<current_state>
No active GSD phase — the v1.0 milestone was already at 100% before this session. Everything in this session (2026-07-07 through 2026-07-09) was reactive quick-task-style work, driven by live-testing on two real systems the user stood up specifically for this purpose:
- Test/staging deploy: https://alpha.tessera.ctl.de, server 192.168.13.12 ("ViCoTest"), SSH root access, app deployed via `docker compose` pulling `git.vicolab.de/schalli/tessera-ctl/{web,api}:latest` from Gitea CI.
- Zentyal/Samba AD test directory: 192.168.13.13 (LDAP), domain `intern.vicolab.de`, base DN `dc=intern,dc=vicolab,dc=de`, bind as `Administrator@intern.vicolab.de`.
v1.2 Plattform-Berechtigungen ist inhaltlich fertig. Phase 15 steht auf 8/8,
Phase 16 auf 5/5, beide in ROADMAP.md und STATE.md nachgezogen. Kein Phase-Work
in Arbeit, Arbeitsverzeichnis sauber, alles auf origin/main (`f38b9f2`).
Last confirmed-working state (verified live via Playwright against alpha.tessera.ctl.de): full LDAP sync now works end-to-end. The per-user exclude/denylist filter (commit 9d1323f) was built, migration applied on the live DB, and verified live: with administrator/krbtgt/guest/dns-ldap/ldap$ on the denylist, "Jetzt synchronisieren" returned Erstellt:0/aktualisiert:2/deaktiviert:4, and a psql check confirmed the 4 excluded service accounts are isActive=false while 2 real LDAP users stay active and 0 were wrongly created. Connection test + group/OU discovery (36 entries) + search box all still working.
All three items that were open at the last pause are now DONE: (1) per-user exclude filter built+verified, (2) live full-sync verified, (3) STATE.md quick-tasks table caught up. No open LDAP work remains.
Die Sitzung war UAT-getrieben: der Browser-Durchlauf zu Phase 16 hat einen
kritischen Fehler in der Loescherkennung gefunden, der ohne den einen
ausfuehrbaren Test unbemerkt in Produktion gegangen waere.
</current_state>
<completed_work>
Completed this session (all committed + pushed to `origin/main`, CI green on each):
- `afef9b2` — fix(db): add missing FavoriteLink migration (found live: prod 500 on every `GET /favorites` because the model existed in schema.prisma but was never captured in a migration file)
- `8e8305c` — revert(ldap): remove CTL-specific AD connection prefill (user: "das war nie das Ziel" after a `dcdown -v` reinstall surfaced it as baked-in defaults)
- `39aa4bf` — feat(ldap): allow testing connection before saving a config (test button was hidden until a config already existed)
- `010aceb` — feat(ldap): support anonymous bind (bindDn/bindPassword now fully optional, schema made nullable via migration)
- `baff7ce` — fix(auth): case-insensitive usernames everywhere (login, admin seed, LDAP sync, plus a data migration lowercasing existing rows)
- `246dc89` — fix(ldap): ldapts empty-array-attribute bug (see Anti-Patterns table above) — found live syncing against real Zentyal AD, every entry after the first crashed with a unique-constraint violation on email
- `aaa2922` — feat(ldap): search box for the discovered groups/OUs list (user liked the existing group/OU filter, asked for a search-as-you-type box over it)
- Earlier same session: Favorites icon proxy for CORP-restricted sites (claude.ai logo wasn't rendering — browser blocked the cross-origin hotlink via `Cross-Origin-Resource-Policy: same-origin`), plus a realistic-User-Agent fix for the same feature (Cloudflare WAF blocked the default `tessera/1.0` UA on some sites).
All of the above were verified live either on the local dev stack or directly on alpha.tessera.ctl.de via Playwright browser automation (not just type-check/build — actual click-through testing).
- Phase-16-UAT: Tests 5, 6, 7 im Browser bestanden; Test 2 read-only gegen das
echte AD; Tests 1, 3, 4, 8 auf Entscheidung des Users uebersprungen
- **KRITISCH behoben** (`d2019dc`): objectGUID-Existenzpruefung baute ihren
Filter als escapten String; ldapts wandelt das nicht in Rohbytes. Beide
Suchen der Pruefung teilten sich den Filter, damit haette der erste echte
Sync jede AD-gebundene Gruppe samt Mitgliedschaften und Modulfreigaben
geloescht. Jetzt EqualityFilter mit rohem Buffer, am echten AD gemessen.
- DOE-Bekanntmachungslinks repariert (`ecf7872`) inkl. Backfill von 2846 Zeilen
- Fehlender Abschlussbericht 15-04 nachgezogen (`149b5aa`) — Phase 15 damit 8/8
- LDAP-Bind-Passwort verschluesselt (`4f687ea`), Stack startet nicht mehr ohne
Schluessel (`7bda56d`), Schluessel umbenannt mit Rueckfallebene (`f574884`)
- Vier Backlog-Punkte geschrieben, zwei alte geschlossen
</completed_work>
<remaining_work>
None open. The three items carried from the previous pause are all resolved:
- ✅ **Per-user LDAP exclude/denylist filter** — built (commit 9d1323f) and live-verified. Excludes individual usernames (service accounts) from sync, independent of the group/OU include-filter. Skips matches case-insensitively BEFORE recording the DN, so an already-imported user added to the denylist gets deactivated on the next sync.
- ✅ **Live full-sync verification** — ran a real "Jetzt synchronisieren" against Zentyal with the denylist saved; Erstellt:0/aktualisiert:2/deaktiviert:4, DB-confirmed.
- ✅ **STATE.md bookkeeping** — Quick Tasks table caught up with the direct-fix commits (8e8305c, 39aa4bf, 010aceb, baff7ce, 246dc89, aaa2922, 9d1323f).
Vier Backlog-Punkte, alle unter `.planning/todos/pending/`:
No GSD phase active; v1.0 milestone was already 100%. Next work is likely new module development — ask the user.
1. `2026-08-11-modulaktivierung-ohne-lizenzpruefung.md` — gross, Produktfragen offen
2. `2026-08-11-tender-radar-einstellungen-mischen-rollen.md` — mittel, Grundsatzfrage offen
3. `2026-08-11-compose-datei-auf-server-driftet.md` — mittel, Weg offen
4. `2026-08-11-verschluesselungsschluessel-vorgabewert.md` — klein, ohne Rueckfrage machbar
Offen ausserdem: ob nach v1.2 ein neuer Meilenstein geplant wird.
</remaining_work>
<decisions_made>
- Reverted CTL-specific AD server/domain values that had been hardcoded as LDAP form defaults — Tessera is a generic multi-tenant product, must not bake one customer's infra into shared admin UI, even when that same customer asked for it the day before.
- Made LDAP `bindDn`/`bindPassword` fully optional end-to-end (DTO, service, Prisma schema via migration, `LdapService.bind()` helper falling back to RFC 4513 anonymous bind) rather than just relaxing frontend validation — user wants to connect to directories that allow anonymous read.
- Centralized username lowercasing in `UserService.create`/`update`/`findByUsername` plus `AuthService.validateUser`, `AdminSeedService`, and the LDAP sync loop, rather than only fixing the login comparison — closes the door on duplicate accounts differing only by case from any creation path (including AD sync where sAMAccountName casing varies).
- Phase 16 mit A1 (objectGUID uebersteht Umbenennung) als offener, dokumentierter
Annahme abgeschlossen — der User hat keinen AD-Schreibzugriff, und der eine
substanzielle Fehler war bereits gefunden
- Ein Plattform-Schluessel statt eines eigenen fuer das AD-Bind-Passwort: zwei
Schluessel in derselben .env auf demselben Host erhoehen die Sicherheit nicht
- Alter Schluesselname bleibt lesbar mit Veraltet-Warnung — ein harter Rename
haette jede bestehende Installation gestoppt
- Verschluesselungs-Backfill zur Laufzeit statt in der Migration: SQL kann nicht
verschluesseln, der Schluessel liegt in der Anwendungsumgebung
</decisions_made>
<blockers>
None currently blocking. Two open items are queued as "remaining_work" above but nothing is stuck.
- Kein AD-Schreibzugriff: UAT 1/3/4 der Phase 16 sind dort dauerhaft nicht
ausfuehrbar. Als Annahme dokumentiert, kein offener Arbeitsauftrag.
- `.env.example` / `.env.prod.example` waren in der Sitzung durch Berechtigungen
gesperrt — die Dokumentation des Schluessels haengt daran.
</blockers>
## Required Reading (in order)
1. `.planning/HANDOFF.json` — structured version of this same state, for programmatic resume
2. This file's Blocking Constraints table — the no-docker-on-testserver rule specifically; it was corrected by the user mid-session and must hold going forward
3. `.planning/STATE.md` — general project state (note: its Quick Tasks table is stale per remaining_work above)
## Critical Anti-Patterns (do NOT repeat these)
- See the Blocking Constraints / Anti-Patterns tables above (ldapts empty-array shape; customer-specific hardcoding in shared UI).
1. `.planning/STATE.md` — Position, Quick-Task-Tabelle mit den Fixes dieser Sitzung
2. `.planning/phases/16-ad-gruppen-synchronisation/16-UAT.md` — Abschnitt
"Entscheidung 2026-08-11", warum vier Tests uebersprungen sind
3. `.planning/quick/260811-f9i-*/SUMMARY.md` — der kritische Fund und warum er
durch Review und Tests kam
4. `.planning/todos/pending/` — die vier offenen Punkte
## Infrastructure State
- Local dev stack (docker compose, this repo checkout): running, all migrations applied, LDAP config currently empty/reset from testing (was deleted mid-session more than once to test the "no config yet" prefill states).
- Test/staging (alpha.tessera.ctl.de / 192.168.13.12): running the latest pushed images as of `aaa2922` (user rebuilt web specifically to pick up the search-box commit and it was confirmed live). LDAP config on that tenant is currently SAVED with real Zentyal values (server `ldap://192.168.13.13:389`, base DN `dc=intern,dc=vicolab,dc=de`, bind `Administrator@intern.vicolab.de`, real password entered by the user directly in that session's browser). No `groupFilterDns` selection has been saved yet (still "Keine Auswahl - importiert alle Benutzer unter der Basis-DN").
- Zentyal/Samba AD (192.168.13.13): live, reachable from the test server via IP (NOT the FQDN `intern.vicolab.de` — that resolved but connections to it timed out/failed; using the raw IP worked). Contains real groups/OUs/users per the `user-files/zentyal/*.png` screenshots the user provided.
- Admin credentials: alpha.tessera.ctl.de admin password is `admin1234` (changed from the `admin123` default multiple times across DB resets this session — if login fails with that, try `admin123` first since a fresh `dcdown -v` reinstall resets to the env-var default and forces a change again).
- **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. Web-Image vom 7.8. —
korrekt, die Web-Quellen sind seit dem 6.8. unveraendert.
- **`/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 11.8. 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), mit Sicherung vorher.
<context>
This was a long, reactive debugging/feature session almost entirely driven by "test this live and see what breaks" rather than planned phase work. The pattern that worked well: make a fix, verify locally (type-check + rebuild + Playwright), commit + push, wait for the user to confirm the real test-server deploy, then verify again live via Playwright against alpha.tessera.ctl.de. Several real bugs were only found this way (the FavoriteLink missing-migration 500, the ldapts empty-array/email-collision bug) — they would not have been caught by type-check or unit tests alone.
Der rote Faden der Sitzung war, dass genau ein ausfuehrbarer Test (UAT 2, weil
read-only) den einen Fehler gefunden hat, den alle Reviews und 500+ Unit-Tests
durchgelassen hatten. Die uebersprungenen Tests sind bewusst uebersprungen, nicht
vergessen — die Begruendung steht in 16-UAT.md.
The user is clearly hands-on and technical, corrects scope/boundary violations immediately and specifically (the CTL-prefill revert, the no-docker-on-testserver rule), and is currently mid-flow testing LDAP against a real AD they just stood up for this purpose. The natural continuation is either the per-user exclude filter or a live full-sync test — let them pick rather than assuming.
Die vier Backlog-Punkte sind allesamt "vor dem Verkauf an externe Kunden".
Intern draengt keiner davon. Drei brauchen zuerst Entscheidungen des Users, der
vierte ist reine Umsetzung.
</context>
<next_action>
No open LDAP work. Ask the user what to pick up next — most likely new module development now that v1.0 plus the LDAP hardening pass are complete. If they resume LDAP, the natural next candidates would be surfacing deactivated LDAP users in the admin UI (currently only visible via the isActive flag) or a sync-preview before applying, but neither has been requested.
Mit dem User klaeren, was drankommt: einer der vier Backlog-Punkte oder die
Planung eines neuen Meilensteins nach v1.2. Ohne seine Entscheidungen sind die
drei groesseren Punkte nicht sinnvoll zu starten — der kleine
(Vorgabewert entfernen, Schluessel in den Beispiel-Umgebungsdateien
dokumentieren) laesst sich sofort umsetzen, sobald der Dateizugriff auf die
`.env*`-Vorlagen moeglich ist.
</next_action>