diff --git a/.planning/.continue-here.md b/.planning/.continue-here.md new file mode 100644 index 0000000..368fac8 --- /dev/null +++ b/.planning/.continue-here.md @@ -0,0 +1,91 @@ +--- +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. + diff --git a/.planning/HANDOFF.json b/.planning/HANDOFF.json new file mode 100644 index 0000000..01276df --- /dev/null +++ b/.planning/HANDOFF.json @@ -0,0 +1,40 @@ +{ + "version": "1.0", + "timestamp": "2026-07-09T14:36:45.919Z", + "phase": null, + "phase_name": null, + "phase_dir": null, + "plan": null, + "task": null, + "total_tasks": null, + "status": "paused", + "completed_tasks": [ + {"id": 1, "name": "LDAP: fix FavoriteLink 500 (missing migration, prod)", "status": "done", "commit": "afef9b2"}, + {"id": 2, "name": "LDAP: revert CTL-specific AD prefill per user feedback", "status": "done", "commit": "8e8305c"}, + {"id": 3, "name": "LDAP: allow testing connection before saving config", "status": "done", "commit": "39aa4bf"}, + {"id": 4, "name": "LDAP: support anonymous bind (optional bindDn/bindPassword)", "status": "done", "commit": "010aceb"}, + {"id": 5, "name": "Auth: case-insensitive usernames (login, seed, LDAP sync, migration)", "status": "done", "commit": "baff7ce"}, + {"id": 6, "name": "LDAP: fix ldapts empty-array-attribute bug causing email collision on sync", "status": "done", "commit": "246dc89"}, + {"id": 7, "name": "LDAP: search box for discovered groups/OUs list", "status": "done", "commit": "aaa2922"}, + {"id": 8, "name": "Favorites: icon proxy for CORP-restricted sites (claude.ai) + realistic UA fix", "status": "done", "commit": "f06a2ff (and related)"} + ], + "remaining_tasks": [ + {"id": 9, "name": "LDAP: per-user exclude/denylist filter (user asked, not yet built -- only group/OU include-filter + search box were done; individual-username exclusion is a separate, still-open ask)", "status": "not_started"}, + {"id": 10, "name": "Live-verify a full LDAP sync run (created/updated counts) against real Zentyal AD -- only connection test + group discovery were verified live, not an actual 'Jetzt synchronisieren' run with the new groupFilterDns applied", "status": "not_started"}, + {"id": 11, "name": "STATE.md quick-tasks table is behind -- several of today's commits (010aceb, 39aa4bf, 8e8305c, baff7ce, 246dc89, aaa2922) were direct fixes/features done without formal /gsd-quick entries; STATE.md only lists up to 260708-cuc", "status": "not_started"} + ], + "blockers": [], + "async_jobs": [], + "human_actions_pending": [ + {"action": "Continue live-testing LDAP sync against Zentyal/Samba AD test server (192.168.13.13 LDAP, test box 192.168.13.12) -- confirm a real sync run imports the right users with the groupFilterDns restriction applied", "context": "Only 'Verbindung testen' and 'Gruppen/OUs suchen' were verified live; an actual sync execution with a group filter selected+saved was not yet run/observed", "blocking": false}, + {"action": "Decide whether the per-user exclude/denylist feature (service accounts like administrator/krbtgt/guest/dns-ldap/ldap$ showing up as importable users) is still wanted, and if so scope it (manual denylist vs. checkbox-deselect in a preview)", "context": "User asked for this explicitly, got sidetracked into praising the existing group filter + requesting the search box instead -- never circled back", "blocking": false} + ], + "decisions": [ + {"decision": "Reverted CTL-specific AD server/domain hardcoded as form defaults", "rationale": "User: 'das war nie das Ziel' -- Tessera is a generic multi-tenant product, must not bake one customer's infra into shared admin UI", "phase": null}, + {"decision": "LDAP bindDn/bindPassword made fully optional (anonymous bind support)", "rationale": "User explicitly requested removing the requirement to enter a bind user/password", "phase": null}, + {"decision": "Usernames normalized to lowercase everywhere (storage + lookup), not just at login", "rationale": "User: login was case-sensitive and shouldn't be; centralized in UserService rather than per-callsite", "phase": null} + ], + "uncommitted_files": [], + "next_action": "Ask user whether to (a) build the per-user LDAP exclude/denylist feature they originally asked for, (b) do a live full-sync verification run against Zentyal, or (c) catch up STATE.md's quick-tasks table for today's commits -- context is at ~71%, so start whichever they pick as a single focused quick task, not all three.", + "context_notes": "This whole session was reactive, ad-hoc fixing driven by live-testing on a real production-style deployment (alpha.tessera.ctl.de, test box 192.168.13.12) plus a freshly stood-up Zentyal/Samba AD test directory (192.168.13.13, domain intern.vicolab.de) that the user built specifically so LDAP could be tested against something real. No GSD phase is active -- the v1.0 milestone was already at 100% before this session; everything today was quick-task-style bugfixing/feature work, several done directly without spinning up the full /gsd-quick planner+executor pipeline (justified each time by being small, fully-diagnosed, and urgent to unblock live testing). CRITICAL boundary: user explicitly does NOT want me running docker compose pull/up/down/restart/rebuild on the test server myself -- only docker logs / psql for read-only debugging. They pull/rebuild themselves and tell me when done, then I test via Playwright in the browser." +}