diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a687e738..4e0de4c4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -139,6 +139,29 @@ jobs:
cp -rf web/worker-pkg site/worker-pkg
rm -f site/pkg/.gitignore site/pkg/package.json
rm -f site/worker-pkg/.gitignore site/worker-pkg/package.json
+ # Cache-bust the two generated bundles. Pages serves everything
+ # `max-age=600` and the wasm-pack filenames carry no content hash,
+ # so for ten minutes after a deploy a returning visitor runs the
+ # PREVIOUS build -- which during improvise-9x6 debugging made a
+ # shipped fix look like it had not shipped, twice. The specifiers
+ # are static import strings, so stamping the copies in site/ is
+ # enough; the tracked sources are left alone and a local
+ # `http.server` is unaffected.
+ # Both halves, because a fresh glue file does NOT pull a fresh
+ # .wasm: the glue locates it with
+ # `new URL('..._bg.wasm', import.meta.url)`, and a URL constructed
+ # against a base does not inherit the base's query string. Stamping
+ # only the import specifier would refresh the wrapper and keep the
+ # stale binary -- the failure mode this is here to end.
+ sed -i "s|\./pkg/improvise_wasm_client\.js|&?v=${GITHUB_SHA}|" site/app.js
+ sed -i "s|\./worker-pkg/improvise_worker_server\.js|&?v=${GITHUB_SHA}|" site/worker.js
+ sed -i "s|'improvise_wasm_client_bg\.wasm'|'improvise_wasm_client_bg.wasm?v=${GITHUB_SHA}'|" \
+ site/pkg/improvise_wasm_client.js
+ sed -i "s|'improvise_worker_server_bg\.wasm'|'improvise_worker_server_bg.wasm?v=${GITHUB_SHA}'|" \
+ site/worker-pkg/improvise_worker_server.js
+ grep -n "?v=" site/app.js site/worker.js \
+ site/pkg/improvise_wasm_client.js \
+ site/worker-pkg/improvise_worker_server.js
find site -type f | sort
du -sh site
diff --git a/README.md b/README.md
index 38f83045..79e5e003 100644
--- a/README.md
+++ b/README.md
@@ -207,13 +207,19 @@ document open in the TUI is shared — start the websocket server and pass
cargo run -p improvise-ws-server -- examples/demo.improv --port 8787
```
-then reload with `?ws=ws://127.0.0.1:8787`.
+then reload with `?ws=127.0.0.1:8787`.
-**That form works only from a page served over `http://localhost`.** From the
-deployed https page the address must be `wss://`, because a `ws://` one is
-blocked as mixed content — and `improvise-ws-server` speaks plain `ws://`
-only, so reaching it from https means terminating TLS in front of it (a
-reverse proxy or a tunnel). Native `wss://` is not implemented.
+**The scheme is not yours to supply and defaults to TLS.** `?ws=` takes a
+`host:port`; the page uses `wss://` when it is itself served over https and
+`ws://` when it is served over http, because an https origin refuses a `ws://`
+address as mixed content. An explicit `ws://` from an https page is upgraded
+rather than passed through — passing it through can only produce a failure
+that reads like "the server is not running".
+
+So the local pairing above works as written. Reaching a server from the
+**deployed** https page means terminating TLS in front of it (a reverse proxy
+or a tunnel): `improvise-ws-server` speaks plain `ws://` only, and native
+`wss://` is not implemented.
Caveat worth stating plainly: **no line of the browser JavaScript has ever
run.** There is no browser and no headless DOM in the development environment,
diff --git a/web/app.js b/web/app.js
index e0f0c44e..c37a053d 100644
--- a/web/app.js
+++ b/web/app.js
@@ -29,11 +29,12 @@
// default an in-page Web Worker hosting the whole App over OPFS
// (improvise-9x6). No server process; this is what makes a
// GitHub Pages copy of this directory a working application.
-// ?ws=URL a websocket to `improvise-ws-server`. That process is how a
+// ?ws=host:port
+// a websocket to `improvise-ws-server`. That process is how a
// document opened in the TUI is shared, so the path is kept; the
// presence of the parameter is the whole selection mechanism,
// because a second knob naming the same fact is a second thing to
-// get wrong.
+// get wrong. The scheme defaults to `wss://` — see `socketUrl`.
//
// Neither is reachable silently. Every transport writes what it is doing to
// `#status` — "starting", "connected", "cannot reach …", "disconnected" — so
@@ -42,7 +43,35 @@
import init, { Client } from "./pkg/improvise_wasm_client.js";
const params = new URLSearchParams(location.search);
-const WS_URL = params.get("ws"); // present => socket, absent => in-page worker
+// present => socket, absent => in-page worker. The value is a host:port, and
+// the scheme is NOT the caller's to get wrong: see `socketUrl`.
+const WS_PEER = params.get("ws");
+
+/**
+ * The websocket URL for `?ws=`, defaulting to TLS.
+ *
+ * `wss://` is the default and a bare `host:port` gets it, because the address
+ * that works from where this page is actually served is the one it should be
+ * easiest to write. This page ships to GitHub Pages, which is https, and an
+ * https origin refuses a `ws://` address as mixed content — so a `ws://`
+ * default would hand the browser a URL that cannot work and report it as a
+ * connection failure, which reads like "the server is not running".
+ *
+ * An explicit `ws://` is honoured only where it can function: a page served
+ * over plain http, which is the local `python3 -m http.server` case that pairs
+ * with `improvise-ws-server` (which speaks plain `ws://` only — it has no TLS,
+ * so reaching it from https means terminating TLS in front of it). From an
+ * https page an explicit `ws://` is upgraded rather than passed through, since
+ * passing it through can only produce a failure the user cannot act on.
+ */
+function socketUrl(peer) {
+ const secure = location.protocol === "https:";
+ if (/^wss:\/\//i.test(peer)) return peer;
+ if (/^ws:\/\//i.test(peer)) return secure ? peer.replace(/^ws:/i, "wss:") : peer;
+ return `${secure ? "wss" : "ws"}://${peer}`;
+}
+
+const WS_URL = WS_PEER ? socketUrl(WS_PEER) : null;
const DOC = params.get("doc") ?? "demo.improv";
const SEED = params.get("seed") ?? "demo.improv";
diff --git a/web/index.html b/web/index.html
index 0f5f32f9..51188856 100644
--- a/web/index.html
+++ b/web/index.html
@@ -29,8 +29,8 @@
?doc= and ?seed= name the document and the file it is first seeded from.
To talk to a separate `improvise-ws-server` process instead — which is
- how a document open in the TUI is shared — pass ?ws=wss://host:port
- (or ws:// when this page is served over http://localhost).
+ how a document open in the TUI is shared — pass ?ws=host:port; the
+ scheme defaults to wss:// on https and ws:// on http.
-->
improvise
@@ -60,13 +60,13 @@
?seed=url. To open a document hosted by a separate
process instead, run
improvise-ws-server model.improv --port 8787 and reload
- with ?ws=wss://host:port. From this https page the
- address must be wss:// — a ws:// one is
- blocked as mixed content — and
- improvise-ws-server speaks plain ws:// only,
- so it needs TLS terminated in front of it. Served over
- http://localhost the same page takes
- ?ws=ws://127.0.0.1:8787 directly.
+ with ?ws=host:port. The scheme defaults to
+ wss://, because this page is served over https and
+ an https origin refuses a ws:// address as mixed content.
+ Note that improvise-ws-server speaks plain
+ ws:// only, so reaching it from here means terminating TLS
+ in front of it. Served over http://localhost the same page
+ defaults to ws:// and pairs with it directly.