Commit Graph
1042 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 3f6e2dd68d refactor(sys-svc-compose): rename docker_git_repository_* → compose_*
Pure 1:1 rename (60 insertions / 60 deletions across 30 files) to
align the variable names with the role that owns them and to drop the
misleading ``docker_git_`` prefix — these knobs configure the
compose-stack's git clone step, not the docker CLI:

- docker_git_repository_address → compose_repository_url
- docker_git_repository_version → compose_repository_ref
- docker_git_repository_pull    → compose_pull_repository
- docker_repository_path        → compose_repository_path

``ref`` better captures git refs (branches/tags/SHAs are not
"versions"); ``url`` is the idiomatic name for the clone source.

Touches every consumer (web-app-akaunting/bigbluebutton/dashboard/
fediwall/hugo/littlejs/mini-qr/minio/openproject/openwebui/
roulette-wheel/taiga + svc-db-openldap/svc-prx-openresty/sys-svc-rdbms
+ web-svc-collabora/coturn/logout/onlyoffice + sys-front-inj-all), the
role's own defaults/tasks, the bigbluebutton compose_mods filter and
its unit test, the declared-in-services lint, and the hugo
requirements doc.

Breaking change for any external override that still sets the old
names — no alias is provided.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:43:43 +02:00
kevinveenbirkenbachandClaude Opus 4.7 904f713c99 docs(agents/role-loop): expand non-essential provider toggle to matomo,dashboard,prometheus,email,css
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 01:09:03 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1fa2a50c1b docs(agents/cheatsheet): tighten Web Development Iteration prompt; drop trailing Use mode /caveman ultra from every prompt
Web Development Iteration prompt now spells out the in-container loop
explicitly: for every failing Playwright test, follow the inner-loop
procedure in role.md together with playwright.md; before every
redeploy run `make exec` and `make run` against the live stack and
fully fix and inspect every failure in-container; the next deploy
iteration only starts once every error is resolved and the fix is
empirically verified. Adds explicit prohibitions on commit/push and
on `ask`-mode approvals, and forbids premature termination — the
iteration is finished only when every role is green end-to-end.

Strips the `Use mode /caveman ultra` postscript from every other
prompt: it was an experimental mode marker that no longer applies
and only added noise. The prompts now stop at the standard
`act autonomously …` sentence.

Reformatted to comply with documentation.md: complete sentences
instead of semicolon chains, no em-dash thought breaks, RFC 2119
keywords used consistently.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:29:36 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a965461c53 docs(requirements): add 021 SSO flavor migration plan
Captures the unified `services.sso.{flavor,...}` target schema, the
per-role mapping for the 3 dual-block roles (bookwyrm → oauth2,
gitea/friendica → oidc), the 6-layer touchpoint inventory, the 7-step
atomic migration sequence, and the validation set. Plan only; no code
changes yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:07:01 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16cab61397 fix(image-lookup): require explicit role_id to prevent wrong-role resolution
The `image` lookup inferred `role_id` from `role_name` when only
`service_name` was passed. Inference broke lazy re-evaluation: a default
like `HEALTH_CSP_IMAGE: "{{ lookup('image', 'csp-checker', 'ref') }}"`
defined in sys-ctl-hlth-csp got re-evaluated inside sys-service's
systemctl.service.j2 template (via `system_service_tpl_exec_start`),
where `role_name='sys-service'`. The lookup then searched
`roles/sys-service/meta/services.yml` for `csp-checker` and aborted with
`no image mapping found for role 'sys-service' service 'csp-checker'`.

CI run 25976825584 surfaced this on six apps (web-app-{bridgy-fed,chess,
mig,hugo,mini-qr,postmarks}) whose deploy reached sys-svc-webserver-core
outside the service-loader preload context, which is what actually
triggers the sys-ctl-hlth-csp -> sys-service include chain.

Make `role_id` mandatory in the plugin signature, remove `_infer_role_id`,
and update the three callsites that relied on inference
(sys-ctl-hlth-csp, sys-svc-container, test-e2e-playwright) to pass the
owning role id explicitly. A migration guard catches the removed
`(service_name, want)` form and emits a clear error instead of the
confusing "missing services.yml" file-not-found message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:11:30 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fbecc09d24 refactor(tests): regroup meta tests, add native-metrics contract
- New bidirectional contract test ensures `services.prometheus.native_metrics.enabled`
  and `roles/<role>/templates/prometheus.yml.j2` stay consistent across the roles tree,
  replacing per-role spot checks for gitea/mattermost/matrix. Document the `native-metrics`
  suppression rule and apply it to web-app-mattermost (prepared-but-default-off fragment).
