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>
Follow-up to the Projekt-card change: the dev/release-control box must list its
actual update source (e.g. private Gitea) AND the public repo — the previous commit
showed only the source on dev. Replaced the single project link with projectLinks():
the public repo is always listed (validated slug, canonical fallback); the dev box
prepends its source when it is a real URL (a garbage/hostless source is dropped, never
an empty link). Staging/stable still list the public repo only — no private leak.
Codex review: no leak path on non-dev; hardened the dev source against a hostless URL.
Verified: 433 tests green (dev-both / non-dev-only / malformed-slug / garbage-source),
pint clean, /versions shows both links on the dev box with zero console errors.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Versions page only rendered the INSTALLED CHANGELOG.md, so the operator
couldn't see what an available update contained until after applying it. Now,
when behind a published release, a "What's new in vX.Y.Z" panel shows the
changelog of the available version(s) — fetched from the remote CHANGELOG.md at
the latest tag (Gitea raw API, anonymous/read-only, briefly cached), parsed with
the shared changelog parser and filtered to versions newer than installed.
Degrades gracefully: a failed fetch yields an empty preview and never blocks the
update button. Refactored releases() to a reusable parseChangelog().
Tests: preview lists only newer versions + renders their entries; no preview
when current. 362 pass, Pint clean, /versions loads 200 with no console errors,
versions lang parity 51/51.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the flat 48-item changelog list with a series browser: releases are
grouped by major.minor (e.g. "0.9"), a rail (vertical from xl, horizontal pills
below) selects the series, and the active series is paginated (8/page) with the
newest release open by default. The installed series is marked; each row shows
its change categories as toned glance-dots.
Also fixes, surfaced by an adversarial review of the change:
- icon: add the missing 'chevron-right' Lucide path — the accordion disclosure
caret and the pagination "Next" button were rendering an empty <svg>.
- blade: wire:ignore.self on each <details> so a user's open/closed toggle is not
reset by an unrelated re-render (autoCheck / check-updates / in-update poll); a
series/page change still re-defaults to first-open via a fresh wire:key.
- blade: 44px touch targets (series pills + pagination) below lg (R7).
- i18n: localised 'Other' series label; pluralised series_count (no "1 Releases").
- Index: render() is the single clamp authority for changelogPage.
Index.php: groupBySeries() + pageWindow() + selectSeries/gotoChangelogPage; render
builds the series list, resolves the active series, paginates. New DE+EN keys.
VersionsChangelogTest covers grouping order, default series, paging, clamp, the
installed-series cue, and the chevron-right icon regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The available-update banner + button now appear automatically when the page
opens, instead of only after clicking 'check'. autoCheck() runs via wire:init
(async, non-render-blocking) and no-ops while an update is in progress so it
never clobbers the running/poll state.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Update progress now resolves: a wire:poll detects the rebuilt container
reporting a newer version than at request time, flips the card back to
'current' and fires a success toast (survives the restart downtime; times out
after ~5min instead of spinning; resumes after a reload while the sentinel
exists). Fixes the 'Update läuft' state that never ended.
- An available update shows on page load straight from the cached check (no
re-click, no network) instead of only after pressing 'check'.
- Displayed times follow the host timezone instead of UTC: app.timezone is now
env-driven and install.sh bakes APP_TIMEZONE from the host (DB stays UTC).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a host-watcher-backed update button on Version & Releases. The container
(no Docker socket) writes an update sentinel; a new root-run systemd unit
(clusev-update.path/.service) runs watch.sh update -> update.sh (git pull +
idempotent install). The sentinel is consumed before running so a persistent
failure can't loop the path unit.
The button only appears once a check finds a newer release, is per-user
throttled (3/10min, auto-expiring) and audited (deploy.update_request); the UI
warns the dashboard is briefly down during the rebuild. install.sh now installs
both the restart and update host units (update unit runs as root for
docker/systemd/apt).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- fix(hardening): fail2ban/unattended apt installs failed because the 12s SSH timeout
aborted the package download. runPrivileged/runPlain take a $timeout; apt actions use
600s. Live-verified: fail2ban installs + is active/enabled on 10.10.90.162.
- feat(release): cut the foundation as the first real semantic release v0.1.0 (no more
-dev). CHANGELOG is Keep-a-Changelog BY VERSION; the Versions page parses it by version,
reads the newest git TAG as "latest release", and honestly compares installed vs latest
in the channel. Channels reduced to stable|beta (no user-facing dev); System clamps a
legacy value back to stable. Tagged v0.1.0.
- fix(domain/.env): the panel domain lives in the DATABASE (Settings); the app never
rewrites .env. AppServiceProvider overrides config('app.url') at runtime from the Setting
(guarded against a missing table); /system states this explicitly. Caddy config is a
standalone generated file; a Caddy reload is the only infra step.
- docs(arch): Caddy is the prod-only TLS reverse proxy IN FRONT of the app container's
nginx (Internet -> Caddy:443 -> nginx:80 -> php-fpm); no Caddy in dev.
- R15 (rules.md + CLAUDE.md): Codex must review every change for errors + security before
"done". The Codex CLI is not installed in this runtime (needs `sudo npm i -g @openai/codex`
+ `/codex:setup`); an independent adversarial security review of this diff returned CLEAN
(no vulnerabilities, no bugs) as the interim gate.
Verified (R12): /system + /versions 200 / 0 console errors; channels stable|beta only,
DB-not-.env callout; versions by-version with v0.1.0; app.url override resolves from the
Setting; fail2ban live install ok.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Service control now works with a sudo-PASSWORD credential (not only NOPASSWD):
FleetService::sudoFn() defines a remote priv() that feeds the vaulted password to
`sudo -S` over stdin (base64 over the encrypted channel, never on the argv) when not
root, else falls back to sudo -n. Verified LIVE on 10.10.90.162: cron restart/stop/
start executed for real (ActiveEnterTimestamp advanced to now, is-active=inactive
after stop, restarted clean), journal reads real entries via sudo.
- Login brand panel: removed fabricated telemetry — fake "24 Hosts erreichbar",
"cpu 31% mem 48% load 0.86", "clusev connect 10.10.90.0/24". Replaced with true
capability lines (agentless SSH/phpseclib, TOFU host-key pinning, 2FA, audit log,
AGPL). Only real claims now (per "nur eintragen was auch wirklich geht").
- New Version & Releases page (/versions, EN route per R13) with REAL data only:
version from config/clusev.php, build SHA + branch read from .git at runtime,
changelog parsed from a real CHANGELOG.md, real Gitea repo + AGPL license, honest
update path (deploy commands) — NO fake updater / stars / forks / CVEs / "update
available v2.5.0". Sidebar nav + tag/git-branch icons added.
- Dummy-data sweep (12-auditor workflow + adversarial verify): 1 confirmed finding —
removed the unused sine/cosine series() fake-sparkline generator in Dashboard.php;
also fixed a stale "static seed" chart comment and a stale "mock listing" comment;
derived the versions repo label from config (DRY). 5 false positives dismissed.
Verified (R12): /login + /versions + all 7 routes 200 / 0 console errors; login
fake-metrics gone + honest lines + fonts load; versions shows real version/build/
changelog/repo with zero fabricated stats.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>