feat(versions): update completion feedback, instant availability, local time
- 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>feat/v1-foundation v0.9.12
parent
deea1a802c
commit
8b0db1b7ef
|
|
@ -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
|
||||
|
|
|
|||
23
CHANGELOG.md
23
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
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
<p class="flex shrink-0 items-center gap-1.5 font-mono text-[11px] text-warning">
|
||||
{{-- 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. --}}
|
||||
<p class="flex shrink-0 items-center gap-1.5 font-mono text-[11px] text-warning" wire:poll.5s="pollUpdate">
|
||||
<x-icon name="rotate" class="h-3.5 w-3.5 shrink-0 animate-spin" />{{ __('versions.update_running') }}
|
||||
</p>
|
||||
@elseif ($isUpdate)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
Loading…
Reference in New Issue