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>
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>
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>
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>
A lockup slot on a page or panel drew its title in the light
--design-primary with a --design-on-primary outline in both color schemes.
With an orange base that is about 3:1 on the light surface, and the SVG
never changed in dark mode; the Sphinx, Jenkins and Jellyfin passes
reported it.
sys-front-inj-design now passes --design-link with a --design-surface-1
outline for light and their dark values for dark. render_assets() hands
the dark pair to every slot without frame: true, and render_svg() adds a
style rule under prefers-color-scheme: dark that overrides the fill and
stroke attributes. Slots on brand chrome keep the frame colors, and the PNG
keeps the light pair. The design_assets action requires the two new
arguments; its two callers, assets.yml and Keycloak's theme task, pass
them.
A unit test asserts the dark rule on page slots and its absence on frame
and logo-only slots; it fails when the dark pair is not handed on. In
headless Chromium an <img> of the SVG renders the title #825200 on #fff5ea
in light and #ffa500 on #140e07 in dark.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The design queue skipped a hand-kept UI_LESS_ROLES list in
cli/meta/roles/design. The decision now lives in each role: a web-app- or
web-svc- role without a UI to design sets design.enabled: false in its
meta/services.yml, and design_disabled() reads that flag. web-svc-design,
the provider, stays out by its entity name.
A new lint, test_design_off_reason.py, requires a design entry in every
web-app- and web-svc- role and, where the entry is switched off, a
"# nocheck: design-off Reason: <text>" on the design: line or the comment
lines directly above it. The rule is in the suppression catalog, and
requirement 037 decision 13 describes it.
Twelve roles switch design off with a reason checked against their code:
asset and simpleicons were already off; file, onlyoffice, bridgy-fed and
mig were on and no longer receive the shared CSS and design script; cdn,
collabora, coturn, legal, mirror and xmpp had no entry. The four that were
on drop their bond and their design variant overrides, which the
disabled-bond and variants-static-override lints reject on a literal false.
The queue lists 77 candidates; HEAD's list also counted bridgy-fed, mig,
collabora and onlyoffice. Cosmos diagrams and the integration matrix are
regenerated.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Provisioning merges into host_vars and never deletes, so the false
enabled/shared toggles that apply_services_disabled writes for a key stayed
in the round inventory after a later run dropped that key from disable=.
That covered the provider's own entry too: with seaweedfs once disabled,
web-app-seaweedfs kept shared: false, the service registry offered no
seaweedfs provider, container_networks attached no consumer, and BookWyrm
ran USE_S3=true against http://seaweedfs:8333 without a route.
reconcile_services_disabled_from_env records the keys of each disable run
in the host_vars key _INFINITO_SERVICES_DISABLED. On the next run it deletes,
in every application block, the enabled/shared toggles that are still
exactly false for a recorded key the current disable no longer lists, drops
a block left empty, and rewrites or removes the record. It runs right after
ensure_host_vars_file, before credentials and the variant bake, so a variant
that wants a toggle off writes it again. With an unchanged or empty disable
it does nothing. An inventory written before this change has no record, so
its older toggles are only reverted after one run with the old list.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GHCR mirror covered role images only. The images the runner pulls on
its own went straight to Docker Hub: coredns from compose.yml, the cache
stack in compose/cache.override.yml and alpine:latest, the init image of
scripts/docker/cache/package-frontend/certs.sh. With the anonymous
githubactions quota spent, the guide step of Postmarks#0 (main run
37954203718) and Shopware#2 (#872) died on "toomanyrequests" while pulling
alpine:latest, after coredns had hit the same limit.
iter_runner_images (utils/docker/image/discovery.py) finds those images,
and the mirror sync and the fork-PR wait now cover them next to the role
images. On a real GitHub runner compose_file_args layers a generated
build/compose/runner-mirror.override.yml that points only the services of
the layered compose files at their mirror, and the certificate generator
gets the mirrored init image. Local runs and act are unchanged, and the
coredns literal stays in compose.yml for Dependabot. test-dns now waits for
images-mirror-missing like the deploy and workspace jobs, since it brings up
the stack too.
Verified with docker compose config on a simulated GitHub runner: the
service set is unchanged and only the runner images point at
ghcr.io/infinito-nexus/core/mirror/docker.io/..., and an existing role mirror
under that scheme pulls anonymously.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`make quality-high` rewrote these files on every branch: the SuiteCRM
README and the root role table lacked the Redis service #881 added, the
session path test was not formatted, and an OpenTalk secret description
from #873 was not wrapped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With three swarm replicas the PHP session files on the shared NFS volume
were read as empty under concurrent requests of one browser, so SuiteCRM
answered 403 on /api/graphql and dropped the session. Sessions now live in
Redis with session locking that waits like flock did; the phpredis version
is pinned in services.yml with its own update source, and the engine lookup
renders the save path.
Refs #879
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ruff format rewraps the read-back line in test_repository.py, and ruff
check reports the nested with in test_fetch.py as SIM117. Both fail
lint-python.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An http_regex source read one document, so a tag whose parts upstream
keeps in two files could only carry the second part as a literal. Every
pin also fetched its document on its own: a failed request left one pin
of a release behind while the others moved.
url now takes a list whose bodies are searched as one text, and every
document is fetched once per run, so all pins that read it resolve against
the same snapshot. The HTTP reads move into utils/update/fetch.py.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The repository updater assigns ref: lines to entities by position: every
entity that pins a value takes the next line carrying that value. A ref
that declares its own source was dropped before it took its line, so a
second entity pinning the same value was given the sourced entity's line,
and an update of it would have rewritten the wrong ref.
A sourced ref now takes its line first and is skipped afterwards, the
same order the nocheck marker already follows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pin that names its own upstream in an update: block was still collected
by the image updater or, for a ref, by the repository updater. Those raise
it to the newest tag of its registry or repository, which for a component
of a release is not the release's value. A tag built from several upstream
values, such as <freeswitch>-<bigbluebutton>, could not be resolved at
all: the resolver takes one capture and keeps a pin on its suffix.
The image updater now skips a version and the repository updater a ref
that declares a source for itself. An http_regex source accepts a template
over the pattern's named groups, and a templated pin moves whenever the
assembled value changes, its suffix included.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pin that names its own upstream in an update: block was still collected
by the image updater, which raises it to the newest tag of its registry.
For a component of a release bundle that tag is not the bundle's, so the
two updaters would propose different versions for the same line and such
a pin had to carry nocheck: docker-version.
The image updater now skips a version that declares a source for itself.
The source resolver alone bumps it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`native_metrics_target` was a Jinja filter, so it could not read
DEPLOYMENT_MODE: filters see no Ansible variables. It returned
`<services.<key>.name>:<port>` unconditionally, which is `matrix-synapse:9000`
for matrix. Compose serves that name; swarm does not, because `docker stack
deploy` ignores `container_name` and the alias never exists. This repository
says as much itself, through `lookup('compose_only', 'container_name', ...)`.
The target was therefore scraped and refused, and `playwright.spec.js:274`
reported `web-app-matrix: 1 matching target(s) reporting up != 1`, meaning one
matching target whose `up` value is not 1.
It becomes a lookup that delegates host resolution to `scrape_target`, which
was already mode-aware, so exactly one plugin knows the difference between the
two modes. The port has to be handed over explicitly: matrix exposes metrics on
9000 while `services.synapse.ports.internal.http` is 8008, so the existing
internal-port lookup cannot serve this case on its own.
`scrape_target` now refuses `port` together with an explicit `port_kind`.
Before, `port` won silently and the error text still named the
`ports.internal.<port_kind>` path that the call had never consulted.
`native_metrics_apps` drops an app that pins itself to compose through
`compose_mode_force` while the cluster runs swarm, because the scrape target
would then name the wrong object. Only the literal case: matrix declares that
override as a Jinja expression over its flavour, which this lookup cannot
resolve from the scraping role's variable scope, so variant 0 on swarm stays a
latent mismatch.
The two comment lines claiming `container_name:port` went with the change;
after it they state the opposite of what the code does.
Verified by mutation: with the swarm branch of `scrape_target` disabled, the
new test emits `matrix-synapse:9000`, the exact string CI reported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A generated lockup drew its title in the primary color with an
on-primary outline. On a header or sidebar that a role maps onto the
frame token, that put primary-colored text on a primary-toned ground.
A slot now takes `frame: true` in the role's design entry. The asset
action receives a second color pair from the palette, the on-frame
color as fill and the frame color as outline, and the renderer picks
the pair per slot. Slots without the flag render as before.
Keycloak calls the asset action directly and already rendered its
brand slot in the on-frame color, so it passes that color for both
pairs and keeps its output.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
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>
A Redis sidecar whose consumer declares no services.redis.mem_limit
runs in a container that inherits the consumer's primary entity limit.
Since the lookup replaced the old filter, maxmemory followed that limit
and rose from 204 MiB to between 819 and 3276 MiB in 17 roles.
lookup('service_memory_mb', ..., provider=<role>) caps the result at
the same share of the provider's own limit when the application does
not declare one for that service. A declared limit is not capped, and a
container smaller than the provider keeps its own share. The Redis
sidecar passes provider='svc-db-redis': those 17 roles get 409 MiB, the
seven with a 256 MiB container stay at 204.
The lookup now rejects unknown options and a provider that is no role
or declares no limit for the service, so a misspelled option fails
instead of silently lifting the cap.
The memcached sidecar's literal -m 64 takes the same lookup and keeps
its value.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
lookup('service_memory_mb', application_id, service_name, share) returns
a share of a service's mem_limit in whole MiB. It resolves the limit the
way sys-svc-container resolves the container's own: the named service,
then the role's primary entity, then RESOURCE_MEM_LIMIT.
It replaces the three filters of memory_filters, each with its own unit
parser and its own idea of the limit: Redis maxmemory at 0.8 of the
limit, and the Confluence and Jira heap at 0.7 and 0.35.
The central Redis (409 MiB), Confluence (5734/2867) and Jira (4300/2150)
keep their values. A Redis sidecar without a mem_limit of its own does
not: the filter fell back to a fixed 256m, which gave 204 MiB, while the
lookup takes the limit that container really runs with, the primary
entity's. That moves 25 roles to between 409 and 3276 MiB.
The heap clamps of the old filters are gone: the 1024 MiB floor, limit
minus 1024, the 12288 cap and Xms bounded by mem_reservation.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>