Compare commits

..
Author SHA1 Message Date
Michael ShimelesandClaude Opus 5.5 5c454c602e ci(web): deploy the site with the browser build from GitHub Actions
Vercel's build doesn't run Rust, so a deploy from Vercel's Git integration
has no /app. The new deploy-site workflow runs scripts/build-web.sh, then
vercel build and vercel deploy --prebuilt. Pushes to master deploy to
production, and pull requests from this repository get a preview.
site/vercel.json turns off Vercel's Git deployments so each commit deploys
once.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 05:50:38 -04:00
Michael ShimelesandClaude Opus 5.5 749b87fe5a fix(web): real downloads for exports, one wasm download, keep /app flags
- Layout and theme exports download a file in the browser instead of
  writing a hidden localStorage key. Imports and "open folder" say they
  need the desktop app for now.
- The page starts the WebGPU wasm with an abortable fetch and cancels it
  when there is no WebGPU adapter, so only the WebGL2 build downloads.
- /app keeps its query string when it redirects to /app/index.html.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:55:25 -04:00
Michael ShimelesandClaude Opus 5.5 222b523fd3 docs(readme): rewrap browser timings
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:33:57 -04:00
Michael ShimelesandClaude Opus 5.5 ab4a8f9020 docs(readme): final-build throttled timing
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:33:45 -04:00
Michael ShimelesandClaude Opus 5.5 32c676aaa3 docs: browser build screenshots
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:32:48 -04:00
Michael ShimelesandClaude Opus 5.5 b0898e6031 docs(readme): use the Retina window run for browser timings
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:32:04 -04:00
Michael ShimelesandClaude Opus 5.5 ea89066fb2 docs(readme): state browser timings as ranges across runs
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:30:27 -04:00
Michael ShimelesandClaude Opus 5.5 156a074988 perf(web): WebGPU-only main build, emoji fonts after first frame
The default browser build drops wgpu's WebGL2 path (naga, wgpu-core,
wgpu-hal), and the page loads a second build with it only when there is
no WebGPU adapter. egui's emoji fonts now load after the first frame
instead of shipping inside the .wasm. Together that takes the WebGPU
bundle from 2.64 MB to 1.69 MB brotli.

Also: label the browser's mixed data source as "Crypto live", tell the
Congress panes the data needs the desktop app, autosave every 2 s in the
browser, and redirect /app to the build on the site.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:28:05 -04:00
Michael ShimelesandClaude Opus 5.5 7ecc35954c feat(web): build the terminal for the browser with WebAssembly
Compile the app for wasm32-unknown-unknown and run it with eframe's web
runner on WebGPU, with a separate WebGL2 build for browsers without it.
Desktop-only pieces sit behind cfg(target_arch = "wasm32") seams: a
tokio/browser task shim, a websocket wrapper, localStorage in place of
files, and stand-ins for SQLite, Congress ingest and SnapTrade.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 02:15:17 -04:00
8 changed files with 34 additions and 71 deletions

No files matched your search

