From 8b0db1b7ef71597c2c5a17b98e0dd3bf6f06e861 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 19 Jun 2026 19:08:54 +0200 Subject: [PATCH] feat(versions): update completion feedback, instant availability, local time MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .env.example | 2 + CHANGELOG.md | 23 ++++++++ app/Livewire/Versions/Index.php | 59 +++++++++++++++++++ config/app.php | 4 +- config/clusev.php | 2 +- install.sh | 9 ++- lang/de/versions.php | 2 + lang/en/versions.php | 2 + .../views/livewire/versions/index.blade.php | 4 +- tests/Feature/VersionUpdateCheckTest.php | 37 ++++++++++++ 10 files changed, 140 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index c89bd30..6ddfe82 100644 --- a/.env.example +++ b/.env.example @@ -85,6 +85,8 @@ ACME_EMAIL= # .git (the prod image ships none) — leave empty; in dev the page reads .git directly. CLUSEV_BUILD_SHA= CLUSEV_BUILD_BRANCH= +# Display timezone for dashboard times (DB stays UTC). install.sh sets this from the host; UTC default. +APP_TIMEZONE=UTC # External reverse-proxy TLS: set to the upstream proxy's address/CIDR so Caddy trusts its # X-Forwarded-Proto. Leave as 127.0.0.1/32 (trust nothing) when Caddy terminates TLS itself. TRUSTED_PROXY_CIDR=127.0.0.1/32 diff --git a/CHANGELOG.md b/CHANGELOG.md index 697e026..cd5c41d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,29 @@ getaggte Releases (Kanal `stable`, optional `beta`) — niemals Entwicklungs-Bui _Keine offenen Änderungen — der nächste Stand wird hier gesammelt und als `vX.Y.Z` getaggt._ +## [0.9.12] - 2026-06-19 + +### Hinzugefügt +- **Fertig-Rückmeldung beim Dashboard-Update.** Nach „Jetzt aktualisieren" zeigte die Seite nur + „Update läuft" ohne Ende. Sie pollt jetzt (alle 5 s) und erkennt den Abschluss daran, dass der + neu gebaute Container eine höhere Version meldet als beim Start des Updates — dann springt die + Karte automatisch zurück auf „Aktuell — vX" und ein Erfolgs-Toast erscheint. Übersteht die + Neustart-Downtime (fehlgeschlagene Polls werden einfach wiederholt) und bricht nach ~5 Min mit + einem Hinweis ab, falls das Update hängt — statt ewig zu drehen. Ein laufendes Update wird auch + nach einem Seiten-Reload wieder aufgenommen (solange die Sentinel-Datei besteht). + +### Geändert +- **Verfügbares Update wird sofort beim Laden angezeigt.** Stand der neueste Tag bereits aus einer + vorherigen Prüfung im Cache, zeigte nur das Badge die neue Version — der „Jetzt aktualisieren"- + Knopf erschien aber erst nach erneutem „Nach Updates suchen". Jetzt erscheint er direkt beim + Seitenaufbau (ohne Netzwerkanfrage — rein aus dem Cache). + +### Behoben +- **Zeitstempel in lokaler Zeit statt UTC.** `app.timezone` ist jetzt env-gesteuert; `install.sh` + ermittelt die Zeitzone des Hosts (systemd / `/etc/timezone` / `/etc/localtime`) und schreibt sie + als `APP_TIMEZONE` in die `.env`. Datenbank-Zeitstempel bleiben UTC; nur die Anzeige (z. B. die + „geprüft"-Zeit) folgt der Host-Zeitzone. Default UTC, wenn nicht ermittelbar. + ## [0.9.11] - 2026-06-19 ### Behoben diff --git a/app/Livewire/Versions/Index.php b/app/Livewire/Versions/Index.php index 285162a..5ef9223 100644 --- a/app/Livewire/Versions/Index.php +++ b/app/Livewire/Versions/Index.php @@ -37,6 +37,63 @@ class Index extends Component /** Set once an update has been requested this session — the stack is rebuilding. */ public bool $updateRequested = false; + /** Running version captured WHEN the update was requested — completion = version moved past it. */ + public ?string $updateBaseVersion = null; + + /** Poll ticks since the update request, to time out a stuck/failed update instead of spinning. */ + public int $updatePolls = 0; + + /** + * On load: resume the "running" state if an update sentinel is still pending (survives a + * reload mid-update), and — without any network call — surface an available update from the + * cached check so the button shows immediately (the badge already knew). No re-click needed. + */ + public function mount(DeploymentService $deployment): void + { + if ($deployment->updateRequested()) { + $this->updateRequested = true; + $this->updateBaseVersion = (string) config('clusev.version'); + } + + $latest = $this->resolveLatestTag($this->channel()); // cached / local .git — never network on load + if (! $this->updateRequested && $latest !== null && $this->isNewer($latest, (string) config('clusev.version'))) { + $this->updateState = 'update'; + $this->updateStatus = __('versions.status_update_available', ['latest' => $latest, 'channel' => $this->channel()]); + } + } + + /** + * Poll while an update runs (driven by wire:poll on the "running" notice). Completion is + * detected purely from the running version moving PAST the value captured at request time — + * the rebuilt container reports the new version, so no host round-trip is needed. Times out + * after ~5 min so a failed update surfaces a hint instead of spinning forever. + */ + public function pollUpdate(): void + { + if (! $this->updateRequested) { + return; + } + $this->updatePolls++; + $current = (string) config('clusev.version'); + $this->updateBaseVersion ??= $current; + + if (version_compare(ltrim($current, 'vV'), ltrim($this->updateBaseVersion, 'vV'), '>')) { + $this->updateRequested = false; + $this->updatePolls = 0; + $this->updateState = 'current'; + $this->updateStatus = __('versions.status_current', ['installed' => $current, 'channel' => $this->channel()]); + $this->dispatch('notify', message: __('versions.update_done', ['version' => $current])); + + return; + } + + if ($this->updatePolls >= 60) { // ~5 min at 5s — almost certainly failed; stop spinning. + $this->updateRequested = false; + $this->updatePolls = 0; + $this->dispatch('notify', message: __('versions.update_stalled'), level: 'error'); + } + } + /** * Honest update check: compare the installed version against the newest release * tag visible in the channel. No external server, no stars, no CVE feed. @@ -107,6 +164,8 @@ class Index extends Component ]); $this->updateRequested = true; + $this->updateBaseVersion = $installed; // completion = the running version moves past this + $this->updatePolls = 0; $this->dispatch('notify', message: __('versions.update_started')); } diff --git a/config/app.php b/config/app.php index aad3922..7eecc33 100644 --- a/config/app.php +++ b/config/app.php @@ -65,7 +65,9 @@ return [ | */ - 'timezone' => 'UTC', + // install.sh sets APP_TIMEZONE from the host's timezone so dashboard times (e.g. the update + // "checked at" stamp) show local time, not UTC. DB timestamps stay UTC. Defaults to UTC. + 'timezone' => env('APP_TIMEZONE', 'UTC'), /* |-------------------------------------------------------------------------- diff --git a/config/clusev.php b/config/clusev.php index 61f5ebb..e8f80b9 100644 --- a/config/clusev.php +++ b/config/clusev.php @@ -2,7 +2,7 @@ return [ // First tagged release is v0.1.0 (semantic, not -dev). - 'version' => '0.9.11', + 'version' => '0.9.12', // Deployed commit + branch. install.sh bakes these into .env from the host's .git (the prod // image ships no .git); the Versions page prefers them and falls back to a live .git read in diff --git a/install.sh b/install.sh index 553f0f6..e51bac1 100755 --- a/install.sh +++ b/install.sh @@ -214,7 +214,14 @@ force_kv HOST_GID "$(id -g clusev)" git config --global --add safe.directory "$(pwd)" 2>/dev/null || true force_kv CLUSEV_BUILD_SHA "$(git rev-parse --short=7 HEAD 2>/dev/null || true)" force_kv CLUSEV_BUILD_BRANCH "$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)" -info "Secrets ok; Proxy/URL aus APP_DOMAIN abgeleitet" +# Display times in the HOST's timezone (dashboard "geprüft"-Zeit etc.) instead of UTC. DB stays +# UTC. Detect via systemd, then /etc/timezone, then the /etc/localtime symlink; default UTC. +HOST_TZ="$(timedatectl show -p Timezone --value 2>/dev/null || true)" +[ -n "$HOST_TZ" ] || HOST_TZ="$(cat /etc/timezone 2>/dev/null || true)" +[ -n "$HOST_TZ" ] || HOST_TZ="$(readlink -f /etc/localtime 2>/dev/null | sed -n 's#.*/zoneinfo/##p')" +[ -n "$HOST_TZ" ] || HOST_TZ="UTC" +force_kv APP_TIMEZONE "$HOST_TZ" +info "Secrets ok; Proxy/URL aus APP_DOMAIN abgeleitet; Zeitzone ${HOST_TZ}" # ── [4/9] image ────────────────────────────────────────────────────── phase 4/9 "Image bauen" diff --git a/lang/de/versions.php b/lang/de/versions.php index 078f8e0..424fb1b 100644 --- a/lang/de/versions.php +++ b/lang/de/versions.php @@ -52,6 +52,8 @@ return [ 'update_running' => 'Update läuft — Stack wird neu gebaut. Bitte gleich neu laden.', 'update_not_available' => 'Kein Update verfügbar — die installierte Version ist bereits aktuell.', 'update_throttled' => 'Zu viele Update-Anfragen — bitte in :seconds Sekunden erneut versuchen.', + 'update_done' => 'Update abgeschlossen — jetzt auf v:version.', + 'update_stalled' => 'Update dauert ungewöhnlich lange — Logs prüfen (journalctl -u clusev-update.service) oder Seite neu laden.', // Project panel 'project_title' => 'Projekt', diff --git a/lang/en/versions.php b/lang/en/versions.php index b274eed..79e2208 100644 --- a/lang/en/versions.php +++ b/lang/en/versions.php @@ -52,6 +52,8 @@ return [ 'update_running' => 'Update in progress — the stack is rebuilding. Reload shortly.', 'update_not_available' => 'No update available — the installed version is already current.', 'update_throttled' => 'Too many update requests — try again in :seconds seconds.', + 'update_done' => 'Update complete — now on v:version.', + 'update_stalled' => 'Update is taking unusually long — check the logs (journalctl -u clusev-update.service) or reload.', // Project panel 'project_title' => 'Project', diff --git a/resources/views/livewire/versions/index.blade.php b/resources/views/livewire/versions/index.blade.php index 2e25920..fe065f4 100644 --- a/resources/views/livewire/versions/index.blade.php +++ b/resources/views/livewire/versions/index.blade.php @@ -69,7 +69,9 @@ the System restart button (direct click → "running" notice via a flag — not data-destructive, so no modal). Only offered once a check found a newer release. --}} @if ($updateRequested) -

