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>
Foundation so Clusev no longer assumes Debian/apt/ufw/systemd. A host's OS is
resolved once (cached) and every package/firewall/service decision asks it.
New app/Support/Os/:
- OsProfile — family + package manager + firewall tool + service manager, plus
supports(feature) returning NULL or a German reason (graceful degradation).
- OsDetector — one unprivileged probe (/etc/os-release + `command -v` with sbin on
PATH + which firewall is RUNNING); family from ID/ID_LIKE cross-checked against
installed binaries; cached 1h (60s when nothing detected). Readability-guards the
`.` source so a host without /etc/os-release still falls back by package manager.
- PackageManager — apt/dnf/zypper command strings (pending count, apply, install,
is-installed); zypper exit 102/103 normalized to success.
- FirewallTool — ufw + firewalld enable/disable. firewalld opens ssh/80/443 in the
PERMANENT config BEFORE the daemon starts filtering (handles running + stopped),
preserving the no-lockout guard for custom SSH ports.
Integration:
- FirewallService enable/disable now OS-aware (ufw or firewalld) with a support gate.
- MaintenanceService: hasApt -> updateSupport(); pendingUpdates() + applyUpgrades()
across managers (dnf check-update exit 100 handled).
- HardeningService: state()/commandFor() per-OS (dpkg vs rpm, ufw vs firewalld, apt
periodic vs dnf-automatic; yum-only hosts gate auto-updates). Each row carries
supported/reason; unsupported features render muted with a German note instead of
a toggle. apply() refuses unsupported features gracefully.
- Server-Details surfaces the detected System / package manager / firewall.
- SystemUpdate modal + view: OS-neutral copy; audit action system.package_upgrade.
Arch (pacman) & Alpine (apk/OpenRC) are detected and gracefully disabled where
unsupported. Debian/ufw/systemd path verified behaviourally identical on the live
fleet. Pint clean; Codex review clean (fixed firewalld lockout + os-release guard +
yum/dual-firewall/zypper edge cases); R12 — 9 routes 200, 0 console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>