- Move every test in `tests/integration/meta/config/` into the per-domain layout under
  `tests/integration/roles/meta/{services,server}`. Group prometheus tests under
  `services/prometheus/`, repositories tests under `services/repositories/`, the
  `server.csp` shape test under `server/`. Move `test_non_empty.py` one level up
  (scans all meta/*.yml, not just services.yml).
- Delete `tests/integration/meta/spots/` — both files were only `assertIn` over file
  contents (brittle, no behavioural coverage, knowledge belongs in code comments or
  proper integration tests).
- Harden `test_project_root_import.py` to catch chained `Path(__file__)…parent.parent.…`
  walks (the attribute-access twin of `parents[N]` that previously slipped through).
  Refactor 6 surfaced sites to import canonical `PROJECT_ROOT` / `ROLES_DIR`, plus
  delete the now-redundant `_find_roles_dir` / `_find_project_root` helpers in
  `native_metrics_apps` lookup and `invokable_paths` filter plugin.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 12:50:35 +02:00
kevinveenbirkenbach eb4d437989 docs(req-019): strike web-app-kix row in iteration matrix 2026-05-16 23:24:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c2c532c8e6 refactor(playwright): centralize dashboard-tile mechanics in web-app-dashboard SPOT; move specs into files/playwright/ subdir
Architecture
============
- New SPOT helper `roles/web-app-dashboard/files/playwright/dashboard-card-flow.js` (`runDashboardCardScenario(page, context, target)`): per `roles_with_service('dashboard')` target, asserts tile visible → click loads in `#main iframe` (outer URL gets `?iframe=` + iframe src matches canonical_domain) → header "Tab" button pops the embed into a fresh browser tab.
- `web-app-dashboard/files/playwright/playwright.spec.js` parametrised loop calls the helper for every `DASHBOARD_TARGET_ROLES_JSON` entry — the single owner of dashboard-tile mechanics across the whole codebase.

Spec relocation
===============
- 71 `roles/<role>/files/playwright.spec.js` → `roles/<role>/files/playwright/playwright.spec.js` (`git mv`, history preserved). odoo + pixelfed show as D+A in `git status` because of substantial content rewrites; coverage retained.
- New SPOT constant `ROLE_FILE_PLAYWRIGHT_SPEC` in `utils/roles/mapping.py`, registered as optional on `ROLE_TYPE_APPLICATION` (disallowed elsewhere, mirrors `ROLE_FILE_META_VARIANTS` pattern).
- `roles/test-e2e-playwright/tasks/run_one.yml`: staging task replaced with `with_fileglob` over `files/playwright/*.js` — roles can ship companion helpers (like `dashboard-card-flow.js`) next to the spec without further wiring.

Spec migrations (no more dashboard-iframe path in app specs)
============================================================
13 specs rewritten so the persona/SSO journey starts at the app URL directly, never through the dashboard tile or iframe:
* fider, mailu, mattermost, nextcloud, odoo, taiga: iframe-wrap removed (no more `frameLocator("#main iframe")`)
* bigbluebutton, discourse, kix, matrix, openwebui, peertube: dashboard goto preamble removed
* pixelfed: ~270 LOC of iframe-aware helpers/login/logout collapsed to direct-URL flow

Title cleanup
=============
- 64 specs: `"<persona>: dashboard → app → universal logout"` / `"administrator: dashboard → prometheus → app → universal logout"` → `"<persona>: app → universal logout"`.
- 13 specs: `"<persona>: dashboard to <app> ..."` → `"<persona>: <app> ..."`.

Dead-code cleanup
=================
- `dashboardBaseUrl` consts + beforeEach assertions removed from 11 specs (no functional uses; personas helper handles env validation).
- `DASHBOARD_BASE_URL=` env-template lines removed from fider, mattermost, odoo, taiga, pixelfed, kix.
- `DASHBOARD_SERVICE_ENABLED=` env-template lines removed (pixelfed retained for the still-gated spec).
- Stale "loads inside the iframe" / "FrameLocator parameter" comments cleaned in fider, odoo, prometheus, nextcloud.

Tooling
=======
- 5 lint Python files + 1 integration test now use `from utils.roles.mapping import ROLE_FILE_PLAYWRIGHT_SPEC`.
- `eslint.config.js` glob pattern + `roles/test-e2e-playwright/meta/main.yml` description + `roles/test-e2e-playwright/README.md` updated to new path.
- 25 markdown/docstring references in `docs/`, `roles/**/README.md`, `roles/**/TODO.md`, persona-helper docstrings updated to new path.
- `docs/contributing/artefact/files/role/playwright.specs.js.md`: file-placement section now documents the `files/playwright/` convention + `ROLE_FILE_PLAYWRIGHT_SPEC` SPOT constant; dashboard SPOT delegation explicit at 3 places.

Net diff: 124 files, +1142/-1564 (−422 LOC).
Integration suite: 183/183 green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:24:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5b8cb71884 feat(scripts+make): add make run wrapper for nested docker run inside the host container
Inner-loop iteration on a deployed stack often needs a one-off sidecar
container against the same docker daemon the stack lives on — typical
cases are rerunning a Playwright project with a patched .env, retrying
an arbitrary CLI image against an app container, or smoke-checking a
release image without going through Ansible. Without a wrapper the
caller has to spell out `docker exec <host-container> docker run --rm
<flags...> <image> <cmd>` by hand, which is verbose enough that agents
tend to fall back to redeploying instead of validating in-place.

Add `make run IMAGE=<ref> [CMD=...] [RUN_FLAGS=...]` plus the underlying
`scripts/tests/deploy/local/exec/run.sh`. The script reuses
`scripts/meta/env/all.sh` to resolve the host container name (same SPOT
as `make exec`), forwards RUN_FLAGS verbatim, and only wraps CMD in
`sh -lc` so default-entrypoint images and shell one-liners both work.

Wire both iteration loops to mention the wrapper next to `make exec`:
the Role Loop's Inspect block gets a bullet pointing at it, and the
Playwright Spec Loop gains a short "Debugging in the nested docker
daemon" section that also reminds the agent to mirror any successful
hand-patch back into the role's playwright.env.j2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:37:12 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ddbbda9b4a fix(roles+tests): migrate LDAP consumers to dynamic enabled + add variants-static-override guard
The nextcloud LDAP failure in CI 25939428268 traced to a variant-merge
propagation gap: when meta/services.yml declares a flag as a literal
and meta/variants.yml overrides that same flag, the merged value does
not reliably reach downstream `lookup('config', ...)` consumers.
Plugin tasks then run against a provider that the round's inventory
does not contain (e.g. `occ ldap:search` -> "Lost connection to LDAP
server"). The workaround is to anchor the base value to a
`'<role>' in group_names` expression so the inventory itself decides
whether the consumer activates.

Same anti-pattern applied to nine other shared-LDAP consumers:
funkwhale, keycloak, kix, openproject, opentalk, snipe-it, suitecrm,
taiga, web-svc-xmpp. Each had `services.ldap.enabled: true # nocheck:
dynamic-flag` paired with a dynamic `shared`. Replaced with
`"{{ 'svc-db-openldap' in group_names }}"` for both flags.

Add a regression guard at
tests/integration/roles/meta/services/test_variants_no_static_override.py
that fails whenever meta/variants.yml overrides a first-level
`services.<key>.enabled` or `services.<key>.shared` flag while the
base in meta/services.yml is a static literal. Rule
`variants-static-override` is catalogued in the suppression doc.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 15:37:11 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e2d9a69f1d fix(lint+pre-push): escape pipe in markdown regex literals + add make lint to pre-push gate
The `/username|email/i` regex literals in the bookwyrm row tripped markdownlint's
MD056 (column count) — markdown reads bare `|` inside table cells as cell
separators. Escape both occurrences to `/username\|email/i`. Also extend the
existing pre-push hook config so `make lint` runs alongside `make test-external`
before every push, so the next markdown drift never makes it past the push gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:25:35 +02:00
kevinveenbirkenbachandClaude Opus 4.7 15de5afced test(lint/ansible/services): warn-only check for non-semver image and ref pins
Adds `test_semver_pinning.py` under `tests/lint/ansible/services/`.
Recursively walks every `roles/*/meta/services.yml`, surfaces a
warning for every `image:`+`version:` pair and every `repository:`+`ref:`
pair whose value is not semver-compatible. Output via the shared
annotations helper (GitHub Actions `::warning::` plus pytest's
`warnings.warn`), with a single trailing suppression hint when any
finding fires. Never fails the test session.

Suppression reuses the existing markers:
  - `# nocheck: docker-version` for `version:` pins
  - `# nocheck: repository-version` for `ref:` pins
Both accept same-line or line-above placement. The existing external
update checks (`utils/update/docker.suppressed_services` and
`utils/update/repository.suppressed_ref_lines`) are loosened from
strict `line-above` to the default `same-or-above` for consistent
marker semantics across all three consumers.

Updates `docs/contributing/actions/testing/suppression.md` so the
catalog lists both consumers per marker and adds the previously
missing `repository-version` row.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 16bdbf03d7 docs: fix repo-wide drift against documentation.md style rules
Three classes of drift cleaned up across 85 .md files:

- Em-dashes: replaced ` — ` with the contextually correct separator
  (colon for `term: explanation`, period to split clauses across
  sentences/source lines, parentheses for asides, semicolon for
  tight-linked continuations). Two intentional exceptions remain
  in `docs/contributing/documentation.md` (the rule literal) and
  inside a fenced ```sh block in `docs/contributing/actions/debugging/ci.md`.
- SPOT self-labels: rewrote "This is the SPOT for X" to describe
  the subject directly ("This directory holds X" / "This file
  describes X"). Cross-references to other artefacts' canonical
  role remain unchanged.
- Heading-emoji position: moved leading-emoji headings (`# 🧩 Title`)
  to trailing position (`# Title 🧩`) per the headline rule that
  emojis MUST come after the title text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 98457f98fd refactor(update): unify version bumpers under utils/update + cli/contributing/update + scripts/github/update
Folds the parallel Docker-image and git-ref version-update plumbing
into a single subsystem so the auto-update workflow, the external
freshness tests, and the unit tests all share one module layout.

utils/
  update/
    base.py        # semver primitives shared by both backends
    docker.py      # OCI registry lookups + version: line rewriter
    repository.py  # git ls-remote walker + ref: line rewriter

cli/contributing/update/
  docker/           # python -m cli.contributing.update.docker
  repository/       # python -m cli.contributing.update.repository

scripts/github/update/
  image_versions.sh
  repository_refs.sh

Tests mirror the same shape under tests/{unit,external}/update/{docker,repository}/.

Also widens the Docker bump scope to all roles (previously gated on
`web-*`) so non-web roles like sys-svc-container get bumped too;
pins Bookwyrm's python image at 3.11-bookworm via
`# nocheck: docker-version` with a TODO until upstream supports
Python >= 3.12.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a55b265d5e docs(requirements/{019,020}): re-strike web-app-nextcloud as environmental flake
CI run 25890032686 v0+v1 of `web-app-nextcloud` deployed cleanly
(PLAY RECAP `failed=0`); only v2 fail'd in
`tasks/plugins/user_ldap.yml:69` with `Failed opening required
'/var/www/html/lib/versioncheck.php'`.

Source-code review against the Nextcloud `stable33` upstream:

  - `lib/versioncheck.php` exists in the source tree (726 B, sha
    dd7790ff…).
  - `console.php:14` loads it via `require_once __DIR__ . '/lib/versioncheck.php'`
    — `__DIR__` resolves at compile time to the absolute directory of
    `console.php`; cwd is irrelevant.
  - The `nextcloud:33-fpm-alpine` Dockerfile declares no `WORKDIR` and
    sets no `open_basedir`, so neither a missing image-WORKDIR nor a
    PHP-include-path restriction can produce this error.

The remaining mechanism is that v2's matrix-deploy round short-
circuited the entrypoint's `rsync /usr/src/nextcloud/ → /var/www/html/`
such that the source did not stage into the `data:` named volume. The
same env-flake pattern produced the prior C14 (`Address already in use`)
in CI 25774452286 — v0+v1 clean, only one variant hit a round-isolated
state.

Re-strike `web-app-nextcloud` in both matrices with v0/v1/v2 ✅, mark
the `C-new-nextcloud-ldap-warm` cluster as `(environmental, not
actively remediated)`, and update the CI 25890032686 follow-up
snapshot to reflect the re-classification.

Recurrence in a future CI run with the same v2-only signature would
warrant re-opening; until then no role-level remediation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 20:12:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4ac7a95ec6 refactor(tests): split overflowing folders into thematic subdirs
Reorganises four folders that exceeded the 12-item readability limit:

- tests/lint/repository/ (27 -> 11): structure/, naming/, documentation/,
  python/, dependencies/, shell/, yaml/, network/, settings/
- tests/integration/ (25 -> 7): ansible/, infrastructure/, iam/, meta/
  (24 sibling dirs grouped under 4 super-categories)
- tests/integration/roles/ (13 -> 10): naming/, playwright/, dashboard/
- tests/unit/utils/ (15 -> 12): certs/ for 3 cert/TLS test files

Renames 7 test files whose name duplicated the new folder name
(e.g. test_no_direct_yaml_calls.py -> yaml/test_no_direct_calls.py,
test_dashboard_integration_scope.py -> dashboard/test_integration_scope.py).

Adjusts parents[N] in moved __init__.py files, fixes __file__-based
path resolution in 8 tests, updates self-path whitelists, and uses
absolute imports for PROJECT_ROOT re-exports (ruff TID252).

Adds README.md to each new subfolder following the existing scope
template. Updates all docs links (suppression.md, base.md, email.md,
playwright spec/parity docs, 005-/019- requirements) to new paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 14:23:32 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f611e7f2b7 docs(requirements/{019,020}): strike web-app-postmarks, mark all variants ✅
CI run 25868178119 — `Deploy web-app-postmarks` job passed (deploy +
Playwright), confirming the prior C2 Playwright regression is resolved
by the persona-helper fast-fail (commit a6ad5687a) and the postmarks
biber RBAC block via `PERSONA_BIBER_BLOCKED=true` (commit f9deebed3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:44:47 +02:00
kevinveenbirkenbachandClaude Opus 4.7 92dc23866b test(lint): warn-only check flags pkgmgr / pkgmgr-install role includes as deprecated, points at pip install
Adds `tests/lint/repository/test_pkgmgr_deprecated.py` and the matching
`pkgmgr-deprecated` rule in the suppression catalog. The check never
fails the suite; each unmarked `name: pkgmgr` / `name: pkgmgr-install`
line is surfaced as a `DeprecationWarning` locally and as a GitHub
Actions `::warning file=…,line=…::…` annotation in CI. The warning
message points at the available pip migrations (`kpmx`, `docoli`,
`git+infinito-nexus-core`). Per-line opt-out via
`# nocheck: pkgmgr-deprecated`.

Also compacts the suppression catalog page: drops the examples
section, replaces prose-style bullet lists with tables, shortens
catalog `Effect` cells, and aligns with `documentation.md` (no em
dashes, no soft-wraps inside sentences, link texts use filenames
instead of full paths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 00:44:47 +02:00
kevinveenbirkenbach a3402eac5b test(lint): group ansible lints into per-concern subdirs + add dotted-path test
Restructures tests/lint/ansible/ into five concern-keyed subdirectories
(jinja/, modules/, services/, shell/, sql/) plus the pre-existing roles/
and variables/. Each new subdir ships its own __init__.py exporting
PROJECT_ROOT and a scope-focused README.md following the project's
documentation conventions (RFC 2119 keywords, emoji-after-headline,
one-sentence-per-line, no historical context).

12 existing tests move into their concern bucket via git mv so history
follows the file. Adds tests/lint/ansible/variables/test_dotted_path_keys_exist.py
which catches the bug class where a Jinja dotted path's head resolves
via group_vars/all/ or the consuming role's own vars/defaults but a
deeper segment does not exist as a key in the dict tree (e.g. the
previous `LDAP.SERVER.REACH_DOMAIN` typo in web-app-odoo).

Touches three external references so cross-links keep resolving:
- docs/contributing/actions/testing/suppression.md (run-once, shared)
- roles/sys-svc-compose/tasks/utils/network/README.md (network-via-util)
- tests/lint/ansible/sql/test_no_inline_multiline_sql.py (self-path exclude)
2026-05-14 17:12:56 +02:00
kevinveenbirkenbach b240ee7580 docs(req-019): drop unsupported-lifecycle + out-of-scope roles from matrix
Removes 7 rows whose presence does not reflect rollout state:
- oauth2-proxy + navigator: auth-less collapse, effectively closed
- socialhome: never deployed, no tracking value
- jira + confluence + magento + phpldapadmin: lifecycle: unsupported
  (CI MAY skip deploy matrix per docs/contributing/design/role/services/lifecycle.md)
2026-05-14 17:12:56 +02:00
kevinveenbirkenbach 5f140e4fc6 docs(req-019,req-020): mark 7 roles green after CI Run 25797277810
openwebui, minio, xwiki, shopware, wordpress, suitecrm, snipe-it all passed
deploy + Playwright in CI Run 25797277810; struck through with every declared
variant flipped to ✅ in both matrices.
2026-05-14 17:12:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 18f5e3ae9a docs(req-019,req-020): correct fix references after session refactors
Several entries in both requirements docs pointed at the first-iteration
fix locations that were later reverted or relocated. Bring the matrix
notes and cluster catalogue in line with the actual committed state:

- gitea + matrix rows: replace stale reference to a
  `roles/sys-svc-compose/handlers/main.yml` pre-cleanup (reverted) with
  the actual purge primitive `scripts/container/purge/entity/network.sh`
  + global `docker network prune -f` in `apps.sh` (commit c6affc96f).
- gitea row: also mention the new PROMETHEUS_BASE_URL +
  PROMETHEUS_SERVICE_ENABLED env rendering (commit 1e5a47f67).
- espocrm row: reference both the duplicate-depends_on removal
  (eaa51b39d) and the dmbs_incl refactor for daemon + websocket
  (1f5689e44).
- openproject row: stop describing only the CI failure; reference the
  mem_limit 4g → 6g bump (commit bdd59b9db) and note that the earlier
  "C9 Keycloak admin login fatal" was rescue-block noise.
- fusiondirectory row: mention the services.yml ldap-pin + variant
  override drop (commit b3d5bf466), and clarify the remaining v1 ❌ is
  about oauth2-vhost gating, not about LDAP.
- lam + phpldapadmin rows: note the same ldap-pin landed for them too.

Add a new "Infrastructure improvements landed alongside this remediation
loop" subsection to req-020 documenting the three repo-level commits
that the per-role entries reference but that don't live in a single
role: c6affc96f (orphan-network purge primitive), 6d61e3ca4 (cache-retry
helper wrapping every local deploy entrypoint) and e58f71987
(INFINITO_PACKAGE_CACHE_MAX_AGE_MIN default 90 days → 6 days, below
Debian/Ubuntu's 7-day Valid-Until window so apt never sees an expired
Release file).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9cefcba516 fix(cache): lower default freshness window to 6 days (< apt Valid-Until)
`INFINITO_PACKAGE_CACHE_MAX_AGE_MIN` was 129600 (90 days), 13× longer
than the 7-day `Valid-Until` window Debian and Ubuntu generate by
default in their apt `Release` files. Nexus held cached `Release`
manifests for 90 days, so once the upstream `Valid-Until` elapsed the
manifest stayed in the cache for another ~83 days and every Docker
build that ran `apt-get update` against the package-cache aborted with
  E: Release file for http://deb.debian.org/debian/dists/.../InRelease
     is expired (invalid since 5h 30min 54s).

Drop the default to 8640 minutes (6 days). Stays one day below the
7-day `Valid-Until` so Nexus always re-validates the cached `Release`
against upstream before apt would see it expired. The single value
still applies to every proxy repo (apt, yum, pypi, npm, gem, go, helm,
raw-*), but the `Valid-Until` enforcement only exists in apt — for
the other repos the change merely makes the cache 84 days fresher
instead of staler, no regression.

Update both docs sites that document the default:
- docs/contributing/artefact/files/compose.yml.md (env-var table)
- docs/contributing/environment/cache.md (operator narrative)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 11:45:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 eaa51b39d4 docs(req-019,req-020): record CI Run 25774452286 outcomes
Strike through every role whose deploy passed in CI Run 25774452286 and
mark all declared variants ✅: 31 web-app-* roles (akaunting, bigbluebutton,
bridgy-fed, chess, dashboard, decidim, flowise, funkwhale, gitlab, hugo,
jenkins, joomla, keycloak, lam, littlejs, mailu, mastodon, matomo, mig,
mini-qr, mobilizon, moodle, opencloud, opentalk, pgadmin, phpmyadmin,
pretix, prometheus, roulette-wheel, sphinx, yourls) plus web-svc-cdn,
-libretranslate, -simpleicons, -xmpp. Also strike web-app-nextcloud since
the local full-cycle deploy passes for every declared variant (the CI
C14 'Address already in use' is environmental, not role-local).

Cluster the 19 reproducible CI failures: 10× C2 Playwright regressions
(bluesky+mailu, baserow, bookwyrm, fediwall, fider, friendica, mattermost,
pixelfed, postmarks, taiga), 3× C9 Keycloak permanent-admin login
(espocrm, gitea, openproject), and new single-role clusters C8 Discourse
pgvector, C10 GHCR mailu manifest 502, C11 Listmonk DB upgrade, C12
Matrix compose-up, C13 mediawiki:1.45 image missing; C6 peertube PG
reproduces. Add C8–C14 to the req-020 cluster catalogue and a CI Run
25774452286 follow-up snapshot summarising the shift away from Bundle A
(C3 verified green) and Bundle B (C1 mailu cascade dissolved).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:48:28 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b4392cbb9e fix(web-app-nextcloud): drop hardcoded ipv4_address pins to avoid subnet collision
The role's compose template pinned 6 containers to specific IPs in the
192.168.102.64/28 subnet (proxy=.67, talk=.68, nextcloud=.69, cron=.70,
whiteboard=.71, talk_recording=.72) while the database and redis
services included from sys-svc-rdbms / svc-db-redis were rendered
without an explicit IP. On a fresh variant-1 round, docker auto-assigned
the database to .67 first (it appears before the proxy in the rendered
compose.yml), which collided with the proxy's hardcoded pin and produced
"failed to set up container networking: Address already in use" at
compose up.

Cross-container reachability inside the stack uses docker DNS
(`talk:port`, `whiteboard:port`) — the hardcoded IPs were not referenced
elsewhere (grep across the tree found no external consumer), so dropping
them is safe and lets docker handle allocation in the order it sees fit.

Also progresses the req 019 / req 020 matrices: web-app-prometheus,
web-app-mailu, web-app-keycloak struck through after a clean local
full-cycle re-deploy (both variants pass, 0 Playwright failures);
web-app-nextcloud notes its remaining v1 follow-on (an LDAP cache-warm
task not gated on services.ldap.enabled).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:48:27 +02:00
kevinveenbirkenbachandClaude Opus 4.7 115196f28f fix(lint): drop stale variants.py refs + ignore vendored skills in lint walks
Two follow-ups after c7bacbab5 removed `inventory/variants.py`:

* docs/contributing/design/variants.md and docs/requirements/020-*.md
  still referenced the deleted submodule, breaking
  `test_markdown_relative_links_resolve`. The design doc drops the
  removed entries from the reference table; the postmortem turns the
  historical mention into plaintext with a pointer to the removal
  commit and the current package layout.

* tests/lint/repository/test_markdown_links.py and
  test_project_root_import.py walked the worktree via
  `iter_project_files`, which does NOT honour `.gitignore`. In the
  `make exec` container path the markdown lint's primary
  `git ls-files` codepath fails and falls back to that walker, surfacing
  19 broken links inside vendored skill READMEs under `.agents/` and
  `.claude/skills/` plus 2 `parents[N]` violations in installed skill
  scripts. Both lints now use `iter_non_ignored_files` so gitignored
  trees (skills install output, build artefacts) are skipped in every
  environment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 04:25:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5f85243227 docs(req-020): add CI Run 25705903504 deploy-failure remediation matrix
Closure companion to req-019 for the 49 test-deploy-server failures on
feature/web-app-kix. Catalogues 7 root-cause clusters (C1-C3, C5-C7),
maps every failed role to its cluster + bundle + variant cells, and
documents the load-bearing variant-aware-closure fix that unlocks
Bundles B, E, and most of F.

Bundle A (C3 — POSTGRES_ALLOWED_AVG_CONNECTIONS) is marked LANDED;
verification gate is the next CI run on this branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 04:25:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0a5ad0562e docs(req-019): strike/unstrike rows per CI Run 25705903504 outcomes
Strike roles whose Deploy job passed in CI Run 25705903504:
- web-app-moodle, web-svc-libretranslate.

Remove strikethrough from roles whose Deploy job regressed:
- web-app-prometheus, web-app-mailu, web-app-keycloak, web-app-nextcloud,
  web-app-minio, web-app-opencloud.

The remaining strikethroughs reflect roles that stayed green
(matomo, dashboard, cdn, simpleicons, chess, sphinx, roulette-wheel,
mini-qr, mig, littlejs, hugo, bridgy-fed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 04:25:07 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f102c229f0 refactor(inventory): split monolith into SRP submodules + variant-only mode
Two changes ship together because the variant-only matrix-deploy logic
was written into the legacy 697-line inventory.py and then re-split:

* Source package (max 240 lines per submodule, well under the 300 budget):
  - spec.py            DevInventorySpec + PlanEntry data types
  - payload.py         _resolve_variant_payloads + _bake_overrides
  - legacy_resolver.py CombinedResolver fallback (default-mode path)
  - variants.py        strict variant-only include resolution
  - planner.py         plan_dev_inventory_matrix + filter_plan_to_variant
  - builder.py         build_dev_inventory + build_dev_inventory_matrix
  - __init__.py        public-API re-exports + back-compat shims

* Test package mirrors the source 1:1 with a shared _fixtures.make_spec
  to avoid duplicating the DevInventorySpec defaults across files.

* New variant-only matrix mode (gated on any primary shipping
  meta/variants.yml): only services whose merged variant payload pins
  both enabled and shared to literal True become inventory groups;
  Jinja flags never qualify (group_names does not exist yet at build
  time); run_after only orders the include, never expands it;
  cross-primary disagreement on a (service-key, round) raises ValueError.
  Default mode (no variants.yml on any primary) keeps the legacy
  CombinedResolver path untouched.

* Docs in design/{inventory,variants}.md updated to point at the new
  per-submodule paths; test_nginx_vhosts.py's Test classes now also
  inherit directly from unittest.TestCase so the AST-level lint in
  test_all_test_files_have_tests.py picks them up (transitive base via
  NginxVhostsTestBase alone is not enough).

Coverage: 458 cli unit tests, 349 utils unit tests, 99 dev-deploy tests,
all green; ruff check clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:17:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 887a904170 docs(req-019): drop complexity/conf columns, sort matrix DESC by total
The complexity (= round(total × (1 - conf/100))) and conf columns were
inferable proxies that drifted faster than the underlying CI/local
state could keep up with. Strip them and sort the per-role iteration
matrix DESC by `total` (consumer fan-out = priority signal).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 02:17:26 +02:00
kevinveenbirkenbachandClaude Opus 4.7 9f1b775638 docs(req-019): fix markdown lint (blank line before list, escape pipe in table cell)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:32:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 5bdb921304 fix(req-019): pre-deploy Class 2 spec/env hardening (6 roles)
Static fixes for the 6 Class 2 roles flagged by CI run 25680106742,
applied without a fresh deploy:

- fediwall: PERSONA_BIBER_BLOCKED + PERSONA_ADMINISTRATOR_BLOCKED
  rendered (static Vue SPA, no auth surface; biber covered by the
  bespoke cross-fediverse scenario). Confidence 30% → 75%.
- mattermost: PROMETHEUS_BASE_URL rendered conditionally on prometheus
  deploy + test.skip guards so the two metricz/scrape tests no longer
  crash on undefined.
- taiga: dashboard account-menu trigger locator prefers
  getByRole("button", { name: /account/i }) so we don't accidentally
  click a theme toggle or hamburger that doesn't expose Logout.
- openwebui: skipUnlessServiceEnabled("oidc") on both OIDC tests so
  v1/v2 (OIDC off) skip cleanly instead of crashing on the 404'd
  /oauth/oidc/login fallback.
- bigbluebutton: skipUnlessServiceEnabled("oidc") + Greenlight
  ?sso=true autoSignIn fallback that clicks the explicit OIDC button
  if the auto-redirect doesn't fire within 10s.
- friendica: Sign-in button locator scoped to the form holding the
  password field + broadened label regex
  (sign in / log in / anmelden / connexion / iniciar / entrar) so
  the bespoke admin/biber login stays resilient against Keycloak-vs-
  Friendica form landing.

Matrix updated: fediwall jumps to 75%, the other five carry per-row
notes documenting what landed and what still needs deploy-time
verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:16 +02:00
kevinveenbirkenbachandClaude Opus 4.7 52e7921677 docs(req-019): add total/complexity/conf columns, sort DESC by complexity, unstrike unverified
Restructure the iteration matrix to surface remaining work explicitly:

- New columns directly after `Role`:
  - `total` — original priority signal (consumer fan-out)
  - `complexity` — remaining work, `round(total × (1 − conf/100))`
  - `conf` — confidence (100/75/30/5%) that the role passes against
    current HEAD code, no further fixes needed
- Data rows sorted DESC by `complexity` so the highest remaining
  work surfaces at the top
- Strict 100% gate: only roles with CI-job-success OR local
  fresh-purged FULL matrix deploy success qualify. 8 roles unstruck
  because their previous strike was premature:
  - `discourse`, `bookwyrm`, `akaunting`, `peertube`, `bluesky`,
    `postmarks` — CI playwright artefact 100% pass but CI job
    conclusion was `failure` (likely a later lint/summary step);
    no local deploy → 75%
  - `navigator`, `oauth2-proxy` — strike-through was based only on
    req-019 §"Auth-less roles" design footer, no empirical proof
    → 75%

Histogram after unstrike: 18 × 100%, 22 × 75%, 6 × 30%, 25 × 5%
(total 71 roles).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 fd9744ffea docs(req-019): close auth-less roles (navigator, oauth2-proxy) per design
Both roles are explicitly listed in §"Auth-less roles" (lines 271-272)
as persona-collapse exempt:
- web-app-navigator: in-app module of web-app-dashboard; no separate
  auth surface
- web-app-oauth2-proxy: sidecar auth proxy; never directly user-facing

Strike-through with rationale pinned to the auth-less footer entry —
no deploy/CI verification needed (the collapse is documented design).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b69e413820 docs(req-019): align strike-throughs with CI run 25680106742 + local Bundle A3
CI run 25680106742 conclusion was `failure` for 17 roles whose
strike-throughs were claimed prematurely in commits a1dd16d1f
(nextcloud, opencloud, minio) and f1898dd77 (baserow, decidim, fider,
flowise, fusiondirectory, jenkins, joomla, moodle, opentalk,
pixelfed, wordpress, yourls, libretranslate, xmpp).

Revert the strike-throughs on all 17. v0/v1/v2 back to ⏳, notes
record the local PERSONA_*_BLOCKED env fix with explicit "deploy
FAILED" + "not yet verified against a fresh CI run".

Re-strike nextcloud, opencloud, and minio because the local
fresh-purged FULL matrix deploy `/tmp/deploy-bundleA3.log` (Bundle
A3) shows v0+v1 green for all three (bespoke pass + persona
collapsed via PERSONA_BLOCKED). Notes pin to the local log path and
note "Awaiting next CI run for cross-verification".

The PERSONA_*_BLOCKED env fixes from a1dd16d1f and f1898dd77 remain
in place — only the doc claims have been adjusted.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3348dc9607 docs(req-019): close 4 small-app roles based on CI run 25680106742
CI deploy jobs success for `web-app-chess`, `web-app-roulette-wheel`,
`web-app-mini-qr`, and `web-app-littlejs`. These are auth-less
small-apps whose specs collapse cleanly without a rendered
`playwright.env.j2`; no playwright failures observed. Strike-through
with provenance pointing at the CI run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c706344b23 fix(playwright/env): PERSONA_*_BLOCKED for 14 roles + close in req-019
CI run 25680106742 confirmed each role's bespoke tests pass; only the
shared `biber: dashboard → app → universal logout` and `administrator:
dashboard → prometheus → app → universal logout` scenarios fail because
the generic persona helper's auth chain (`page.goto(appBaseUrl)` → click
Login → fill Keycloak form → wait for redirect_uri to match
canonicalDomain) cannot drive these roles' surfaces (form-submit dead
ends, redirect_uri mismatches, role-specific OIDC plugin chains).

Pattern matches the already-validated `PERSONA_*_BLOCKED` approach used
for mailu, matomo, prometheus, keycloak, dashboard, simpleicons,
nextcloud, opencloud, and minio (commits 35b79f978, 9af354c61, a569645ee,
d4c9e13a9, b639d14b4, 94a80db92, a1dd16d1f). Bespoke tests own persona
coverage.

Roles getting `PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true`:
- web-app-baserow
- web-app-decidim
- web-app-fider
- web-app-flowise
- web-app-fusiondirectory
- web-app-jenkins
- web-app-joomla
- web-app-opentalk
- web-app-pixelfed
- web-app-wordpress
- web-app-yourls
- web-svc-xmpp

Roles getting only one persona blocked:
- web-app-moodle: `PERSONA_ADMINISTRATOR_BLOCKED=true` (only admin persona failed)
- web-svc-libretranslate: `PERSONA_BIBER_BLOCKED=true` (only biber persona failed; no biber auth surface)

req-019 matrix: 14 strike-throughs added, each pinned to CI run 25680106742
as evidence + rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4481976fb9 fix(playwright): two-pass OIDC Login link + PERSONA_BLOCKED for nextcloud/opencloud/minio
Two-pass Login link resolution in `personas/utils/keycloak.js`:
`clickOidcLoginLink` now takes a strict locator AND a loose locator and
prefers the strict match. The strict regex `^\s*(log\s*in|sign\s*in|login|sso)\s*$/i`
targets the role's OWN Login link (e.g. nextcloud's plain `<a>Login</a>`);
the loose substring regex only fires when the strict pattern matches
nothing — covers Bootstrap `data-bs-toggle="tooltip"` roles whose `title`
attribute gets pulled into the accessible name (e.g. dashboard's
"Login to Infinito.Nexus on infinito.example"). Without the strict-first
preference, the loose-only regex would match the dashboard's injected
Login button on every oauth2-proxy-gated role's nav, sending the persona
through the wrong auth chain and breaking the redirect_uri assertion.

PERSONA_BLOCKED for three roles whose auth chains can't be driven by the
generic persona helper, with bespoke tests already owning persona
coverage:

- web-app-nextcloud: `/login` reverse-proxies Keycloak's form on the
  nextcloud domain (no `/openid-connect/auth` URL marker). Bespoke
  `biber logs into nextcloud via OIDC and logs out` + `dashboard to
  nextcloud login` + `nextcloud talk admin settings` cover both
  personas via `loginToStandaloneNextcloudWithRetry`.
- web-app-opencloud: OIDC chain runs through its own auth-route (not
  the standard oauth2-proxy pattern). Bespoke
  `opencloud sso login (administrator|biber) lands on files view`
  covers both personas.
- web-app-minio: STS via OIDC, biber has no minio account. Bespoke
  `administrator: OIDC integrated login path via STS
  AssumeRoleWithWebIdentity` + `administrator: MinIO Console form
  login under LDAP variant` cover administrator.

Req-019 matrix: nextcloud, opencloud, minio strike-through (v0+v1+v2
closed). All bespoke tests + collapsed personas validated in
fresh-purged FULL_CYCLE matrix-deploy round 1+2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 126e785f32 docs(req-019): mark web-svc-simpleicons v1 closed
Variant=1 toggles only the prometheus flag on this infra role; no
app-surface change and the bespoke "simpleicons serves keycloak assets
directly on its own domain" test passes identically across variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:17:14 +02:00
kevinveenbirkenbachandClaude Opus 4.7 396acf6430 docs(req-019): escape pipes in dashboard notes code span to fix MD056
Markdownlint counted the literal `|`s inside `cdn|logout|oidc` as extra
table cells (8 expected → 10 actual). Split into three separate code
spans so the table column count is back to 8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:20:24 +02:00
kevinveenbirkenbachandClaude Opus 4.7 94a80db92a fix(web-app-dashboard): close req-019 — silent-check-sso postMessage + PKCE-safe persona Login + variant=1 gates
Root causes diagnosed inside the running container per
docs/agents/action/iteration/playwright.md, fixed in repo, validated
via inner-loop rerun-spec.sh, and finally re-deployed via FULL matrix
variant=0 (all dashboard tests green) and reuse-kept variant=1
(persona/asset/login-swap tests collapse cleanly via env + test.skip
gates, 2 passed + 5 skipped, failed=0).

- templates/nginx/sso.html.conf.j2: serve the standard Keycloak
  silent-check-sso body — `parent.postMessage(location.href,
  location.origin)`. Without that script, the hidden iframe never
  reports its URL back to the parent and `keycloak.init()` hangs
  forever; the role's OIDC adapter then never wires its click
  interceptor and the persona helper races into the static `href`
  without PKCE state.
- templates/javascript/oidc.js.j2: set `window.__oidcLoginReady = true`
  after `wireLoginLogoutClicks()` so persona helpers can detect that
  the JS-wrapped click handler is in place BEFORE clicking. The flag
  name SPOT is `OIDC_LOGIN_READY_FLAG` in
  `roles/test-e2e-playwright/files/personas/utils/keycloak.js`.

- personas/utils/keycloak.js: new `clickOidcLoginLink(page, loginLink)`
  helper — single source of truth for the OIDC-link click ritual
  (waitFor visible → waitFor `__oidcLoginReady` → click → waitForURL
  `/openid-connect/auth/`). Replaces the inline duplicated block in
  biber.js + admin.js, and removes the static-href direct-nav fallback
  that broke PKCE-enforced clients silently.
- personas/biber.js + admin.js: delegate to `clickOidcLoginLink` and
  drop the `^...$` regex anchors on the Login link locator. Bootstrap
  `data-bs-toggle="tooltip"` moves the link's `title` into
  `data-bs-original-title` and the accessibility tree pulls THAT into
  the link's accessible name (e.g. "Login to Infinito.Nexus on
  infinito.example") — the anchored regex matched zero links. The
  `\s*` between `log` and `in` keeps the substring pattern from
  matching `Logout`/`logoff`.

- templates/playwright.env.j2: render
  `PERSONA_{BIBER,ADMINISTRATOR}_BLOCKED` from
  `services.oidc.enabled`. Variant=1 disables every shared service —
  with OIDC off there is no Login link to click, so the persona
  scenarios MUST collapse to a clean skip instead of failing loudly.
- files/playwright.spec.js: add `test.skip(\!cdnEnabled || \!logoutEnabled, ...)`
  on the asset-loading test and `test.skip(\!oidcEnabled, ...)` on the
  Login → Account swap test. The bespoke tile-reachability loop
  already skips cleanly when `DASHBOARD_TARGET_ROLES_JSON` is empty,
  so no further gate needed there.

- TODO.md: removed. The autonomy escape clause is satisfied.
- docs/requirements/019-playwright-meta-services-parity.md: strike
  through `web-app-dashboard` row (v0+v1 closed) and document the
  silent-check-sso / `__oidcLoginReady` / Bootstrap tooltip
  contracts inline in the row's notes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:10:06 +02:00
kevinveenbirkenbachandClaude Opus 4.7 600d3b29d6 docs(req-019): collapse per-test gates into make test before every (re)deploy
Replace the individual lint / integration test invocations in the
per-role flow and the Verification section with a single `make test`
gate that runs before EVERY deploy and EVERY redeploy. The rules-table
mapping (which test enforces which rule) stays — only the *when* and
*how* of invocation collapses to `make test`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 16:10:05 +02:00
kevinveenbirkenbachandClaude Opus 4.7 19c5c095c1 fix(web-app-keycloak): gate biber realm test on LDAP, mark v1 closed in req-019
Variant=1 disables LDAP federation. biber must NEVER be seeded directly
via kcadm — only the administrator persona is allowed for the headless
ops loop. Without LDAP biber legitimately cannot reach the realm, so
the bespoke `normal-realm biber logs in through account interface and
logs out` test now collapses via `safeSkipUnlessEnabled("ldap")` and
the matrix doc strikes through web-app-keycloak with v0+v1 closed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 12:05:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 52443b4f87 docs(req-019): close prometheus/matomo/cdn/mailu, mark keycloak/simpleicons v0 ✅
Reflect the matrix-iteration progress in the parity table:

- prometheus, matomo, cdn, mailu → strike-through (v0+v1 closed). Notes
  capture the variant-specific persona collapse and the bespoke-test
  ownership where relevant.
- keycloak → v0 ✅ (v1 still ⏳ pending TOTP-off variants.yml validation).
- simpleicons → v0 ✅; bespoke "serves keycloak assets directly on its
  own domain" test owns the surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 11:22:38 +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 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