Commit Graph
142 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.8 9ad8fa2971 feat(guide): default self_signed and test the whole README block
The guide deploys to a .localhost target, where letsencrypt cannot issue (Cloudflare DNS-01 needs a real domain). Default TLS_MODE=self_signed in the generated Production block so the guide uses the working self-signed provider. Document every deploy parameter as a top-level shell var (APP, HOST, TLS_MODE, SSH_PUBLIC_KEY) instead of an inline placeholder.

The compose guide now extracts and runs the whole README block, sourcing params from it (no more -e injection); HOST is forced to localhost. Skip checkout/login under ACT so the guide workflow is iterable locally.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 21:23:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 972596a758 fix(guide): document the administrator SSH key step and seed it in CI via --vars
The README Production block went provision -> deploy with no admin-key step, so user-administrator failed early (authorized_keys empty) for both the Guide CI jobs and any real user copying the README. Add a visible 'provision --vars' step that sets users.administrator.authorized_keys (merged into host_vars) in both Production variants of the role README template, regenerate all role READMEs, and replace the invisible CI payload splice with a visible substitution of the documented <your-ssh-public-key> placeholder.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 05:45:26 +02:00
kevinveenbirkenbachandClaude Fable 5 d88f272f9f Merge branch 'feature/swarm-nfs' into feature/svc-net-tor
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-19 15:31:08 +02:00
kevinveenbirkenbachandClaude Fable 5 17cd53609a feat(readme): merge the Production deploy into one docker command
The role README Production section now runs provision and deploy in a
single docker call: APP and HOST are shell variables passed via -e,
INVENTORY is the in-container SPOT for the duplicated paths, and the
redundant --vars-file (optional, default None) and --id (the inventory
is already narrowed by --include) flags are gone. All 153 role READMEs
regenerated from the template; test-run.yml executes exactly this block.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 13:03:25 +02:00
kevinveenbirkenbachandClaude Fable 5 194671d90f feat(readme): uniform description schema and clearer role titles
Rewrite all 254 role descriptions to a uniform noun-phrase schema
(function-first, no leading verb, no trailing period, <=120 chars) and
enforce it via the extended description lint. Rename 12 role README H1
titles for clarity (e.g. Backup Container to Local -> Backup Docker
Volumes, CSS Service -> Corporate Design, NGINX Legal -> Imprint) and
regenerate their Cosmos and Quick Setup sections. Overview table
regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-18 01:11:48 +02:00
kevinveenbirkenbachandClaude Opus 4.8 dd3d83148e feat(readme): show Ansible role dependencies in Cosmos with a gear marker
Roles declared under meta/main.yml dependencies now appear on both the
Dependencies and Dependents side, labelled with the glossary gear
symbol and connected by a fixed 1:1 edge; the legend explains it. All
role READMEs regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 a9fd37907b feat(readme): cardinality labels and a symbol legend on Cosmos diagrams
Edges now carry their relation: solid 1:1 for fixed consumption, dashed
0..1 for conditional (group-membership gated) consumption. Below every
diagram the generator renders a reading key for the edge kinds and the
node markers, with the glyphs drawn from the shared symbol glossary.
All role READMEs regenerated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 14:44:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 90b3845998 feat(readme): role README generator with Cosmos diagrams, Quick Setup, and lint
Add cli/build/readme: derives each role README's managed sections from
templates/roles/README.md.j2.tmpl (the Cosmos mermaid diagram from
meta/services.yml, an invokable-gated Quick Setup, and Credits).

The Cosmos diagram shows dependencies, every declared service, and
dependents (roles that consume this one, matched by role name and by
provides provision-name in both directions). Role nodes carry deploy-mode
emoji markers (compose/swarm/host); disabled and shared services get a
stop mark; dependents are capped at 12.

test_readme enforces the Quick Setup body is byte-exact with the template
render. `make readme-generate` and `make cosmos` regenerate; `make cosmos`
also runs inside `make quality`.

Refactor: utils/symbol_glossary.py is the single source of truth for the
word/emoji mapping (unique project-wide), consumed by the complexity
matrix and the Cosmos builder. utils/roles/deploy.py holds role_has_stack
and role_deploy_modes, shared by both so the logic never drifts.

