Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 c0ef883fc1 refactor(compose_volumes): auto-resolve database volume, drop manual override
Filter no longer takes a `database_volume=` parameter. The volume name is
derived from the central provider's `services.<dbtype>.name` (shared) or
`<entity>_database` (dedicated) — mirroring `plugins/lookup/database.py`'s
`volume_prefix + host` construction. Simultaneous postgres + mariadb on
the same role now raises an AnsibleFilterError with a clear message;
support is deferred per the docstring TODO.

The integration test (renamed from `test_shared_service_volumes.py` to
`test_compose_volumes_call_required.py`) now requires every
`roles/*/templates/compose.yml.j2` to call `compose_volumes`, so the
top-level `volumes:` block is always rendered. Without it, flipping a
shared service to dedicated mode adds a named-volume reference that
Compose rejects with `service "<name>" refers to undefined volume`.

Adds the call to 14 templates that were missing it (dashboard, hugo,
sphinx, bridgy-fed, fediwall, littlejs, mig, mini-qr, navigator,
roulette-wheel, simpleicons, collabora, logout, openresty) and strips
the `database_volume=...` argument from every other role plus Taiga's
now-unused `TAIGA_DATABASE_VOLUME` var.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:22:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ba5085901a fix(web-app-keycloak): emit top-level volumes block via compose_volumes
Keycloak's compose.yml.j2 included the embedded postgres service via
`sys-svc-rdbms/templates/services/postgres.yml.j2`, but never rendered
a top-level `volumes:` block. The embedded postgres template
references a named volume (`source: database`), so when
`postgres.shared=false` (dedicated DB, e.g. the variant-1 matrix run)
`compose config` failed validation with
`service "database" refers to undefined volume database`, blocking the
deploy at the compose-pull handler. Call `compose_volumes` like every
other consumer role (bookwyrm, nextcloud, …) so the top-level block
is emitted unconditionally — empty in the shared-DB variant, with the
`database` volume declared in the dedicated-DB variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:22:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 279cd16cb4 fix(plugins/lookup/database): resolve image from central provider
`lookup('database', <consumer>, 'image')` previously returned the bare
dbtype (`postgres`), so an embedded dedicated database
(`postgres.shared=false`) rendered `image: postgres:<svc-db-postgres
version>` even when the central provider shipped a different image
(`svc-db-postgres` declares `postgis/postgis`). `compose pull` then
failed with `manifest unknown` because `postgres:17-3.5` does not
exist on Docker Hub. Look the image up from the central provider's
`services.<dbtype>.image` and fall back to the bare dbtype only when
the provider declares none, so dedicated instances ship the same
image as the shared provider.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:22:37 +02:00
github-actions[bot] ae09df7850 update(images): update Docker image versions 2026-05-11 04:43:44 +00:00
github-actions[bot] 5eb74b23f9 update(skills): update skills-lock.json to latest versions 2026-05-11 04:43:27 +00:00
kevinveenbirkenbachandClaude Opus 4.7 2c0d5fb9fc fix(playwright/personas): treat Keycloak round-trip as auth proof + wait for injected logout
* Set a `keycloakRoundTripCompleted` flag once `performKeycloakLogin`
  has returned and the page is on the role's canonical hostname.
  oauth2-proxy-gated roles whose upstream UI ships no Account/Logout
  menu (Prometheus, status pages, raw upstream surfaces) now satisfy
  the persona auth-marker without a probe-able in-app control. The
  visible-marker poll still runs for roles that DO surface one.

* Add `[data-injected-logout]` as the first logout candidate so the
  shared `inAppLogout` helper picks up the per-role JS-injected
  fallback button (e.g. the Prometheus navbar patch) before falling
  through to the regex-driven candidates.

* Replace the implicit "snapshot isVisible" gate at the top of
  `inAppLogout` with a `waitForAnyLogoutCandidate` poll that pings the
  candidate set for up to 10 s. Required because the universal /
  per-role logout helpers inject their button asynchronously after
  DOMContentLoaded plus a MutationObserver settle; the previous
  synchronous check failed before the button could mount.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 72fb5c79b7 feat(web-app-prometheus): inject Logout icon into upstream navbar
The Prometheus UI ships no in-app logout entry, so an oauth2-proxy-
gated operator can authenticate but never reach the universal-logout
chain from inside the app. Add a per-role `templates/javascript.js.j2`
(activated via `services.javascript.enabled=shared=true` in
`meta/services.yml`) that injects a Logout SVG button to the right of
Prometheus' existing icon group (theme toggle, settings, docs) once
the navbar has mounted. The injection is gated server-side on
`services.oauth2.enabled` because oauth2-proxy session cookies are
`HttpOnly` and unreachable from `document.cookie`. The button carries
`data-injected-logout` so the persona helpers can find it via the
shared `inAppLogout` candidates list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 15b5348931 feat(web-app-dashboard): bump port-ui 1.1.0 → 1.2.0
Picks up the upstream navigation fix that renders
`data-bs-toggle="dropdown"` on top-level dropdown toggles, so
Bootstrap 5 binds the click handler natively and the Apps / Account
dropdowns open reliably on click. Drops the runtime JS stopgap the
previous version needed, and clears the now-resolved entries from the
role's TODO.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:48:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9763388229 fix(playwright/personas): harden auth chain + logout discovery
* Wait for the role's Login link with `Locator.waitFor` instead of the
  snapshot-only `isVisible` — `isVisible({timeout})` only governs locator
  resolution and never polls for visibility, so an OIDC-driven UI that
  injects the Login link asynchronously (e.g. dashboard's `oidc.js`
  fetched at runtime from the CDN) was treated as logged-out and the
  helper returned in milliseconds without ever clicking the link.

* Once the link is visible, prefer a direct `page.goto(href)` to the
  embedded openid-connect/auth URL when the link exposes one; fall back
  to a real click plus `page.waitForURL(/openid-connect\/auth/)` so a
  JS-handler-intercepted click (keycloak adapter, single-page-app shell)
  still drives the test through Keycloak.

