--- context: default phase: null task: null total_tasks: null status: in_progress last_updated: 2026-07-09T14:36:45.919Z --- # 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._ 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`. Last confirmed-working state (verified live via Playwright against alpha.tessera.ctl.de): LDAP connection test succeeds, group/OU discovery returns 36 real groups/OUs from Zentyal AD, and the new search box correctly filters that list (typed "Personal" → only "Personalabteilung" remained). 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). - **Per-user LDAP exclude/denylist filter** — user explicitly asked for a way to exclude *specific individual users* (not just group/OU-based inclusion) from LDAP sync, giving `administrator`, `krbtgt`, `guest`, `dns-ldap`, `ldap$` as examples of service accounts they don't want imported. The existing `groupFilterDns` include-filter (+ new search box) does NOT cover this — it only restricts which OUs/groups are searched, not individual usernames within an included scope. This request got sidetracked (user pivoted to praising the existing feature + asking for the search box) and was never revisited. **This is the most likely next thing the user wants.** - **Live full-sync verification** — only "Verbindung testen" and "Gruppen/OUs suchen" (discovery) were exercised live against the real Zentyal AD. An actual "Jetzt synchronisieren" run with a `groupFilterDns` selection saved and applied has not been observed/verified end-to-end yet. - **STATE.md bookkeeping** — the "Quick Tasks Completed" table only lists through `260708-cuc`. Commits `010aceb`, `39aa4bf`, `8e8305c`, `baff7ce`, `246dc89`, `aaa2922` were done as direct fixes (fully diagnosed, small, urgent-to-unblock-live-testing) without spinning up the formal `/gsd-quick` planner+executor pipeline each time, so they have no `.planning/quick/` entries or STATE.md rows. Purely cosmetic/audit-trail catch-up, not urgent. - 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). None currently blocking. Two open items are queued as "remaining_work" above but nothing is stuck. ## 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). ## 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). 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. 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. Start with: ask the user which of the three remaining_work items to pick up next (per-user LDAP exclude/denylist, live full-sync verification run, or STATE.md catch-up) — do not assume; they were mid-conversation about LDAP filtering when this pause was triggered by a context-budget warning, not by reaching a natural stopping point.