Skip to main content

max / makenotwork

multithreaded: keep 4xx and 5xx out of the swap under htmx 4 Finishes the htmx 4 move here. The bundle and the event renames landed with the server's; what was left is the one behaviour change that actually reaches this app. htmx 4 swaps every response but 204 and 304. The only htmx-driven element in multithreaded is the search box, and a failed /search answers with a whole rendered error page (src/error_page.rs), so the default would paint that page inside #search-results. base.html states noSwap for 4xx and 5xx above the script tag, since htmx reads the config once as it runs. The toast in mt.js is again the whole response to an error, which is what 2.x gave; htmx fires htmx:response:error before the noSwap check, so that handler is untouched. Checked and left alone: the search box's hx-get, hx-trigger and hx-swap all mean what they meant. hx-swap="innerHTML" is 4's default, and an input with a name still contributes its own value to a GET that it issues itself, so no hx-include is owed. No hx-push-url anywhere, so the history cache the server had to restore is not needed here.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-20 17:35 UTC
Signed with PGP, not checked
Commit: 084e419d5583213ae57e3e5632f0dc0d6b5c292b
Parent: 87681c7
6 files changed, +83 insertions, -40 deletions
@@ -138,6 +138,12 @@
138 138 - `session_user: Option<TemplateSessionUser>`: for header login state
139 139 - `mnw_base_url: Arc<str>`: for links back to MNW
140 140
141 + ### htmx
142 +
143 + `static/htmx.min.js` is htmx 4.0.0-beta6, the same vendored bundle the MNW server carries. Events are `htmx:phase:action` (`htmx:before:request`, `htmx:config:request`, `htmx:response:error`, `htmx:finally:request`), and the request lives on `evt.detail.ctx` rather than spread across the detail: the element that made the request is `ctx.sourceElement` and outgoing headers are `ctx.request.headers`.
144 +
145 + `base.html` sets `<meta name="htmx-config" content='{"noSwap":[204,304,"4xx","5xx"]}'>` above the script tag. htmx 4 swaps every response by default, and this app's error paths answer with a whole rendered page (`src/error_page.rs`), so without it a failed request paints an error page inside whatever the request targeted. htmx reads the meta once as the script runs, so it has to stay above `_head_assets.html`.
146 +
141 147 ## Database Layer
142 148
143 149 Queries and mutations live in `crates/mt-db/`. Same sqlx patterns as MNW server:
@@ -2119,7 +2119,7 @@
2119 2119
2120 2120 [[package]]
2121 2121 name = "mt-core"
2122 - version = "0.5.0"
2122 + version = "0.5.1"
2123 2123 dependencies = [
2124 2124 "chrono",
2125 2125 "serde",
@@ -2128,7 +2128,7 @@
2128 2128
2129 2129 [[package]]
2130 2130 name = "mt-db"
2131 - version = "0.5.0"
2131 + version = "0.5.1"
2132 2132 dependencies = [
2133 2133 "chrono",
2134 2134 "mt-core",
@@ -2157,7 +2157,7 @@
2157 2157
2158 2158 [[package]]
2159 2159 name = "multithreaded"
2160 - version = "0.5.0"
2160 + version = "0.5.1"
2161 2161 dependencies = [
2162 2162 "askama",
2163 2163 "async-trait",
@@ -4863,42 +4863,6 @@
4863 4863 source = "registry+https://github.com/rust-lang/crates.io-index"
4864 4864 checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
4865 4865
4866 - [[patch.unused]]
4867 - name = "synckit-client"
4868 - version = "0.8.0"
4869 -
4870 - [[patch.unused]]
4871 - name = "synckit-config"
4872 - version = "0.2.0"
4873 -
4874 - [[patch.unused]]
4875 - name = "quasi-type"
4876 - version = "0.1.0"
4877 -
4878 - [[patch.unused]]
4879 - name = "makeover-build"
4880 - version = "0.47.0"
4881 -
4882 - [[patch.unused]]
4883 - name = "makeover-immediate"
4884 - version = "0.30.0"
4885 -
4886 - [[patch.unused]]
4887 - name = "makeover-layout"
4888 - version = "0.31.0"
4889 -
4890 - [[patch.unused]]
4891 - name = "makeover-touch"
4892 - version = "0.22.0"
4893 -
4894 - [[patch.unused]]
4895 - name = "makeover-tui"
4896 - version = "0.30.0"
4897 -
4898 - [[patch.unused]]
4899 - name = "makeover-webview"
4900 - version = "0.52.0"
4901 -
4902 4866 [[patch.unused]]
4903 4867 name = "quasi-axum"
4904 4868 version = "0.44.0"
@@ -4935,6 +4899,14 @@
4935 4899 name = "quasi-webview"
4936 4900 version = "0.44.0"
4937 4901
4902 + [[patch.unused]]
4903 + name = "synckit-client"
4904 + version = "0.8.0"
4905 +
4906 + [[patch.unused]]
4907 + name = "synckit-config"
4908 + version = "0.2.0"
4909 +
4938 4910 [[patch.unused]]
4939 4911 name = "kberg"
4940 4912 version = "0.1.0"
@@ -4946,3 +4918,31 @@
4946 4918 [[patch.unused]]
4947 4919 name = "painhours"
4948 4920 version = "0.1.0"
4921 +
4922 + [[patch.unused]]
4923 + name = "quasi-type"
4924 + version = "0.1.0"
4925 +
4926 + [[patch.unused]]
4927 + name = "makeover-build"
4928 + version = "0.47.0"
4929 +
4930 + [[patch.unused]]
4931 + name = "makeover-immediate"
4932 + version = "0.30.0"
4933 +
4934 + [[patch.unused]]
4935 + name = "makeover-layout"
4936 + version = "0.31.0"
4937 +
4938 + [[patch.unused]]
4939 + name = "makeover-touch"
4940 + version = "0.22.0"
4941 +
4942 + [[patch.unused]]
4943 + name = "makeover-tui"
4944 + version = "0.30.0"
4945 +
4946 + [[patch.unused]]
4947 + name = "makeover-webview"
4948 + version = "0.52.0"
@@ -7,7 +7,7 @@
7 7 default-members = ["."]
8 8
9 9 [workspace.package]
10 - version = "0.5.0"
10 + version = "0.5.1"
11 11 edition = "2024"
12 12 license = "LicenseRef-PolyForm-Noncommercial-1.0.0"
13 13
@@ -76,6 +76,9 @@
76 76 }
77 77 });
78 78
79 + // The toast is the whole response to a 4xx or 5xx: base.html's htmx-config
80 + // keeps those statuses out of the swap, so nothing has replaced the target by
81 + // the time this runs.
79 82 document.body.addEventListener('htmx:response:error', function(evt) {
80 83 var container = document.getElementById('notifications');
81 84 if (!container) return;
@@ -5,6 +5,13 @@
5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 6 {% if let Some(token) = csrf_token %}<meta name="csrf-token" content="{{ token }}">{% endif %}
7 7 <title>{% block title %}Multithreaded{% endblock %}</title>
8 + {# htmx 4 swaps every response except 204 and 304. The one htmx-driven element
9 + here is the search box, and a failed /search answers with a whole rendered
10 + error page (src/error_page.rs), so the default would paint that page inside
11 + #search-results. Turning 4xx and 5xx back off keeps the toast in mt.js as the
12 + only thing an error produces, which is what 2.x did. Read once as the script
13 + below runs, so it has to stay above that include. #}
14 + <meta name="htmx-config" content='{"noSwap":[204,304,"4xx","5xx"]}'>
8 15 {% include "_head_assets.html" %}
9 16 {% block head %}{% endblock %}
10 17 </head>
@@ -725,3 +725,30 @@
725 725 pub users: Vec<AdminUserViewRow>,
726 726 pub search_query: String,
727 727 }
728 +
729 + #[cfg(test)]
730 + mod base_layout_tests {
731 + use super::Error404Template;
732 + use askama::Template;
733 +
734 + /// htmx reads `meta[name=htmx-config]` once, as the script runs. Below the
735 + /// script tag the meta is inert, and htmx 4's swap-everything default puts a
736 + /// rendered error page inside whatever the failed request targeted.
737 + #[test]
738 + fn htmx_config_precedes_the_bundle() {
739 + let page = Error404Template {
740 + csrf_token: None,
741 + session_user: None,
742 + mnw_base_url: std::sync::Arc::from("https://makenot.work"),
743 + }
744 + .render()
745 + .expect("error page renders");
746 +
747 + let meta = page
748 + .find("name=\"htmx-config\"")
749 + .expect("the config is stated");
750 + let script = page.find("htmx.min.js").expect("htmx is linked");
751 + assert!(meta < script);
752 + assert!(page.contains("\"noSwap\""));
753 + }
754 + }