From 3dff91e5b8938209f2a7b38e8323227ba4a12a71 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 22:38:37 -0700 Subject: [PATCH] feat: Live Share links: snowbound.paperclover.net/ opens the share in Snowbound Live Share's dialog gains Copy Link beside Copy: https://snowbound.paperclover.net/ followed by the code. That address is a new small service, snowbound-site (a second binary in crates/relay): it serves the hosted web build's folder, and for a path that is a code, read as loosely as the app reads one, a page with Open in Snowbound (snowbound://join/), and Open in Web once --web names where the web build joins. It listens on 127.0.0.1:23593 behind Caddy, with a systemd unit and pm2 notes in crates/relay/README.md; release_relay.py builds it beside the relay. It takes over from tools/web/serve.py, with the same cache rules (b// kept for good, index.html checked every load), and release_web.py --deploy now also builds it for the VPS and restarts pm2's snowbound-site only when its binary changed. The relay's unit now listens on 23592. snowbound:// opens the share: macOS's bundle declares the scheme and the app takes application:openURLs:, Windows registers it for this user beside the OneNote file types, and Linux's desktop entry handles x-scheme-handler/ snowbound with %U (files arrive as file:// URLs, which the app now reads). A link pasted into Open Shared Notebook works as its code does. The code module moves to the relay crate, which the site and the app both read it from. Assisted-by: claude-opus-5.5 --- Cargo.lock | 1 + crates/notebook/src/live.rs | 2 +- crates/relay/Cargo.toml | 8 + crates/relay/README.md | 50 +++++- crates/relay/deploy/snowbound-relay.service | 2 +- crates/relay/deploy/snowbound-site.service | 33 ++++ .../{notebook/src/live => relay/src}/code.rs | 0 crates/relay/src/lib.rs | 2 + crates/relay/src/main.rs | 6 +- crates/relay/src/site.rs | 161 ++++++++++++++++++ crates/relay/src/site_main.rs | 72 ++++++++ crates/relay/tests/site.rs | 107 ++++++++++++ crates/snowbound/linux/snowbound.desktop | 4 +- crates/snowbound/src/desktop_linux.rs | 2 +- crates/snowbound/src/library.rs | 2 +- crates/snowbound/src/live.rs | 36 ++++ crates/snowbound/src/macos.rs | 20 +++ crates/snowbound/src/main.rs | 6 + crates/snowbound/src/share.rs | 47 +++-- crates/snowbound/src/sidebar.rs | 4 + crates/snowbound/src/windows.rs | 8 + crates/snowbound/tests/replay.rs | 3 +- tools/canvas/build_macos.py | 2 + tools/release_relay.py | 16 +- tools/release_web.py | 26 ++- tools/web/serve.py | 31 ---- 26 files changed, 577 insertions(+), 74 deletions(-) create mode 100644 crates/relay/deploy/snowbound-site.service rename crates/{notebook/src/live => relay/src}/code.rs (100%) create mode 100644 crates/relay/src/site.rs create mode 100644 crates/relay/src/site_main.rs create mode 100644 crates/relay/tests/site.rs delete mode 100644 tools/web/serve.py diff --git a/Cargo.lock b/Cargo.lock index 9945cdff77c13f2a68a1ce8f98ff72004d263818..542707cc6ad71ccef512cef18e90500febe714bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3147,6 +3147,7 @@ dependencies = [ "base64", "getrandom 0.4.3", "sha1", + "tempfile", ] [[package]] diff --git a/crates/notebook/src/live.rs b/crates/notebook/src/live.rs index d1bc29fe1e3360c16e32c4d6f4ae4d16b60f9ad7..36b03a9bd8a82874e764bcb6e6bee771cf2591f4 100644 --- a/crates/notebook/src/live.rs +++ b/crates/notebook/src/live.rs @@ -7,7 +7,7 @@ //! and one writing. A connection whose frames arrive out of order is dropped and met again //! from scratch. -pub mod code; +pub use ::relay::code; mod relay; pub mod share; pub mod wire; diff --git a/crates/relay/Cargo.toml b/crates/relay/Cargo.toml index c9aefd33faa8225c66dfb90faa0755627628f257..5c952c7234bc6a3674ad8b8ecbf9242f5d4f57aa 100644 --- a/crates/relay/Cargo.toml +++ b/crates/relay/Cargo.toml @@ -10,7 +10,15 @@ publish = false name = "snowbound-relay" path = "src/main.rs" +# snowbound.paperclover.net: the hosted web build, and a page for each Live Share code. +[[bin]] +name = "snowbound-site" +path = "src/site_main.rs" + [dependencies] base64 = { version = "0.23.1", default-features = false, features = ["std"] } getrandom = "0.4.3" sha1 = "0.11.0" + +[dev-dependencies] +tempfile = "3" diff --git a/crates/relay/README.md b/crates/relay/README.md index b751f0843f2b55f89a9644c172a2707973edb63b..e708f15f835a21d3cad1d4eca0f4606d5fc08721 100644 --- a/crates/relay/README.md +++ b/crates/relay/README.md @@ -12,11 +12,11 @@ HTTP and WebSocket; a proxy in front of it terminates TLS. ## Build ```sh -python3 tools/release_relay.py # target/relay/snowbound-relay-linux-{x86_64,aarch64} +python3 tools/release_relay.py # target/relay/snowbound-{relay,site}-linux-{x86_64,aarch64} ``` It links with Rust's own lld against Rust's own musl, so it needs only `rustup`. The folder -it makes holds both executables, `SHA256SUMS`, this README and `snowbound-relay.service`. +it makes holds the executables, `SHA256SUMS`, this README and both systemd units. ## Deploy @@ -28,7 +28,7 @@ sudo install -m 755 /tmp/snowbound-relay /usr/local/bin/snowbound-relay sudo install -m 644 /tmp/snowbound-relay.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable --now snowbound-relay -curl -s http://127.0.0.1:7650/health # {"rooms":0,"peers":0,"connections":1,"seconds":3} +curl -s http://127.0.0.1:23592/health # {"rooms":0,"peers":0,"connections":1,"seconds":3} ``` Then point a name at the server and put a TLS proxy in front. Caddy fetches its own @@ -36,7 +36,7 @@ certificate and passes WebSocket upgrades through as they are: ```text live.example.net { - reverse_proxy 127.0.0.1:7650 + reverse_proxy 127.0.0.1:23592 } ``` @@ -49,7 +49,7 @@ server { ssl_certificate /etc/letsencrypt/live/live.example.net/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/live.example.net/privkey.pem; location / { - proxy_pass http://127.0.0.1:7650; + proxy_pass http://127.0.0.1:23592; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -60,14 +60,50 @@ server { } ``` -The app then uses `wss://live.example.net` (for now, `SNOWBOUND_LIVE_RELAY=wss://live.example.net` -with a build that has the `live` feature). +The app uses `wss://relay.snowbound.paperclover.net` unless Options ▸ Sync & Storage ▸ Live +Share names another relay. `--trust-forwarded true`, as the unit sets it, counts each peer by the last `X-Forwarded-For` entry, the one the proxy added. Without a proxy, leave it off: a client could otherwise claim any address. Listening on a public address without TLS works but lets anyone on the path see room tags and nameplates. +## The site: snowbound.paperclover.net + +`snowbound-site` serves the hosted web build's folder, and for a path that is a Live Share +code (`/7KQ-4MZ-9XR`, read as loosely as the app reads one) a page that opens it with +`snowbound://join/`, and in the web build where `--web` names where (once the web build +joins shares). Anything else under the folder is a file; `/` is its `index.html`. A build's +module and JavaScript sit in `b//` and are cached for good; `index.html` and the +codes' pages are checked on every load, and the rest (fonts, dictionaries) for a day. It +holds no state. + +`python3 tools/release_web.py --deploy` builds the web app and the site for the VPS's +architecture, copies the build into `~/snowbound-web/site/` (keeping the last three builds' +folders for pages still running them) and the binary to `~/snowbound-web/snowbound-site`, +and restarts pm2's `snowbound-site` only when the binary changed. The first time: + +```sh +ssh vps +mkdir -p ~/snowbound-web/site +# after the first `release_web.py --deploy` has put the binary there: +pm2 start ~/snowbound-web/snowbound-site --name snowbound-site -- \ + --listen 127.0.0.1:23593 --root "$HOME/snowbound-web/site" +pm2 save +``` + +Caddy in front: + +```text +snowbound.paperclover.net { + encode gzip + reverse_proxy 127.0.0.1:23593 +} +``` + +`snowbound-site.service` runs it under systemd instead (`--root /srv/snowbound/web`). +`snowbound-site --help` lists the options, each also `SNOWBOUND_SITE_