Commit Graph
274 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5 eda9474e37 test(lint): refuse a default in os.environ.get, and resolve the 25 sites
The deploy decides what ADAPTER_PORT or OPENWEBUI_ADMIN_NAME is and
writes it into the environment. A default in the reader is a second place
that value lives, and it is the one nobody greps for. os.environ["X"]
fails loudly on a missing key, which is the behaviour a misconfiguration
deserves.

Empty values stay free, because they add no second source and only say
absent: "", None, False, 0, the empty containers, and the "[]"/"{}" a
json.loads reader hands an absent variable. A default that is itself an
environ.get is free too, since it reads the one source again under
another key; a literal ending such a chain is still reported. Detection
runs on the parsed tree, so a call split across lines reads the same.

Five sites were real duplication and lost their default: the adapter
port, openwebui's admin name (a role that runs as a bot user), odoo's
OIDC uid field, seaweedfs' public-primary flag, and the swarm hosts
entry IP. Nineteen carry a nocheck naming why the value is not the
deploy's: contributor tooling whose default is the only source, test
scaffolding that writes the file it names, and two readers of
CA_TRUST_CERT_EXTRA_HOST, which nothing in the repo sets.

utils/diagnostics/container.py resolves without a marker: RESCUE_DEPTH is
a recursion base case, and the integer 0 says what the string "0" said.
The file sits exactly at the 500-line cap, so a comment line was not
available.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-14 00:26:55 +02:00
kevinveenbirkenbachandClaude Opus 5 33504a3b7b Merge branch 'feature/designer' into feature/store-i18n
Resolved 35 conflicts per the designer x store-i18n checklist: store-i18n stays the single point of truth for credentials and role logic, designer's verified design work is kept.

Credential convergers: store-i18n's realign contract wins where both sides have one (mediawiki realign_admin_password.sh over converge_admin.php, pgadmin realign over administrator.yml, matomo utils/converge_superuser.yml over 09_admin_password.yml); the designer-only ones with no counterpart stay.

READMEs: test_readme.py hard-fails a committed Quick Setup or Credits block, both of which designer's side carries, so those go and only the Further Resources heading rename and the gallery links remain. SuiteCRM takes main's session fix, leaving exactly one SUITECRM_SESSION_CONFIG_HOST and dropping the Symfony cache-rebuild task the entrypoint now covers.

Two defects the rename detection hid: git paired store-i18n's web-app-meta against designer's web-app-sphinx, so sphinx's design slots would have been lost and its require(./test-design) landed in web-app-meta, whose spec has no such file. The sphinx design spec and its require now sit in web-app-docs, the role store-i18n renamed sphinx to.

Designer's eleven new Playwright design specs read env with || "" defaults, which test_unguarded_env_default.py refuses because an empty string matches every substring assertion; 35 bindings now go through requireDotenvValue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 17:26:43 +02:00
kevinveenbirkenbach 8c82e5262e Merge branch 'main' into feature/designer
# Conflicts:
#	roles/web-app-suitecrm/files/session.yaml
#	roles/web-app-suitecrm/meta/volumes.yml
2026-10-13 15:27:59 +02:00
kevinveenbirkenbachandClaude Opus 5.5 0709de6fa1 feat(web-app-pgadmin): bring the corporate design to pgAdmin and converge its administrator
templates/style.css.j2 maps page, panes, object tree, tabs and primary
actions onto the design tokens and makes the menu bar the brand frame;
templates/design.js.j2 follows pgAdmin's native theme switch. The logo
and login-logo slots meta/services.yml declares sit on frame chrome, and
with design on templates/env.j2 sets PGADMIN_CONFIG_APP_NAME to the
configured title.

pgAdmin keeps its internal user in its own database, and
PGADMIN_DEFAULT_PASSWORD only seeds the first start. tasks/main.yml now
flushes the compose handlers and, on the stack host outside server
mode, runs tasks/administrator.yml with files/python/administrator.py:
it creates the internal administrator if missing, otherwise realigns
password, active and locked state, failed login attempts and the
Administrator role. The identity is the platform administrator as on
feature/store-i18n: PGADMIN_ADMIN_EMAIL and PGADMIN_ADMIN_PASSWORD in
vars/main.yml, meta/users.yml declares the user, and meta/secrets.yml,
whose only credential the role no longer reads, is gone.
tests/unit/python/roles/web-app-pgadmin/files/test_administrator.py
covers the script. meta/services.yml names the container pgadmin for
PGADMIN_CONTAINER_ADDRESS.

files/playwright/test-design.js checks tokens, that the administrator
signs in with that password and holds admin rights, panes, tree, tabs,
the menu bar frame and its focus indicator, primary actions, the theme
switch, logo, favicon and title, and captures the gallery against the
role's own database server; templates/playwright.env.j2 hands the
platform administrator, the design URLs and the database to it.
README.md links the gallery; TODO.md records the Gravatar request and
the memory pressure at the 256m limit.

