Commit Graph

8 Commits (feat/v1-foundation)

Author SHA1 Message Date
boban b0c46f2b18 perf/cleanup: one SSH round-trip for Docker, drop dead i18n, dedupe row
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>
2026-07-06 20:06:27 +02:00
boban b4c309bd54 feat(nav): 3-group sidebar; split Docker + Terminal into host page + per-server tabs
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>
2026-07-06 19:21:13 +02:00
boban 00a9d8b9a3 feat(ui): shield-only 2FA indicator in the sidebar + terminal search from 2 servers
- sidebar: the "2FA on/off" text badge next to the user name was visually noisy. Replace it
  with a single status shield — a cyan shield-check when 2FA is on, a muted struck-through
  shield when off; the label moves to the hover/aria title so it stays reachable.
- terminal: the server-target search now appears once there is more than one server (was 5+),
  so it is there as soon as filtering helps; added a clear (×) button + an accent focus ring.

Browser-verified: sidebar shows just the shield (no text); search filters the rail (homelab →
only homelab-01) with a working clear button; both badge states render correctly. 465 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:20:46 +02:00
boban 2b1e98c5d4 fix(terminal): make the saved host-login state unmistakable in the modal
The stored secret is never echoed back (security), so on reopen the blank password field
read as "not configured" / "it forgot my password". The login DOES persist (single
HostCredential row, kept across reloads); only the modal was ambiguous.

- when configured, show a green status: "Configured as <user> — password is stored. Leave
  blank to keep it; enter a new one to change it." The Remove button + gear stay, so the
  operator can still change the password or switch access.

No behaviour change — set once, stays until changed/removed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:08:50 +02:00
boban e1a886671b refactor(terminal): host login targets the local machine only — drop the HOST/IP field
The "Clusev host" terminal is for the LOCAL machine Clusev runs on. Letting the operator
point it at an arbitrary host/server makes no sense (that is what fleet servers are for)
and the pre-filled host.docker.internal read as a confusing mandatory magic value.

- the host is now FIXED to host.docker.internal server-side (HostShell::HOST), not a form
  field; the modal asks only for the login — user + password|key, and the port.
- the rail tile sub-label shows just the login user (the host is implied/local).

Browser-verified: modal has no HOST/IP field (user/port/auth/password only); saving shows
the user on the tile; opening the host reaches the real host sshd (a wrong password yields
an SSH auth error, proving the connection), zero console errors. 16 terminal tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 20:00:04 +02:00
boban 0ec9f3664a feat(terminal): explain the host-login HOST/IP default in the setup modal
The field is pre-filled with host.docker.internal — a Docker DNS name that resolves to
the machine the containers run on (the Clusev host) — but had no explanation, so it read
as a mandatory magic value. Add a one-line helper under the field: it's the default for
the Clusev host; leave it, or enter another IP/host. The field stays editable on purpose
(some setups want the host's LAN IP or a different machine).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:52:13 +02:00
boban 83626352e2 feat(terminal): Clusev-host SSH login tile + server search; drop the inline hint
The "Clusev host" terminal was a shell inside the sidecar container (node@clusev, no
sudo, not the host). It is now a REAL SSH login into the host machine, like any fleet
server — so it shows the real root@<host> prompt with full rights.

- HostCredential (encrypted singleton) holds the host SSH login; a HostShell modal lets
  the operator set host/port/user + password|key. The clear-text secret is never echoed
  back; on edit a blank secret keeps the stored one, and switching auth method requires a
  fresh secret (no password silently reused as a key).
- open('host') mints a kind=host token only when a login is configured, else opens the
  setup modal; the internal resolve endpoint returns a server-shaped SSH spec (decrypted,
  internal-net only) for kind=host, or 404 when unconfigured.
- compose: extra_hosts host.docker.internal:host-gateway so the sidecar reaches the host
  sshd; Caddy now 404s /_internal/* at the public edge (the sidecar uses app:80 directly).
- rail gains a debounced search box past 5 servers; removed the now-obvious PTY hint line.

Browser-verified (R12): host tile shows "not configured" + gear → modal → SSH login as
root runs commands, zero console errors; search filters; resolve returns the host spec.
15 terminal tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 19:46:55 +02:00
boban 07fe404ce9 feat(terminal): web terminal — per-server SSH + Clusev host PTY (xterm.js + node sidecar)
A dedicated /terminal page with a target rail: one terminal per fleet server (SSH)
plus one for the Clusev host itself. php-fpm cannot hold an interactive PTY, so a
small Node sidecar (ws + ssh2 + node-pty) bridges xterm.js to the PTY.

Flow: the Livewire page mints a single-use, 60s TerminalSession token and dispatches
it to an Alpine xterm island; the browser opens a same-origin WS (/terminal/ws,
proxied by nginx in dev / Caddy in prod to terminal:3000); the sidecar burns the
token at an internal resolve endpoint (shared-secret header, private network only)
for a connection spec, then opens an SSH PTY (decrypted credential, never sent to the
browser) or a local host PTY. Real shell = native Tab/Shift-Tab completion.

Security:
- token burned ATOMICALLY (single conditional UPDATE) — no double-open race
- resolve endpoint guarded by hash_equals shared secret, exempt from CSRF + PanelScheme
  host enforcement (private net only), returns decrypted creds over the internal net
- sidecar enforces same-origin on the WS upgrade (hostname match, port-agnostic)
- single-use 60s tokens, swept daily so the table can't grow unbounded

Verified in-browser (R12): host + server terminals connect, accept keyboard input,
run commands, Tab-complete; cross-origin WS rejected; zero console errors. 9 feature
tests cover minting, the resolve spec, single-use burn, expiry, and locked creds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-25 03:21:10 +02:00