Skip to main content

max / makenotwork

Nest the settings deep link: ?tab=settings&section=<name> The tab strip's shown_at / route_at / FILLABLE shape, one level down. The user dashboard reads ?tab= to choose the tab and the settings sub-nav now reads &section= to choose within it, so a link that means the Creator Plan opens on it filled at first paint rather than on Profile. Three sections are fillable: Profile, which is what the tab has always opened on, plus Creator Plan and SSH Keys, which is what the five linking sites want. A name outside that list answers Profile rather than opening a blank frame, which is the rule the tab level already follows. Fillability follows the switch as well as the name. A described section answers for itself through quasi_router, which the page handler cannot call, so with QUASI_SCREENS naming SSH Keys the deep link opens Profile instead of filling the frame with the Askama rendering the switch exists to replace. Threading Billing into the page handler is what the Creator Plan section costs. build_creator, build_profile and build_ssh_keys are split out of their route handlers the way build_settings and build_payments already were, and only the shown section is built, so a reader opening on Profile pays for nothing else. build_profile also retires the copy of itself that build_settings held. The five sites: both Apply for Creator Access buttons, which had been pointing at /creators since step 5 (the tier page, which sells the same thing but is not the section the button names); the join wizard's I want to sell; the creators page's Apply from Dashboard; and Manage SSH Keys on the project code tab. The last three spelled it #tab-plan or #tab-ssh-keys, ids this page has never had.
Author: Max Johnson <me@maxj.phd> · 2026-08-20 19:05 UTC
Signed with PGP, not checked
Commit: 6584f46e1e8b010ddb5b8b3af32774de5d7b0d66
Parent: 3a9fc50
15 files changed, +361 insertions, -147 deletions
@@ -5260,7 +5260,7 @@
5260 5260
5261 5261 [[package]]
5262 5262 name = "makenotwork"
5263 - version = "0.13.0"
5263 + version = "0.14.0"
5264 5264 dependencies = [
5265 5265 "ammonia",
5266 5266 "anyhow",
@@ -10744,14 +10744,6 @@
10744 10744 name = "painhours"
10745 10745 version = "0.1.0"
10746 10746
10747 - [[patch.unused]]
10748 - name = "synckit-client"
10749 - version = "0.8.0"
10750 -
10751 - [[patch.unused]]
10752 - name = "synckit-config"
10753 - version = "0.2.0"
10754 -
10755 10747 [[patch.unused]]
10756 10748 name = "makeover-immediate"
10757 10749 version = "0.30.0"
@@ -10760,6 +10752,14 @@
10760 10752 name = "makeover-tui"
10761 10753 version = "0.30.0"
10762 10754
10755 + [[patch.unused]]
10756 + name = "synckit-client"
10757 + version = "0.8.0"
10758 +
10759 + [[patch.unused]]
10760 + name = "synckit-config"
10761 + version = "0.2.0"
10762 +
10763 10763 [[patch.unused]]
10764 10764 name = "quasi-immediate"
10765 10765 version = "0.44.0"
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makenotwork"
3 - version = "0.13.0"
3 + version = "0.14.0"
4 4 edition = "2024"
5 5 license = "LicenseRef-PolyForm-Noncommercial-1.0.0"
6 6 # Server binary: never published to a registry. Marks the crate private so
@@ -6,7 +6,7 @@
6 6 "license": {
7 7 "name": "PolyForm Noncommercial 1.0.0"
8 8 },
9 - "version": "0.13.0"
9 + "version": "0.14.0"
10 10 },
11 11 "paths": {
12 12 "/api/git/{owner}/{repo}/notes": {
@@ -298,6 +298,7 @@
298 298 // library half above is. The hand-written nav this used to read is gone.
299 299 let settings = crate::quasi::settings_tabs::html(
300 300 &crate::config::QuasiScreens::default(),
301 + 0,
301 302 "",
302 303 true,
303 304 true,
@@ -37,6 +37,16 @@
37 37 //! so both moved to the frame that is theirs, and the frame ids are theirs to
38 38 //! keep: `ssh_keys::REGION` and `forum_memberships::SETTINGS_REGION`.
39 39 //!
40 + //! # The deep link nests, and this is the second level
41 + //!
42 + //! `3a7de032`. `?tab=settings` opens this strip and `&section=creator` chooses
43 + //! within it, the same `shown_at` / `section_at` / [`FILLABLE`] shape the five
44 + //! tab strips have, one level down. Five sites wanted a section rather than a
45 + //! tab: two "Apply for Creator Access" buttons, the join wizard's "I want to
46 + //! sell", the creators page's "Apply from Dashboard", and "Manage SSH Keys" on
47 + //! the project code tab. The last three spelled it as `/dashboard#tab-plan` or
48 + //! `#tab-ssh-keys`, ids this page has never had, so they did nothing at all.
49 + //!
40 50 //! # What this retires
41 51 //!
42 52 //! `static/tab-user-settings.js` entirely, its `<script>` tag, the
@@ -72,6 +82,8 @@
72 82 /// One section: what it is called, where its frame is, and what serves it.
73 83 struct Section {
74 84 label: &'static str,
85 + /// What a `?section=` names it, which is also the tail of its route.
86 + name: &'static str,
75 87 gate: Gate,
76 88 /// The id this section's answer lands in. Also the described screen's own
77 89 /// region name, for the two that have one.
@@ -85,6 +97,7 @@
85 97 const SECTIONS: &[Section] = &[
86 98 Section {
87 99 label: "Profile",
100 + name: "profile",
88 101 gate: Gate::Always,
89 102 panel: "settings-profile",
90 103 route: "/dashboard/tabs/profile",
@@ -92,6 +105,7 @@
92 105 },
93 106 Section {
94 107 label: "Account",
108 + name: "account",
95 109 gate: Gate::Always,
96 110 panel: "settings-account",
97 111 route: "/dashboard/tabs/account",
@@ -99,6 +113,7 @@
99 113 },
100 114 Section {
101 115 label: "Creator Plan",
116 + name: "creator",
102 117 gate: Gate::Always,
103 118 panel: "settings-creator",
104 119 route: "/dashboard/tabs/creator",
@@ -106,6 +121,7 @@
106 121 },
107 122 Section {
108 123 label: "Media",
124 + name: "media",
109 125 gate: Gate::Media,
110 126 panel: "settings-media",
111 127 route: "/dashboard/tabs/media",
@@ -113,6 +129,7 @@
113 129 },
114 130 Section {
115 131 label: "SSH Keys",
132 + name: "ssh-keys",
116 133 gate: Gate::Git,
117 134 panel: super::ssh_keys::REGION,
118 135 route: super::ssh_keys::PATH,
@@ -120,6 +137,7 @@
120 137 },
121 138 Section {
122 139 label: "Forums",
140 + name: "forums",
123 141 gate: Gate::Forums,
124 142 panel: super::forum_memberships::SETTINGS_REGION,
125 143 route: super::forum_memberships::SETTINGS_PATH,
@@ -127,20 +145,20 @@
127 145 },
128 146 ];
129 147
130 - /// The markup, for `partials/tabs/user_settings.html` to drop in.
148 + /// The sections whose contents the settings builder can render inline.
131 149 ///
132 - /// `profile` is the first section's contents, rendered by the caller, exactly as
133 - /// the `{% include %}` did. The settings tab has never fetched on open and still
134 - /// does not.
135 - #[must_use]
136 - pub fn html(
137 - screens: &QuasiScreens,
138 - profile: &str,
139 - has_media: bool,
140 - git_enabled: bool,
141 - has_mt_memberships: bool,
142 - ) -> String {
143 - let shown: Vec<&Section> = SECTIONS
150 + /// The shown section is the one that does not fetch, so a name outside this list
151 + /// would open a blank frame rather than a slow one and answers Profile instead.
152 + /// Profile is here because it is what the tab has always opened on; Creator Plan
153 + /// and SSH Keys because five sites link to one of the two, measured 2026-08-19.
154 + /// Account, Media and Forums are pressed rather than linked to.
155 + const FILLABLE: &[&str] = &["profile", "creator", "ssh-keys"];
156 +
157 + /// The sections this reader sees, in strip order.
158 + ///
159 + /// Never empty: Profile and Account are [`Gate::Always`].
160 + fn visible(has_media: bool, git_enabled: bool, has_mt_memberships: bool) -> Vec<&'static Section> {
161 + SECTIONS
144 162 .iter()
145 163 .filter(|section| match section.gate {
146 164 Gate::Always => true,
@@ -148,15 +166,79 @@
148 166 Gate::Git => git_enabled,
149 167 Gate::Forums => has_mt_memberships,
150 168 })
151 - .collect();
169 + .collect()
170 + }
171 +
172 + /// Which section a `?section=` asks for, or Profile.
173 + ///
174 + /// The second level of the deep link the tab strip reads: `?tab=settings` opens
175 + /// this strip and `&section=creator` chooses within it, so a link that means the
176 + /// Creator Plan lands on it filled rather than on Profile.
177 + ///
178 + /// Fillability depends on the switch as well as on the name. A described section
179 + /// answers for itself through `quasi_router`, which the page handler is not in a
180 + /// position to call, so with `QUASI_SCREENS` naming SSH Keys the page cannot fill
181 + /// it: the alternative would be filling the frame with the Askama rendering the
182 + /// switch exists to replace. The link opens Profile there and the section is a
183 + /// press away. Unset, which is every deployment today, it fills.
184 + #[must_use]
185 + pub fn shown_at(
186 + asked: Option<&str>,
187 + screens: &QuasiScreens,
188 + has_media: bool,
189 + git_enabled: bool,
190 + has_mt_memberships: bool,
191 + ) -> usize {
192 + let Some(asked) = asked else { return 0 };
193 + if !FILLABLE.contains(&asked) {
194 + return 0;
195 + }
196 + visible(has_media, git_enabled, has_mt_memberships)
197 + .iter()
198 + .position(|section| {
199 + section.name == asked && !section.screen.is_some_and(|name| screens.enabled(name))
200 + })
201 + .unwrap_or(0)
202 + }
203 +
204 + /// The name of a section by index, so the caller knows which one to render.
205 + #[must_use]
206 + pub fn section_at(
207 + shown: usize,
208 + has_media: bool,
209 + git_enabled: bool,
210 + has_mt_memberships: bool,
211 + ) -> &'static str {
212 + let sections = visible(has_media, git_enabled, has_mt_memberships);
213 + sections
214 + .get(shown)
215 + .map_or(sections[0].name, |section| section.name)
216 + }
217 +
218 + /// The markup, for `partials/tabs/user_settings.html` to drop in.
219 + ///
220 + /// `section` is the shown section's contents, rendered by the caller, exactly as
221 + /// the `{% include %}` did for Profile. The settings tab has never fetched on
222 + /// open and still does not.
223 + #[must_use]
224 + pub fn html(
225 + screens: &QuasiScreens,
226 + shown: usize,
227 + section: &str,
228 + has_media: bool,
229 + git_enabled: bool,
230 + has_mt_memberships: bool,
231 + ) -> String {
232 + let sections = visible(has_media, git_enabled, has_mt_memberships);
233 + let shown = shown.min(sections.len() - 1);
152 234
153 235 let mut strip = Slot::new(STRIP, RegionKind::TabGroup)
154 236 .across(layout::Fallback::Menu)
155 - .showing_one(0);
237 + .showing_one(shown);
156 238
157 - for (at, section) in shown.iter().enumerate() {
239 + for (at, section) in sections.iter().enumerate() {
158 240 let mut region = Slot::bespoke(section.panel, "settings-panel").label(section.label);
159 - if at > 0 {
241 + if at != shown {
160 242 let mut call = Action::get(section.route).awaiting();
161 243 // A described route names its own region and must be left to. Same
162 244 // branch as the library strip, and it follows the switch rather than
@@ -173,7 +255,7 @@
173 255 use quasi_axum::Serves as _;
174 256
175 257 Webview::new()
176 - .with_fill(SECTIONS[0].panel, profile)
258 + .with_fill(sections[shown].panel, section)
177 259 .fragment(&Node::Region(strip))
178 260 }
179 261
@@ -184,6 +266,7 @@
184 266 fn strip(media: bool, git: bool, forums: bool) -> String {
185 267 html(
186 268 &QuasiScreens::default(),
269 + 0,
187 270 "<p>your profile</p>",
188 271 media,
189 272 git,
@@ -226,7 +309,7 @@
226 309 // rather than the section: with the screen off the Askama route answers
227 310 // and names nothing, so the strip has to say where its answer goes.
228 311 let screens = QuasiScreens::parse(super::super::ssh_keys::SCREEN);
229 - let html = html(&screens, "<p>your profile</p>", true, true, true);
312 + let html = html(&screens, 0, "<p>your profile</p>", true, true, true);
230 313
231 314 assert!(
232 315 !html.contains("hx-target=\"#settings-ssh-keys\""),
@@ -248,6 +331,73 @@
248 331 );
249 332 }
250 333
334 + #[test]
335 + fn a_deep_link_arrives_showing_the_section_it_asked_for() {
336 + // The whole of `3a7de032`: `?tab=settings&section=creator` opens the
337 + // Creator Plan filled rather than opening Profile and making the reader
338 + // find it.
339 + let screens = QuasiScreens::default();
340 + let shown = shown_at(Some("creator"), &screens, true, true, true);
341 + assert_eq!(shown, 2);
342 + assert_eq!(section_at(shown, true, true, true), "creator");
343 +
344 + let html = html(&screens, shown, "<p>your plan</p>", true, true, true);
345 + assert!(html.contains("<p>your plan</p>"), "{html}");
346 + assert!(!html.contains("hx-target=\"#settings-creator\""), "{html}");
347 + // Profile is a press now, and it is the one the strip used to fill.
348 + assert!(html.contains("hx-target=\"#settings-profile\""), "{html}");
349 + assert!(html.contains("data-shows=\"2\""), "{html}");
350 + }
351 +
352 + #[test]
353 + fn a_section_the_page_cannot_fill_answers_profile() {
354 + let screens = QuasiScreens::default();
355 + // Real sections nothing links to, so nothing pays to render them inline.
356 + assert_eq!(shown_at(Some("account"), &screens, true, true, true), 0);
357 + assert_eq!(shown_at(Some("media"), &screens, true, true, true), 0);
358 + assert_eq!(shown_at(Some("forums"), &screens, true, true, true), 0);
359 + // Not a section at all, and no section asked for.
360 + assert_eq!(shown_at(Some("nonsense"), &screens, true, true, true), 0);
361 + assert_eq!(shown_at(None, &screens, true, true, true), 0);
362 + // A gated section asked for by a reader who cannot see it. The index
363 + // shifts under the gate, so the answer has to be read as a name.
364 + assert_eq!(shown_at(Some("ssh-keys"), &screens, true, false, true), 0);
365 + assert_eq!(section_at(0, true, false, true), "profile");
366 + assert_eq!(shown_at(Some("ssh-keys"), &screens, false, true, false), 3);
367 + assert_eq!(section_at(3, false, true, false), "ssh-keys");
368 + }
369 +
370 + #[test]
371 + fn a_switched_on_screen_is_not_fillable() {
372 + // The described screen answers its own address through `quasi_router`,
373 + // which the page handler cannot call. Filling the frame with the Askama
374 + // rendering instead would show a screen the deployment switched off, so
375 + // the deep link opens Profile and the section is a press away.
376 + let screens = QuasiScreens::parse(super::super::ssh_keys::SCREEN);
377 + assert_eq!(shown_at(Some("ssh-keys"), &screens, true, true, true), 0);
378 + // Unset, which is every deployment today, it fills.
379 + assert_eq!(
380 + shown_at(Some("ssh-keys"), &QuasiScreens::default(), true, true, true),
381 + 4
382 + );
383 + }
384 +
385 + #[test]
386 + fn a_shown_index_past_the_end_cannot_panic() {
387 + // A caller that computed an index against a different membership must
388 + // clamp rather than take the page down.
389 + let html = html(
390 + &QuasiScreens::default(),
391 + 99,
392 + "<p>your profile</p>",
393 + false,
394 + false,
395 + false,
396 + );
397 + assert!(html.contains("<p>your profile</p>"), "{html}");
398 + assert!(html.contains("data-shows=\"1\""), "{html}");
399 + }
400 +
251 401 #[test]
252 402 fn the_three_gated_sections_leave_when_their_test_fails() {
253 403 let all = strip(true, true, true);
@@ -436,6 +436,7 @@
436 436 // hand-written nav's `hx-target`, which no longer exists.
437 437 let nav = crate::quasi::settings_tabs::html(
438 438 &crate::config::QuasiScreens::default(),
439 + 0,
439 440 "",
440 441 true,
441 442 true,
@@ -54,11 +54,12 @@
54 54 //! own. A hash restore that cannot find its button does nothing and says nothing,
55 55 //! which is how six dead links sat in the tree.
56 56 //!
57 - //! The three that mean a settings section become `?tab=settings`, which is
57 + //! The three that mean a settings section became `?tab=settings`, which is
58 58 //! Settings showing Profile, since Profile is what the sub-nav opens on. Landing
59 - //! on the section that was asked for wants a second level
59 + //! on the section that was asked for wanted a second level
60 60 //! (`?tab=settings&section=creator`) and a fillable builder behind it; five sites
61 - //! want one, which earns it, and it is its own task rather than this one.
61 + //! wanted one, which earned it, and `3a7de032` built it in
62 + //! [`super::settings_tabs`]. The link a section names is `&section=` now.
62 63 //!
63 64 //! # What this retires
64 65 //!
@@ -109,7 +109,7 @@
109 109 {% else %}
110 110 {% if let Some(user) = session_user %}
111 111 <p>Ready to create?</p>
112 - <a href="/dashboard?tab=settings" class="btn-primary">Apply from Dashboard</a>
112 + <a href="/dashboard?tab=settings&amp;section=creator" class="btn-primary">Apply from Dashboard</a>
113 113 {% else %}
114 114 <p>Join to get started.</p>
115 115 <a href="/join" class="btn-primary">Join</a>