The page's script is TypeScript in web/src, built and minified with swc

- web/src/*.ts: the script that was inline in index.html and login.html, split along its
  existing sections. Still one scope, concatenated in order, not modules.
- web/build.mjs strips the types, puts the script in the page and minifies it with swc;
  build.rs runs it into OUT_DIR and web.rs include_str!s the result. 137 KB -> 106 KB.
- npx tsc -p . type-checks web/src, loosely; the handful of annotations it needed
  change no behaviour.
- The Docker build installs node and swc (npm ci --omit=dev).
- Two list requests racing no longer let the older one win, and switching tabs clears
  the selection it closes, which made a browser test flaky.

Closes #23, #24.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-18 12:41:34 +00:00
parent fbba447ca6
commit 26802d2b23
22 changed files with 2669 additions and 1666 deletions

View File

@@ -441,7 +441,7 @@ 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!("../web/login.html")).into_response()
Html(include_str!(concat!(env!("OUT_DIR"), "/login.html"))).into_response()
}
/// The 2004 icon, served once for both pages rather than inlined as base64 into each. The
@@ -471,8 +471,10 @@ fn constant_time_eq(a: &str, b: &str) -> bool {
a.iter().zip(b).fold(0u8, |acc, (x, y)| acc | (x ^ y)) == 0
}
const INDEX: &str = include_str!("../web/index.html");
const LOG_BUTTON: &str = r#"<button id="logs" "#;
// Built by build.rs from web/index.html and web/src, and minified: attribute values lose their
// quotes, so LOG_BUTTON is spelled the way the minifier leaves it.
const INDEX: &str = include_str!(concat!(env!("OUT_DIR"), "/index.html"));
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.
@@ -484,7 +486,7 @@ fn page_for(admin: bool) -> std::borrow::Cow<'static, str> {
if admin {
INDEX.into()
} else {
INDEX.replacen(LOG_BUTTON, r#"<button id="logs" hidden "#, 1).into()
INDEX.replacen(LOG_BUTTON, "<button id=logs hidden ", 1).into()
}
}
@@ -822,8 +824,8 @@ mod tests {
#[test]
fn only_an_admin_is_sent_the_log_button() {
// If the markup drifts from LOG_BUTTON, replacen matches nothing and says nothing.
assert!(page_for(false).contains(r#"<button id="logs" hidden "#));
assert!(!page_for(true).contains(r#"id="logs" hidden"#));
assert!(page_for(false).contains("<button id=logs hidden "));
assert!(!page_for(true).contains("id=logs hidden"));
}
#[test]