Metric history: the per-minute sampler (clusev:sample-metrics) now reads
cpu/mem/disk from the `servers` DB row — the same source the live gauges read,
shared via MariaDB — instead of a cross-container Redis cache the poller may
not share in prod. It gates on last_seen_at freshness (skip offline/unpolled)
and keeps `load` as a best-effort cache read. History now fills whenever the
gauges have data, independent of the cache driver or container split — the
prod detail page no longer shows "Noch keine Verlaufsdaten" after hours.
Service-action failures are persisted: a failed systemctl start/stop/restart
writes a red service.<op>.failed AuditEvent with the FULL systemctl reason in
meta['output'] (mb_scrub-ed, 4000-cap), shown expandable in the audit log — a
truncated, ephemeral toast is no longer the only trace. The failure toast is a
short pointer to the audit log, level=error, and error toasts now linger 9s.
Alert delivery: a warning banner appears on the alerts page when NO channel
would reach anyone (no SMTP + no safe webhook + no Gotify). A new Gotify push
channel (server URL + encrypted app token) posts title/message/priority with
the token in X-Gotify-Key; a self-hosted LAN Gotify is allowed (scheme-checked,
not private-range-blocked like the SSRF-guarded webhooks) — a real push when a
service goes down, no e-mail needed.
Secret-retarget hardening (Codex-found class): changing a target while leaving
its secret blank could send the OLD secret to the NEW endpoint. Closed for the
Gotify token, the SMTP password (save + test-send + the runtime mailer), and
the host-terminal SSH credential (username/port change) — each drops/forbids
the stale secret before the new target is stored. Setting::uncached() reads the
security-sensitive keys DB-direct so a cache-repopulation race can't re-pair an
old secret with a new target. Three Codex rounds, clean; 778 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
Re-review (completeness sweep + Codex) found: Audit\Index::saveRetention wrote the global
audit_retention_days policy ungated (a non-admin could shrink retention and prune audit evidence)
-> manage-panel guard + @can-hidden control; Settings\Email::sendTest lacked the role guard that
mount/save already had -> manage-panel; DetectHoneytoken read only form-encoded bodies via post(),
missing a JSON {"api_key":"<canary>"} replay -> now scans json() too (still body-only, no query).
The audit log loaded the latest 50 entries and filtered them in memory, so it
couldn't grow and search only spanned those 50. Now it is paginated (25/page)
with the page deep-linked in the URL via Livewire WithPagination (?page=), and
the search runs in SQL across the whole history — matching actor / action code /
target / server name AND the readable labels (a term is translated to the action
codes whose localized label contains it, so "Endpoint geändert" still finds
wg.set-endpoint). Pagination control mirrors the changelog's windowed pager.
Tests: 25/page with page 2 overflow, search resets to page 1, search filters in
SQL across all pages (a match on page 2 surfaces when filtered). 365 pass, Pint
clean, /audit loads 200 with no console errors, audit lang parity 62/62.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The audit log showed raw machine codes ("Administrator · wg.set-endpoint").
Now each entry renders a localized human-readable label ("WireGuard endpoint
changed"), via an AuditEvent::action_label accessor backed by an audit.actions
lang map (DE/EN); unmapped/dynamic codes (e.g. harden.*.on|off) fall back to a
tidied form, never a bare code. Search also matches the readable label.
Failures are now visible: AuditEvent::is_error flags failed/security events
(failed sign-in, IP ban, failed 2FA, wg.action-failed) which render in red with
an alert icon. And failed WireGuard dashboard actions are now written to the
audit log with the host error message (not just a transient toast), so the
operator can read later what went wrong.
Tests: label mapping + fallback, is_error, page renders label not code, search
by label, WG failure auditing. 360 pass, Pint clean, /audit loads 200 with no
console errors, audit lang parity 59/59.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
A global command palette and keyboard shortcuts, mounted once in the persistent
layout with a single global keydown listener.
- Strg/⌘-K opens the palette: fuzzy-filter nav targets (German, from the sidebar) +
"Server hinzufügen"; ↑/↓ to move, Enter to run, Esc to close. Navigation uses
window.Livewire.navigate (SPA-fast).
- Leader "g" + key: d=Dashboard, s=Server, i=Dienste, f=Dateien, l=Audit-Log,
e=Einstellungen, y=System, v=Version.
- "/" focuses the page search ([data-page-search] on services/servers/audit) — but
only consumes the key when such a field exists, so the browser quick-find still
works elsewhere.
- "?" shows a shortcut-help overlay.
- Typing guard: shortcuts never fire inside inputs/textarea/select/contentEditable.
- Alpine destroy() removes the navigation listener (no leak across wire:navigate);
topbar gains a "Strg K" trigger button. New icons: command, corner-down-left.
R5/R3/R9 respected: the palette only navigates or opens the existing (R5) create-server
modal — zero direct destructive actions; tokens-only styling, German, no emoji.
Pint clean; Codex review clean (destroy cleanup + slash-passthrough hardened); R12 —
verified in-browser: Ctrl-K opens, filter works, Esc closes, g+f navigates, ? help,
HTTP 200, 0 console errors.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>