Foundation for the feature program (groups are the scoping primitive alerts, ad-hoc
commands, bulk actions and patch/posture will target). Lean by design: named groups,
no hierarchy, no per-group RBAC.
- server_groups (uuid route key, unique name, color = a @theme token key coerced to a real
token on save) + server_server_group M2M pivot (cascade on delete; servers survive a group
delete). Server::groups() + a `scopeInGroup`.
- Servers\Groups full-page component, manage-fleet (admin) only — route can:-mw + mount()
abort_unless + a per-method gate() on every mutation (create/rename/setColor/toggleMember/
confirmDelete/deleteGroup), since /livewire/update does not re-run route mw. create/rename/
recolour/membership audit directly; DELETE goes through the signed single-use ConfirmToken +
the R5 confirm modal (which writes group.delete from the sealed descriptor, so the handler
does not double-audit). 'groupConfirmed' added to ConfirmToken::ACTIONS.
- /servers/groups is registered BEFORE /servers/{server} so "groups" isn't captured as a uuid.
- Servers\Index: a #[Url] group filter (uuid; shareable) — an unknown uuid falls back to all,
never errors. Filtering is open to every role; only management is admin-gated. A "Gruppen"
button (admin) on the fleet header; colour pills use a literal @theme token map (R3).
- lang/{de,en}/groups.php + audit.php group.* labels (de/en parity). No emoji.
19 new tests (model + component: uuid/colour-allow-list/relations/cascade, RBAC route gating,
create/rename/recolour/membership, delete-via-confirmed-token, forged-token no-op, filter).
638 tests green, Pint, lang parity, Codex-reviewed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Operator feedback: no way to add servers; irrelevant storage-internals text; fail2ban
only on/off; raw shell commands shown; no apt update/upgrade.
- Add server: CreateServer modal (name, IP/host, SSH port, user, password|key, optional
label) on the Servers index → creates server + encrypted credential ATOMICALLY (DB
transaction). Strict IP/hostname validation (filter_var + hostname; rejects 999.x, ':').
- System updates (Debian/Ubuntu only): MaintenanceService + SystemUpdate modal — shows the
pending-update count (or "unbekannt" when undeterminable) and runs apt update && upgrade
as root; gated to apt hosts.
- fail2ban configuration: Fail2banConfig modal — Sperrdauer / Max. Fehlversuche / Zeitfenster,
written to a Clusev-owned jail.d drop-in (zz-clusev.local, last-wins; never touches the
operator's jail.local/jails). Durations kept verbatim in fail2ban's native grammar
(600, 10m, 1h 30m, -1). Reads the EFFECTIVE [DEFAULT] across files; refuses to save when the
current policy couldn't be read (no overwrite with unseen defaults); reloads fail2ban only
when already active (never starts it).
- Modals: removed the raw "Befehle (als root)" preview + raw stdout dumps from the hardening
modal — clean German description + result only.
- System page: dropped the .env/Datenbank storage-internals callout (irrelevant to the user).
R15 — Codex gate: `codex review --uncommitted` run iteratively; fixed every finding across 9
rounds (fail2ban jail clobbering, section/precedence, composite/-1 durations, reload-starting-
inactive, read-failure propagation, glob exit code, apt-count failure, atomic server create,
IP validation) until clean — 0 security issues throughout. Live-verified on 10.10.90.162;
R12: Servers/Detail/System 200 / 0 console errors, modals open, hardening modal shows no commands.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>