The design was verified green on the compose stack (10 passed); the
switch to the platform administrator is checked statically only, the
first live run is the verification on the merged state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:27:27 +02:00
kevinveenbirkenbachandClaude Opus 5.5 9a797a805a feat(web-app-openwebui): style Open WebUI through its custom.css and converge a native administrator
Open WebUI loads /static/custom.css on every page, so tasks/00_core.yml
renders templates/custom.css.j2 when design is on and meta/volumes.yml
mounts it read-only there; it maps the sign-in page, the chat shell,
Markdown, tables and code onto the design tokens and leaves the OLED
dark theme to the user. templates/design.js.j2 mirrors the native theme
setting into data-design-theme. meta/services.yml keeps logo and title
replacement off, so Open WebUI keeps its own branding.

With sso and ldap both off, tasks/utils/admin.yml runs files/python/
administrator.py, which creates the native administrator or resets its
password and promotes it, so the native sign-in has an account; the
identity is lookup('users', 'platform_administrator') as on
feature/store-i18n, declared in meta/users.yml next to the user the
role still reads elsewhere. tests/unit/python/roles/web-app-openwebui/
files/test_administrator.py covers the script with fake values.

files/playwright/test-design.js checks tokens, sign-in page, chat shell,
a stored answer, the native theme setting, the provisioned
administrator, the in-house custom.css, logo and title, and captures
the gallery; files/playwright/_shared.js waits for editable password
fields. README.md links the gallery; TODO.md records the open
self-registration with sso and ldap off and the bot password and
bearer tokens in container exec argv.

The design was verified green on the compose stack (10 passed); the
switch to the platform administrator is checked statically only, the
first live run is the verification on the merged state.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 13:25:44 +02:00
kevinveenbirkenbach de3127683f Merge branch 'feature/designer' into feature/store-i18n 2026-10-13 12:56:06 +02:00
kevinveenbirkenbachandClaude Opus 5 7c13e96c96 Merge branch 'main' into feature/store-i18n
Twelve files conflicted; most resolutions keep both sides rather than pick
one.

cli env.py and compose.py: both branches extend the same spots. The tools
override is appended before write_override, because that call filters the
mirror override to the compose files it is handed. compose.py keeps this
branch's CACHE_ROLE_SHELL_DIR, since the cache scripts moved into a role and
main's literal scripts/docker/cache path is their old home, and takes main's
mirrored init image alongside it.

utils/docker/image/discovery.py: this branch's version_variants expansion
plus main's whole runner-image block, which runner_mirror imports.

sys-svc-compose network create: main's side wins on substance and this
branch's on structure. main carries eda6249d7b, the fix for a probe that
matched an id prefix and let a sibling shadow the exact name; this branch had
refactored the task into create/overlay.yml and create/bridge.yml but brought
back the docker_network_info probe that fix replaced. The subfiles stay and
now probe through the exact-name lookup. main's half also read
SWARM_CLUSTER_AVAILABLE, which this branch removed in favour of
IS_SWARM_MODE, as the merge base confirms, so the gates follow this branch.
The lookup's import follows this branch's get_entity_name to entity_name
rename.

web-app-pixelfed: the custom entrypoints stay removed, since the new image
brings its own and the variables behind them no longer exist, and main's
host_alias=true comes along.

web-app-suitecrm: main's session.yaml and its config/packages path, because
only that version wires framework.session and only that directory is read by
Symfony.

web-app-odoo: main's onionOptions import is required by the test.use below
it; this branch's test-seaweedfs registration stays.

Four defects surfaced that no conflict marker shows.

The auto-merge of web-app-suitecrm/vars/main.yml duplicated
SUITECRM_SESSION_CONFIG_HOST, which ansible-lint rejects. Removing it exposed
the second: SUITECRM_SESSION_CONFIG_DOCKER still named config/session.yaml
while the mount had moved to config/packages, so the freshness comparison in
tasks/01_manager_ops.yml would have tested a path nothing writes.

svc-db-elasticsearch carried environment: on two include_tasks, which Ansible
accepts on include_role only. ELASTIC_PW now sits on the five tasks that read
it, which is also the tighter scope for a credential.

The openclaw templates render NETWORK_TRUSTED_PROXY_CIDRS while their test
renderer passes only its explicit kwargs, so seven tests raised Undefined.
That predates the merge: the template line is on this branch and not on main,
and both sides share the same test file.

test_runner_mirror neutralises four environment variables under clear=False
and missed three, so the gpu and tools overrides leaked in from the container
and the assertion on the layered list broke. It now neutralises all seven.

