Skip to main content

max / makenotwork

Say the waits on the described screens, and take quasi 0.29.1 Task 0da6a218's first half. Seven writes across three described screens carry awaiting: the CSV export, three forms and three destructive acts. The renderer locks each from the mark, so the double-submit guard the server writes by hand in loading.ts is described here instead of implemented there. Why all seven rather than the two slow ones. The mark says a call resolves once in finite time, which is true of every write on these screens, and a spinner on some buttons and not others is a distinction the reader cannot account for. The git-token form is the one with teeth: its answer is a secret shown once, so two from one impatient double-press is a token the creator never sees. Nothing is marked determinate. Awaiting::of takes a measurement and none of these knows one before it starts; the export knows its row count and not its bytes, which is exactly the estimate-wearing-a-fact's-clothes that type refuses. Takes quasi 0.29.1 for the form half of the guard, which was inert until it landed: hx-disabled-elt="this" on a <form> sets an attribute HTML does not have and does not reach the controls inside. Two clauses of the task are unmet and it stays open. The five spinner spellings are still five, and they live in the unconverted templates rather than here.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 21:30 UTC
Signed with PGP, not checked
Commit: 436ea7a2bb59c74b7814d99624663f9f5c6994c0
Parent: 06ad3ce
4 files changed, +87 insertions, -30 deletions
M server/Cargo.lock +23 -23
@@ -2881,7 +2881,7 @@
2881 2881 checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
2882 2882 dependencies = [
2883 2883 "libc",
2884 - "windows-sys 0.61.2",
2884 + "windows-sys 0.52.0",
2885 2885 ]
2886 2886
2887 2887 [[package]]
@@ -3666,7 +3666,7 @@
3666 3666 checksum = "1a791e6620676a875f362f3156ed213e73ca099a09bf992c18812abe65cc37b1"
3667 3667 dependencies = [
3668 3668 "bstr",
3669 - "hashbrown 0.17.1",
3669 + "hashbrown 0.15.5",
3670 3670 ]
3671 3671
3672 3672 [[package]]
@@ -4817,7 +4817,7 @@
4817 4817 source = "registry+https://github.com/rust-lang/crates.io-index"
4818 4818 checksum = "82cb6a9f675da968c63b6208c641b9dca58fc0133ae53375736b1767b0cab8bd"
4819 4819 dependencies = [
4820 - "windows-sys 0.61.2",
4820 + "windows-sys 0.59.0",
4821 4821 ]
4822 4822
4823 4823 [[package]]
@@ -5712,7 +5712,7 @@
5712 5712 source = "registry+https://github.com/rust-lang/crates.io-index"
5713 5713 checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
5714 5714 dependencies = [
5715 - "windows-sys 0.61.2",
5715 + "windows-sys 0.59.0",
5716 5716 ]
5717 5717
5718 5718 [[package]]
@@ -6632,7 +6632,7 @@
6632 6632
6633 6633 [[package]]
6634 6634 name = "quasi-axum"
6635 - version = "0.29.0"
6635 + version = "0.29.1"
6636 6636 dependencies = [
6637 6637 "axum",
6638 6638 "http 1.4.2",
@@ -6643,7 +6643,7 @@
6643 6643
6644 6644 [[package]]
6645 6645 name = "quasi-basics"
6646 - version = "0.29.0"
6646 + version = "0.29.1"
6647 6647 dependencies = [
6648 6648 "makeover-layout",
6649 6649 "quasi-router",
@@ -6651,7 +6651,7 @@
6651 6651
6652 6652 [[package]]
6653 6653 name = "quasi-http"
6654 - version = "0.29.0"
6654 + version = "0.29.1"
6655 6655 dependencies = [
6656 6656 "form_urlencoded",
6657 6657 "http 1.4.2",
@@ -6660,7 +6660,7 @@
6660 6660
6661 6661 [[package]]
6662 6662 name = "quasi-router"
6663 - version = "0.29.0"
6663 + version = "0.29.1"
6664 6664 dependencies = [
6665 6665 "makeover-layout",
6666 6666 ]
@@ -6683,7 +6683,7 @@
6683 6683
6684 6684 [[package]]
6685 6685 name = "quasi-webview"
6686 - version = "0.29.0"
6686 + version = "0.29.1"
6687 6687 dependencies = [
6688 6688 "docengine",
6689 6689 "makeover-layout",
@@ -7335,7 +7335,7 @@
7335 7335 "errno",
7336 7336 "libc",
7337 7337 "linux-raw-sys 0.12.1",
7338 - "windows-sys 0.61.2",
7338 + "windows-sys 0.52.0",
7339 7339 ]
7340 7340
7341 7341 [[package]]
@@ -7415,7 +7415,7 @@
7415 7415 "security-framework 3.7.0",
7416 7416 "security-framework-sys",
7417 7417 "webpki-root-certs",
7418 - "windows-sys 0.61.2",
7418 + "windows-sys 0.52.0",
7419 7419 ]
7420 7420
7421 7421 [[package]]
@@ -8541,7 +8541,7 @@
8541 8541 "getrandom 0.4.3",
8542 8542 "once_cell",
8543 8543 "rustix 1.1.4",
8544 - "windows-sys 0.61.2",
8544 + "windows-sys 0.52.0",
8545 8545 ]
8546 8546
8547 8547 [[package]]
@@ -9980,7 +9980,7 @@
9980 9980 source = "registry+https://github.com/rust-lang/crates.io-index"
9981 9981 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
9982 9982 dependencies = [
9983 - "windows-sys 0.61.2",
9983 + "windows-sys 0.48.0",
9984 9984 ]
9985 9985
9986 9986 [[package]]
@@ -10768,14 +10768,6 @@
10768 10768 "pkg-config",
10769 10769 ]
10770 10770
10771 - [[patch.unused]]
10772 - name = "makeover-immediate"
10773 - version = "0.30.0"
10774 -
10775 - [[patch.unused]]
10776 - name = "makeover-tui"
10777 - version = "0.30.0"
10778 -
10779 10771 [[patch.unused]]
10780 10772 name = "synckit-client"
10781 10773 version = "0.8.0"
@@ -10796,9 +10788,17 @@
10796 10788 name = "painhours"
10797 10789 version = "0.1.0"
10798 10790
10791 + [[patch.unused]]
10792 + name = "makeover-immediate"
10793 + version = "0.30.0"
10794 +
10795 + [[patch.unused]]
10796 + name = "makeover-tui"
10797 + version = "0.30.0"
10798 +
10799 10799 [[patch.unused]]
10800 10800 name = "quasi-immediate"
10801 - version = "0.29.0"
10801 + version = "0.29.1"
10802 10802
10803 10803 [[patch.unused]]
10804 10804 name = "quasi-store"
@@ -10806,4 +10806,4 @@
10806 10806
10807 10807 [[patch.unused]]
10808 10808 name = "quasi-tauri"
10809 - version = "0.29.0"
10809 + version = "0.29.1"
@@ -117,10 +117,18 @@
117 117 /// the behaviour needed, positionally. Said here it is one sentence, the host
118 118 /// performs it from one attribute, and a terminal renderer can write the file to
119 119 /// disk without being told which button this is.
120 + ///
121 + /// `awaiting` because the server assembles the file before any of it comes
122 + /// back, which is the report case `Action::awaiting`'s own docs name. Nothing
123 + /// countable to say about it: the row count is known here but the bytes are
124 + /// not, and `layout::Awaiting` takes a measurement rather than a stand-in for
125 + /// one.
120 126 fn export() -> Node {
121 127 Node::act(
122 128 "Export CSV",
123 - Action::post("/api/export/contacts").saving("contacts.csv"),
129 + Action::post("/api/export/contacts")
130 + .saving("contacts.csv")
131 + .awaiting(),
124 132 )
125 133 }
126 134
@@ -249,7 +249,7 @@
249 249 // This screen's own route, under its own nest. The API's
250 250 // answers 204, which htmx never swaps, so the row stayed
251 251 // after a successful revoke. See `revoke`.
252 - Action::delete(format!("{PATH}/revoke/{}", creator.seller_id)),
252 + Action::delete(format!("{PATH}/revoke/{}", creator.seller_id)).awaiting(),
253 253 )
254 254 // The template asked with hx-confirm. Said here, a
255 255 // terminal host asks in its own way and no host can
@@ -253,7 +253,11 @@
253 253 // answer is the pane it changed. It addressed the API
254 254 // route until 2026-08-11 and swapped a whole Askama
255 255 // table into this button; see `remove_key`.
256 - Action::delete(format!("{PATH}/keys/{}", key.id)),
256 + // `awaiting` for the same reason the forms carry it: the
257 + // answer is the whole pane rebuilt, so there is a wait
258 + // with nothing on screen saying so. The confirm gates
259 + // the first press, not the second one after it.
260 + Action::delete(format!("{PATH}/keys/{}", key.id)).awaiting(),
257 261 )
258 262 // The template asked with hx-confirm. Said here, a terminal
259 263 // host asks in its own way and no host can forget to ask.
@@ -269,9 +273,15 @@
269 273 }
270 274
271 275 /// The add-a-key form.
276 + ///
277 + /// `awaiting` is the double-submit guard: this creates a record, so a second
278 + /// submit while the first is in flight is a duplicate key. It reads as one word
279 + /// here and the renderer locks the submit button from it, which is what
280 + /// `frontend/src/core/loading.ts` was written to do by hand and is losing
281 + /// ground against.
272 282 fn add_key_form() -> Node {
273 283 Node::Form {
274 - action: Action::post("/api/users/me/ssh-keys"),
284 + action: Action::post("/api/users/me/ssh-keys").awaiting(),
275 285 submit: "Add SSH Key".into(),
276 286 fields: vec![
277 287 Field::new(layout::FieldKind::Textarea, "public_key", "Public Key")
@@ -298,7 +308,10 @@
298 308 field = field.value(chosen.id.clone());
299 309 }
300 310 Node::Form {
301 - action: Action::put("/api/users/me/console-theme"),
311 + // A PUT, so a second submit overwrites rather than duplicating. Marked
312 + // anyway: the wait is real and the reader has no other way to tell the
313 + // save landed from the save being slow.
314 + action: Action::put("/api/users/me/console-theme").awaiting(),
302 315 submit: "Save Theme".into(),
303 316 fields: vec![field],
304 317 }
@@ -339,7 +352,7 @@
339 352 Cell::new(token.last_used.clone()),
340 353 Cell::acts([Act::new(
341 354 "Revoke",
342 - Action::delete(format!("{PATH}/tokens/{}", token.id)),
355 + Action::delete(format!("{PATH}/tokens/{}", token.id)).awaiting(),
343 356 )
344 357 .confirm("Revoke this token?")
345 358 .tone(layout::Tone::Danger)]),
@@ -363,7 +376,10 @@
363 376 /// again here would be a second place for it to drift.
364 377 fn add_token_form() -> Node {
365 378 Node::Form {
366 - action: Action::post("/api/users/me/git-tokens"),
379 + // Creates a record, and unlike an SSH key the answer is a secret shown
380 + // once. Two of these from one impatient double-press is two tokens, one
381 + // of which the creator never sees and cannot recognise later.
382 + action: Action::post("/api/users/me/git-tokens").awaiting(),
367 383 submit: "Create Token".into(),
368 384 fields: vec![
369 385 Field::new(layout::FieldKind::Text, "name", "Name").required(),
@@ -468,6 +484,39 @@
468 484 );
469 485 }
470 486
487 + #[test]
488 + fn every_write_on_this_screen_locks_itself_while_it_waits() {
489 + // `0da6a218`. Both halves of the guard, because they are emitted
490 + // differently and only one of them was ever right: a form names the
491 + // button it has to lock (`930947c3`), an act locks itself.
492 + let html = render(&pane("max", &[key("k1", "SHA256:aaa")], &[], &[]));
493 +
494 + // Three forms, three buttons named. `find` rather than `this` is the
495 + // whole of the fix, so asserting the count catches a fourth form added
496 + // without the mark as well as a regression in the emitter.
497 + assert_eq!(
498 + html.matches(r#"hx-disabled-elt="find button[type='submit']""#)
499 + .count(),
500 + 3,
501 + "every form guards its submit: {html}"
502 + );
503 +
504 + // The delete is an act, and an act is a button already.
505 + assert!(
506 + html.contains(r#"hx-disabled-elt="this""#),
507 + "the remove control locks itself: {html}"
508 + );
509 +
510 + // Nothing here is a measured wait. A determinate mark would mean a
511 + // figure was written down, and none of these knows one.
512 + assert!(!html.contains("data-awaiting-amount"), "{html}");
513 + assert_eq!(
514 + html.matches(r#"data-awaiting="indeterminate""#).count(),
515 + 4,
516 + "three forms and one remove: {html}"
517 + );
518 + }
519 +
471 520 #[test]
472 521 fn the_forms_post_to_the_addresses_the_api_actually_answers() {
473 522 // The conversion's real risk: a described form that posts somewhere the