* Restrict the post-login URL fallback to nested frames. The main
  frame can park on the role's canonical domain without an authenticated
  session (the role's auth chain failed silently), so accepting the main
  frame URL as proof of authentication produced false positives. The
  iframe fallback still covers the legitimate "app surface rendered via
  embed" case.

* Broaden the auth marker so role=link account/profile/user-menu
  controls and `aria-label='account'`, `aria-haspopup`, Bootstrap
  `data-bs-toggle='dropdown'`, and a `href*='logout|end_session'` link
  count as the post-login surface signal. Roles that hide the auth
  marker behind a closed dropdown still surface a probe-able anchor.

* Rebuild `inAppLogout` to iterate every visible menu trigger (Bootstrap
  dropdowns, `.user-menu-toggle`, `data-testid='user'`, ARIA-popup
  controls), not just the first match. After each click the helper
  polls for logout/menuitem/link candidates inside the dropdown,
  closes the menu again before trying the next trigger, and falls back
  to a top-level `a[href*='logout']` so static-only logout links work
  even when no dropdown wraps them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 03:48:52 +02:00
kevinveenbirkenbachandClaude Opus 4.7 df555326d8 docs(web-app-jira): link Atlassian root instead of /software/jira
Per-product Atlassian URLs are unstable; point at the root domain so
the link does not rot when Atlassian reorganises the product pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 faad246f65 fix(playwright/env): render CANONICAL_DOMAIN to unmask silent persona collapse
The shared biber/admin persona helpers in
roles/test-e2e-playwright/files/personas/*.js collapse the persona
scenario when either APP_BASE_URL or CANONICAL_DOMAIN is empty. The
twelve roles below shipped their env template without
CANONICAL_DOMAIN, so the helper fell into the auth-less collapse
branch and silently `test.skip()`-ed the persona scenarios despite
the roles having an actual Keycloak-backed auth surface. Render
CANONICAL_DOMAIN on every role whose spec drives a real persona so
the scenario runs end-to-end and a real regression cannot hide
behind a silent skip.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 63424efc74 refactor(playwright/env): resolve CANONICAL_DOMAIN via lookup('domain')
Replace the verbose
`(lookup('applications', application_id).server.domains.canonical | first)`
chain with the dedicated `lookup('domain', application_id)` plugin
across every playwright.env.j2 that renders `CANONICAL_DOMAIN`. The
helper already exists at plugins/lookup/domain.py and resolves the
canonical primary domain via the cached domain-merge pipeline, so the
inline canonical-list traversal is unnecessary noise that obscures
the actual contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3c11b67fce fix(playwright/env): wire BIBER+ADMIN credentials so personas actually log in
Render BIBER_USERNAME/BIBER_PASSWORD on the role-local env templates
that drive the shared biber persona helper and, where the
administrator branch shares the same template, ADMIN_USERNAME/
ADMIN_PASSWORD too. Without these the shared persona helper read an
empty credential pair, skipped the Keycloak login step, parked on the
auth page, and the auth-marker assertion failed loudly with no clue
that the cause was a missing env declaration.

For web-app-pixelfed the previous indirection through
PIXELFED_PLAYWRIGHT_FIRST_USER_KEY / _SECOND_USER_KEY rendered the
same biber/administrator credentials under the role-local
LOGIN_USERNAME_{FIRST,SECOND} aliases. Drop the indirection and
consume BIBER_*/ADMIN_* directly in the role's spec so the env
contract matches the rest of the persona-driven roles.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6cfbbccbd8 fix(playwright/env): declare PERSONA_*_BLOCKED for auth-less roles
bridgy-fed, hugo, mig, and sphinx have no local auth surface
(static-site generators, an in-app module of web-app-dashboard, and a
federation-only bridge). The shared biber/admin persona helpers
otherwise park on a non-existent login form and fail loudly; declare
the explicit opt-out so they skip cleanly and surface a real
regression only when an actual auth surface appears.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4b8514db33 fix(sys-utils-service-loader): preload providers in group_names
Extend the preload condition so a provider role is loaded by the
service-loader when it sits in the round's group_names, not only when
some enabled consumer requires its shared service. In matrix-deploy
rounds where every consumer flips the service to disabled (e.g. a
variant collapsing email/oidc/matomo to false), the provider role is
still in the inventory and its own tasks rely on the preload token,
so the previous narrow `required`-only check left the provider
unloaded and asserts further down the chain failed.

Drop the now-obsolete sys-svc-mail ↔ web-app-mailu preload-ordering
TODO line: the assertion that fired when mailu sat in group_names
but no consumer asked for email is resolved by the new branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a0e715ce91 test(lint): recognize readEnv() wrapper in env-keys-used scan
The env-keys lint previously matched only direct `process.env.<KEY>`
references. Persona helpers under roles/test-e2e-playwright/files/
personas/ read most env keys via a shared `readEnv("KEY")` wrapper, so
keys consumed only through the wrapper were flagged as unreferenced.
Extend the pattern so the wrapper counts as a real consumer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 00:06:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8d0a0e7bc0 chore(ansible-lint-ignore): refresh allowlist after network-utils refactor
Refactor commit `64ff6ecf` moved
`roles/sys-svc-compose/tasks/utils/network.yml` →
`utils/network/routine.yml` and `network_create.yml` →
`utils/network/create.yml`, and added two new include sites
(svc-db-openldap + web-app-prometheus + the wrapper-internal call).
ansible-lint statically resolves the `{{ playbook_dir }} | path_join`
include paths and reports each as a load-failure under the caller's
`tasks/roles/...` virtual subdir; the per-file allowlist needs the new
literal strings. Updates the three changed entries and adds three
new ones, none of which are real missing files at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:33 +02:00
kevinveenbirkenbachandClaude Opus 4.7 8055ad5d78 refactor(sys-svc-compose): centralise docker network creation under utils/network/
Splits the network-create helper out of the all-in-one wrapper so roles
can pick the right entry point:

  utils/network/create.yml   network-only, derives name + subnet from
                             `network_role_id` (use when the role still
                             needs to do work between the network create
                             and the compose-up, or when pre-creating
                             foreign networks in a loop).
  utils/network/routine.yml  former wrapper, now delegates to create.yml
                             and re-flushes sys-svc-compose right after
                             (use for self-contained single-container
                             services).

A new lint
`tests/lint/ansible/test_network_create_via_util.py` forbids ad-hoc
`community.docker.docker_network` calls or `docker network create` /
`container network create` shell-outs anywhere in `roles/` outside
`utils/network/create.yml`. The existing 5 callers (mariadb, postgres,
ollama, openldap, web-app-prometheus) move to the new layout and shed
the now-redundant `docker_network_name` / `docker_network_subnet`
caller-side vars; that retires 5 unused role constants
(MARIADB_NETWORK / MARIADB_SUBNET / POSTGRES_SUBNET / OLLAMA_NETWORK /
OPENLDAP_NETWORK).

The `web-app-prometheus` task gains a pre-create loop over
`lookup('native_metrics_apps')` so the deploy unblocks the bootstrap
deadlock observed on test-deploy-server CI run 25634428082 for
web-app-gitea and web-app-matrix (`network <X> declared as external,
but could not be found`): prometheus is loaded as a shared service
*before* the target role, so its `external: true` references to the
target's network needed pre-creation to satisfy compose-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0adfa10c5d fix(web-app-discourse): rename disable_local_logins → local_login_enabled
Two bugs in one config option:

1. `SiteSetting.enable_local_logins = {{ ... | bool | string }}` emitted
   Python's capitalized `True`/`False`, which Ruby parses as undefined
   constants → `uninitialized constant True` aborts `./launcher rebuild`
   (CI run 25634428082 deploy job for web-app-discourse).
2. `enable_local_logins` was set to the *raw* `disable_local_logins`
   value, so when the config truly meant "disable", the live setting
   was stuck on "enable", and vice versa.

Renames the field across `meta/services.yml`, `vars/main.yml`, and
`templates/config.yml.j2` to its positive form and applies De Morgan to
keep the original semantics:

    disable_local_logins = oidc.enabled AND NOT ldap.enabled
    local_login_enabled  = NOT oidc.enabled OR ldap.enabled

Truth table is preserved across all four (oidc, ldap) combinations; the
LDAP-needs-the-local-mask path stays open.

The template now uses `| string | lower` so the rendered Ruby literal
is `true`/`false`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7af0b30469 chore(format): collapse single-line TypeError in ldap_roles_mapper_payload
Auto-formatter follow-up to c53d6ce67: ruff inlines the
`raise TypeError("...")` for `ldap_cmp_id` since the message fits on
one line. No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2b72d8b550 test(lint): forbid redundant default(...) on module IO in same-task conditionals
Adds tests/lint/ansible/test_no_redundant_default_on_module_io.py which
flags `<reg>.{stdout,stderr,rc} | default(...)` inside `changed_when:`,
`failed_when:`, or `until:` lines. Those conditionals only fire after
the module has actually executed, so the fallback can never apply --
keeping it just hides bugs and noise.

Cleans up the 21 existing offenders across 14 role files. The taiga
upsert-admin task additionally gains a `not ASYNC_ENABLED and ...`
short-circuit so its async-poll-0 path no longer leans on a silent
empty fallback for stdout/stderr; with the short-circuit, the
`default('')` is genuinely dead code.

Cross-task references (a later task reading a registered result of an
earlier task that may have been skipped via `when:`) stay out of scope
and remain a legitimate use of `| default(...)`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 579c6f47f9 refactor(web-app-keycloak/ldap): inline mapper payload via filter + block
Replaces the four sibling tasks gated on `(grp_mapper_id | trim) in
["", "null"]` (Render payload via set_fact / Create / Refresh / Set-id)
with a single `block:` carrying that `when:` once. The set_fact for the
JSON payload is dropped in favour of a new `ldap_roles_mapper_payload`
Jinja filter that deepcopy + overrides the canonical fields and
returns the JSON string directly to `printf '%s' … | kcadm create
components -f -`.

Fixes both `test_excessive_repeated_when_in_tasks_files` (4× duplicate
`when:` → 1×) and `test_blocks_with_when_and_sections_have_max_three_tasks`
(4 → 3 tasks under the block).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 70318a7172 fix(web-app-espocrm/patch_config): extract idempotent PHP-config patcher
The four `Update DB host/name/user/password (idempotent)` tasks shared
identical grep-then-sed logic, but their YAML folded-scalar layout
landed `&& echo CHANGED;` on a separate line in some cases. Inside the
container's dash, `&&` at the start of a line is a syntax error, so
`Update DB name` aborted the deploy whenever the seed value already
differed from the desired one (CI run 25634428082).

Extracted the loop body into roles/web-app-espocrm/files/patch_config_key.sh
(POSIX sh, env-driven contract: CONFIG_KEY/CONFIG_VALUE/CONFIG_FILE,
escapes \, &, |, / before the sed rewrite) and collapsed the four
tasks into a single Ansible loop that pipes the script via
`container exec -i ... sh < {{ role_path }}/files/patch_config_key.sh`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4b829edc1b docs(req-019): add post-deploy log-inspection rule + per-variant matrix columns
Adds Rule 14 (mandatory log inspection after every deploy cycle: confirm
the intended persona / per-service behavior is wired AND actually
executed) and threads it through the per-role flow + role-closure gate +
verification checklist. Existing assertions stay per "Preserving
existing tests"; the rollout extends, never replaces.

Matrix gains v0/v1/v2 columns sourced directly from each role's
meta/variants.yml (⏳ = untested, flips to ✅ once the per-variant flow
incl. log inspection passes; empty cell = variant not declared).

Drops every "re-derive from `infinito meta roles applications
complexity ...`" reference: the matrix itself is the source of truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b1b73bc1df refactor(lint): tighten inline-script lint to 11 lines + extract overflows
Drops `MAX_LINES` from 12 to 11 in `test_inline_script_block_size.py`,
catching the five blocks that were sitting exactly on the previous
threshold. Pure-bash blocks move to dedicated `.sh` files invoked via
`ansible.builtin.script:` or `run: bash …`; the keycloak heredoc with a
templated JSON payload splits into a `set_fact` plus a folded
`printf | kcadm … -f -` one-liner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 576fad84ff refactor(images): move role images from defaults/main.yml to meta/services.yml
Consolidates image declarations under a single schema: every role's
`<svc>.{image,version}` lives in `meta/services.yml` alongside compose
service entries. The `image` lookup now reads from disk per role,
`iter_role_images` collapses to a single source, and the mirror chain
(resolver + apply_mirror_overrides) drops the dead `images:` branch.

`test-e2e-playwright` resolves `TEST_E2E_PLAYWRIGHT_IMAGE` directly via
`lookup('image', 'playwright', 'ref')` in vars/main.yml; the runtime
set_fact + override-empty-string dance is gone.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 14700e0658 feat(test-e2e-playwright): add PLAYWRIGHT_KEEP_ALL artefact-retention toggle
Forwards `PLAYWRIGHT_KEEP_ALL` from host env / GitHub Actions repo var
through `TEST_E2E_PLAYWRIGHT_KEEP_ALL` (inventory-overridable) into the
container and flips trace / screenshot / video to `on` for every test
when truthy.

SPOT for the propagation chain lives in
`docs/contributing/actions/testing/playwright.md#artefact-retention-`;
CI Configuration and the agent iteration loops link to it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:06:31 +02:00
kevinveenbirkenbach 96cc229b7e test(playwright/matomo): park biber-deny test until oauth2-proxy gate is wired
The `matomo: biber is denied access at the admin surface` test
expects matomo's `/` to redirect through Keycloak — that requires
oauth2-proxy in front of matomo. Today
`roles/web-app-matomo/meta/services.yml` carries `oauth2.enabled:
false` (TODO at line 12 says "Enable oauth2 for matomo"), so the
URL returns matomo's own UI directly and the redirect-toContain
poll times out.

Path 2 fix: gate the test on `isServiceEnabled("oauth2")` so it
reports `skipped` until matomo's deploy actually wires the proxy.
The test stays in the spec as the canonical contract for "biber
cannot reach Matomo"; once oauth2-proxy lands in matomo's
services.yml the gate flips and the test starts running.

The 019 matrix note for matomo cross-references the parking +
points at the services.yml TODO for the future iteration.
2026-05-10 18:53:40 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f6f4236ffd fix(playwright/personas): wire PERSONA_*_BLOCKED + handle oauth2-proxy auth chain
Five fixes against the matomo deploy CI failure mode:

* The persona helpers ([admin.js], [biber.js]) now perform the
  Keycloak login on either auth shape — oauth2-proxy gate (the goto
  is intercepted and the page lands directly on
  `openid-connect/auth`) AND in-app OIDC plugin (Login link → click
  → redirect). Previously the Keycloak step was gated on
  `oidcEnabled`, which left oauth2-proxy-only roles like prometheus
  with no auth path. The login-link regex also tolerates leading /
  trailing whitespace (`/^\s*(log\s*in|...)\s*$/i`), unblocking the
  dashboard's `" Login"` link whose accessible name carries an
  icon-induced leading space.
* `roles/web-app-keycloak/templates/playwright.env.j2`: declare
  `PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`.
  The persona stubs in the keycloak spec exist purely to satisfy the
  persona-naming lint; Keycloak IS the auth provider so neither
  persona has an in-role app surface. The bespoke
  master-realm / normal-realm tests already cover the contract.
* `roles/web-app-matomo/templates/playwright.env.j2`: same — matomo's
  local login form is not OIDC / oauth2-proxy, so the generic helper
  has no useful flow. The bespoke "matomo administrator" test covers
  the admin journey.
* `roles/web-app-prometheus/templates/playwright.env.j2`: only
  `PERSONA_BIBER_BLOCKED=true`. Prometheus is admin-only via
  oauth2-proxy with `allowed_groups:
  web-app-prometheus-administrator`; biber's session is denied at the
  proxy and biber has no in-app surface to drive a logout from.
  Administrator goes through the standard oauth2-proxy → Keycloak
  chain. Also rendered the missing `PROMETHEUS_BASE_URL`,
  `CANONICAL_DOMAIN`, and pointed `APP_BASE_URL` at prometheus itself
  so the persona helper drives the role under test (not the dashboard).
* `roles/web-app-prometheus/files/playwright.spec.js`: read
  `process.env.CANONICAL_DOMAIN` directly so the env-keys-used lint
  recognises the new env entry as consumed.

The 019 matrix note for prometheus is updated to describe the
biber-only opt-out and the admin's oauth2-proxy chain.

Out of scope: the matomo CSP biber-deny test still fails because
`roles/web-app-matomo/meta/services.yml` has `oauth2.enabled: false`
(TODO at line 12 — wire matomo's oauth2-proxy gate). That is a
deploy-config change, not a test fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:53:39 +02:00
kevinveenbirkenbach 0907162a09 docs(req-019): consolidate matrix as priority queue + clean stale framing
Major restructure of the per-role matrix and surrounding prose so the
document reflects the current code state (Test A / Test B green
tree-wide, persona-naming + persona-strict + persona-required-envs +
dashboard-integration-scope lints in place, complexity command grew
the `total` priority signal):

* The matrix is now a single 71-row priority queue sorted by
  `total` descending (the agent walks top-to-bottom). Strike-through
  remains the role-closure progress marker, applied after a
  full-cycle deploy plus passing Playwright spec.
* Drop the old per-row "missing flags" column; collapse to
  `Role | total | has env | has spec | notes`. The `notes` column
  carries role-specific contract context (PERSONA_*_BLOCKED opt-outs
  for akaunting / bluesky, admin-only-skip rationale for matomo /
  prometheus / keycloak); blank for everyone else.
* Drop the 10 fully-red rows (web-svc-* / web-opt-rdr-* without any
  Playwright artefacts) — out of scope until they grow a spec.
* Fix the 27 stale-`❌ ❌` rows that actually ship a spec but no env
  template (auth-less collapse pattern); they now read `❌ ✅`.
* Drop the now-redundant "Self-gate `# nocheck` list" section (Test
  A enforces it; the eleven `[ ]` checkboxes were misleading).
* Drop the separate Tier 12-16 iteration-order plan; the matrix
  itself is now the iteration order.
* Re-frame "Closure paths per matrix row" as forward-looking
  guidance for future drift (no missing-flag rows currently).
* Trim the Pattern transfer learnings list to consumer-scope items
  only; a new closing paragraph explicitly excludes the SPOT-owned
  probes (dashboard tile reachability, prometheus scrape parity,
  matomo tracker presence, prometheus / matomo deny + accept
  checks) that Rule 13 places in the provider specs.
* Fix three "two persona scenarios" / "both" wordings that
  contradicted Rule 3's "three persona scenarios" (`guest`,
  `biber`, `administrator`).
* Tighten Rule 3's "Enforced by" column: the persona-naming lint is
  the role-closure gate for *spec shape*; full role closure also
  needs Test A, Test B, strict-mode lint, and a passing full-cycle
  deploy.
* Tighten the `dashboard` line in Verification: per Rule 1,
  consumers do not render `DASHBOARD_SERVICE_ENABLED=`, so coverage
  for that service runs through the dashboard provider's
  parameterised tile-reachability test (Rule 13).
* Add three Verification check items for the new persona /
  dashboard-scope lints, and add Rule 1's dashboard-scope sub-rule
  cross-link to test_dashboard_integration_scope.py.
* Translate the last German tokens in the matrix headers and prose
  (Rolle / hat env / hat spec / fehlende Flags) to English.
2026-05-10 18:18:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4d4ff507a6 feat(cli/complexity): direct-only counts, total column, --order, --filter
Four additive changes to `infinito meta roles applications complexity`:

* Render two new columns for the one-hop view — `embeds_direct` /
  `consumers_direct` — alongside the existing transitive `embeds` /
  `consumers`. The direct fields are independent of `--level`: a
  reader can always see what the role embeds / is-embedded-by
  without its indirect tail.
* Render a `total` column that sums the four numeric columns. It
  serves as a coarse "overall touch points" score; `--sort total`
  surfaces the most-coupled roles.
* `--order {asc,desc}` (default `asc`) controls sort direction.
  Applies to every `--sort` choice; `--sort total --order desc`
  puts the hubs at the top of the table.
* `--filter SUBSTRING` (case-insensitive) keeps only rows whose
  role name contains SUBSTRING. The complexity scores are computed
  against the full role tree first, then filtering applies to the
  rendered rows only — a role's transitive counts still reflect the
  whole codebase.

The tuple shape grew by appending the new fields, so existing
index 0..4 assertions in the unit tests stay valid; new
assertions cover the direct-vs-transitive distinction and the
total computation.

Bonus: trailing-newline fix on `roles/web-svc-file/meta/services.yml`
(the autoformat surfaced a missing EOL after the recent dashboard
cleanup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 630b5927c3 test(lint): regroup persona lints under web-app/playwright/persona/
Move the three persona-contract lints from
`tests/lint/ansible/roles/web-app/playwright/test_persona_*.py` into
their own `persona/` subdirectory and drop the now-redundant
`_persona` filename prefix:

* `test_persona_naming.py`        -> `persona/test_naming.py`
* `test_persona_required_envs.py` -> `persona/test_required_envs.py`
* `test_persona_strict_mode.py`   -> `persona/test_strict_mode.py`

The new `persona/__init__.py` re-anchors `PROJECT_ROOT` one level
deeper (`parents[7]`). Doc references in requirements 017, 018, and
019 now point at the new paths.

Renames are tracked via `git mv`, so commit history is preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d30839f638 docs(req-017/018/019): align with persona-naming + dashboard-scope lints
Fold the recently-landed Playwright lint guards into the three
playwright-related requirement docs so the rules table and per-rule
"Enforced by" column point at the actual mechanical enforcement
rather than at "review (this requirement)".

* 019 Rule 1 now distinguishes the dashboard treatment for
  `web-app-*` consumers (per-consumer flag exemption via
  `*_TARGET_ROLES_JSON`, owned by the provider's own spec) from the
  hard prohibition for non-`web-app-*` roles
  (`test_dashboard_integration_scope.py` forbids any truthy
  `dashboard.{enabled,shared}` declaration outside `web-app-*`).
* 019 Rule 3 / Rule 11 / Rule 12 now reference
  `test_persona_naming.py`, `test_persona_required_envs.py`, and
  `test_persona_strict_mode.py` as the enforcing lints.
* 019 "Closure paths" gains a dashboard-scope-exception paragraph:
  paths 1 (render flag) and 3 (`# nocheck` annotation) collapse for
  non-`web-app-*` roles; only path 2 (drop the entry) or a static
  `dashboard: { enabled: false, shared: false }` declaration remain.
* 019 matrix: `dashboard` removed from the "fehlende Flags" column
  of `web-svc-cdn`, `web-svc-libretranslate`, and `web-svc-simpleicons`
  (no longer a closure target on these roles).
* 019 Verification block: four new green-tree-wide check items for
  the persona / dashboard-scope lints.
* 017 + 018 each gain a Policy bullet anchoring the
  `<persona>: <flow>` naming convention to 019 Rule 3 and the
  `test_persona_naming.py` lint, and 018 explicitly forbids parallel
  ungated `ldap login` tests (per 019 Rule 5).
* Touched-rule cleanup: replace existing em-dash clause separators in
  Rule 3 and Rule 11 with `;` / `,` per the documentation style guide.

006 stays untouched: it is already a closed retro doc with the
`MODE_CI`-retired banner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:22 +02:00
kevinveenbirkenbach 3420bf8f00 chore(roles/web-svc): drop redundant logout: false declarations
`web-svc-collabora` and `web-svc-onlyoffice` carried explicit
`logout: { enabled: false, shared: false }` blocks. They opt the role
out of the universal-logout integration, but `false` is already the
default when the key is absent, so the block adds no information and
the matching `# nocheck: dynamic-flag` annotation only existed to
silence a lint that does not apply to a static-false block. Drop both.
2026-05-10 18:18:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6d27cf5578 feat(roles/dashboard): forbid non-web-app dashboard integration + drop the six existing offenders
Add `tests/integration/roles/test_dashboard_integration_scope.py`,
which fails when any non-`web-app-*` role declares a `dashboard:`
block in `meta/services.yml` or in a `meta/variants.yml` override
whose `enabled` or `shared` flag is truthy according to the canonical
`is_explicit_truth` helper (literal `True` *or* the
`"{{ '<role>' in group_names }}"` Jinja form).

The dashboard tile grid is reserved for end-user-visible web-app
surfaces. Shared services (`web-svc-*`), system roles (`sys-*`),
desktop roles (`desk-*`), and driver roles (`drv-*`) integrating it
would spam the grid with non-clickable infrastructure entries.

Drop the now-forbidden `dashboard:` blocks from the six web-svc-*
roles that previously gated dashboard tiles via the dynamic-flag form
(both in `meta/services.yml` and in their `meta/variants.yml`
matrix-pinning entries):

* web-svc-collabora
* web-svc-file
* web-svc-html
* web-svc-libretranslate
* web-svc-logout
* web-svc-onlyoffice

Also drop a stray `q` keystroke at the tail of
`web-svc-file/meta/services.yml` that broke YAML parsing once the
trailing `dashboard:` block was removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:22 +02:00
kevinveenbirkenbach 8c5bb289bd feat(cli/complexity): add reverse direction (consumers) and --format {cli,json}
Surface the reverse direction of the shared-service dependency graph
in `infinito meta roles applications complexity`. Each row now also
counts and lists the application roles that transitively embed *this*
role (the "consumers" / "consumed-by" closure), with the same
`--level` depth cap as the forward direction.

* Forward closure (column `embeds`, list `services`) and reverse
  closure (column `consumers`, list `consumed_by`) share a single
  BFS over (forward, reverse) adjacency maps built once per
  invocation.
* `--sort` accepts `embeds` (default), `consumers`, or `name`. The
  former `points` label is renamed to `embeds` because the column
  header reads as a sentence with the role name (e.g. `web-app-foo
  embeds 12, consumers 78`), which is sharper than a generic
  "points" score.
* `--format cli` (default) renders three numeric columns — name,
  embeds, consumers — for a compact terminal view.
  `--format json` emits the full payload with the resolved role
  lists for each direction.
* Tests cover the chain case in both directions and verify that
  `--level` caps the reverse-direction BFS the same way.
2026-05-10 18:18:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4320d6b640 chore(lint): tighten prefer-template and convert remaining +-concats
Promote `prefer-template` to error in the spec/persona JS rule block
and convert the nine remaining `a + 'literal' + b` URL-builders to
template literals. Behaviour-equivalent; brings stylistic consistency
with the rest of the codebase, which already uses template literals
throughout, and prevents new `+`-concatenations from sneaking past
the lint floor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7db57c2fd8 chore(lint): add eight free defensive ESLint rules
All eight currently flag zero offenders in the codebase, so promoting
them costs nothing and raises the floor for new code:

* `prefer-const` — `let` is reserved for mutable bindings.
* `no-throw-literal` — only Error instances may be thrown.
* `no-self-compare` — catch `x === x` typos.
* `default-case-last` — `default:` must be the last switch arm.
* `no-eval`, `no-implied-eval`, `no-new-func`, `no-script-url`
  forbid string-driven code execution shapes outright.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:18:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0b5db3e421 fix(install/lint): auto-install npm so markdownlint/eslint installs work on virgin distros
`make install-lint` failed on every test-environment matrix runner
(manjarolinux/base, debian:bookworm, ubuntu:latest, fedora:latest,
quay.io/centos/centos:latest) with:

    npm not found; install Node.js/npm first to get markdownlint-cli2.
    make: *** [Makefile:227: install-lint] Error 1

The two npm-driven installers (`install_markdownlint_with_npm` and
`ensure_eslint`) bailed out as soon as `npm` was missing instead of
following the same auto-install pattern used elsewhere in the
script (e.g. `ensure_command shellcheck` / `shfmt` / `ruff`).

* Add an `npm:<manager>` mapping to `install_command`. pacman /
  apt-get / dnf / yum get the candidate list `npm nodejs`; brew
  gets `node`. `install_package_candidates` returns on the first
  candidate that installs, which covers every matrix distro
  (Arch's `npm` pulls `nodejs` as a hard dep; Debian/Ubuntu's `npm`
  Depends: nodejs; Fedora/EL9+ ship `npm` as a direct package; brew's
  `node` bundles npm).
* `install_markdownlint_with_npm` and `ensure_eslint` now call
  `ensure_command npm` when `npm` is missing, before failing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0e651e4a22 chore(lint): tighten ESLint rules and clean up flagged offenders
Promote four cheap rules in `eslint.config.js`:
* `no-unused-vars`: warn → error (with `argsIgnorePattern: "^_"`)
* `no-var`: error
* `eqeqeq`: error (always)
* `playwright/no-wait-for-navigation`: off → error
* `linterOptions.reportUnusedDisableDirectives`: off → warn

Add a repo-root CommonJS block so `eslint.config.js` itself sees Node
globals (`require`, `module`).

Fix every flagged offender:
* 138 stale persona-helper imports across 70 spec files
  (`isServiceEnabled`, `skipUnlessServiceEnabled`, `runAdminFlow`,
  ... — leftovers from the SPOT consolidation).
* 4 unused catch bindings → modern `catch {}` syntax.
* 2 unused `dashboardBaseUrl` declarations (minio, moodle).
* 1 unused `label` arg → renamed to `_label` to opt out per the
  argsIgnorePattern.
* 1 intentionally-kept legacy helper in web-app-wordpress's spec
  receives a per-line `eslint-disable-next-line` with reason; the
  preceding docstring already explained why it stays.
* 1 `page.waitForNavigation` in web-app-fediwall's Mastodon OIDC flow
  receives a per-line disable with reason — the navigation target
  depends on the runtime OIDC issuer; `waitForURL` would need a
  runtime-built pattern.
* 10 stale `// eslint-disable-next-line no-console` directives now
  surfaced by `reportUnusedDisableDirectives`; removed.

ESLint after this pass: 0 errors, 154 warnings (148 pre-existing
`playwright/expect-expect` plus 6 `playwright/no-force-option` that
were already inherited from `playwright/flat/recommended`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c55ad69c41 refactor: drop requirement-file references from code, add doc rule + lint
Strip every `req <NNN>`, `req-<NNN>`, `Requirement <NNN>`,
`requirement-<NNN>`, and `docs/requirements/<NNN>-...md` mention from
non-Markdown source files. Comments anchored to requirement numbers
rot when requirements are renumbered or rewritten and conflate
present-state behaviour (which the comment should describe) with the
audit trail of how the code got there (which belongs in commit
messages, PR descriptions, and the requirement file itself).

* Document the rule in `docs/contributing/documentation.md` under
  "Comments". Code MUST describe the *why* in present-state language;
  contract enforcement lives in tests, not folklore.
* Clean 254 source files across `roles/`, `tests/`, `plugins/`,
  `cli/`, `scripts/`, `utils/`, `group_vars/`, and the repo-root
  `eslint.config.js`. Pure comment / docstring / log-message edits;
  no behavioural change.
* Add `tests/lint/repository/test_no_req_references_in_code.py` to
  enforce the rule going forward across `.js`, `.py`, `.yml`,
  `.yaml`, `.j2`, and `.sh` files. The legacy
  `test_no_stale_requirement_refs.py` is exempt as the only file
  whose purpose is to detect these very tokens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9ff9c629c8 test(lint): enforce <persona>: <flow> naming convention
Adds tests/lint/ansible/roles/web-app/playwright/test_persona_naming.py
to fail the build whenever a `web-app-*/files/playwright.spec.js`
omits one of the three persona scenarios required by the SPOT
contract in docs/contributing/artefact/files/role/playwright.specs.js.md
without an explicit suppression.

Two suppression channels are recognised, both already documented in
the SPOT contract:

  - PERSONA_<PERSONA>_BLOCKED=true rendered in the role's
    templates/playwright.env.j2 (req 019 Rule 11 explicit role-
    contract opt-out).
  - Auth-less collapse exception: roles that ship no
    templates/playwright.env.j2 are not orchestrated by
    test-e2e-playwright and the persona contract does not apply.

Without one of those, every web-app-* role MUST contain
`test("guest: …")`, `test("biber: …")`, and `test("administrator: …")`.

Reason
------
The previous commit fixed seven roles that were silently violating
this rule. Without an automated guard, the same regression class
returns whenever a future role is added or an existing spec's test
title is renamed away from the `<persona>:` prefix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6a92143d21 fix(playwright): add missing persona scenarios for Rule 3 compliance
Seven web-app-* specs failed the Rule 3 contract from
docs/contributing/artefact/files/role/playwright.specs.js.md:

  - bigbluebutton, decidim, discourse, matrix, openwebui, peertube
    each shipped `administrator:` and `biber:` test titles but no
    `guest:` test.
  - keycloak shipped four detailed admin / biber tests under non-
    canonical names (`master-realm super administrator…`, `normal-
    realm administrator logs in through account interface…`, etc.)
    and no `<persona>: <flow>`-prefixed scenarios at all.

None of the seven declared a `PERSONA_<persona>_BLOCKED=true`
opt-out in their `templates/playwright.env.j2`, and none qualify
for the auth-less collapse exception (every role has an OIDC auth
surface; keycloak IS the auth surface).

Changes
-------
- Each of the six guest-only-missing specs gains a single appended
  scenario:
    test("guest: public-landing → auth chain → never authenticated", …)
      → runGuestFlow(page);
- Keycloak gains all three persona scenarios (guest, biber,
  administrator) using the shared `runGuestFlow` / `runBiberFlow` /
  `runAdminFlow` runners. Existing detailed tests stay untouched
  alongside the persona contract.
- The `./personas` import in each spec is amended with whichever
  runners are now needed.

Verification
------------
- 420 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- 0 Rule-3 violators remain across all `web-app-*/files/playwright.spec.js`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:22 +02:00
kevinveenbirkenbachandClaude Opus 4.7 eb7344cc04 refactor(playwright): share DOM + regex utility helpers
Five role specs (dashboard, nextcloud, odoo, pixelfed, taiga)
duplicated four small utility helpers across one-cluster-each
inline copies:

  - isVisible(locator)               — `locator.first().isVisible()`
                                       with the strict-mode / detached
                                       error swallowed.
  - waitForFrameUrl(iframe, ...)     — poll the embedded iframe URL
                                       until it contains a matcher.
  - findFirstVisibleCandidate(list)  — pick the first visible
                                       candidate from a label/locator
                                       list and return the resolved
                                       single-element locator.
  - escapeRegex(value)               — escape regex metacharacters
                                       so a raw string can be used as
                                       a `new RegExp(...)` source.

Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dom.js` collects
  all four helpers (canonical 1-cluster bodies, byte-equivalent to the
  former inline copies).
- `personas/utils/index.js` re-exports the new module so callers reach
  the helpers via `require("./personas")`.
- The five specs strip their inline definitions and amend the
  destructuring with whichever helpers their body still references.

Drift handling
--------------
None. All four helpers were 1-cluster across the call sites; this is
mechanical substitution.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- 0 inline `function isVisible|waitForFrameUrl|findFirstVisibleCandidate|escapeRegex`
  definitions remain in any role spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 16:25:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 47196731d7 refactor(playwright): migrate spec-local performKeycloakLogin to shared form helper
Nine role specs (akaunting, baserow, bluesky, bookwyrm, flowise,
fusiondirectory, jenkins, joomla, kix) carried a 3-arg
`performKeycloakLogin(page, username, password)` shadowing the shared
4-arg `performKeycloakLogin(page, username, password, canonicalDomain)`
from `personas/utils/keycloak.js`. The local definitions were
functionally identical to `performKeycloakLoginForm` (added in the
previous commit); they were missed by the earlier `performOidcLogin`
sweep because they happened to live under the canonical name.

Changes
-------
- The 9 specs strip their inline definition, rename every call site
  and comment reference `performKeycloakLogin` -> `performKeycloakLoginForm`
  (function, callers, and inline prose), and pull
  `performKeycloakLoginForm` from `./personas`.

Drift handling
--------------
- Cluster 1c681583 (5 specs) and 4b3fd133 (3 specs) used the
  input-name selector strategy (`input[name='username']` family).
  The canonical helper covers those via the `.or()` fallback in
  every selector chain.
- Cluster ffa30940 (kix) used role-based selectors with an
  exact-string `getByRole("textbox", { name: "Password" })`. The
  canonical's case-insensitive regex `/^password$/i` matches "Password"
  and any reasonable Keycloak theme variant (strictly more
  permissive).
- Sign-in button: kix's `/sign in/i` becomes the canonical's
  `/sign in|login|log in/i` plus the `input#kc-login` family via
  `.or()` (strictly broader).

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- 0 inline `performKeycloakLogin` definitions remain in any role
  spec; the only definition lives in `personas/utils/keycloak.js`
  alongside the 4-arg variant and the deny-expecting variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 370f381d1c refactor(playwright): share Keycloak login-form helper
Fourteen role specs (bigbluebutton, discourse, fider, gitea, mailu,
matrix, mattermost, odoo, openwebui, peertube, postmarks, prometheus,
yourls, libretranslate) each carried an inline `performOidcLogin`
that drove the Keycloak login form. The bodies fell into four
clusters split between the role-based selector strategy
(`getByRole`) and the legacy input-name strategy
(`input[name='username']`); the existing shared
`performKeycloakLogin` already covered both via `.or()` chains but
also asserted post-login navigation, so it could not be reused
verbatim.

Changes
-------
- `personas/utils/keycloak.js` gains `performKeycloakLoginForm
  (target, username, password)` — form-fill + submit only, accepts
  `Page` OR `FrameLocator` so iframe-embedded Keycloak pages work
  without branching at the call site. The selectors are the union
  of all four prior cluster shapes (role-based getter chained with
  `.or(input-name fallback)`, sign-in matched against
  `/sign in|login|log in/i` plus `input#kc-login` family). The
  existing `performKeycloakLogin` and
  `performKeycloakLoginExpectingDenial` now delegate to the new
  helper, so their public API is unchanged.
- The 14 specs strip their inline `performOidcLogin` and amend
  `require("./personas")` with `performKeycloakLoginForm`. Every
  call site and comment reference is renamed in lockstep.

Drift handling
--------------
- Cluster 1 (5 specs) expected the accessible name `"Password"`
  exactly; the canonical regex `/^password$/i` is case-insensitive
  and matches every prior name plus lowercase variants — strictly
  more permissive.
- Cluster 1's sign-in button matched `/sign in/i`; the canonical
  matches `/sign in|login|log in/i` plus the legacy `input#kc-login`
  family — strictly broader.
- Cluster 1 awaited `usernameField.press("Tab")` without a `.catch`;
  the canonical wraps the press in `.catch(() => {})` so an
  unexpected Tab failure no longer aborts the flow.
- Cluster 2 / 3 used `expect(usernameField).toBeVisible({ timeout })`
  for the visibility wait; the canonical uses
  `usernameField.waitFor({ state: "visible", timeout })`, the
  Locator-method counterpart of the same wait — functionally
  equivalent.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- 0 inline `performOidcLogin` definitions remain in any role spec,
  and 0 stale references to the legacy name in any context (call
  sites and comments are renamed in the same pass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 7018b3c991 refactor(playwright): share CSP header/meta/violation helpers
Nine role specs (bigbluebutton, dashboard, discourse, keycloak,
matomo, matrix, moodle, openwebui, wordpress) each carried inline
copies of the same six CSP helpers and the `EXPECTED_CSP_DIRECTIVES`
constant. The bodies were semantically identical; only message
wording, brace style, and one captured-field-set differed.

Changes
-------
- Extends `roles/test-e2e-playwright/files/personas/utils/csp.js`
  (already home to `assertCspInjections`) with
  `installCspViolationObserver`, `readCspViolations`,
  `parseCspHeader`, `assertCspResponseHeader`,
  `assertCspMetaParity`, `expectNoCspViolations`, and the
  `EXPECTED_CSP_DIRECTIVES` constant. The helpers are re-exported
  through `personas/utils/index.js` so specs that
  `require("./personas")` get them via the existing aggregator.
- Strips the inline definitions from the nine role specs and amends
  each spec's `require("./personas")` destructuring with whichever
  helpers the body still references.

Drift handling
--------------
- moodle's reduced violation-record shape (only `violatedDirective`
  + `blockedURI`) is replaced by the canonical 5-field record.
  Moodle's downstream consumer asserts `.toEqual([])` on the array
  only, so the extra fields are diagnostic-only — no behaviour
  change.
- dashboard's 2-arg `expectNoCspViolations(page, label)` call site
  is updated to the canonical 3-arg form
  `expectNoCspViolations(page, null, label)`. The shared helper
  guards `if (diagnostics && Array.isArray(diagnostics.cspRelated))`
  before checking the diagnostics arm, so passing `null` keeps the
  exact pre-migration behaviour for that call site.
- bigbluebutton and matrix had subtly different failure-message text
  in `assertCspResponseHeader` and `expectNoCspViolations`. The
  canonical wording is more descriptive; passing tests are
  unaffected, failing tests get clearer diagnostics.

Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` is
extended in two places: the invariants section names the full CSP
test surface and pins the "import from `./personas`, no inline copy"
rule, and the per-service catalogue's CSP / injectors row lists the
helpers by name.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline).
- `markdownlint-cli2`: 0 errors on the touched doc.
- 0 inline definitions of any extracted helper / constant remain in
  any role spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 122c82910b refactor(playwright): share decodeDotenvQuotedValue + normalizeBaseUrl
Both helpers were inlined verbatim in 71 of the role-local
`playwright.spec.js` files (52 with the compact form, 19 with the
verbose form, plus one parameter-name variant). All bodies were
semantically identical.

Changes
-------
- New `roles/test-e2e-playwright/files/personas/utils/dotenv.js` holds
  the canonical pair (`decodeDotenvQuotedValue` and `normalizeBaseUrl`),
  re-exported through `personas/utils/index.js` so specs that
  `require("./personas")` get them via the existing aggregator.
- Every role spec drops its inline copies and pulls the helpers from
  `./personas` instead. Specs that only reference one of the two
  carry only that one in the destructuring (no unused-import noise).

Doc
---
`docs/contributing/artefact/files/role/playwright.specs.js.md` no
longer carries the "once a third spec needs them, MUST be promoted"
directive: the helpers live at the shared path and inline copies are
forbidden.

Verification
------------
- 419 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors, 283 warnings (unchanged from
  baseline; no new unused-imports introduced).
- 0 inline `function decodeDotenvQuotedValue|normalizeBaseUrl`
  definitions remain in any role spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 db2a2d9638 refactor(playwright): move cross-service tests to provider specs
Cross-service surface assertions for dashboard, prometheus and matomo
no longer run in every consumer role's persona. They are owned by the
provider's own spec, parameterised once over a `<NAME>_TARGET_ROLES_JSON`
manifest rendered into the provider's `templates/playwright.env.j2` via
the new `lookup('roles_with_service', '<svc>')` Ansible lookup.

Changes
-------
- New `plugins/lookup/roles_with_service.py` enumerates roles that
  declare `services.<svc>.{enabled, shared}` truthy in the merged
  applications config, returning `[{id, canonical_domain}, …]`.
- `roles/web-app-{dashboard,prometheus,matomo}/files/playwright.spec.js`
  parse `<NAME>_TARGET_ROLES_JSON` and parameterise the per-consumer
  assertion (dashboard tile reachability, prometheus `up=1`, matomo
  SitesManager registration). The matomo spec gains an explicit
  biber-denial test (request body validated against admin DOM markers
  per the body-marker rule); the prometheus admin-reach test drops
  the dashboard-iframe path in favour of direct goto.
- Persona helpers `roles/test-e2e-playwright/files/personas/{admin,
  biber,guest}.js` enter at `${APP_BASE_URL}/` directly. The
  `clickRoleTileFromDashboard` step and the `assertPrometheus*` /
  `assertMatomo*` cross-service probes are gone; `utils/{dashboard,
  prometheus,matomo}.js` are deleted. `utils/csp.js` switches to
  literal `process.env.<KEY>` reads so the env-keys-used parity guard
  recognises matomo's CSP injector consumption.
- 42 consumer-role `templates/playwright.env.j2` templates drop
  `DASHBOARD_BASE_URL`, `DASHBOARD_SERVICE_ENABLED`, `PROMETHEUS_BASE_URL`,
  `PROMETHEUS_SERVICE_ENABLED`. `MATOMO_BASE_URL` and
  `MATOMO_SERVICE_ENABLED` stay because the per-role CSP injection
  helper still gates on matomo's tracker host.
- `tests/integration/roles/test_playwright_env_services_match.py`
  globally exempts `dashboard` and `prometheus` from the
  services.yml-to-env parity rule.
- `tests/unit/plugins/lookup/test_roles_with_service.py` adds 14
  unit tests for the lookup, including an explicit-contract test
  that pins the over-inclusive behaviour on raw Jinja strings (the
  lookup expects a templar-rendered payload).
- Docs: `docs/requirements/019-playwright-meta-services-parity.md`
  rewords Rules 1, 3, 12 and adds Rule 13 describing the SPOT shift.
  `docs/contributing/artefact/files/role/playwright.specs.js.md`
  reflects the new persona shape and per-service catalogue, and is
  cleaned per `docs/contributing/documentation.md` (no SPOT self-
  labels, no req-references, em dashes replaced with complete
  sentences, multi-clause lines split onto separate lines, broken
  anchor `#two-personas-fixed-flow-` corrected).

Verification
------------
- 418 passed / 1 skipped / 0 failed across `tests/lint/`,
  `tests/integration/roles/`, `tests/unit/plugins/lookup/`.
- `make lint-javascript`: 0 errors.
- `markdownlint-cli2`: 0 errors on all touched docs.
- `make autoformat`: clean (FURB171 single-item-set inequality fixed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2480cd1ce3 ci(deps): cover repo-root package.json + regenerate .dockerignore
Two follow-ups to 2986e8042 (introduces repo-root package.json with the
ESLint dev-deps and adds node_modules/ to .gitignore):

- `.github/dependabot.yml`: add `/` to the existing npm directories
  list. The new package.json was uncovered, so
  `tests/integration/workflows/test_dependabot_coverage.py` failed
  ("ecosystem: npm" required for /package.json).
- `.dockerignore`: regenerated by `make dockerignore` (the Makefile
  target that mirrors .gitignore into .dockerignore plus a trailing
  `.git`); now carries the new `node_modules/` line so build contexts
  don't ship the local install tree into images.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:55:04 +02:00