make quality-high passes both phases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 10:57:22 +02:00
kevinveenbirkenbachandClaude Opus 5.5 5efd82b9c4 feat(web-app-listmonk): carry the design in Listmonk's appearance settings
Listmonk serves its admin and public pages with the custom CSS and JS it
keeps in the settings table, so the role writes the design there:
tasks/03_design.yml, included after the settings reaper in
tasks/02_manager_ops.yml, pipes templates/design/appearance.sql.j2 into
psql. With design on it writes the marked admin stylesheet, admin script
and public stylesheet, but only over an empty value or one that starts
with the role's own marker, so custom CSS an operator wrote stays; with
design off it empties only its own values. The same SQL sets
app.logo_url, app.favicon_url and app.site_name from the design lookup
and restores the former values when design is off; those keys move from
LISTMONK_SETTINGS to LISTMONK_DESIGN_BRANDING in vars/main.yml.

templates/design/admin.css.j2 maps the colors of the Bulma and Buefy
admin onto the design tokens, templates/design/public.css.j2 does the
same for the subscription pages, and templates/design/admin.js.j2 keeps
favicon and title on the configured ones while the single-page app
rewrites its head. meta/services.yml declares the 182x34 logo slot of
the navbar; the emptied files/style.css goes.

files/playwright/test-design.js checks tokens, public pages, the
administration, navigation and tags, the code editors, logo, favicon and
title, that the role writes only its marked settings, and captures the
gallery; the native form takes ADMIN_NATIVE_PASSWORD. The unit test
renders the SQL for design on, design off and design off without a
fallback. README.md links the gallery; TODO.md records the settings task
that always reports changed, the secrets it renders into the shell argv,
and the logo URLs taken unguarded when the logo replacement is off.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 02:01:02 +02:00
kevinveenbirkenbachandClaude Opus 5.5 f363c9d075 feat(web-app-funkwhale): theme the instance and converge its administrator
Funkwhale stores its theme in global preferences, so
files/python/design_preferences.py sets the instance name, primary,
accent and background color, ui__custom_css from
templates/custom.css.j2 and the logo and banner as own uploads. It only
writes while ui__custom_css is empty or starts with the role's marker;
custom CSS an administrator wrote reports FOREIGN and stays untouched,
and design off takes back only the role's own values. tasks/main.yml
flushes the compose handlers and includes tasks/01_manager.yml, and
templates/compose.yml.j2 names the api container so container exec can
reach it.

tasks/01_manager.yml probes the administrator with
files/python/admin_state.py (MISSING, DRIFT or OK for active, staff,
superuser and password), creates it with fw users create --superuser
or realigns password and rights with fw users set, the password always
through the task environment. The account is platform_administrator:
meta/users.yml and the ADMIN_* lines of templates/playwright.env.j2 are
identical to feature/store-i18n (3c72c2002a), so the host
administrator's sudo password never becomes a Funkwhale password and
the later merge keeps these hunks clean. store-i18n provisions no
Funkwhale administrator at all.

templates/design.js.j2 follows Funkwhale's own theme switch, and
files/playwright/test-design.js checks the tokens in both modes, the
instance theming with palette, custom CSS and pod name, the start page
hero and sign-in card, the signed-in interface, a theme picked in
Funkwhale, logo, favicon and title, the platform administrator's sign-in
with administration rights and a visitor's refused sign-up, and captures
the gallery. Unit tests cover
both scripts. TODO.md records the Typesense key in the sidecar's argv
and the icons the front fetches from api.iconify.design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 23:51:09 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d6970cfc58 feat(web-app-bookwyrm): ship the corporate design as a BookWyrm theme
BookWyrm styles its pages from compiled Bulma themes, so the design
arrives as one of them. files/scss/infinito.scss maps the theme onto the
design tokens; the Dockerfile copies it into css/themes/ and
tasks/00_core.yml stages it in the build context and triggers the build.
The old files/style.css overrides are dropped, the theme replaces them.

tasks/01_site.yml reads the generated logos and runs
files/python/site_design.py through manage.py shell, with every value
handed over in the task environment. The script provisions the
administrator as superuser with the admin and moderator groups and
realigns its password, sets the site name and the navigation logo, touch
icon and favicon, registers the corporate theme and makes it the default
only on a fresh site, and closes the setup wizard. With design switched
off it removes only its own theme and logos. It prints BOOKWYRM_RESULT,
which changed_when and failed_when read.

templates/design.js.j2 mirrors the light or dark theme a user picks in
BookWyrm into data-design-theme, meta/csp.yml admits scripts, styles and
fonts from the object store that serves the static files, and
meta/services.yml declares a 100x100 navigation logo slot.
files/playwright/test-design.js checks the tokens in both modes, the
theme's page, primary action, selection and text, the divider, the
closed setup wizard, an upstream theme a user picks, title and logo, and
captures the gallery.

TODO.md records the registration keys BookWyrm 0.9.3 never reads, the
stale reason that blocks the administrator persona, the SeaweedFS test
that only signs in through Keycloak, and the missing static files of the
variant without an object store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 22:30:15 +02:00
kevinveenbirkenbachandClaude Opus 5.5 9364abfe26 feat(web-app-baserow): carry the corporate design and sign the administrator in by e-mail
templates/style.css.j2 maps the colors of Baserow's compiled stylesheet
onto the design tokens, with the vendor pseudo-element lists split so no
browser drops a rule. templates/design.js.j2 sets the favicon, and
meta/services.yml declares a 160x32 logo and a 32x32 symbol slot.
files/playwright/test-design.js checks the tokens in both modes, the
sign-in page, the signed-in interface, the API documentation, logo,
title and the collapsed sidebar symbol, and captures the gallery.

Baserow's sign-in form asks for the e-mail address and authenticates it
against the username. files/python/bootstrap_admin.py therefore stores
the superuser under its normalized e-mail address. It moves a superuser
kept under the username, or found by its first name after the address
changed, onto the current address, gives an adopted account staff and
superuser rights, realigns the password through check_password,
completes the profile and closes the first-administrator signup page.
It prints BOOTSTRAP_STATE, which the task's changed_when reads.

Ported from feature/store-i18n as identical text, so the later merge
keeps these hunks clean: the platform_administrator account in
roles/user-administrator/meta/users.yml as store-i18n has it now
(username platformadmin, administrator role), roles/web-app-baserow/
meta/users.yml and the BASEROW_BOOTSTRAP_ADMIN_* switch (3c72c2002a and
its follow-ups), and the argv form of the migration, bootstrap and MCP
tasks, which hand only the secrets through -e NAME plus environment
(177255329f). The bootstrap task keeps its own changed_when, because
store-i18n's 'created' never matches this script's output; the
password realign of 23e642a3f3 is part of the script.
templates/playwright.env.j2 reads ADMIN_* from platform_administrator,
the README persona note names the e-mail identifier, and TODO.md is
gone with the argv port that resolves it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 22:22:49 +02:00
kevinveenbirkenbachandClaude Opus 5 0dd8dbbb89 fix(web-app-docs): preload the configured languages and wait for them
After a version build the builder queued every translated language, not the
configured ones. The list now reaches the library: server.py reads
DOCS_I18N_PRELOAD_LANGUAGES, Library freezes it into a set that drops the empty
string ''.split(',') yields when nothing is configured, and builder.py queues a
language only when it is in that set. Every other translation is built when a
visitor opens it.

The warm-up task accepted 202, which means queued, so the deploy walked on
while the sites were still building. A second task now waits for 200. Both read
DOCS_I18N_PRELOAD_PATHS, hoisted out of the loop it used to be written inline
in, because two tasks need the same language-code-to-URL-path mapping.

DOCS_BUILD_PARALLEL divides by overhead + 2 * process_mb instead of
overhead + process_mb: with a translation building beside the version build a
parallel slot holds two process-sized allocations. The new divisor is strictly
larger, so the value can only fall, never rise; with the configured 3072 and
2048 MB it differs from the old one only between 10 and 16 GB of service
memory, where it goes from 2 to 1.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-12 22:03:23 +02:00
kevinveenbirkenbachandClaude Opus 5 a289dac85b fix(web-app-docs): give every build subprocess a deadline
Twelve jobs of run 37865281419 failed on the docs language spec:

    tests/test-languages.js:25:7 the deployed working tree is published in de
    with a language switcher (54.6m)
    Error: Expected the de site of the deployed working tree to be built

The spec polls /deployed/de/ until the suite budget runs out, so a build that
hangs is reported as a spec timeout with nothing to read. None of the five
subprocess calls the build makes carried a timeout, so any of them could hold
the queue open forever:

  library.py  _git()     rev-parse, tag --list and remote update --prune
  library.py  fetch()    git clone --mirror
  i18n.py                each generator of generate_commands
  i18n.py                sphinx -b gettext
  builder.py  _run()     the sphinx build of one version

The two git paths reach the network, which is where an unbounded call hangs
without failing.

`_run` streams its child's output into the build state, so it cannot take
`communicate(timeout=)` without losing the progress that `_append_log` and
`_save_state` record, and Popen has no timeout of its own. A threading.Timer
kills the child instead. The kill lands in the existing returncode branch, so
the build is reported through `_failed`, which already writes the log tail into
the state file.