+3 -3
View File
@@ -22,7 +22,7 @@ jobs:
arch: x86_64
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
@@ -67,7 +67,7 @@ jobs:
grep -q "smoke:" live.log
- name: Bundle .app
run: ./scripts/bundle-macos.sh
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
if: always()
with:
name: macos-${{ matrix.arch }}
@@ -80,7 +80,7 @@ jobs:
linux:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
+5 -5
View File
@@ -27,7 +27,7 @@ jobs:
name: Build the browser app
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
# Pull requests build the branch head, like Vercel's Git previews did, not the merge commit.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
@@ -51,7 +51,7 @@ jobs:
tar xzf "$RUNNER_TEMP/binaryen.tar.gz" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/binaryen-$BINARYEN/bin" >> "$GITHUB_PATH"
- run: scripts/build-web.sh
- uses: actions/upload-artifact@v7
- uses: actions/upload-artifact@v4
with:
name: web-app
path: site/static/app
@@ -84,14 +84,14 @@ jobs:
fi
done
exit $missing
- uses: actions/checkout@v7
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/download-artifact@v8
- uses: actions/download-artifact@v4
with:
name: web-app
path: site/static/app
- uses: actions/setup-node@v7
- uses: actions/setup-node@v4
with:
node-version: 24
cache: npm
+2 -2
View File
@@ -207,13 +207,13 @@ rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli --version 0.2.129 --locked # must match Cargo.lock
brew install binaryen # wasm-opt
scripts/build-web.sh # writes site/static/app
cd site && npm run dev # then open http://localhost:5173/app/index.html
cd site && npm run dev # then open http://localhost:5173/app
```
`scripts/build-web.sh <dir>` writes somewhere else instead; any static file server works. Add
`?sample` to the URL to run offline on sample data, or `?fps` for the frame-time readout.
Every push to `master` builds it and deploys it with the site to
[terminal.rasmic.xyz/app](https://terminal.rasmic.xyz/app/). Pull requests get a preview;
[terminal.rasmic.xyz/app](https://terminal.rasmic.xyz/app). Pull requests get a preview;
[site/README.md](site/README.md#deploys) covers the workflow and the secrets it needs.
![The browser build in Chrome: a live BTCUSD chart from Binance beside a demo-account position](docs/img/web-live-crypto.png)
+4 -7
View File
@@ -10,11 +10,8 @@
// The WebGPU build when the browser has WebGPU, else the larger WebGL2 one (no charts).
// Start downloading now; the module below cancels this if there is no WebGPU adapter.
window.terminalLoad = { build: 'gpu' in navigator ? 'terminal' : 'terminal-webgl', abort: new AbortController() };
// Each build's JS and wasm sit in a folder named after a hash of its files, so they can be
// cached for good; scripts/build-web.sh fills in these paths.
terminalLoad.paths = { terminal: './{terminal}', 'terminal-webgl': './{terminal-webgl}' };
terminalLoad.wasm = fetch(`${terminalLoad.paths[terminalLoad.build]}_bg.wasm`, { signal: terminalLoad.abort.signal });
document.head.append(Object.assign(document.createElement('link'), { rel: 'modulepreload', href: `${terminalLoad.paths[terminalLoad.build]}.js` }));
terminalLoad.wasm = fetch(`./${terminalLoad.build}_bg.wasm`, { signal: terminalLoad.abort.signal });
document.head.append(Object.assign(document.createElement('link'), { rel: 'modulepreload', href: `./${terminalLoad.build}.js` }));
</script>
<style>
html,
@@ -59,11 +56,11 @@
load.wasm.catch(() => {}); // the abort below rejects it
load.abort.abort();
load.build = 'terminal-webgl';
load.wasm = fetch(`${load.paths[load.build]}_bg.wasm`);
load.wasm = fetch(`./${load.build}_bg.wasm`);
}
window.terminalBuild = load.build;
try {
const { default: init } = await import(`${load.paths[load.build]}.js`);
const { default: init } = await import(`./${load.build}.js`);
await init({ module_or_path: load.wasm }); // runs the app, which removes #status after its first frame
} catch (e) {
console.error(e);
+8 -25
View File
@@ -7,11 +7,6 @@
# WebGL2 fallback, which is larger and draws no charts). The page loads the second only when
# the browser has no WebGPU adapter.
#
# Each build's JS glue and wasm go in immutable/<hash>/, a folder named after a hash of its
# files, and index.html points at those folders. A new build gets new URLs, so the site can
# cache everything under immutable/ for good (site/vercel.json), and a page can't pair one
# build's JS with another build's wasm.
#
# Needs the wasm32-unknown-unknown target (rustup target add wasm32-unknown-unknown),
# wasm-bindgen-cli at the version pinned in Cargo.lock, and wasm-opt from binaryen.
set -euo pipefail
@@ -35,32 +30,19 @@ features=(--enable-bulk-memory --enable-bulk-memory-opt --enable-call-indirect-o
--enable-mutable-globals --enable-nontrapping-float-to-int --enable-reference-types --enable-sign-ext)
mkdir -p "$out"
rm -rf "$out"/terminal* "$out/immutable" "$out/fonts"
# sed expressions that replace each {name} in index.html with that build's path
paths=()
# SHA-256 of stdin. Older macOS has shasum but no sha256sum.
sha256() { if command -v sha256sum >/dev/null; then sha256sum; else shasum -a 256; fi; }
rm -rf "$out"/terminal* "$out/fonts"
# build <name> [cargo args...]
build() {
local name=$1 tmp="$out/immutable/.$1" hash
local name=$1
shift
cargo build --profile web --target "$target" -p terminal-rs "$@"
wasm-bindgen --target web --no-typescript --out-dir "$tmp" --out-name "$name" "$wasm"
wasm-opt -Os "${features[@]}" --strip-debug --strip-producers "$tmp/${name}_bg.wasm" -o "$tmp/${name}_bg.wasm"
hash=$(cd "$tmp" && find . -type f | LC_ALL=C sort | xargs cat | sha256 | cut -c 1-16)
mv "$tmp" "$out/immutable/$hash"
paths+=(-e "s#{$name}#immutable/$hash/$name#g")
wasm-bindgen --target web --no-typescript --out-dir "$out" --out-name "$name" "$wasm"
wasm-opt -Os "${features[@]}" --strip-debug --strip-producers "$out/${name}_bg.wasm" -o "$out/${name}_bg.wasm"
}
build terminal
build terminal-webgl --features webgl
sed "${paths[@]}" app/web/index.html >"$out/index.html"
if grep -n '{terminal[a-z-]*}' "$out/index.html" >&2; then
echo "index.html names a build that this script doesn't make" >&2
exit 1
fi
cp app/web/index.html "$out/index.html"
# egui's emoji fonts, which the page fetches after start-up (see app/src/web.rs), with their licenses.
fonts=$(cargo metadata --format-version 1 --locked --filter-platform "$target" | python3 -c '
@@ -72,8 +54,9 @@ cp "$fonts"/{NotoEmoji-Regular.ttf,OFL.txt,emoji-icon-font.ttf,emoji-icon-font-m
size() { wc -c <"$1" | tr -d ' '; }
echo "wrote $out"
for f in "$out"/immutable/*/*_bg.wasm; do
for name in terminal terminal-webgl; do
f="$out/${name}_bg.wasm"
gz=$(gzip -9c "$f" | wc -c | tr -d ' ')
br=$(command -v brotli >/dev/null && brotli -c -q 11 "$f" | wc -c | tr -d ' ' || echo "n/a")
echo "${f#"$out"/}: $(size "$f") bytes raw, $gz gzip -9, $br brotli -q 11; $(basename "${f%_bg.wasm}").js: $(size "${f%_bg.wasm}.js") bytes"
echo "${name}_bg.wasm: $(size "$f") bytes raw, $gz gzip -9, $br brotli -q 11; ${name}.js: $(size "$out/$name.js") bytes"
done
-12
View File
@@ -63,18 +63,6 @@ The workflow reads three repository secrets:
To redeploy without a new commit, run the workflow from the Actions tab. Run on `master`, it
deploys to production.
`vercel.json` also routes the browser app. `/app` redirects once to `/app/`, which serves
`static/app/index.html` without a redirect, so the page's relative URLs resolve under `/app/`.
`scripts/build-web.sh` puts each build's JS and wasm in `static/app/immutable/<hash>/`, named
after a hash of those files, and the browser caches everything under `/app/immutable/` for a
year. A new build gets new URLs, so a page never loads one build's JS with another build's wasm.
`index.html` and the fonts keep Vercel's default `max-age=0, must-revalidate`, so the browser
checks them on every visit. A request under `/app/immutable/` for a file that doesn't exist gets a
404 with `no-store`, so that year-long header never sticks to a 404. `vercel.json` uses `routes`
rather than `headers` and `rewrites` because only `routes` can add that rule after the filesystem
check, the same way the SvelteKit adapter guards `/_app/immutable/`. `npm run dev` doesn't apply
`vercel.json`; open `/app/index.html` there.
## Emails
`POST /download` adds each email to the `emails` sorted set in Upstash Redis, scored by when the
+11
View File
@@ -0,0 +1,11 @@
import { redirect } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
/**
* The browser build lives in `static/app` (written by `scripts/build-web.sh`, not committed). Static
* folders have no index page here, so send `/app` to the page itself.
*/
export const GET: RequestHandler = ({ url }) => {
// Keep the query (`?sample`, `?fps`).
redirect(308, `/app/index.html${url.search}`);
};
+1 -17
View File
@@ -2,21 +2,5 @@
"$schema": "https://openapi.vercel.sh/vercel.json",
"git": {
"deploymentEnabled": false
},
"routes": [
{ "src": "^/app$", "status": 308, "headers": { "Location": "/app/" } },
{
"src": "^/app/immutable/.+",
"headers": { "Cache-Control": "public, max-age=31536000, immutable" },
"continue": true
},
{ "handle": "filesystem" },
{ "src": "^/app/$", "dest": "/app/index.html", "check": true },
{
"src": "^/app/immutable/.+",
"status": 404,
"headers": { "Cache-Control": "no-store" },
"continue": false
}
]
}
}