Skip to content

Update the panel from the About tab, and stop shipping vite apps that host-block their own visitors - #124

Merged
jhd3197 merged 7 commits into
mainfrom
dev
Aug 31, 2026
Merged

Update the panel from the About tab, and stop shipping vite apps that host-block their own visitors#124
jhd3197 merged 7 commits into
mainfrom
dev

Conversation

@jhd3197

@jhd3197 jhd3197 commented Aug 31, 2026

Copy link
Copy Markdown
Owner

Updating ServerKit used to mean SSHing into the box and running a script — which is fine, right up until you're on a phone and the "Update available" badge is just taunting you. This adds a real Update Now button to the About tab: it launches the existing scripts/update.sh (blue/green slot switch, migrations, health check, automatic rollback — none of that is reimplemented here), streams the updater's own log into the panel, and rides out the restart it triggers. The one genuinely tricky bit is that the updater restarts the systemd unit the panel is running in, so a plain child process would get killed at the exact point of no return; systemd-run puts the script in its own transient unit that survives. Status is deliberately stateless — an update destroys in-memory state by definition, so "is it running" is a systemctl query and "how's it going" is the tail of the log file the script already writes, both readable by the new process after the restart. Riding along is a buildpack fix in the same spirit of things that only break in production: auto-detected vite apps were started with vite preview, which host-checks every request against preview.allowedHosts and therefore answered localhost health probes happily while blocking every real visitor arriving through the panel's proxy.

Highlights

  • One-click self-update from Settings → About, with a confirmation step that says plainly what will happen: the panel restarts briefly, hosted apps stay online, and a failed version rolls back on its own.
  • Live updater output in the panel while it runs, so a slow or stuck update is visible instead of being a blank spinner.
  • The panel survives its own restart mid-update — losing the connection is treated as an expected phase, and the UI reports the new version once the backend answers again.
  • The button only appears where it can actually work. Docker installs get a note explaining that the container can't replace its own image (with the host command to run), and dev servers don't get a working update button at all.
  • Vite apps deployed by the buildpack now serve on their real domain instead of returning a host-blocked error page to everyone but the health check.
Technical changes
  • New app/services/panel_update_service.py: get_capability() reports systemd / docker / unsupported with a human reason, gating on Linux, /.dockerenv, root, the presence of scripts/update.sh, and systemd-run + systemctl on PATH.
  • start_update() launches the updater via systemd-run --unit serverkit-panel-update --collect, so the script runs in its own cgroup and survives the systemctl restart it performs on the panel's own unit. A fixed unit name makes concurrency a single systemctl is-active query and forces two concurrent starts to collide inside systemd rather than race through the updater.
  • A best-effort systemctl reset-failed precedes the launch — a previously failed run leaves the fixed-name unit in failed and systemd would otherwise refuse to reuse the name.
  • latest_log() reads the newest /var/log/serverkit/update-*.log, seeks to the last 8 KiB rather than reading the whole file, strips ANSI escapes, and derives a best-effort outcome (success / rolled_back) from the markers scripts/update.sh already prints.
  • _is_windows() is a module function rather than an inline os.name check specifically so tests can fake the platform without patching os (a leaked os.name patch takes down the whole pytest run on Windows).
  • GET /system/update and POST /system/update in app/api/system.py, both @admin_required. The POST requires {"confirm": true} in the body and returns 202; it writes a panel.update audit entry recording the transient unit and the version being upgraded from. Both routes are recorded in docs/API_SURFACE.md.
  • ConflictError for an update already in flight, ValidationError for an install that can't self-update or a failed launch.
  • AboutTab.jsx gains a phase machine (confirmstartingrunning/restartingdone/failed/timeout) polling every 3s for up to 15 minutes; a request failure maps to restarting rather than an error, since the panel going unreachable is the expected middle of the operation. Success is detected by the reported version differing from the one at start, not by a completion flag.
  • Cleanup guard: a cancelledRef set on unmount stops the poll loop from writing state after the tab is gone.
  • The capability probe is fired only when an update is available, and a 403 is swallowed — a non-admin simply sees the existing View Release link, with the accent styling moving between the two buttons depending on whether self-update is offered.
  • getPanelUpdateStatus() / startPanelUpdate() added to services/api/system.js; .update-confirm, .update-capability-note, .update-progress and the log <pre> styled in _settings.scss using existing design tokens.
  • All 15 non-English locales carry the nine new strings; the log tail renders the last 12 lines with white-space: pre-wrap inside a scroll-capped box.
  • BuildpackService.detect() now defaults vite projects to serve -s dist -l tcp://0.0.0.0:4173 instead of vite preview --host, because preview validates the incoming Host against preview.allowedHosts and rejects any domain the repo's config doesn't list. A repo-authored start script still wins.
  • generate_dockerfile() emits RUN npm install -g serve only when the resolved start command's first token is serve — the buildpack chose it, so the repo won't have declared it, but a repo supplying its own start command shouldn't pay for the install.
  • _deploy_and_probe() in the docker-builds leg now re-requests every booted container with Host: app.example.com and fails on a non-200 or a body containing "not allowed", pinning the class of bug rather than just this instance: a container that answers 127.0.0.1 but refuses the proxied domain is broken for every real visitor.

jhd3197 and others added 7 commits August 31, 2026 08:09
The About tab could only detect a new release and link to it; applying the
update meant SSH and scripts/update.sh. Wire that script to the UI:

- panel_update_service launches update.sh via systemd-run in its own
  transient unit — a plain subprocess child would be killed when the
  updater restarts the panel's own systemd unit. Status is stateless
  (transient-unit state + the updater's own update-*.log) so it survives
  the mid-update backend restart.
- Capability gates keep the button honest: root systemd installs only.
  Docker installs get the docker compose pull instructions; dev servers
  and hosts without systemd-run get a reason instead of a button.
- GET/POST /api/v1/system/update (admin_required, confirm-gated,
  audited). API surface doc regenerated.
- About tab gains Update Now with inline confirm, live log tail while
  the update runs, a restarting phase that tolerates the panel being
  briefly unreachable, and success/rollback/timeout outcomes. New
  aboutTab locale keys in en + es.

The heavy lifting (blue/green switch, migrations, health check,
automatic rollback) stays in update.sh, which is unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The self-update feature commit added the 10 new app.aboutTab keys to en
and es only; every other locale fell back to English in an otherwise
fully translated About tab. Add ar, bn, de, fr, id, it, ko, pl, pt, ru,
th, tr, vi, zh-Hans and zh-Hant, matching each file's existing register
(Sie/vous/formal).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
`vite preview` host-checks every request against `preview.allowedHosts`
and blocks any domain the repo's vite config doesn't list -- the deployed
container answered localhost probes fine and refused every request the
panel proxy forwarded ("Blocked request. This host ... is not allowed").
Preview is a dev convenience, not a production server, per vite's own docs.

The vite default start command is now `serve -s dist -l tcp://0.0.0.0:4173`
(SPA fallback, no host check); the generator preinstalls serve globally
when the start command needs it, since the repo never declares it. A
repo-authored start script still wins and gets no serve install.

The @docker_builds leg now sends a foreign `Host:` header to every booted
container -- the localhost-only probe was exactly the blind spot that let
this class through.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jhd3197
jhd3197 merged commit 339b5fe into main Aug 31, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant