Skip to content

web: opt-in self-hosted update notice and admin-triggered update (feature self-hosted-update) - #501

Open
Dejan-Port wants to merge 1 commit into
storytold:mainfrom
Dejan-Port:self-hosted-update
Open

Dejan-Port wants to merge 1 commit into
storytold:mainfrom
Dejan-Port:self-hosted-update

Conversation

@Dejan-Port

Copy link
Copy Markdown

Summary

Adds an opt-in, off-by-default self-hosted-update feature to apps/deckcraft-web, for one case only: people who host DeckCraft's web build on their own server. It is not for desktop users. The desktop app, the CLI and the release packages are untouched, and a web build without the feature makes no update requests and shows nothing.

Why. We run self-hosted web deployments of GridCraft, WordCraft and DeckCraft. Nothing told us when a new release came out, and deploying one was manual, so we fell two releases behind (deployed 0.4.0, upstream 0.6.1). I know this is outside what most DeckCraft users need, which is why it's behind a feature flag and confined to the web shell. If you'd rather not carry it, that's understandable.

With trunk build --release --features self-hosted-update:

  • every visitor sees a notice above the presentation when a newer release exists ("A new version (0.6.1) is available. This site runs 0.4.0."), with a link to the release notes and a Hide button;
  • Update now stays disabled until the administrator signs in from the notice; a wrong password keeps it disabled;
  • while an update runs, everyone sees "Updating to 0.6.1…". Afterwards, pages still running the old version get "Version 0.6.1 is installed. Reload the page to use it." with a Reload button;
  • the administrator is emailed when a new release is first seen and when an update finishes.

⚠️ Depends on storytold/craft-libs#68: merge that first

The notice, the browser client and the companion service live in the new craft-update crate proposed in storytold/craft-libs#68. This PR pins it the same way as craft-print/craft-ui, with git = "https://github.com/storytold/craft-libs", but the rev (9b85a7c3973b6c799a28553a5376789d33846aa0) is that PR's commit, not a commit on craft-libs main. Cargo can fetch it from the upstream repo through the PR ref, so this builds today, but:

  1. Please merge Add craft-update: opt-in update notices and admin-triggered updates for self-hosted web deployments craft-libs#68 first.
  2. Then the rev in apps/deckcraft-web/Cargo.toml (and Cargo.lock) has to be re-pinned to the merged commit on craft-libs main. If macOS: open presentations from Finder, the Dock and open -a #68 is squash-merged, 9b85a7c won't exist on main at all. I'll push that re-pin to this branch as soon as macOS: open presentations from Finder, the Dock and open -a #68 merges, or you're welcome to do it while merging.

How the update works, and why it isn't done by the app

apps/deckcraft-web is a client-side wasm bundle served as static files; there is no server part to extend. So the browser never builds or writes anything. The update is done by craft-updated, a small companion process from craft-update (feature server) that the self-hoster runs next to their web server:

  • it listens on loopback only and is reached through a reverse-proxied path next to the app (craft-update/);
  • it checks the administrator's credentials itself on every request, using a bearer token from a sign-in, held only in its memory for 15 minutes and never in a cookie. The enabled button in the app is a convenience, not the protection; a browser devtools user gets 401 from the service;
  • requests can't choose what is built: the only action is "update to the newest release the service found on GitHub". The tag (held to v<major>.<minor>.<patch>), the git clone, the build command (run without a shell) and the folders come from its private 0600 config file. The admin password is stored hashed (salted SHA-512 repeated 210,000 times, the ECMA-376 construction GridCraft already uses for workbook protection);
  • the new build replaces the old one in a single step: copied into its own release folder, then a symbolic link is switched over. A failed build changes nothing.

Setup is a server-side command (craft-updated setup), not a first-run web page, so whoever reaches a fresh site first can't claim the admin account. Full details and limitations are in craft-libs#68 and the crate README.

DeckCraft didn't depend on craft-libs before (#66 adds craft-print), so this also adds sha2 0.11 and its small RustCrypto dependencies to Cargo.lock. They are only compiled with the feature.

Changes

  • apps/deckcraft-web/Cargo.toml: the self-hosted-update feature (optional craft-update with its egui and web features) and egui_kittest as a dev-dependency.
  • apps/deckcraft-web/src/updates.rs (compiled only with the feature): Updates, which draws craft-update's notice at the top of the web shell's Ui before the app. DeckCraft has no interface translations yet, so the notice is in English.
  • apps/deckcraft-web/src/web.rs, main.rs: one field and one call in WebShell, behind #[cfg(feature = "self-hosted-update")].
  • xtask: a self-hosted-update step in cargo xtask ci (clippy -D warnings, the tests and the wasm32 check with the feature on), since every other step builds without it.
  • docs/self-hosted-updates.md and a README pointer.