RUN_TIMEOUT_SECONDS is set to catch a hang, not to beat the spec's clock: no
measurement of a successful de build exists, so a tighter value risks turning a
slow build into a failing one. With one, the value belongs below the poll
window the spec scales to.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 22:50:14 +02:00
kevinveenbirkenbach 196c6f707b Merge remote-tracking branch 'origin/update/version-sources-web-app-n8n-20261008-279346d' into feature/store-i18n 2026-10-09 16:34:07 +02:00
kevinveenbirkenbachandClaude Opus 5.5 ad99ded588 fix(web-app-n8n): stop paging when n8n hands back a page it already sent
Both provisioners follow nextCursor for workflows and step skip for API
keys until a short page arrives. A cursor n8n repeats, or a key listing
that ignores skip, would keep that loop running forever and hang the
deploy instead of failing it.

workflows_named now remembers every cursor it followed and exits with
"FAILED listing workflows: nextCursor repeats" when one comes back.
api_keys_labelled exits when a non-empty page brings no key id it has not
already seen. Normal paging is unchanged.

Four regression tests drive both scripts against a fake API that repeats
its cursor or ignores skip; without the guards exactly those four fail.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 10:06:29 +02:00
kevinveenbirkenbachandClaude Opus 5 466c89b2b5 refactor(svc-ai-agent-broker): move the swarm backend into its own module
tests/lint/repository/structure/test_python_file_size.py caps a Python file at 500 lines and asks for a split rather than a raised limit, and engine.py reached 522 once the network claim took a lock. engine.py keeps the engine client, the labels, EngineError and the compose backend at 273 lines; swarm.py takes the swarm backend at 258.

The methods moved verbatim, so nothing changes behaviour. swarm.py imports the labels and the error from engine while engine imports nothing back, which is what the first attempt got wrong: a Pool mixin, following the build_queue.py precedent, would have made the two modules import each other. server.py splits its import, and agents.py reads only labels so it is untouched.

The pool tests follow the code into test_swarm.py, the mirrored path test_mirrored_unit_tests.py asks for, and test_engine.py keeps the transport, connect and compose cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 01:34:05 +02:00
kevinveenbirkenbachandClaude Opus 5 e83cbcdffd fix(svc-ai-agent-broker): give swarm agents a pool of networks the broker already holds
c4ea94c778 read the 404 from POST /networks/<id>/connect as the swarm allocator not having realized the overlay yet, and retried for realize_timeout. 03ed2f1ca1 then recorded the real constraint: a swarm service task cannot be attached to a network it was not created with, so the retry could only burn 131 to 133 seconds per attempt and backend.create was never reached (run 37252453141, job 111587113070).

ensure_pool therefore creates MAX_RUNNING overlays up front and adds the missing ones to its own service spec, which costs one reschedule of this task per deploy and none afterwards. network_for then hands an agent the lowest overlay nobody holds, and gives back the one it holds already; _scale writes that network in the same update that lifts the replica count, so claiming a slot adds no further restart. A pool network carries the broker and at most one agent, so the isolation the README promises holds.

The claim is taken under a lock and remembered until the agent stops, and a service counts as holding its networks from the moment the engine records its desired replica count rather than from the moment a task reports running. Without both, two owners starting their first agent at once are handed the same network and reach each other: four concurrent claims against the unguarded form all returned the first slot. Because the claim and the agent keys live in one process, ensure_pool refuses to start when the broker itself is scaled past one replica.

Compose keeps attaching at runtime, because a bridge network is local and realized at create. connect stays for that path, with its docstring corrected to name which mode calls it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 01:09:56 +02:00
kevinveenbirkenbachandClaude Opus 5 ae482e750b fix(web-app-keycloak): render the username cap instead of raising on it
7916c0177e wrote the cap as {{ IAM_USERNAME_MAX_LENGTH }} inside the {% set _base_attrs %} block. A set body is already a Jinja expression, so the nested delimiters are a syntax error rather than an interpolation, and jinja2 answers TemplateSyntaxError: expected token :, got }. realm.json.j2 includes the component unconditionally, so every realm render raised, not just the username path: that single typo is the whole of the 9 unit and 7 integration failures the gate reported.

