From 084c21a869e78ea75cfaf3e3a2cd415233ce87cb Mon Sep 17 00:00:00 2001 From: boban Date: Sat, 20 Jun 2026 22:08:41 +0200 Subject: [PATCH] feat(update): real phase progress feed + deep-linkable changelog series/page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update-progress phases were a pure time guess, so a fast update sat on "fetch" then snapped all four green at once. Now the host updater publishes its real macro-stage and the page tracks it: - update.sh / install.sh write the current stage (fetch|build|restart|migrate| done) to run/update-phase.json — best-effort (|| true, never aborts an update). - new public GET /update-status.json serves that stage (whitelisted) to the page. - update-progress.blade.php drives the checklist from the feed in REAL order (fetch → build → restart → migrate), falling back to the time heuristic when no feed is present, and completes on the feed's 'done' (or the version flip, with a 25s grace fail-safe). Like the 502 fix, the live experience lands one update after this ships (the page shown DURING an update is the old version's). Also: Versions changelog series + page are now #[Url]-synced (?series=&page=), so a series/page is shareable and survives reload / back-button. Tests: /update-status.json (null / whitelisted / rejected stage), the page polls the feed, and the changelog deep-link reads ?series=&page=. Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 17 +++++ app/Livewire/Versions/Index.php | 8 +- config/clusev.php | 2 +- install.sh | 18 +++++ resources/views/update-progress.blade.php | 92 ++++++++++++++++++----- routes/web.php | 19 +++++ tests/Feature/UpdateProgressTest.php | 53 +++++++++++++ tests/Feature/VersionsChangelogTest.php | 9 +++ update.sh | 12 +++ 9 files changed, 207 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fa849b..cde85d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,23 @@ 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.32] - 2026-06-20 + +### Hinzugefügt +- **Serie & Seite als URL-Parameter.** Die gewählte Versionsreihe und die Seite des Änderungs­ + protokolls stehen jetzt in der Adresse (`?series=0.8&page=2`) — teilbar und per Reload/Zurück-Taste + erhalten. +- **Echter Update-Fortschritt statt Zeit-Schätzung.** Der Host-Updater meldet seine aktuelle Phase + über eine Datei (`run/update-phase.json`); die Update-Seite liest sie und schaltet die Schritte + **abrufen → Image bauen → Dienste neu starten → migrieren** der Reihe nach echt weiter, statt alle + vier am Ende auf einmal grün springen zu lassen. Abschluss erfolgt über das reale „done"-Signal des + Updaters (Fallback: Versions-Wechsel). Greift — wie der 502-Fix — erst beim Update, das von einer + bereits mit 0.9.32 laufenden Instanz gestartet wird (die während eines Updates angezeigte Seite ist + stets noch die der alten Version); ohne Feed bleibt die bisherige Zeit-Heuristik als Fallback. + +### Geändert +- Update-Phasen in realer Reihenfolge (Dienste-Neustart vor Datenbank-Migration). + ## [0.9.31] - 2026-06-20 ### Geändert diff --git a/app/Livewire/Versions/Index.php b/app/Livewire/Versions/Index.php index 776f417..228fb5c 100644 --- a/app/Livewire/Versions/Index.php +++ b/app/Livewire/Versions/Index.php @@ -10,6 +10,7 @@ use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\RateLimiter; use Livewire\Attributes\Layout; +use Livewire\Attributes\Url; use Livewire\Component; /** @@ -39,10 +40,13 @@ class Index extends Component /** Patch releases shown per page within a series — keeps the changelog from endless-scrolling. */ private const CHANGELOG_PER_PAGE = 8; - /** Selected major.minor changelog series ('' = auto: the newest). Drives the series rail. */ + /** Selected major.minor changelog series ('' = auto: the newest). Drives the series rail. + * Synced to ?series= so a series is deep-linkable / survives reload + back-button. */ + #[Url(as: 'series')] public string $series = ''; - /** 1-based page within the selected series' release list. */ + /** 1-based page within the selected series' release list. Synced to ?page= (omitted while 1). */ + #[Url(as: 'page')] public int $changelogPage = 1; public ?string $lastChecked = null; diff --git a/config/clusev.php b/config/clusev.php index 3a6fb03..42606c9 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.31', + 'version' => '0.9.32', // 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 7888851..50c8f3a 100755 --- a/install.sh +++ b/install.sh @@ -22,6 +22,18 @@ warn() { printf '\033[33m ! %s\033[0m\n' "$*"; } die() { printf '\033[31m x %s\033[0m\n' "$*" >&2; exit 1; } phase() { printf '\n\033[1m[%s] %s\033[0m\n' "$1" "$2"; } +# set_stage: publish the current macro-stage (fetch|build|restart|migrate|done) to a file the +# still-running app container serves to the browser's update-progress page, so it shows REAL +# progress instead of a time guess (run/ is bind-mounted into the container). Strictly best-effort — +# a write failure must NEVER abort the update, hence the guards + `return 0`. +STAGE_FILE="run/update-phase.json" +set_stage() { + mkdir -p run 2>/dev/null || true + printf '{"stage":"%s","at":%s}\n' "$1" "$(date +%s 2>/dev/null || echo 0)" > "$STAGE_FILE" 2>/dev/null || true + chmod 644 "$STAGE_FILE" 2>/dev/null || true + return 0 +} + # ── .env mutators ──────────────────────────────────────────────────── # force_kv: always write (config/derived). set_kv: write only when empty/placeholder. force_kv() { @@ -225,6 +237,7 @@ info "Secrets ok; Proxy/URL aus APP_DOMAIN abgeleitet; Zeitzone ${HOST_TZ}" # ── [4/9] image ────────────────────────────────────────────────────── phase 4/9 "Image bauen" +set_stage build if [ "${CLUSEV_PULL:-0}" = "1" ]; then $COMPOSE pull; else $COMPOSE build; fi info "Image bereit" @@ -239,6 +252,7 @@ esac # ── [5/9] stack ────────────────────────────────────────────────────── phase 5/9 "Stack starten" +set_stage restart $COMPOSE up -d # Caddyfile changes ship as edits to a bind-mounted file; `up -d` does NOT recreate caddy for a # content-only change, so force it to re-read the (possibly updated) Caddyfile on every deploy. @@ -298,6 +312,7 @@ done # ── [7/9] migrate + caches ─────────────────────────────────────────── phase 7/9 "Migrationen" +set_stage migrate $COMPOSE exec -T -u app app php artisan migrate --force $COMPOSE exec -T -u app app php artisan config:cache >/dev/null $COMPOSE exec -T -u app app php artisan route:cache >/dev/null @@ -355,6 +370,9 @@ else info "Statisches MOTD geschrieben (/etc/motd)" fi +# Update fully applied — tell the in-browser progress page it may finish (real completion signal). +set_stage done + # ── closing banner (passwords shown ONLY here) ─────────────────────── echo bold "Installation erfolgreich." diff --git a/resources/views/update-progress.blade.php b/resources/views/update-progress.blade.php index 402d458..4bffc92 100644 --- a/resources/views/update-progress.blade.php +++ b/resources/views/update-progress.blade.php @@ -69,11 +69,12 @@
    @php + // Real order of the host updater: git pull → docker build → stack up → migrate. $phases = [ ['key' => 'fetch', 'label' => __('update.phase_fetch')], ['key' => 'build', 'label' => __('update.phase_build')], - ['key' => 'migrate', 'label' => __('update.phase_migrate')], ['key' => 'restart', 'label' => __('update.phase_restart')], + ['key' => 'migrate', 'label' => __('update.phase_migrate')], ]; @endphp @foreach ($phases as $i => $phase) @@ -103,8 +104,9 @@
{{-- Inline JS — no external dependencies. Reads ?return= (redirect target) and ?from= (the - pre-update version). Polls /version.json every 2000 ms and finishes only when the running - version has moved past ?from= (or, for a same-version redeploy, after a down→up cycle), + pre-update version). Drives the phase checklist from the real /update-status.json feed + (falling back to a time heuristic when absent) and finishes when that feed reports 'done', + or when /version.json shows the running version moved past ?from= (or a down→up cycle), stable for 2 polls. Times out after 10 minutes and shows a manual-reload prompt. --}}