+ {{-- Poll until the rebuilt stack reports a newer version (pollUpdate flips the state + + fires a success toast). Survives the rebuild downtime: failed polls just retry. --}} +

{{ __('versions.update_running') }}

@elseif ($isUpdate) diff --git a/tests/Feature/VersionUpdateCheckTest.php b/tests/Feature/VersionUpdateCheckTest.php index 31f576c..a33f4fe 100644 --- a/tests/Feature/VersionUpdateCheckTest.php +++ b/tests/Feature/VersionUpdateCheckTest.php @@ -137,6 +137,43 @@ class VersionUpdateCheckTest extends TestCase Livewire::test(Index::class)->assertViewHas('latestTag', null); } + public function test_update_available_is_shown_on_load_without_clicking_check(): void + { + // The cached check (badge already knew) surfaces the update + button on mount, no re-click. + config()->set('clusev.version', '0.9.10'); + Cache::put('clusev:latest-release:stable', '0.9.11', now()->addMinutes(30)); + + Livewire::test(Index::class) + ->assertSet('updateState', 'update') + ->assertSee('0.9.11'); + } + + public function test_a_pending_update_sentinel_resumes_the_running_state_on_load(): void + { + app(DeploymentService::class)->requestUpdate(); // sentinel present (update in flight) + + Livewire::test(Index::class)->assertSet('updateRequested', true); + } + + public function test_poll_update_reports_completion_when_the_version_moves_past_the_base(): void + { + config()->set('clusev.version', '0.9.10'); + + $c = Livewire::test(Index::class) + ->set('updateRequested', true) + ->set('updateBaseVersion', '0.9.10'); + + // Still the old version mid-rebuild → keeps waiting. + $c->call('pollUpdate')->assertSet('updateRequested', true); + + // Rebuilt container now reports a newer version → done + back to "current". + config()->set('clusev.version', '0.9.11'); + $c->call('pollUpdate') + ->assertSet('updateRequested', false) + ->assertSet('updateState', 'current') + ->assertSee('0.9.11'); + } + public function test_build_info_prefers_the_baked_config_over_a_git_read(): void { // Production bakes sha/branch into config via .env (no .git in the image).