The test context gains the value because render() builds its environment with StrictUndefined, which raises on an absent name instead of rendering empty, and a new case asserts the rendered validations.length.max equals it so the cap cannot fall back to the Keycloak default of 255 unnoticed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-09 00:20:29 +02:00
kevinveenbirkenbach 0abc52302e Merge remote-tracking branch 'origin/main' into feature/designer 2026-10-08 23:20:55 +02:00
kevinveenbirkenbachandClaude Opus 5.5 75f024e71a fix(web-app-n8n): page through API keys and workflows when provisioning
n8n 2 returns ten API keys and a hundred workflows per page by default. A
stale managed key behind the first page survived, and a managed workflow
behind it was created a second time. Both scripts now follow every page,
narrowed by label or name, and keep the exact match in the script.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 22:13:28 +02:00
kevinveenbirkenbach cb74bb73ec Merge branch 'main' into feature/store-i18n 2026-10-08 19:52:53 +02:00
kevinveenbirkenbach 9dbda607e0 Merge remote-tracking branch 'origin/main' into update/version-sources-web-app-n8n-20261008-279346d 2026-10-08 19:41:07 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e4078c05fd fix(web-app-n8n): provision the MCP surfaces against the n8n 2 API
n8n 2 pages GET /rest/api-keys as {data: {items, counts, totals, owners}},
ten keys at a time, and matches `label` as a substring, so walking the body
raised "'str' object has no attribute 'get'". It also enforces API key
scopes, so activating the MCP server workflow needs `workflow:activate`.
Both scripts now read every list and create response through one
normalisation that unwraps the page, look the managed key up by label, and
name an unexpected shape by its keys only. The normalisation is taken over
from a392e2dd49 on feature/store-i18n.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-08 19:41:06 +02:00
kevinveenbirkenbach b3563f4ea3 Merge remote-tracking branch 'origin/main' into fix/web-app-prometheus/blackbox-targets/792 2026-10-08 08:57:13 +02:00
kevinveenbirkenbach 7035169500 Merge branch 'main' into feature/store-i18n
# Conflicts:
#	roles/web-app-moodle/files/playwright/test-litellm-gateway.js
#	roles/web-app-moodle/tasks/04_oidc.yml
#	roles/web-app-moodle/templates/nginx-moodle.conf.j2
#	roles/web-app-moodle/vars/main.yml
#	roles/web-app-pixelfed/meta/services.yml
2026-10-08 02:34:58 +02:00
kevinveenbirkenbach fb8dd810c6 Merge remote-tracking branch 'origin/main' into feature/designer 2026-10-07 16:28:26 +02:00
kevinveenbirkenbach e78056db08 Merge pull request #816 from infinito-nexus/update/version-sources-web-app-wordpress-20261005-950916f
update(versions): update pins with a declared source (web-app-wordpress)
2026-10-07 13:35:57 +02:00
kevinveenbirkenbach 9fc68a5477 Merge remote-tracking branch 'origin/main' into update/version-sources-web-app-moodle-20261005-4f455e2 2026-10-07 13:21:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 0c748f74ed fix(web-app-moodle): swap the code tree only after a complete copy
The entrypoint removed the old tree before it copied the new one. A
copy that failed left the volume without a working Moodle, and every
restart removed again instead of recovering. It now copies into a
staging directory inside the volume and swaps the trees once the copy
is complete, so a failed copy leaves the old tree in place.

An image that ships an older release than the volume records no longer
replaces it and exits with a message instead. Two instances on
different releases would otherwise overwrite each other's tree.

upgrade.php reports "No upgrade needed" on stderr, so the task counted
every run on a current site as a change. It reads stderr now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 10:45:29 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d0ec38017a fix(test-e2e-cli): name the negotiated MCP protocol version on requests
MCP Adapter 0.7.0 for WordPress refuses a request that follows
initialize without the MCP-Protocol-Version header: 'Invalid Request:
MCP-Protocol-Version header is required for a 2025-06-18 session'. The
contract probe never sent it, so the CLI test of web-app-wordpress
failed at tools/list.

The probe now keeps the version the server answers initialize with and
sends it on every later request of the streamable HTTP transport, which
the specification asks of a client since 2025-06-18.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:58:03 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d1eee3e26f fix(web-app-moodle): run Moodle 5.3 and upgrade existing sites to it
The pin moved from 4.5.11 to 5.3.0, and the role fitted neither a new
install nor an existing one.

Moodle 5 serves from public/ and routes every other path through
r.php. nginx now uses public/ as its root, hands unknown paths to the
router and passes PATH_INFO for scripts with slash arguments, and
config.php declares the router as configured. The paths the role checks
and writes (version.php, auth, webservice, the OIDC plugin) moved below
public/, the URL of the OIDC plugin stays auth/oidc. The source archive
no longer contains vendor/, so the image installs the Composer
dependencies it declares. The OIDC plugin is pinned to v5.2.1, the
newest release.

An existing site kept its old code: the entrypoint filled the code
volume once and never again. It now records the release it copied and
replaces the tree when the image carries another one, and a new task
runs admin/cli/upgrade.php. Plugins that were installed through the web
interface are not carried over, the README says how to ship them.

A site installed with 5.3 forces login by default, which 4.5 did not,
so its start page answers 303 to the login page and the webserver
health check refused it. The role declares 200 and 303 as the codes
that check accepts: 200 is what an upgraded site keeps answering.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:10:13 +02:00
kevinveenbirkenbach dba49ab0a0 Merge remote-tracking branch 'origin/main' into feature/designer 2026-10-07 00:33:16 +02:00
kevinveenbirkenbachandClaude Opus 5 39d8de790b test(web-app-wordpress): cover the OIDC payload instead of exempting it
Two lint suppressions shipped with the multisite OIDC work. Both are replaced
by the thing they were standing in for.

