Optimizations from a multi-agent analysis pass (low-risk, verified):
- Docker pages did TWO SSH connect/login/exec cycles per load —
available() (`command -v docker`) then containers() (`docker ps`).
Fold availability into the single `docker ps` call: a missing binary
now throws App\Exceptions\DockerNotInstalled (detected from the ps
error), which the pages catch for the "not installed" state. Halves
the SSH round-trips on the host Docker page AND the per-server Docker
tab, and drops each container action from 3 connects to 2 (action +
one reload probe instead of two). available() stays for its own test/
reuse but is no longer on the hot path.
- Remove 13 dead translation keys left by the Docker/Terminal split
(terminal: heading/subtitle/targets_*/servers_heading/search_*/no_*/
pick_target; docker: target_label/server_target) — confirmed 0
references, de+en kept in parity.
- Extract the duplicated container row + state→pill mapping into
x-docker-container-row, shared by the host Docker page and the
per-server Docker tab (one place to restyle/fix the row).
749 tests (new DockerNotInstalled unit + component coverage);
R12-verified (host Docker + server Docker tab render, zero console
errors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Restructure the navigation so host-level tools are grouped separately and
the host vs per-server split is explicit.
Sidebar — three groups instead of two:
• Flotte: Dashboard, Servers, Services, Commands, Files, Audit
• Host: Docker, Terminal, WireGuard, System, Security posture, Patch,
Certificates, Uptime (things that act on the Clusev host)
• Konto: Settings, Alerts, Threats, Versions, Release, Help
Docker & Terminal are now split by target:
• Sidebar Docker = the CLUSEV HOST's containers; sidebar Terminal = the
host shell. Both admin-only (manage-fleet, route + mount guarded) — the
control-plane machine, same bar the host shell already used.
• Per-server Docker + Terminal move to the server-details page as tabs
(Übersicht | Docker | Terminal, ?tab= deep-linkable). New lean
components Servers\ServerDocker + Servers\ServerTerminal; the metrics
poll is suppressed off the overview tab. Viewing a server's containers
is open to any role; actions/logs/shell require operate.
Also: Docker\Index + Terminal\Index reduced to host-only (no target
toggle / server rail); ServerDocker checks credential()->exists() (the
withExists attribute does not survive Livewire hydration); container rows
drop the noisy port list. Docs at ~/clusev-site updated to match; a
.gitignore guard keeps the separate marketing site out of this repo.
Tests: Docker/Terminal component + RBAC gate tests reworked for the split;
new ServerDockerTest + ServerTerminalTest. 747 pass. R12-verified in a
browser (3-group sidebar, host Docker/Terminal, server tabs switch, real
per-server containers, zero console errors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two findings from the Codex review of the host-Docker changes:
1. Under-gated host target (security). Listing/actioning/log-reading the
CLUSEV HOST's containers only required `operate`, but the host is the
control-plane machine — a `Stop` on clusev-mariadb takes Clusev down —
and the host TERMINAL is already `manage-fleet`-only. Align it:
- effectiveTarget() resolves to the host only for manage-fleet users;
everyone else is pinned to a fleet server (no host default, no toggle).
- action()/viewLogs() and the ContainerLogs host sentinel (serverId 0)
now abort_unless manage-fleet.
A viewer/operator can no longer reach the host, even by forcing
target=host.
2. HostCredential::toServer() dropped a key passphrase equal to the string
"0" via a falsy `if ($this->passphrase)` check, which would break key
auth. Use an explicit null/'' check.
Added tests: non-admin never targets the host, a forced target=host still
hits a fleet server, operator cannot open host logs. 751 tests green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Docker page only ever targeted fleet servers, so an operator whose
containers run ON the Clusev host itself (the clusev stack + anything
else on that machine) saw nothing — the page has "nothing to do with
the fleet" from their point of view. It now has a target switch:
• Clusev host (default when a host SSH login exists) — lists the
containers on the machine Clusev runs on, reached over the local
Docker gateway with the existing encrypted HostCredential, the same
login the host terminal uses.
• the active fleet server — the previous behaviour, still available.
Mechanics:
- HostCredential::toServer() builds a TRANSIENT Server (never persisted)
carrying the host login; DockerService/FleetService run unchanged
against it. The logs modal resolves serverId 0 to this host server.
- VerifiesHostKey only pins (writes ssh_host_key) for persisted fleet
servers now; a transient host connection no longer spawns a junk
fleet row on every request. The host is the local gateway — no
network path to MITM — so skipping the pin there is safe.
- The `app` service (dev + prod compose) gains the
host.docker.internal:host-gateway mapping the terminal sidecar already
had, so the PHP layer can reach the host's sshd. Without it the app
container cannot resolve the host at all — the real reason host Docker
never worked.
- Absent runtime still renders the clean "not installed" state; a
host target with no login yet shows a setup hint.
Verified end-to-end in a browser: the page defaults to the host and
lists the full running stack (clusev-app/mariadb/redis/… + others) with
Logs/Restart/Stop actions. 748 tests green (5 new host-target tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When a fleet host has no container runtime at all (e.g. a native
postfix/dovecot mail server), `docker ps` returns "sh: docker: not
found". The page surfaced that raw shell text inside a red error box,
which reads as a broken feature — the user reasonably expected the tab
to show containers.
Probe `available()` first: if the docker binary is genuinely absent,
render a clean, neutral "Docker nicht installiert — es gibt keine
Container zum Anzeigen" panel (box icon, no alarm styling) rather than
the shell error. A real fault (daemon down / permission) still shows
the actual error as before. Verified end-to-end against a real SSH host
with no runtime (the honest state) and against a real docker host (full
container list + Logs/Restart/Stop actions render).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Manage the containers running ON a fleet server, agentlessly — the integrated Portainer.
List containers + state, view logs, start/stop/restart, all via `docker` over SSH.
- DockerService (over FleetService::runPrivileged → base64 transport, shell-injection-safe):
containers() parses `docker ps -a --format '{{json .}}'`; containerAction() with an op allow-list
(start|stop|restart|pause|unpause); logs() with a clamped tail; composeStacks() read-only. Every
container ref is validated `^[a-zA-Z0-9][\w.-]*$` before interpolation — blocks shell metacharacters
AND a leading-dash argument injection (mirrors serviceAction). A missing/errored docker → [] (never
throws to the UI).
- Docker\Index page (route /docker, sidebar "Docker" with a new box icon): lazy container list for the
active server, open to any role. Container actions are `operate`-gated (abort_unless per method) and
audited (docker.action). Reading LOGS is also operate-gated (container output can leak secrets —
consistent with gating file CONTENT reads); the list/state stays open. ContainerLogs modal re-gates
the read itself.
- lang/{de,en}/docker.php + audit.php docker.action + shell.nav_docker (de/en parity). No emoji.
Codex hardening applied: logs output is byte-capped (head -c 262144) AND mb_scrub'd before it reaches
a Livewire property (invalid UTF-8 / a huge line can't break the JSON snapshot); the ref regex uses
\A…\z anchors so a trailing newline can't slip past.
16 new tests: service (json parse, empty-on-unavailable, valid-op command, reject unknown-op / shell-
metachars / leading-dash / trailing-newline ref, logs clamp+byte-cap, available probe), component
(viewer browses the list; viewer 403 on action AND on logs open + modal read; operator/admin act +
audited; logs modal tail). 685 tests green, Pint, lang parity, Codex-reviewed (fixes applied).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>