max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
4 files changed,
+87 insertions,
-30 deletions
| @@ -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 |