Add `make lint-mermaid` and its CI workflow, rendering every diagram via
mermaid-cli; the render step provisions chrome-headless-shell and unzip
on demand so it works in the minimal container.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 14:44:33 +02:00
Alejandro Roman ab25684714 adrees all commens and fix CI usses on prev run 2026-07-16 19:19:23 +02:00
kevinveenbirkenbachandClaude Opus 4.8 687759572f feat(lifecycle): promote checkmk, jellyfin, n8n and both registries to beta
Moves web-app-checkmk, web-app-jellyfin, web-app-n8n,
svc-registry-cache and svc-registry-docker from alpha to beta, pulling
them into the default CI test-deploy lifecycle envelope.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 05:20:54 +02:00
kevinveenbirkenbachandClaude Opus 4.8 7a6268fa96 refactor(deploy): replace skip with services modes
Per-role deploy-mode opt-out moves from meta/tests.yml 'skip:' to meta/services.yml <entity>.modes.{compose,swarm}.enabled on the primary entity. get_role_skip now derives from it; get_role_mode_enabled added. A new lint requires every role shipping templates/*compose*.yml.j2 to declare both mode flags. Migrates 107 services.yml and drops 11 skip-only tests.yml. Also removes the variant_status deployability gate so swarm runs one job per variant unconditionally (variant_status.py and deployable_variant_indices deleted, complexity model simplified). Adds the i8matix alias.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-16 03:44:17 +02:00
kevinveenbirkenbachandClaude Opus 4.8 15e7647d2f refactor(env): central TZ+LANG base included by every role env.j2
New roles/sys-svc-container/templates/env.j2 sets TZ={{ HOST_TIMEZONE }} and LANG=C.UTF-8. Every role env.j2 includes it at the top; it renders via stack_host_template, the same engine that resolves the includes in compose.yml.j2, so it works in both compose and swarm. TZ/LANG are removed from the individual role env.j2 so each value stays single-source.

New lint tests/lint/ansible/test_env_base_include.py verifies every role env.j2 includes the base and never re-sets a base-owned key (keys derived from the base file).

mattermost and gitea meta/volumes.yml drop the /etc/timezone host-path bind: swarm hard-rejects a task whose bind source is missing on the node (Debian-trixie nodes have no /etc/timezone). TZ now comes from the base env. /etc/localtime bind also dropped; the container timezone follows TZ.

Non-directive comments were removed from the touched env.j2 (banners, restatements, reference URLs, narration) so the changed files pass the comment-lint; only tool directives remain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-15 22:13:57 +02:00
kevinveenbirkenbachandClaude Opus 4.8 c41343e8e2 fix(dr-drill): restore the volume subtree it proves and stop exhausting the disk
The marker discovery matched any .dr-drill-marker under files/, so
SRC_REL could resolve to a parent tree and step 8 restored the wrong
subtree into the live export as root, leaving n8n_data owned root:root
and n8n (uid 1000) crash-looping on EACCES. Scope both repo layouts to
the primary volume's own marker and declare uid/gid 1000 on the n8n
data volume so nfs_prep enforces ownership on every deploy instead of
relying on Docker's copy-on-first-use.

The drill also stacked four copies of the backup tree on shared space
(live export, pulled tree, fully-allocated LUKS image, final restore
into a tmpfs /tmp) and ENOSPCed on large apps. Restore to /var/tmp,
drop the pulled tree before the device recover, make the loop image
sparse and free it after the marker verify, and stage only the volume
generation instead of the whole machine tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 13:46:22 +02:00
kevinveenbirkenbachandClaude Opus 4.8 07c6ad8e7c test(e2e): scale every nav click + timeout literal for onion targets
Over Tor the redirect chain behind a login/logout/OIDC click routinely exceeds Playwright's fixed 30s action timeout, so the click throws before the flow's scaled waits run. Sweep all role Playwright suites and the shared persona helpers: give every navigation/auth-transition click an onion-scaled timeout (.click({ timeout: resolveTimeout(30_000) })) and route the remaining raw millisecond literals (timeout options, waitForTimeout, sleep-promise setTimeout, positional wait args) through resolveTimeout. Local UI clicks (menus, toggles, modal dismissals, DOM clicks inside evaluate) stay untouched.

Two new lints keep it that way: test_no_raw_timeout_literals forbids raw numeric timeout literals in Playwright specs (opt-out: nocheck: raw-timeout), test_persona_clicks_scaled requires every click in the shared persona auth flows to carry a resolveTimeout timeout (opt-out: nocheck: unscaled-click).

Verified: node --check on all 133 changed JS files, all 13 playwright lints green, make autoformat clean, make test green (2745+196+163+4 tests OK).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 11:35:53 +02:00
kevinveenbirkenbachandClaude Opus 4.8 f76bfcf531 refactor(svc-bkp-volume-2-local): rename role from svc-bkp-container-2-local
Rename the role, its test tree, the primary entity key
(volume-2-local), the derived vars (BKP_VOLUME_2_LOCAL_*,
SYS_SERVICE_BACKUP_VOLUME_2_LOCAL), the run-once fact, and every
group_names consumer flag, include path, doc, and lint reference. The
container_backup service key and the on-disk backup-docker-to-local
tree keep their names: both are contracts with existing consumer
entries, snapshot trees, and the baudolo pull tooling.

Operators must rename the svc-bkp-container-2-local inventory group and
disable the previous versioned units on already-provisioned hosts; the
pre-deploy snapshot glob derives its base from the new role name and
will not find them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:52:02 +02:00
kevinveenbirkenbachandClaude Opus 4.8 664993f819 fix(n8n): run owner bootstrap once per cluster and accept already-setup
Both workers raced POST /rest/owner/setup; the loser spun 31 retries on
400 'Instance owner already setup'. Hoist run_once via apply and assert
200/201 or an already-setup body.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 18:43:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 3875eb2cf9 feat(variants): pin container_backup per matrix variant
Variant 0 pins the backup consumer flag enabled so the matrix baseline exercises the in-group branch; every other variant pins it disabled (shared: false carries the static-override suppression since the base value stays literally true). The matrix and wordpress variant entries move their services mapping to the list-item head so the static-override line parser resolves the override paths; yaml semantics are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4473edafb2 feat(roles): declare the container_backup consumer flag on stack roles with volumes
Every invokable stack role that declares named volumes in meta/volumes.yml now carries the standard container_backup consumer entry: enabled resolves via 'svc-bkp-container-2-local' in group_names, shared stays literally true since there is no per-consumer backup instance. Playwright/dynamic-flag/bond-baseline suppressions ride on the block per the established idioms; stale explanatory comments in the touched files are dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 23:04:40 +02:00
kevinveenbirkenbachandClaude Opus 4.8 ef17f09136 fix(n8n): pin single replica and add healthz healthcheck
n8n CE caches isInstanceOwnerSetUp per process without cross-replica invalidation, so the swarm topology default of 3 replicas left stale replicas bouncing logged-in sessions to /setup (variant 1) and caused session-crash churn during the reschedule test (variant 0). Pin to 1 replica; multi-main needs licensed queue mode. Add the conventional /healthz wget healthcheck so swarm convergence and the chaos-harness Health.Status probe carry a real readiness signal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 17:30:35 +02:00
kevinveenbirkenbachandClaude Opus 4.8 95a7a0fd65 fix(n8n): gate EXTERNAL_HOOK_FILES behind SSO
The no-auth variant does not stage the trusted-header hook file, but env.j2 emitted EXTERNAL_HOOK_FILES unconditionally, so n8n crash-looped on a missing /home/node/hooks.js ("Cannot find module"). Wrap it in the N8N_SSO_ENABLED block alongside the other SSO-only vars.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 00:03:34 +02:00
kevinveenbirkenbachandClaude Opus 4.8 2c20e1598c fix(networks): move n8n + svc-runner off mailu's /24, gate overlaps
n8n (192.168.106.16/28) and svc-runner (192.168.106.0/28) sat inside mailu's 192.168.106.0/24, so co-deploying either with mailu aborted with "Pool overlaps with other one on this address space". Reassign both to free /24s (218, 219). The overlap test read meta/server.yml, which has no networks key, so it passed vacuously; point it at meta/networks.yml so it actually gates duplicate/overlapping subnets.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 00:03:33 +02:00
kevinveenbirkenbachandClaude Opus 4.8 84b22b37a8 fix(web-app-n8n): complete tor service onboarding + resolve subnet collision
n8n was merged (from main) serving a canonical domain without a tor service block, and its local subnet 192.168.106.16/28 collided with svc-net-tor. Declare the complete tor block (meta/services.yml with the playwright-service-flag nocheck, meta/variants.yml pinning services.tor.enabled true in variant 0 and false in the no-auth variant) and move svc-net-tor's nominal (host-networked) subnet to 192.168.106.32/28. Fixes the tor-contract, variant-coverage, bond-baseline, playwright-env and subnet-overlap/duplicate test failures.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 21:02:27 +02:00
kevinveenbirkenbachandClaude Opus 4.8 4c91df5de7 test(e2e): forbid raw page.goto, migrate all specs to Tor-resilient gotoOnion
New lint test_no_raw_goto.py forbids raw `page.goto(...)` in role playwright specs + persona flows (the gotoOnion wrapper in personas/utils is exempt) so navigation retries transient Tor-transport errors over onion. Migrates every raw call across ~75 roles (343 sites) to `gotoOnion(page, ...)` and teaches gotoOnion to detect relative onion URLs (`isOnionCanonical`). gotoOnion is a behaviour-identical drop-in on clearnet (single attempt, same return). Verified: no-raw-goto lint green, eslint 0 errors, make test green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:59:38 +02:00
kevinveenbirkenbachandClaude Opus 4.8 8988fdf158 test(web-app-n8n): route playwright timeouts through resolveTimeout
n8n's playwright specs arrived un-migrated via a branch update; wrap their raw numeric timeouts in resolveTimeout so they satisfy the no-raw-timeouts lint and scale over onion. Clearnet-identical (resolveTimeout(N) === N).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 13:00:16 +02:00
Alejandro Roman 2f7d0cd461 fix port collision after merge 2026-07-08 11:13:31 +02:00
kevinveenbirkenbach 7af3e0e744 Merge branch 'main' into feature/swarm-nfs 2026-07-08 03:42:20 +02:00
kevinveenbirkenbachandClaude Fable 5 8c6632f9b2 style: apply autoformat to n8n schema, test-e2e-cli and svc-runner
Normalize YAML formatting flagged by make autoformat: document-start
markers and long-description line wrapping. No behavior change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 03:07:30 +02:00
Prageeth Panicker c59d9daf7b fix(web-app-n8n): resolve subnet collision with web-app-semaphore
192.168.105.208/28 -> 192.168.106.16/28
2026-07-07 13:14:45 +00:00
Prageeth Panicker 48032e3eb9 fix(web-app-n8n): resolve port collisions with new roles
- http port: 8086 -> 8080 (conflict with web-app-semaphore)
- sso port: 16509 -> 16510 (conflict with web-app-checkmk)
2026-07-07 13:01:04 +00:00
Prageeth Panicker 2b831f56d1 fix(web-app-n8n): address Kevin's review comments
- tasks/02_bootstrap.yml: set retries=4 delay=30 on owner probe to
  stay within n8n's 5-attempt/5-minute rate limit window
- files/playwright: remove test-oidc-login.js - redundant with
  test-login-administrator.js which already covers the full OIDC flow
- rebase onto latest main
2026-07-07 12:53:39 +00:00
Prageeth Panicker c4a73d61d7 fix(web-app-n8n): gate hooks.js copy and mount on SSO being enabled
- tasks/01_core.yml: add when: N8N_SSO_ENABLED | bool to hooks.js copy task
- templates/compose.yml.j2: wrap hooks.js volume mount in SSO conditional

Per Kevin's review - hooks.js should only be staged and mounted
when SSO is actually enabled, adding extra redundancy.
2026-07-07 12:51:06 +00:00
Prageeth Panicker 167bbe9e24 fix(web-app-n8n): fix hooks.js for n8n 1.95.3 API
Remove relations: ['role'] from UserRepo.findOne() - in 1.95.3
user.role is a plain string column, not a relation. The relations
array caused EntityPropertyNotFoundError which produced 500 on
every SSO login attempt. Also removed dead RoleRepo/dbCollections.Role
references dropped in 1.95.3's DropRoleTable migration.

Verified: biber@infinito.example now auto-provisioned as global:member
after Keycloak SSO. All 5 Playwright tests pass locally.
2026-07-07 12:51:06 +00:00
Prageeth Panicker 31f3434d66 feat(web-app-n8n): implement community SSO via trusted-header hook
- Add files/hooks.js (adapted from PavelSozonov/n8n-community-sso)
  Auto-provisions users and issues n8n session from Remote-Email header
- Add templates/proxy.conf.j2 - openresty vhost forwarding Remote-Email
  trusted header in SSO-gated branch
- Update env.j2: EXTERNAL_HOOK_FILES + N8N_FORWARD_AUTH_HEADER when SSO enabled
- Update compose.yml.j2: bind-mount hooks.js read-only into container
- Update tasks/01_core.yml: stage hooks.js before container starts
- Fix Playwright: V1 SSO tests now assert direct workflow editor landing
- Fix Playwright: administrator test works in both V1 (SSO) and V2 (no SSO)
- Update README: document auto-provisioning SSO behavior
2026-07-07 12:51:06 +00:00
Prageeth Panicker 58ce233f97 fix(web-app-n8n): fix Playwright tests for CE edge-SSO model
- Remove LDAP variant (V3) - unsupported on n8n Community Edition
- Fix test-login-administrator: Keycloak SSO + n8n native login form
- Fix test-login-biber: verify oauth2-proxy gate only (no n8n account)
- Add performN8nLoginForm() helper to _shared.js
- Add ADMIN_EMAIL and N8N_OWNER_PASSWORD to playwright.env.j2
- Update README to clarify edge-SSO vs native n8n session
2026-07-07 12:51:06 +00:00
Prageeth Panicker f2388849e9 fix(web-app-n8n): fix LDAP sign-in button selector and related fixes
- Fix button selector in _shared.js: button[type=submit] → getByRole('button', {name: /sign in/i})
- Update test-login-via-ldap.js accordingly
- Fix 03_ldap.yml LDAP configuration task
- Update playwright.env.j2
2026-07-07 12:51:06 +00:00
Prageeth Panicker faa8b6b3ee fix(ci): resolve 4 CI failures across n8n, pihole, and mastodon roles
- Add bond:1 to all 9 enabled+shared services in web-app-n8n/meta/services.yml
- Fix web-app-n8n/meta/variants.yml V3 postgres shared:false → shared:true (only V2 is standalone)
- Remove dead PIHOLE_SSO_PROXY_CA_CONTAINER_PATH var from web-app-pihole/vars/main.yml
- Add nocheck:url marker for dead link in web-app-mastodon/README.md
- Fix LDAP sign-in button selector in _shared.js (button[type=submit] → getByRole)
2026-07-07 12:51:06 +00:00
Prageeth Panicker 8eabd71cea fix(web-app-n8n): add 405 to status_codes.default to skip puppeteer CSP probe
n8n is SSO-gated and redirects unauthenticated requests cross-origin to
Keycloak, causing ERR_ABORTED (405) in the headless Chromium probe. Adding
405 opts out of the probe, matching the web-app-prometheus pattern.
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 2cb5f902fc style(web-app-n8n): fix MD031 lint error in README (blank lines around code block)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 6cf7769997 fix(web-app-n8n): address review comments — info, health check, CA trust, docs
1. meta/info.yml: add homepage and video fields.
2. meta/server.yml: add status_codes.default [200, 302] so the CSP/webserver
   health check accepts the oauth2-proxy redirect response.
3. Move self-signed CA trust for oauth2-proxy into the shared
   oauth2-proxy-keycloak.cfg.j2 template (provider_ca_files, conditional on
   TLS_MODE=self_signed). This fixes the root cause for all oauth2-proxy roles
   (including web-app-pihole) without role-local lineinfile hacks.
4. Drop compose_handlers_flush: false + explicit meta: flush_handlers from
   both web-app-n8n and web-app-pihole — now that no post-render file surgery
   is needed, compose_handlers_flush: true lets sys-stk-backend flush normally.
5. Remove the now-unused N8N_SSO_PROXY_CA_CONTAINER_PATH and
   PIHOLE_SSO_PROXY_CA_CONTAINER_PATH role variables.
6. README: document the V2 (no-auth) native administrator login flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 22d0432a91 fix(web-app-n8n): suppress seaweedfs-required CI check
n8n persists all data in postgres, not object storage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth PanickerandClaude Sonnet 4.6 e3591fc526 fix(web-app-n8n): fix 3 CI issues in n8n role
- tasks/02_bootstrap.yml: replace legacy uri: with uri_retry: (platform convention)
- README.md: fix credits author to canonical Kevin Veen-Birkenbach
- vars/main.yml: remove N8N_EMAIL_ENABLED (declared but never referenced; env.j2 reads email config directly via lookup)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-07 12:51:06 +00:00
Prageeth Panicker 8332c7edf0 feat(web-app-n8n): add n8n workflow automation role
- Community edition with edge oauth2-proxy SSO (Decision #1b)
- PostgreSQL backend via svc-db-postgres
- Three variants: V1 sso+ldap, V2 no-auth, V3 ldap-only
- Bootstrap owner account via /api/v1/owner/setup
- LDAP config via direct PostgreSQL upsert (CE API limitation)
- Playwright: baseline, CSP, OIDC, administrator and biber personas
- All 5 active tests pass, 2 skipped correctly for this variant
2026-07-07 12:51:06 +00:00