This is web-shell chrome, not a document command, so it doesn't go through the engine's command catalog; nothing below apps/ changes.

Verification

  • cargo xtask ci with stable 1.99, every step passing: fmt, clippy, test, assets, layers, wasm, self-hosted-update. The new self-hosted-update step runs cargo clippy --all-targets -p deckcraft-web --features self-hosted-update -D warnings, the tests, and cargo check --target wasm32-unknown-unknown with the feature. The existing wasm step checks the build without it.
  • Tests in apps/deckcraft-web/src/updates.rs (egui_kittest) drive the real SlideApp below the notice, in the web shell's drawing order:
    • a visitor sees the notice, the File tab moves down below it rather than being covered, Update now is disabled, and clicking it sends nothing;
    • a wrong password ("Wrong username or password.") keeps it disabled;
    • a successful sign-in enables it, and clicking sends the session's token;
    • everyone sees "Updating to …", and afterwards the reload prompt, whose Reload button reloads the page.
  • I also rendered those states to PNGs (visitor with the "Only the administrator can update. Sign in first." tooltip, sign-in with wrong password, admin, running, installed) and checked them by eye. I didn't commit the images.
  • The service side, meaning that craft-updated itself rejects missing, forged and expired sessions and that a request can't choose what gets built, is tested in craft-libs#68. There I also ran a manual end-to-end test of the real binary: a real GitHub release check, git fetch and checkout of the tag, build, switching the live link, and both emails sent to a local SMTP sink.
  • Not verified: a full trunk build --release --features self-hosted-update served behind a real reverse proxy and driven in a browser. The wasm32 cargo check with the feature passes, but I didn't load the bundle in a browser against a running craft-updated.

🤖 Generated with Claude Code

…ture `self-hosted-update`)

For self-hosted web deployments only; off by default and never part of the desktop app. With
`--features self-hosted-update`, deckcraft-web draws craft-update's notice above the
presentation: every visitor sees that a newer release exists, and the administrator can sign
in and choose Update now. The update is performed on the server by `craft-updated`
(craft-libs' craft-update crate, loopback-only behind a reverse-proxied path), which checks
the sign-in on every request; the browser can't choose what gets built.

- apps/deckcraft-web: the feature, `updates.rs` and two lines in the web shell; tests drive
  the real SlideApp with the notice for the visitor, wrong-password, administrator, running
  and reload states.
- xtask ci: a `self-hosted-update` step (clippy, tests and the wasm check with the feature on).
- docs/self-hosted-updates.md and a README pointer.

Depends on storytold/craft-libs#68 (craft-update); the rev is pinned to that PR's head commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anishguduru pushed a commit to anishguduru/deckcraft that referenced this pull request Oct 16, 2026
…g comment tools (storytold#501)

Picking any comment tool opened the Comments panel whenever no right panel
was open, so closing it was undone by the next tool switch. Now closing
Comments (its close button or the rail icon) is remembered, across restarts,
and comment tools leave it closed until the user opens it again (rail, View
menu, Comments ▸ list, or a Reply action). Switching to or closing another
panel doesn't count. Opening a document with comments still shows them, as
Acrobat does.


Claude-Session: https://claude.ai/code/session_01VNJqS2dEQawHJn1qTRJiKp

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

@FredApps FredApps left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ReviewerA — source-read COMMENT (not an approval)

I read the head of the diff: the web build gets an opt-in self-hosted update notice and an admin-triggered update. The lock gains block-buffer 0.12.1 and hybrid-array, i.e. a newer RustCrypto stack, probably for hashing or signature checking of the update. An update mechanism that downloads and replaces the code a server serves is security-sensitive: it must verify a signature or at least a pinned hash of what it fetches, fetch only from the configured origin over HTTPS, require an authenticated administrator for the trigger (not any visitor), and never run without the host operator opting in. I read the lock only and cannot say whether these are satisfied; a maintainer should read the update path line by line. A newer block-buffer also means a second version of it beside the one in the tree, which should be explained. I have not run it, and no exact-head CI result exists for this head. Nothing was run.

I ran nothing and have no exact-head CI result for this PR, so this is not an approval. Where a diff was long I read its code hunks rather than every generated or documentation line.

Status: waiting_author.

This branch has not been deployed

No deployments
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.

3 participants