diff --git a/CHANGELOG.md b/CHANGELOG.md index ec67237..b1b2df6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ The long form, with what was wrong before and how it was found, is in them, where long notes left them looking missing. - The page is served minified, about a quarter smaller. Its script is now TypeScript in `web/src`, type-checked, and built with swc; building ipx needs node. +- The script is its own file, `/app.js`, rather than inside the page. Your browser keeps it + between visits and fetches it again only when an update changes it. ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 4a21e1f..aa5c264 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,8 +62,10 @@ the shell running the command and kills the session (exit 144). This has happene ## Before you touch the page The page is markup and CSS in `web/index.html` and TypeScript in `web/src/`. `build.rs` runs -`web/build.mjs`, which uses swc to strip the types, put the script into the page and minify the -whole thing, and the result is `include_str!`d into the binary. So **every page change needs a +`web/build.mjs`, which uses swc to strip the types and minify the script into `app.js` (and +`login.js`), and minifies the page, and the results are `include_str!`d into the binary. The page +loads its script as `/app.js?v=`: the page is served `no-cache` and the +script `immutable`, so a browser keeps the script until a deploy changes it and its name. So **every page change needs a rebuild** before it is visible, and building needs node and `npm ci` run once. The files in `web/src` are not modules. They are one script split up, concatenated in the order diff --git a/docs/architecture.md b/docs/architecture.md index 168269c..4f953db 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -21,7 +21,7 @@ it to a running daemon. | `src/logbuf.rs` | Ring buffer behind the UI's Log view | — | | `web/index.html` | The page's markup and CSS | — | | `web/src/*.ts` | The page's script, one scope split across files, type-checked by `npx tsc` | — | -| `web/build.mjs` | swc: strips the types, puts the script in the page, minifies it | — | +| `web/build.mjs` | swc: strips the types into `app.js`/`login.js`, named in the page by a hash of their contents, and minifies | — | | `build.rs` | Runs `web/build.mjs` into `OUT_DIR`, where `web.rs` `include_str!`s the result | — | The page is compiled in, so **editing `web/index.html` or `web/src` needs a rebuild**, and a diff --git a/src/web.rs b/src/web.rs index 1a8b489..65b0491 100644 --- a/src/web.rs +++ b/src/web.rs @@ -65,6 +65,8 @@ pub fn router(state: WebState) -> Router { .route("/login", get(login_page)) .route("/api/login", post(login)) .route("/icon.png", get(icon)) + .route("/app.js", get(app_js)) + .route("/login.js", get(login_js)) .route("/inter.woff2", get(inter)) .layer(middleware::from_fn(access_log)) .with_state(state) @@ -441,7 +443,29 @@ async fn login_page(State(state): State, req: Request) -> Response { if vouched_name(&state.ctx.cfg(), &req).is_some() { return Redirect::to("/").into_response(); } - Html(include_str!(concat!(env!("OUT_DIR"), "/login.html"))).into_response() + ([(header::CACHE_CONTROL, PAGE_CACHE)], Html(include_str!(concat!(env!("OUT_DIR"), "/login.html")))) + .into_response() +} + +/// The pages are checked on every visit, so a browser always has the one naming the current +/// scripts; the scripts, named by a hash of their contents (/app.js?v=, see +/// web/build.mjs), are kept a year and never asked for again. A deploy that changes a script +/// changes its name in the page, and the browser fetches it. +const PAGE_CACHE: &str = "no-cache"; +const SCRIPT_CACHE: &str = "public, max-age=31536000, immutable"; + +/// The page's script, and the sign-in page's. Outside the auth layer, like the icon: the sign-in +/// page needs its own before anyone has signed in, and neither holds anything private. +async fn app_js() -> impl IntoResponse { + script(include_str!(concat!(env!("OUT_DIR"), "/app.js"))) +} + +async fn login_js() -> impl IntoResponse { + script(include_str!(concat!(env!("OUT_DIR"), "/login.js"))) +} + +fn script(js: &'static str) -> impl IntoResponse { + ([(header::CONTENT_TYPE, "text/javascript; charset=utf-8"), (header::CACHE_CONTROL, SCRIPT_CACHE)], js) } /// The 2004 icon, served once for both pages rather than inlined as base64 into each. The @@ -478,8 +502,8 @@ const LOG_BUTTON: &str = "