settings.php claimed an exemption from the mirrored-test rule because every
call it makes belongs to the WordPress runtime. The six functions stub in a
few lines, so the claim does not hold. SettingsTest.php declares WP_CLI and
the globals over a state holder and includes the real payload per case:

  * a single site is written without switching blogs
  * every site of a network is written, switch_to_blog paired with restore
  * a site whose write does not persist is named by blog id
  * every site is still visited before that failure is reported
  * a payload that is not a map halts, and so does an absent one

The fourth case is the one worth having: a `break` on the first failure would
pass the other three and silently stop writing.

Neither existing house pattern fits. `eval` suits the tag-less tinker snippet
of web-app-akaunting, `proc_open` the standalone script of web-app-joomla;
this is a tagged file that needs a runtime, so it is stubs plus `include`,
which also avoids the `nocheck: eval` the akaunting test carries.

The installer test's cache-read exemption rested on a cached read serving a
stale call log. Measured, that risk is confined to the suite and sits around
3e-13: the log lives in a fresh TemporaryDirectory per run, mkdtemp verifies
non-existence, and each path is read once. What complying actually costs is
roughly 25 dead entries in an 8192-slot LRU, so the read goes through
utils.cache.files.read_text.

Proven by `make test-unit-php`: 32 tests, 68 assertions. The first version of
this test passed nothing at all -- it sits one directory deeper than the
other PHP tests, so dirname(__DIR__, 6) resolved to tests/ and the include
found no file. Without the test that would have stayed invisible.

The suite drives stubs, so it proves the payload's control flow and not that
WordPress behaves as stubbed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 22:54:27 +02:00
kevinveenbirkenbach 0f403630d7 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 21:10:40 +02:00
kevinveenbirkenbach bb0fff43a4 Merge remote-tracking branch 'origin/main' into fix/web-app-prometheus/blackbox-targets/792 2026-10-06 21:10:33 +02:00
kevinveenbirkenbach b76cc8e4ad Merge branch 'main' into feature/store-i18n 2026-10-06 21:06:25 +02:00
kevinveenbirkenbachandClaude Opus 5 4ba57c1fce fix(web-app-wordpress): activate plugins across a multisite network
A Multisite subsite served `/wp-login.php` with no SSO entry point even once
the OIDC settings reached every site (e1901fb88e), because the plugin was
never running there:

    wp plugin activate <name> --path=<docroot>

Without `--network` that activates the main site only, so a subsite had the
settings and no plugin. `--network` appears on no branch of this repository,
so the gap was everywhere.

`services.wordpress.multisite.enabled` now resolves once, in vars/main.yml,
and feeds the activation, the multisite conversion gate and playwright.env.j2,
which until now recomputed the same lookup inline. The conversion gate also
drops a `| default(false)` on a key meta/services.yml always declares.

The installer moves out of Ansible into files/shell/install_plugin.sh. Eight
tasks interlocking through register/when/until/failed_when read as one linear
script, and the contract arrives as environment assignments. The container
address stays an inline shell assignment rather than an `environment:` entry
because in swarm it is the literal text `"$(resolve-container-id ...)"`, which
only the shell evaluates; the body streams over stdin because swarm workers
carry no role checkout (03ed52eea5).

Moving the logic across languages would have dropped the WP-CLI 255 contract
to shellcheck coverage, since the suite has no shell tests. The unit suite
therefore now drives the real script against a stubbed `container` wrapper
instead of evaluating the task file's `when:` expressions, which exercises the
behaviour rather than the text describing it.

A mutation pass over that suite -- eight mutations, each required to turn its
own guard red -- found two defects before any deploy:

  * `set -e` is suspended inside an `if` condition, so a probe folded into
    `if needs_install; then` turned an unreadable version into "nothing to
    do". The version read is now a standalone call that aborts with 67.
  * a bare `return` propagates the status of the preceding test, so
    `[ -n "$WP_PINNED_VERSION" ] || return` aborted the script whenever an
    unpinned plugin was already present -- the path every redeploy takes.

The second hid behind a vacuously passing check, which is why the suite gained
exit-code assertions where it previously only counted calls.

This commit carries a fix and a refactor together. The `--network` fix lives
inside the extracted script's activate(), so separating them would mean
reverting the extraction, committing the flag, and reapplying it.

