Feedback on the new feature pages: buttons were oversized and Docker showed an empty list where
containers exist.
- Buttons: the new pages (groups, alerts×2, commands×2, certs, health) plus settings/login-protection
used size="lg" (h-11) on inline form-submit buttons, dwarfing the h-9 inputs and the surrounding
pills. Drop to the default `sm` (h-8) — matching every other form in the app (e.g. the e-mail save
button). The auth full-width CTAs keep size="lg" (their intended use); x-ring size="lg" is unrelated.
- Docker "empty" bug: DockerService::containers used `docker ps --format '{{json .}}'` (needs a modern
Docker AND depends on JSON key casing) and returned [] on ANY failure — so a daemon/permission/
rootless-socket error read as "no containers". Switched to a tab-separated `--format` (portable to
much older Docker, state derived from the Status text) and it now THROWS the real error on failure;
the page shows that reason instead of a misleading empty state (so the actual cause is visible).
- Patch rows: the pending/security/up-to-date pills now match the action button height (h-8) so the
row reads cleanly.
DockerServiceTest updated (tab-parse + state-from-status + throws-real-error). 742 tests green, Pint,
vite build clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fleet-wide package-update visibility: pending + SECURITY update counts per server, one-click patch.
Extends MaintenanceService (single-server pending/apply already existed) with the security subset
and a fleet aggregation.
- MaintenanceService::updateCounts(Server) → { pending, security } in ONE round-trip via a new
PackageManager::countsScript() (apt/dnf/zypper). Security is best-effort per family (apt path solid;
dnf/zypper degrade to 0 when the metadata isn't there) and clamped ≤ pending. Either value is null
when the query can't run (shows "unknown", never a misleading 0).
- Patch\Index page (route /patch, `operate`-gated: route + mount + per-method). Lazy per-server scan,
each guarded (an unreachable host → error row). Fleet KPIs (total pending / total security). Applying
upgrades is a stateful action → signed ConfirmToken + R5 modal (modal audits patch.apply from the
sealed token; handler does not re-audit) resolving a client uuid → the sealed server id; a forged
token is a no-op. Reuses the existing applyUpgrades; the result output is mb_scrub'd before it lands
in a Livewire prop.
- countsScript strings are STATIC per manager (no operator input) — no injection surface.
- lang/{de,en}/patch.php + audit.php patch.apply + shell.nav_patch (de/en parity). No emoji.
11 new tests: MaintenanceService (parse pending+security, clamp security≤pending, missing-sentinel /
unsupported-manager → unknown), component (route gating, scan shows counts + survives an unreachable
server, patch opens confirm, applyPatch runs upgrades over the SEALED server, forged token no-op).
720 tests green, Pint, lang parity, self-reviewed (RBAC, ConfirmToken seal, static scripts, scrub).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>