Serve the script as /app.js, cached until a deploy changes it

The page loaded its script inline. It now names /app.js?v=<hash> (login.js for
the sign-in page), the hash of the script's contents: the script is served
immutable for a year and the page no-cache, so a browser fetches the script
again only when a deploy changes it and so its name.

Also fixes a race in the mark-everything-read test: it waited on a badge that
was seldom 0 to begin with, so a mark-unread still in flight could land after
the read-all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 13:03:25 +00:00
parent e2969bcee8
commit 5483355021
7 changed files with 84 additions and 20 deletions

View File

@@ -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<WebState>, 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=<hash>, 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 = "<button id=logs ";
/// The page, with the log button left out for anyone but an admin. Hiding it from the page's
/// script instead showed it for a moment on every load, until /api/me answered.
async fn index(user: crate::db::User) -> Html<std::borrow::Cow<'static, str>> {
Html(page_for(user.is_admin))
async fn index(user: crate::db::User) -> impl IntoResponse {
([(header::CACHE_CONTROL, PAGE_CACHE)], Html(page_for(user.is_admin)))
}
fn page_for(admin: bool) -> std::borrow::Cow<'static, str> {