No deploy has run any of this, in compose or in swarm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-06 21:00:44 +02:00
kevinveenbirkenbach 09894d5d79 Merge pull request #838 from infinito-nexus/fix/review-follow-ups/802
fix(review): handle the findings left on #825, #826 and #828
2026-10-06 21:00:36 +02:00
kevinveenbirkenbach 2f2cbd900b Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 19:51:27 +02:00
kevinveenbirkenbach 37461b87ab Merge remote-tracking branch 'origin/main' into fix/web-app-prometheus/blackbox-targets/792 2026-10-06 19:51:20 +02:00
kevinveenbirkenbachandClaude Opus 5.5 a43f15b539 Merge branch 'main' into feature/designer
Conflicts:
- roles/web-app-pihole/meta/services.yml: main dropped the unused redis
  entry, this branch changed the javascript entry directly below it. The
  redis entry is gone and the javascript entry keeps the flag bound to
  the SSO flag of the role and its nocheck reasons.
- README.md: the generated role table keeps the renamed web-svc-design
  role and lists Pi-hole without Redis and without the CDN.

The role diagram of Pi-hole is regenerated, because both sides removed
an edge and the index of the red link moved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 17:09:52 +02:00
kevinveenbirkenbachandClaude Opus 5.5 c73bc0ca85 test(web-app-keycloak): follow every async kcadm result to its reap
The guard required a register on every async kcadm job and nothing more.
A result that is registered but no longer handed to async_reap_jobs,
directly or through the list it is collected into, passed it, although
the reap then neither polls that job nor sees it fail.

The test now follows each registered result to an async_reap_jobs
expression, by variable name with word boundaries, so kc_declared_client
does not pass for kc_declared_client_jobs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:48 +02:00
kevinveenbirkenbachandClaude Opus 5.5 601b21a8f4 test(web-app-penpot): require both service names in the start guard
The stub answered every absolute name, so a guard that asked for the
backend twice and never for the exporter passed both tests. The stub now
records the names it is asked for, and the tests require the backend and
the exporter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:48 +02:00
kevinveenbirkenbachandClaude Opus 5.5 8834cc124a fix(web-svc-collabora): build the WOPI host from Nextcloud's own domain
COLLABORA_WOPI_HOST read the scheme and the port with
lookup('tls', 'web-app-nextcloud', ...) and the host with
lookup('domain', 'web-app-nextcloud'). The tls lookup aligns an app term
to its consumer: for a clearnet Collabora it answers for the clearnet
sibling of an onion-primary Nextcloud, while the domain lookup keeps the
onion name. Such a deployment renders https://<onion>:443, an origin
Nextcloud does not use, so the allowlist does not match its WOPI calls.

Nextcloud pins overwritehost and overwriteprotocol to its primary domain,
so the allowlist now asks the tls lookup for that domain instead of for
the app.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:48 +02:00
kevinveenbirkenbach 415a9cb91a Merge remote-tracking branch 'origin/main' into fix/web-app-prometheus/blackbox-targets/792 2026-10-06 16:40:34 +02:00
kevinveenbirkenbachandClaude Opus 5.5 2a1c5e13ce fix(web-app-prometheus): judge each probe by its own target
Since the targets take their scheme from the domain they probe, two
switches of the http_ready module no longer fit. Both still followed the
TLS state of the Prometheus vhost.

fail_if_not_ssl refused an http target, an onion vhost for example, as
soon as Prometheus itself was served over TLS. It is false now. The
module does not follow redirects, so the scheme of the target URL already
decides the transport.

insecure_skip_verify switched certificate checks off whenever Prometheus
itself had no TLS, also for an https target. Checks now stay on, except in
a self-signed deployment whose CA is not mounted into the Prometheus
containers, where the exporter has nothing to verify against.

The per-role test of the spec parsed every answer of the query API as
JSON. An answer that is not OK now counts as "no probe yet", so the poll
keeps waiting instead of ending on a parse error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:34 +02:00
kevinveenbirkenbach 0e9354f1a0 Merge remote-tracking branch 'origin/main' into fix/svc-net-tor/chutney-node-workers/824 2026-10-06 16:40:29 +02:00
kevinveenbirkenbachandClaude Opus 5.5 4f8c1f00f1 fix(svc-net-tor): fail the worker check when no torrc can be read
The check listed the torrcs without the limit through "grep -L ...; true".
With no torrc under /opt/chutney/net/nodes, or with one that cannot be
read, grep printed nothing, the trailing true turned its error into
success, and the test reported that every node runs one worker.

The check now counts the limit per file with "grep -cH" and reads the exit
status. A status above 1 or an empty answer fails the test, and a file
with a count of 0 is named as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:28 +02:00
kevinveenbirkenbach e714039515 Merge branch 'main' into feature/store-i18n
# Conflicts:
#	README.md
#	roles/web-app-odoo/README.md
#	roles/web-app-odoo/tasks/03_install_modules/module_ops.yml
#	roles/web-app-odoo/templates/playwright.env.j2
#	roles/web-app-pihole/README.md
2026-10-06 14:19:59 +02:00
kevinveenbirkenbach 5d854cc005 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 13:43:32 +02:00