Commit Graph
1295 Commits
Author SHA1 Message Date
kevinveenbirkenbach fcc2441247 Merge branch 'fix/web-svc-collabora/onion-frame-ancestors' into feature/store-i18n 2026-10-31 19:00:59 +01:00
kevinveenbirkenbach e11e2817df Merge branch 'fix/web-app-docs/builder-timeout-and-prepared-glob' into feature/store-i18n
# Conflicts:
#	roles/web-app-docs/README.md
#	roles/web-app-docs/files/python/infinito_docs/build_queue.py
2026-10-31 19:00:53 +01:00
kevinveenbirkenbachandClaude Opus 5.5 bdc7408357 test(sys-svc-webserver-core): stub IS_SWARM_MODE for the access-log render
The access-log test from main (#1322) rendered nginx.conf.j2 with DEPLOYMENT_MODE, while store-i18n's template reads IS_SWARM_MODE, so every case failed with an undefined variable after the main merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:56:30 +01:00
kevinveenbirkenbachandClaude Opus 5.5 42983ded11 refactor(web-app-weblate): ship the network modules as the weblate_network package
Gate passes 12 and 13 rejected the prefix-grouped weblate_network_*.py modules, the stand_ins.py test helper and unmirrored test paths. The modules are now weblate_network/strategy.py and middleware.py, mounted as a namespace package under /app/infinito and referenced by their dotted package names; the helper is _stand_ins.py and the module tests mirror the package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:55:29 +01:00
kevinveenbirkenbachandClaude Opus 5.5 fa7555d2eb fix(web-app-docs): keep only the three release tags opened last on disk
Publishing tag v1.2.3 globbed prepared/v1.* and deleted every other v1.x tag's prepared tree, including one a parallel slot could be cloning from. With the exact glob each opened tag would keep its ~2.3 GB site and ~1 GB prepared tree forever, which 66 tags and a crawler following the version switcher turn into a full disk. Requests now stamp opened/<tag>, and after every build the builder moves out, under the slot lock, every tag beyond the three opened last that is neither queued nor building; latest and deployed always stay, and a dropped tag rebuilds on its next visit. The builder also clears scratch a killed predecessor left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:49:11 +01:00
kevinveenbirkenbachandClaude Opus 5.5 5d2410f7e9 fix(web-svc-collabora): let every Nextcloud network sibling frame the editor
The onion Nextcloud page frames the clearnet Collabora editor, but the proxy's frame-ancestors only allowed 'self' and *.DOMAIN_PRIMARY, which matches neither the onion host nor its http scheme, so Chrome blocked the iframe and the editor websocket never opened. Collabora now adds every Nextcloud network origin, each with its own scheme, to its frame-ancestors through webserver_csp_whitelist_extra_by_app before the proxy stack renders.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:42:09 +01:00
kevinveenbirkenbach e96dae47c2 Merge branch 'fix/web-app-weblate/onion-handover' into feature/store-i18n 2026-10-31 18:24:09 +01:00
kevinveenbirkenbachandClaude Opus 5.5 54eccfcee6 feat(web-app-weblate): serve clearnet and onion from one instance
Weblate built every absolute URL, the social-auth redirect and the CSRF origin from WEBLATE_SITE_DOMAIN, so the onion sibling either redirected to clearnet or failed the OIDC round trip. A settings override swaps in a network-aware social strategy and proxy middleware that take scheme and host from the request, drops the forced HTTPS redirect the http onion vhost cannot satisfy, and ALLOWED_HOSTS now lists every network sibling plus the container name and loopback. The two modules are separate because social_django loads the strategy at app-ready before defining load_strategy, which weblate.middleware imports back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:23:59 +01:00
kevinveenbirkenbach 1cae39e9e0 Merge branch 'fix/web-app-prometheus/cadvisor-oom' into feature/store-i18n 2026-10-31 18:02:31 +01:00
kevinveenbirkenbachandClaude Opus 5.5 06372544de fix(web-app-prometheus): keep cadvisor's scrape cost flat as containers grow
cadvisor ran with its defaults: every series of every cgroup carried the union of all containers' label keys (97 on echoserver) and every cgroup was re-read each second, so each scrape grew with containers times images and the 0.25-CPU cadvisor was OOM-killed in its 1g cgroup about every 13 minutes. It now stores only the compose and swarm identity labels and keeps housekeeping at 10s, below the 30s scrape interval; limits and metric groups stay unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 18:01:14 +01:00
kevinveenbirkenbach c8eb0d319a Merge remote-tracking branch 'origin/main' into feature/store-i18n
# Conflicts:
#	roles/sys-svc-webserver-core/README.md
2026-10-31 17:50:28 +01:00
kevinveenbirkenbachandClaude Opus 5.5 43bd6583e6 test(web-app-docs): reuse StopBuilderError in the timeout loop test and merge its nested with
Gate pass 9 on the merged store-i18n stopped at ruff N818 on the local Stop exception that the builder-timeout test defined next to the module's StopBuilderError from the build-before-clone tests, plus SIM117 on the nested with in the healthy-mirror test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:44:51 +01:00
kevinveenbirkenbach 69e0d967a0 Merge branch 'feature/swarm-inventory-extra-vars' into feature/store-i18n
# Conflicts:
#	Makefile
2026-10-31 17:19:25 +01:00
kevinveenbirkenbach afaf4eb659 Merge branch 'perf/ci/deploy-skips-unused-local-build' into feature/store-i18n 2026-10-31 17:18:08 +01:00
kevinveenbirkenbach 373330d040 Merge branch 'fix/web-app-docs/builder-timeout-and-prepared-glob' into feature/store-i18n
# Conflicts:
#	tests/unit/python/roles/web-app-docs/files/infinito_docs/test_build_queue.py
2026-10-31 17:18:02 +01:00
kevinveenbirkenbachandClaude Opus 5.5 1e804115fc fix(web-app-docs): keep the builder alive through git timeouts and never wipe a healthy mirror
fetch() leaked subprocess.TimeoutExpired, which ended the builder thread while it kept the builder lock, so no replica built again. A clone killed at its timeout also left a HEAD that never resolves, and the HEAD-file check ran remote update on it forever. The loop and refs() now catch SubprocessError, fetch() re-clones when HEAD does not resolve, and the clone lands in repo.git.partial and replaces the mirror only after it succeeds.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:07:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 50b4fa38b2 perf(ci): skip the local stack image build the deploy jobs never run
On real GitHub Actions INFINITO_BUILD passed through from default.env as 1, so every deploy job built the infinito image locally (~5 min) and then pulled and ran the published ci image anyway (pull_policy always). An env handler now defaults INFINITO_BUILD to 0 there; act and jobs that set it themselves keep building.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 17:03:37 +01:00
kevinveenbirkenbach 620fafa6d1 feat(swarm): pass INFINITO_INVENTORY_EXTRA_VARS into the swarm-zombie inventory 2026-10-31 16:38:30 +01:00
kevinveenbirkenbachandClaude Opus 5.5 9f36a7ac6b perf(web-app-docs): build the deployed snapshot while the first mirror clone runs
run_builder fetched the git mirror before it claimed any build, so the deployed site, which copies the snapshot baked into the image and reads no mirror, waited for the first git clone --mirror (about 60-150 s on CI, 967 MiB with 1113 refs/pull/* heads). The builder now claims and submits DEPLOYED inside the pool before the first fetch; latest and the tags still wait for it, so none builds from an outdated head. The README no longer claims the fetch waits for a running build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 15:59:25 +01:00
kevinveenbirkenbachandClaude Opus 5.5 4592f2bf78 test(web-app-taiga): pass a neutral config-append marker in the static URL test
test_used_keys_declared reads every INFINITO_* literal in code as an environment key, so the marker string copied from vars/main.yml failed the lint without a registry entry. The marker only lands in the append's header comment, which the test does not inspect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 14:07:46 +01:00
kevinveenbirkenbach c976f51504 Merge branch 'fix/web-app-weblate/swarm-static-mountpoint' into feature/store-i18n 2026-10-31 13:27:52 +01:00
kevinveenbirkenbach d5a8997ff4 Merge branch 'fix/web-app-taiga/onion-design' into feature/store-i18n 2026-10-31 13:27:46 +01:00
kevinveenbirkenbach 5cb5acae6d Merge branch 'fix/lint/resource-budget-honours-pins' into feature/store-i18n 2026-10-31 13:27:42 +01:00
kevinveenbirkenbach ffcbd49883 Merge branch 'fix/web-app-docs/languages-404-duration' into feature/store-i18n 2026-10-31 13:27:39 +01:00
kevinveenbirkenbach 65bd855e24 Merge branch 'fix/prod/galaxyserver-branding-stall' into feature/store-i18n 2026-10-31 13:27:36 +01:00
kevinveenbirkenbach 394d58db85 Merge branch 'fix/prod/echoserver-ldap-users' into feature/store-i18n 2026-10-31 13:27:32 +01:00
kevinveenbirkenbach bb8c5236f7 Merge remote-tracking branch 'origin/main' into feature/store-i18n 2026-10-31 13:27:13 +01:00
kevinveenbirkenbachandClaude Opus 5.5 880eeeac5e fix(web-app-taiga): serve Django admin static files from the vhost's own origin
taiga-back's image settings build STATIC_URL from TAIGA_URL, the canonical clearnet domain, so the Django admin on the onion vhost linked base.css and its scripts to the clearnet host and the onion CSP blocked them (header background rgba(0, 0, 0, 0) in the design spec). STATIC_URL is now the same-origin path below FORCE_SCRIPT_NAME. The Jinja static() global of the e-mail engine keeps the canonical absolute URL, because a mail client resolves no same-origin path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 13:08:17 +01:00
kevinveenbirkenbachandClaude Opus 5.5 8d6040948c fix(sys-svc-webserver-core): match token routes on the decoded path
The access-log map read $request_uri, which keeps percent-encoding and
repeated slashes. A request such as /%70assword/reset/<token>,
/password%2Freset/<token> or //password//reset/<token> missed the
token-route expression, fell through to the plain-path branch and was
logged with the token in plain text, while the backend still decoded it
to its reset handler.

The map now reads $uri: decoded, slash-merged (merge_slashes is not
overridden anywhere) and without the query string, so the existing
expressions match every spelling of a token route. Trade-off: after an
internal redirect the log shows the internal URI; with the generic
vhost's error_page (ws_generic.conf.j2) a 5xx line now carries
/500.html instead of the client's path, with the status unchanged.
Nothing in the repository parses the proxy access log.

The unit test resolves the map against the variable it reads, modelling
$uri as nginx builds it, and adds the three encoded spellings above.
Negative control: with the map back on $request_uri, exactly those three
cases fail (3 failed, 8 passed).

Reported by CodeRabbit on #1322.

Part of #1192

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:46:31 +01:00
kevinveenbirkenbachandClaude Opus 5.5 76fc6992c4 fix(variants): apply a variant pin only while its carrier is in the round
Pins of every role applied at every round index, so a role the round did not deploy could pull a partner into a heavier variant (web-app-discourse#1 measured 35.46 GB against the 32 GB reservation cap). resolve_round now starts every role on its clamped index and recomputes the include and the pins in force until the assignment settles, raising VariantPinError on a cycle. The planner resolves each round once (39.1 s to 16.0 s for all rounds), affected judges every (role, index) with the round the planner deploys for it, and the resource budget lint measures the pin-aware rounds. The stale web-app-matomo pin above the primary-only docs entry is dropped, and test_variant_pin_fixed_point.py guards every planned round against unsettled pins and pins above a disabled entry. docs/contributing/artefact/files/role/variants.md explains the settle loop with diagrams.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 12:45:53 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f0a70b8164 fix(web-app-docs): answer 404 for an untranslated language before the deployed build
The server answered 202 for every /deployed/<code>/ path until the deployed Sphinx build finished (about 80 min with -j1), so test-languages.js:61 outwaited its poll budget on attempt one and passed only on retry. The untranslated decision now reads the snapshot's meta/languages.yml and docs catalogs, the same files the build writes into the language index, so the 404 set is identical before and after the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:58:23 +01:00
kevinveenbirkenbachandClaude Opus 5.5 49387850dc fix(deploy): check the deploy extra before running ansible-playbook
The dedicated deploy launched whatever ansible-playbook PATH found. A stale pkgmgr venv without Pillow crashed the galaxyserver deploy after 4h23m at the first branding task. The runner now verifies every distribution of the pyproject deploy extra in the running interpreter before make setup, exits naming the missing ones with the install command, and runs the ansible-playbook next to that interpreter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:56:54 +01:00
kevinveenbirkenbachandClaude Opus 5.5 299aa8e707 fix(svc-db-openldap): name the user whose record lacks LDAP entry fields
A user key that no role declares, such as an inventory leftover, gets no username, firstname, lastname, uid or gid derived. openldap_user_entries then crashed with a bare KeyError 'username'. The filter now raises an AnsibleFilterError that names the user and the missing fields.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 11:56:12 +01:00
kevinveenbirkenbach dbcc78e376 Merge remote-tracking branch 'origin/main' into fix/sys-svc-webserver-core/access-log-no-tokens/1192 2026-10-31 10:40:44 +01:00
kevinveenbirkenbachandClaude Opus 5.5 10d2b11cc9 fix(sys-svc-webserver-core): keep URL tokens out of the openresty access log
The front proxy logged the full request line: production had no
access_log, so nginx fell back to `combined` ($request, Referer,
$remote_user), and the debug format logged $request and the Referer.
Query-string tokens (?access_token=, ?key=, ?token=) and token routes
such as /password/reset/{token} and /i/confirm-email/{user}/{random}
ended up in plain text in the container log.

nginx.conf.j2 now declares a map from $request_uri to
$infinito_access_log_path:

- the path is written without the query string;
- the segments after a known token route (password/reset,
  password-reset, reset-password, confirm-email,
  lostpassword/reset/form, email-login, activate-account; matched
  case-insensitively at any depth) are written as [redacted], so the
  request, its status and timings stay visible without the token.

Both modes write an explicit access_log to /dev/stdout:

- production uses the new `redacted` format, laid out like `combined`
  (remote address, time, "method path protocol", status, bytes,
  user agent) with Referer and basic-auth user written as "-", so
  combined-style parsers keep working;
- debug keeps its fields (host, timings, upstream time, lengths,
  X-Forwarded-For, scheme, server name) but logs
  "method path protocol" instead of $request and drops the Referer.

The path comes from $request_uri instead of $uri so internal rewrites
(error_page, try_files) do not replace the path the client requested.
Nothing in the repository parses the proxy access log (Wazuh, the
health checks and the Prometheus log_by_lua metrics do not read it),
so no consumer needs to change. The error log still carries
`request:` and `referrer:` on error-level entries; that is outside
this change.

A unit test renders nginx.conf.j2 in both modes and asserts that every
access_log uses a format declared there, that no format references
$request, $request_uri, $args, $query_string, $is_args, $arg_*,
$http_referer or $remote_user, and, by resolving the map as nginx does,
that query strings are dropped, token routes are redacted and
lookalike paths stay unchanged.

Verified live with web-svc-html in production mode (deploy failed=0, so
openresty accepted the config). A probe request to
/password/reset/<probe>?token=<probe> was logged as
"GET /password/reset/[redacted] HTTP/1.1" 404 62 "-" "curl/8.14.1",
and /plain/page as "GET /plain/page HTTP/1.1". Negative control: the
same request against the previous config logged the probe path token
and the query token in plain text. The debug format with the fix is
covered by the unit test only.

Closes #1192

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:40:40 +01:00
kevinveenbirkenbach 2f2b004b1c Merge branch 'triage/nextcloud0-conductor-design' into feature/store-i18n 2026-10-31 10:05:41 +01:00
kevinveenbirkenbachandClaude Opus 5.5 815714592c fix(web-svc-logout): keep each conductor's sweep on its own network
CI run 41147980556 Nextcloud#0 failed both conductor design specs on a
multi-network node. e0179a0893 handed every conductor the other
network's conductor in LOGOUT_SIBLINGS. With that list set,
universal-logout v1.5.0 hides #iframe-warning on the stand-alone page
("'#iframe-warning' matches nothing visible"), and v1.5.1 fetches the
sibling after its sweep, which the conductor's per-network CSP blocks
("Connecting to 'https://logout.main.infinito.test/' violates ...
connect-src 'self' *.<onion>"). Requirement 037 limits each vhost's CSP
to its own network, so the browser hand-off could never complete in
either direction; its only effects were the two red specs.

Drop the LOGOUT_SIBLINGS wiring and its README bullet, so the compose
template and README match their state before e0179a0893. Each conductor
still logs out of every domain of its own network; ending the sessions
of the other network belongs on the server side, not in the browser.
The test now asserts that no conductor on a single-network, tor-only or
multi-network node is handed a conductor of another network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:05:29 +01:00
kevinveenbirkenbach e3e99f9fd5 Merge branch 'fix/svc-opt-swapfile/keep-active-swapfile' into feature/store-i18n 2026-10-31 10:03:27 +01:00
kevinveenbirkenbachandClaude Opus 5.5 0590104ce2 fix(svc-opt-swapfile): keep an active swapfile that already meets the requested size
A production redeploy of echoserver hung at the swap-forge task and the
host stopped answering SSH. swap-forge v1.0.1 recreates the swapfile
whenever its size differs from the requested MiB by any amount, starting
with swapoff on the live file. The request is the host's MemTotal, which
drifts by a few MiB across kernels, so a routine deploy paged the whole
swap back in, charged it to the containers' memory cgroups and starved
sshd until the OOM killer took swapoff.

Read the active swap files with swapon before swap-forge runs and skip
it when an active file is within 5 % of the requested size, reporting
the kept size against the request. The sizing formula is unchanged, so
no host is forced into a one-time recreate; a deliberate resize beyond
the tolerance still reaches swap-forge.

The unit test resolves the real vars through Ansible's Templar for MiB
drift both ways, no swap, zram and partitions, a halved size, an exact
pin, a lowercase unit and an escaped file name, and shows that the exact
MiB comparison swap-forge uses misses the drift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 10:03:15 +01:00
kevinveenbirkenbachandClaude Opus 5.5 79e88c5ea4 fix(mirror): refresh a mirrored tag that upstream re-pushed
Fork CI run 41147980556 pgAdmin#0 failed in sys-ctl-hlth-runtime with
"stalwart_webmail: PHP 8.4.26 is below the security floor 8.4.27".
The deploy ran the fork's GHCR mirror of roundcube/roundcubemail:1.7.5-
apache, copied on 10-19 with PHP 8.4.26, while upstream re-pushed the
same tag on 10-27 with PHP 8.4.27. Branch CI runs the sync with
--only-missing, which skipped the tag because it existed ("destination
exists, skipping") without comparing digests, and the full refresh only
runs on the fork's default branch, which still pins 1.7.4.

- utils/docker/registry.py: manifest_digest() reads the
  Docker-Content-Digest of a reference with a HEAD request, which does
  not count against Docker Hub's pull allowance.
- GHCRProvider.is_stale() reports a mirrored tag as stale only when both
  registries answer with a digest and the two differ.
- Under --only-missing an existing tag is re-copied when it is stale; a
  failed re-copy is reported on stderr with the image and keeps the
  existing copy, so one refresh cannot block every deploy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 09:50:14 +01:00
kevinveenbirkenbach c37190495b Merge remote-tracking branch 'origin/main' into fix/svc-prx-openresty/worker-connections/1300 2026-10-31 03:00:09 +01:00
kevinveenbirkenbachandClaude Opus 5.5 f52da1c875 fix(svc-prx-openresty): size worker_connections independently of pids_limit
The shared proxy's nginx.conf derived worker_connections from the
openresty container's pids_limit. pids_limit caps processes and threads,
worker_connections caps simultaneous sockets per worker (client and
upstream), so with pids_limit 256 and one worker the whole host's proxy
held about 256 sockets, roughly 128 proxied requests in flight across
every vhost. Raising pids_limit to buy connections would loosen the
fork-bomb guard.

svc-prx-openresty now declares services.openresty.worker_connections
(4096 per worker, the common nginx production range, enough for several
chunk-heavy apps such as OpenWebUI at ~150-170 requests per page load).
WEBSERVER_PROXY_WORKER_CONNECTIONS reads it through lookup('config'),
without a default, so a missing key fails loudly. pids_limit stays 256.

WEBSERVER_PROXY_WORKER_RLIMIT_NOFILE is derived as 2x worker_connections
(8192), one descriptor for each client and upstream socket of a proxied
request, and nginx.conf renders it as worker_rlimit_nofile. The openresty
container pins its nofile ulimit (soft and hard) to the same value:
workers call setrlimit as root without CAP_SYS_RESOURCE, so they can only
lower the hard limit, and the inherited hard limit depends on the host
daemon (Docker >= 25 defaults to 1024:524288, DiD nests whatever the
outer container has). An explicit ulimit makes the rlimit succeed on
every host instead of logging a setrlimit alert.

The unit test pins worker_connections 4096, worker_rlimit_nofile 8192 and
the container ulimit, and fails when worker_connections follows
pids_limit again.

Live proof (compose deploy of web-svc-html, both plays failed=0): the
deployed nginx.conf renders worker_rlimit_nofile 8192 and
worker_connections 4096; openresty's master, worker and cache manager
run with "Max open files 8192 8192"; the openresty log carries no
setrlimit or "worker_connections are not enough" line.

Closes #1300

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 03:00:03 +01:00
kevinveenbirkenbachandClaude Opus 5.5 9aa56b71e9 test(lookup): restore sys.modules after loading the sso plugins fresh
test_sso.py, test_sso_oidc_plugin.py and test_sso_proxy_consumers.py
evict utils, utils.cache, utils.cache.applications and the roles
packages from sys.modules to import their plugin against fresh modules,
and never put the originals back. Ansible's plugin loader keeps the
applications lookup it loaded earlier in sys.modules, so that lookup
stayed bound to the evicted utils.cache.applications, whose
_MERGED_APPLICATIONS_CACHE utils.cache._reset_cache_for_tests() no
longer reaches.

On main this turned test_webserver_worker_limits red whenever one xdist
worker ran a test that loads the applications lookup through Ansible,
then one of these sso tests, then the worker-limits test: the stale
cache served the cpus 0.5 payload of test_a_sub_cpu_proxy_still_runs_one_worker,
so the later methods rendered worker_processes 1 instead of 4 (main run
41139196554, Workspace centos and fedora, second make test on gw1).

_load_module now runs inside mock.patch.dict(sys.modules): the plugin
keeps the fresh modules it was built against, and sys.modules returns to
its prior state afterwards. This is the change store-i18n carries since
ec0fde6561, ported unchanged so both lines stay identical.

Evidence:
- Before: pytest -n 0 test_tor_ports.py test_sso_oidc_plugin.py
  test_webserver_worker_limits.py -> "3 failed, 33 passed", the CI
  failure set ('1' != '4' twice, worker_processes 1 vs 4).
- After: test_tor_ports.py, the four sso test files and
  test_webserver_worker_limits.py -> "68 passed".

(cherry picked from commit ec0fde6561)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:17:21 +01:00
kevinveenbirkenbachandClaude Opus 5.5 ae40a573c2 fix(web-app-weblate): give the static tmpfs mountpoint its mode on swarm NFS volumes
CI run 40979252521 Weblate#1 (swarm) never converged: every task after
the first died in collectstatic with "PermissionError: [Errno 13]
Permission denied: '/app/cache/static/weblate_fonts'". The tmpfs at
/app/cache/static sits inside the shared weblate_cache volume; the first
task's runc creates the mountpoint as root 0755, and runc gives a tmpfs
on an existing mountpoint that directory's mode. 99a7d28067 declared
mode 1777, which compose honours, but docker stack deploy keeps only the
tmpfs size (docker/cli 29.9.2 convert/volume.go), so swarm still got a
root 0755 tmpfs.

- utils/storage/nfs.py: nested_tmpfs_mountpoints() derives every tmpfs
  mountpoint whose innermost enclosing volume of the same service is
  NFS-backed, with its declared mode; a nested tmpfs without a mode, or
  one mountpoint declared with two modes, raises.
- plugins/lookup/nfs_tmpfs_mountpoints.py feeds it the rendered compose
  volumes, so compose_volumes stays the source of which volumes live on
  NFS.
- sys-svc-compose nfs_prep.yml creates those mountpoints on the NFS
  server with the declared mode before every stack deploy, which also
  repairs a mountpoint an earlier deploy left at 0755.
- web-app-weblate: the cache volume declares nfs uid 1000, gid 0,
  mode 0770; a pre-created mountpoint makes the volume non-empty, so
  Docker no longer copies the image's /app/cache root in.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 02:05:59 +01:00
kevinveenbirkenbachandClaude Opus 5.5 043d81f78e test(web-app-keycloak): stub the entity_name filter under its current name
main's admin allowlist test renders sys-svc-proxy's location/html.conf.j2,
which calls the entity_name Jinja filter. The test stubbed it as
get_entity_name, the name store-i18n retired when it registered the
filter as entity_name (plugins/filter/get/entity_name.py), so all eleven
cases failed with "No filter named 'entity_name'" after the main merge.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:54:06 +01:00
kevinveenbirkenbach 3504f17557 Merge remote-tracking branch 'origin/main' into feature/store-i18n
# Conflicts:
#	.github/workflows/call-test-deploy.yml
#	roles/sys-svc-compose/tasks/main.yml
#	roles/web-app-keycloak/TODO.md
#	utils/docker/registry.py
2026-10-30 23:36:38 +01:00
kevinveenbirkenbach 166da8f8d2 Merge branch 'feature/shared-stack-caches' into feature/store-i18n 2026-10-30 23:31:00 +01:00
kevinveenbirkenbach b17fc884ba Merge branch 'triage/cursor-deb-hold' into feature/store-i18n 2026-10-30 23:30:57 +01:00
kevinveenbirkenbach 7d8feb6386 Merge branch 'fix/svc-ai-ollama/cache-probe-path' into feature/store-i18n 2026-10-30 23:30:50 +01:00
kevinveenbirkenbach b97f38ca49 Merge pull request #1261 from infinito-nexus/fix/web-app-keycloak/admin-network-allowlist/1121
feat(web-app-keycloak): serve /admin only to declared source networks
2026-10-30 23:15:41 +01:00