Commit Graph

4 Commits (182cb0fedd23073199e2f0eecb081667d04e579c)

Author SHA1 Message Date
boban 182cb0fedd UI consistency batch: docker names, chart axis, empty states, control heights
Docker container display names drop the compose replica suffix ("clusev-app-1"
-> "clusev-app") in the host page and per-server tab; the real name/id still
drives every action/log call and shows on hover. The logs modal title now
shows the display name instead of the raw hex id.

Dashboard chart: the x-axis label row moves outside the relative chart wrapper
so the y-rail spans only the plot -> the "0" tick lands on the bottom gridline
instead of colliding with "-15 Min", and the 75/50/25 ticks align to their
gridlines.

Uptime + Certificates empty states become a centered icon-chip stack
(matching the threats/audit pattern) instead of a centered title with a
left-anchored hint.

Server-detail tab strip: overflow-y-hidden so the -mb-px overhang can no
longer make it scroll vertically; only the x-axis scrolls.

App-wide badge/button height sweep: x-badge gains a size prop (md = h-8) so
status chips that sit beside h-8 pills/buttons line up (SSH auth-type chip,
WireGuard SSH-lock, settings role). x-btn gains a "field" size (h-9, no coarse
min-h-11 bump) for form-row submit buttons that must match their h-9 input on
every device (audit/system/health/certs/groups/webauthn/commands). Topbar
Ctrl-K + language switch pinned to h-8; the triplicated pagination prev/next
buttons switched from py-sizing to h-11/lg:h-7 to match the page-number
buttons in the same row.

Codex-reviewed (one finding: h-9 class override lost to the coarse min-h-11
on touch -> fixed with the dedicated "field" size). 761 tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 21:20:18 +02:00
boban 4c67b278bd fix(docker): admin-gate the host target + honour a "0" key passphrase (Codex review)
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>
2026-07-05 23:43:18 +02:00
boban c253e79c0c feat(docker): show the Clusev host's own containers (host target)
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>
2026-07-05 22:43:36 +02:00
boban 35b7c15598 feat(docker): container management over SSH (feature 3/8)
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>
2026-07-05 20:23:56 +02:00