Commit Graph

3 Commits (f9a08a98d275d0725758938f7430806ac9b3323a)

Author SHA1 Message Date
boban f9a08a98d2 fix(ui): uniform action-element height + live fleet status / alerts
Two things the badge/live-update fixes missed:

Height — the enable/disable TOGGLES (Alerts, Servers, Commands, Settings)
were custom buttons at min-h-9 (36px) while x-btn / x-modal-trigger /
status-pill are h-8 (32px), so a status toggle never lined up with the
Delete/Create buttons next to it. Normalise those toggles to h-8.
Verified: Enabled/Delete/Create all render at 32px now.

Live — the topbar "X/Y online" pill and the sidebar server-switcher were
static Blade, so a server going up/down (shown live on the dashboard via
Reverb) didn't reach them until a navigate/refresh. Make the topbar pill
a small polled Livewire island (Shell\FleetStatus) and add wire:poll to
the server-switcher + the Alerts page, so fleet status and firing/
resolved incidents update live without a refresh.

(The alert e-mail is confirmed sending over SMTP — queued mail no longer
hits the log mailer, and a synchronous send to the admin returned SEND_OK
from mail.thinkidoo.com:587. Non-delivery is downstream: spam filtering
or missing SPF/DKIM for hello@clusev.com via that relay — a DNS/mail-
server config, not a panel code issue.)

750 tests; R12-verified (uniform heights, topbar is a live island, zero
console errors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:42:09 +02:00
boban 7535ee2d63 fix(ui,docker): normalise oversized buttons + surface the real Docker error
Feedback on the new feature pages: buttons were oversized and Docker showed an empty list where
containers exist.

- Buttons: the new pages (groups, alerts×2, commands×2, certs, health) plus settings/login-protection
  used size="lg" (h-11) on inline form-submit buttons, dwarfing the h-9 inputs and the surrounding
  pills. Drop to the default `sm` (h-8) — matching every other form in the app (e.g. the e-mail save
  button). The auth full-width CTAs keep size="lg" (their intended use); x-ring size="lg" is unrelated.
- Docker "empty" bug: DockerService::containers used `docker ps --format '{{json .}}'` (needs a modern
  Docker AND depends on JSON key casing) and returned [] on ANY failure — so a daemon/permission/
  rootless-socket error read as "no containers". Switched to a tab-separated `--format` (portable to
  much older Docker, state derived from the Status text) and it now THROWS the real error on failure;
  the page shows that reason instead of a misleading empty state (so the actual cause is visible).
- Patch rows: the pending/security/up-to-date pills now match the action button height (h-8) so the
  row reads cleanly.

DockerServiceTest updated (tab-parse + state-from-status + throws-real-error). 742 tests green, Pint,
vite build clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 21:22:49 +02:00
boban 5e4f29216d feat(commands): ad-hoc fleet commands + runbooks (feature 4/8)
The multi-server amplifier: run a command (ad-hoc or a saved runbook) across the whole fleet,
a group, or a selection, with a per-server result. Because it is arbitrary remote execution,
authorization + audit are the whole feature.

- The command is intentionally arbitrary; it runs via FleetService::runPlain (the credential's
  login user, base64 transport, NO extra sudo) — the same privilege the web terminal already
  grants, batched. No injection to prevent; the concern is who may run + that every run is logged.
- CommandRunner.run: per-server runPlain, one server failing never aborts the batch, output
  byte-capped (256 KiB) + mb_scrub'd before it reaches a Livewire property.
- Commands\Index page, fully `operate`-gated: route can:operate + mount() + a per-method gate() on
  run/runRunbook/execute/createRunbook/confirmDeleteRunbook/deleteRunbook. Every run goes through a
  signed single-use ConfirmToken + R5 confirm modal ("run on N servers?"); the modal audits
  command.run from the sealed token so execute() does not re-audit. Targets resolve client UUIDs →
  ids server-side (withActiveCredential + inGroup + whereIn), and {command, serverIds} is SEALED in
  the token — a client can't swap the command or targets after confirm. A forged token is a no-op.
- Runbooks: saved name+command, operate-gated CRUD, delete via ConfirmToken.
- lang/{de,en}/commands.php + audit.php command.run / runbook.* + shell.nav_commands (de/en). No emoji.

15 new tests: CommandRunner (runs each server, one-failing-doesn't-abort, nonzero=not-ok, byte-cap
+scrub), component (route gating, ad-hoc run opens confirm, empty-command / no-target errors, execute
runs the SEALED targets, forged token runs nothing, group scope targets only members, runbook create
+audit + run + delete-via-token). 700 tests green, Pint, lang parity, Codex-reviewed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-05 20:38:09 +02:00