Commit Graph
5 Commits
Author SHA1 Message Date
kevinveenbirkenbach 15930945fe Merge branch 'main' into feature/store-i18n 2026-10-19 01:54:37 +02:00
kevinveenbirkenbachandClaude Opus 5.5 2ceba492b8 fix(web-app-zammad): render the search precheck service in compose only
In swarm, docker stack deploy ignores depends_on, so the precheck
service and every init replica ran search_allocation_precheck.sh at the
same time. They shared one probe index: one invocation's PUT could fail
the shards_acknowledged check because another had created the index
first, and either one's cleanup could delete the other's probe. The
init service carries no replica override, so compose_replicas gives it
one task per host in the application group and the collision also
happens between init replicas.

A failing precheck task also never reached has_fatal_task in
stack_ready.sh: with restart_policy none it records a single error,
while has_fatal_task needs three. stack_ready.sh then kept answering
"not converged" until the handler ran out of retries. And a Running
init task counts as converged through running_current_spec, so the
swarm deploy never depended on init failing; the role's "wait for
Zammad init" task is what stops it.

The probe index now carries the container's hostname, which docker
sets to the container id, so concurrent invocations no longer share an
index. compose.yml.j2 renders the precheck service, and init's
service_completed_successfully dependency on it, only outside swarm.
Compose keeps its fast stop on a refusal; in swarm, init runs the same
check in front of zammad-init, a refusal leaves Zammad unseeded, the
wait for Zammad init fails the deploy, and the init service logs carry
the allocation explanation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 12:33:54 +02:00
kevinveenbirkenbachandClaude Opus 5.5 fcf1cca129 fix(web-app-zammad): stop the deploy when Elasticsearch allocates no shard
On a data disk with less free space than the high disk watermark asks
for, Elasticsearch creates indices but assigns none of their primary
shards. `zammad-init` then reports every search index update as failed
and never exits, so `compose up` waits on
`service_completed_successfully` until its 1800 s task timeout and the
deploy fails without naming the cause (#850). Elasticsearch's default
high watermark is an absolute 150 GB headroom on a large disk.

A new one-shot `precheck` service runs `search_allocation_precheck.sh`
with `restart: "no"`, and `init` depends on it with
`service_completed_successfully`:
- it waits a bounded time for `_cluster/health`;
- it creates a one-shard, zero-replica probe index with
  `wait_for_active_shards=1`;
- once Elasticsearch allocates it, it deletes the probe and exits 0;
- otherwise it prints the creation answer and the answer of
  `_cluster/allocation/explain` and exits 1.

The check sits in its own service because `init` keeps
`restart: on-failure`: wrapped into `init`, the failing check was
restarted seven times within eight minutes on a local deploy while
`compose up` kept waiting. Inside the stack, a `restart: "no"`
dependency that exits 1 made `compose up` fail after one second with
`service "pre" didn't complete successfully: exit 1`.

Every curl call carries --connect-timeout and --max-time, so a hanging
request cannot outlast the bounded wait. The image's entrypoint passes
any non-`zammad-*` command through `exec "$@"` (bin/docker-entrypoint of
7.1.2), and the image ships `curl`. The watermark values themselves come
with feature/store-i18n (b82591ba9d); this change covers inventories
without a variant cap.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-18 12:33:53 +02:00
kevinveenbirkenbachandClaude Opus 5 1cea2f9ffe fix(web-app-zammad): mirror the RBAC group into Zammad roles on every OIDC login
Under SSO the platform administrator signed in and landed on #profile as a
Customer, so the design spec never reached #manage/users. Three defects
stacked.

The group-to-role map was written with Jinja in its mapping keys. Ansible
renders a mapping's values and leaves its keys alone, so the rendered Ruby
keys were literal {{ ... }} and could never match a group path. The map is now
one expression that computes its keys through rbac_group_path, the shape
meta/addons already uses elsewhere.

The initializer patched User.create_from_hash and User.update_from_hash behind
`next unless User.respond_to?`. Zammad 7.1.2 defines only create_from_hash!,
so both guards fell through in silence and nothing was installed: the
initializer's name does not appear once in the failing run's Rails log. Patch
Authorization.find_from_hash and Authorization.create_from_hash instead, which
sessions_controller calls before current_user_set, so first and subsequent
logins are both mirrored.

administrator mapped to Admin alone. Zammad's Admin carries no ticket
permission, and the Dashboard navbar entry is gated on ticket.agent, so an
Admin-only account has neither Dashboard nor Overviews. Grant Admin and Agent,
matching what the password path converges.

The write is defended rather than assumed. Zammad's has_roles concern runs
validate_roles, validate_agent_limit_by_role and last_admin_check_by_role on
collection assignment and they raise, which inside the omniauth callback would
answer the sign-in with 500 - a lockout, worse than the demotion being fixed.
The assignment is wrapped, resets the association and logs on refusal, and the
sign-in completes on the roles the account already has. Role lookup filters on
active and reports a missing role instead of silently granting the subset. A
map that rendered as a String is refused at boot, because map[path] would
otherwise be String#[] returning a substring and the feature would be a silent
no-op. The patch targets are checked by signature, not by respond_to?, since
respond_to? was exactly what hid defect two.

An absent groups claim leaves the roles untouched and logs, rather than
demoting every account to Customer on a misconfiguration. A present but empty
claim still means "in no group" and maps to Customer.

biber is unaffected: inventories/development/default.yml declares roles: [] for
it, so it is in no RBAC group, maps to Customer as before, and keeps
#customer_ticket_new.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-16 12:31:56 +02:00
kevinveenbirkenbachandClaude Opus 5 a1abe022bd fix(test-e2e-playwright): reject a readability check that can measure nothing
assertReadable takes a selector list and measures contrast on whatever it
finds. An entry marked optional is skipped when it matches nothing, so a list
whose entries are all optional can measure nothing at all - and then the call
asserts nothing while reporting green. Reject such a list before the browser
is touched. A plain string normalises to a required entry, so only a
deliberately all-optional list is refused, and an empty list with it.

web-app-matomo found the defect the hard way in CI run 38818071194. Its KPI
card check listed four optional selectors, all of them trend modifiers that
the dashboard only renders at a non-zero change; every card read 0%, so
KPICard emitted kpiTrendNeutral and SitesTableSite left its evolution cell
unclassed, nothing matched, and the test failed on the minimum-measured guard
rather than on contrast. The sibling evolution-badge check has the same
all-optional shape and was green only by luck.

Give every all-optional call site an anchor that is already proven visible in
its own spec: .kpiCardEvolutionTrend and .evolutionBadge for matomo,
header.header for web-app-dashboard (waitForDashboardReady asserts it
unconditionally) and #login .panel-body for web-app-espocrm. The espocrm case
needs care: #btn-login and #sign-in each exist in only one branch of the sso
conditional, so neither is variant-safe, while assertLightAndDark measures
.panel-body on the line above.

The matomo theme rule is part of the fix, not a cosmetic extra. Without it the
new anchor keeps Matomo's hardcoded #76838f, which measures 3.60 against
--design-surface-1 and 3.82 against --design-surface-2 in light mode and would
fail the 4.5 threshold the anchor now enforces. The role already replaces
Matomo's hardcoded trend colours; this closes the same gap for the neutral
caption. Equal-specificity rules resolve by order and the role's block is
concatenated about 204 KB after Matomo's in the single served bundle, measured
by byte offset in the failing run's own trace.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-16 11:53:36 +02:00