Skip to main content

max / makenotwork

Put the SSH keys pane on the residual seam Fourteen of MNW's seventeen described screens now serve from a template generated at build time. This one was blocked on the theme picker: a select said which option was marked once, at the field, as a value makeover-webview compared against each option, and a residual holds one compiled body per loop. Said per option (quasicoherent c32bb877) it is a branch inside the row. Three other things the seam asked for on the way. The two lists dispatched on emptiness, and a dispatch is refused because only one arm reaches the residual; said as two guards they need two emissions, so each answers a run of nodes and the pane splices it. The two parameterless forms are `#[constant]`, which is what holds them to the promise the caller makes. And the three reads are split out of the handler so the mount answers the screen and its markup from one read rather than two that can disagree. Filled with each theme marked in turn and with none, crossed against both tables empty and not, byte for byte against the renderer. /dashboard/tabs/analytics stays off the seam and the reason is now a ruling rather than a gap: describe the chart, do not compile around it. mnw-server 14d42beb.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 13:41 UTC
Signed with PGP, not checked
Commit: d2288e86b15b68f28c46e8bdfdce46ad4fe0d940
Parent: 58b486c
5 files changed, +317 insertions, -141 deletions
@@ -163,7 +163,7 @@
163 163 # through quasi beside its Askama equivalent, so the cost of describing a screen
164 164 # here is measured rather than argued. Not load-bearing for any shipped route.
165 165 # See wiki look-wave-2, tier G.
166 - quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.104" }
166 + quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.105" }
167 167 # The declared form. A screen written as `declare!` is compiled to the same
168 168 # `quasi_router` constructors the hand-written shape called, at build time, so
169 169 # nothing new reaches the binary and the macro is not in the request path.
@@ -183,13 +183,13 @@
183 183 # For the request head the per-viewer state factory reads. axum re-exports it,
184 184 # but the factory's signature is quasi-axum's and names `http::request::Parts`.
185 185 http = "1.3.1"
186 - quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.104" }
187 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.104" }
186 + quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.105" }
187 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.105" }
188 188 # The first-party widget set. The carousel was proved here against three pages
189 189 # and then moved there; what is left in `quasi/widgets/carousel.rs` is the
190 190 # Askama glue. Not `makeover-basics`: a widget is an assembly of `Node`s, so it
191 191 # sits above quasi-router rather than inside the description suite.
192 - quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.104" }
192 + quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.105" }
193 193
194 194 # Git source browser
195 195 gix = { version = "0.86", default-features = false, features = ["blame", "mailmap", "revision", "sha1", "max-performance-safe"] }
@@ -288,7 +288,7 @@
288 288 # Written from the crate rather than copied into the repo for the reason the
289 289 # constants exist: a stale copy in a static directory is a silently dead bulk
290 290 # bar, and the version has to be the one the emitter agrees with.
291 - quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.104" }
291 + quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.105" }
292 292
293 293 [dev-dependencies]
294 294 tower = { version = "0.5.3", features = ["util"] }
@@ -516,7 +516,22 @@
516 516 vec![
517 517 (
518 518 ssh_keys::PATH,
519 - mount(app, ssh_keys::screen, ssh_keys::WRITES, ssh_keys::renderer),
519 + served_panel_mount(
520 + app,
521 + ssh_keys::REGION,
522 + ssh_keys::WRITES,
523 + ssh_keys::renderer,
524 + |viewer, _| {
525 + let (username, keys, tokens, themes) = ssh_keys::reading(viewer)?;
526 + Ok(ssh_keys::pane_serve(
527 + &residuals::SSH_KEYS,
528 + &username,
529 + &keys,
530 + &tokens,
531 + &themes,
532 + ))
533 + },
534 + ),
520 535 ),
521 536 (
522 537 library_contacts::PATH,
@@ -101,6 +101,7 @@
101 101 ("CREATORS", |plan| {
102 102 Node::Region(super::creators::page_region_staged(plan))
103 103 }),
104 + ("SSH_KEYS", |plan| super::ssh_keys::pane_staged(plan)),
104 105 ]
105 106 }
106 107
@@ -709,6 +710,67 @@
709 710 }
710 711 }
711 712
713 + /// The SSH keys pane fills to what the renderer builds.
714 + ///
715 + /// The first residual holding a **select whose options mark themselves**,
716 + /// which is what quasicoherent `c32bb877` added and what this screen was
717 + /// blocked on. A picker used to say which option was marked once, at the
718 + /// field, as a value makeover-webview compared against each option -- and a
719 + /// residual holds one compiled body per loop, so "exactly one row differs"
720 + /// was not something the body could carry. Filled here with each theme
721 + /// marked in turn and with none, because a residual that had baked one
722 + /// row's mark would still pass on the shape where that row is the marked
723 + /// one.
724 + ///
725 + /// Two tables behind guards as well, each with its own empty state, so the
726 + /// counts are crossed the way the contact panes' are.
727 + #[test]
728 + fn the_ssh_keys_residual_fills_to_what_the_renderer_builds() {
729 + use crate::quasi::ssh_keys::{pane, pane_serve, sample_key, sample_token};
730 + use quasi_axum::Serves as _;
731 +
732 + let installed = crate::theming::console_theme_options(None);
733 + assert!(
734 + installed.len() > 1,
735 + "the picker needs more than one option to be worth filling"
736 + );
737 +
738 + for key_count in [0, 2] {
739 + for token_count in [0, 2] {
740 + let keys: Vec<_> = (0..key_count)
741 + .map(|n| sample_key(&format!("k{n}"), &format!("SHA256:{n}")))
742 + .collect();
743 + let tokens: Vec<_> = (0..token_count)
744 + .map(|n| sample_token(&format!("t{n}"), &format!("token{n}")))
745 + .collect();
746 +
747 + // Every theme marked in turn, and then a list with none marked:
748 + // an account whose stored theme is no longer installed.
749 + let none_marked: Vec<_> = installed
750 + .iter()
751 + .map(|theme| crate::theming::ThemeOption {
752 + id: theme.id.clone(),
753 + name: theme.name.clone(),
754 + selected: false,
755 + })
756 + .collect();
757 + let cases = installed
758 + .iter()
759 + .map(|theme| crate::theming::console_theme_options(Some(&theme.id)))
760 + .chain(std::iter::once(none_marked));
761 +
762 + for themes in cases {
763 + let marked = themes.iter().position(|theme| theme.selected);
764 + assert_eq!(
765 + pane_serve(&SSH_KEYS, "max", &keys, &tokens, &themes),
766 + Webview::new().fragment(&pane("max", &keys, &tokens, &themes)),
767 + "ssh keys, {key_count} keys, {token_count} tokens, theme {marked:?}",
768 + );
769 + }
770 + }
771 + }
772 + }
773 +
712 774 /// The payout card fills to what the renderer builds.
713 775 ///
714 776 /// The first residual holding a **figure strip**, which is the other half
@@ -804,18 +866,16 @@
804 866
805 867 /// The addresses still served by building a `Node` per request.
806 868 ///
807 - /// - `/dashboard/tabs/ssh-keys`: its theme `Select` marks the chosen
808 - /// option by comparing one value against each option, which is
809 - /// structure rather than a gap, and a compiled loop body cannot carry
810 - /// "exactly one row differs". quasicoherent `c32bb877`.
811 869 /// - `/dashboard/tabs/analytics`: its revenue chart is a ceded region,
812 870 /// whose markup the renderer looks up while it renders, so there is
813 - /// nowhere in a residual for it to go. quasicoherent `7d6ad166`.
871 + /// nowhere in a residual for it to go. Ruled 2026-09-08: the answer
872 + /// is to describe the chart rather than to compile around it, so this
873 + /// screen stays here until a chart is in the vocabulary.
874 + /// quasicoherent `7d6ad166`.
814 875 /// - `/git` and `/feed`: both page, and a pager is a supplied `Rest`
815 876 /// with no sentinel. The answer is to describe `more` rather than
816 877 /// supply it. quasicoherent `cbb63155`.
817 878 const OFF_THE_SEAM: &[&str] = &[
818 - crate::quasi::ssh_keys::PATH,
819 879 crate::quasi::user_analytics::PATH,
820 880 crate::quasi::git_explore::PATH,
821 881 crate::quasi::feeds::PATH,
@@ -859,9 +919,9 @@
859 919 fn every_screen_on_the_seam_is_checked() {
860 920 /// Screens with their own filling test: `/use-cases`, `/fan-plus`,
861 921 /// `/c/{username}/{slug}`, `/dashboard/export`, `/git/{owner}`, and the
862 - /// two forum panes, the two contact panes, the payout card and
863 - /// `/creators`.
864 - const HOLED: usize = 11;
922 + /// two forum panes, the two contact panes, the payout card,
923 + /// `/creators` and `/dashboard/tabs/ssh-keys`.
924 + const HOLED: usize = 12;
865 925
866 926 assert_eq!(
867 927 roster().len(),
@@ -102,12 +102,22 @@
102 102 }
103 103
104 104 /// The tab.
105 - pub fn screen(viewer: &Viewer, _request: Request) -> Result<Response, RouteError> {
105 + /// The three reads this pane makes, for the mount that serves it from a
106 + /// residual.
107 + ///
108 + /// Everything the pane needs from one call, because the mount answers the
109 + /// screen and its markup from one read: two closures read the database twice
110 + /// and can disagree about what they saw.
111 + ///
112 + /// Each of these round trips holds a blocking thread. The thing S3 exists to
113 + /// measure; see the module header on `super`.
114 + pub(crate) fn reading(
115 + viewer: &Viewer,
116 + ) -> Result<(String, Vec<KeyView>, Vec<TokenView>, Vec<ThemeOption>), RouteError> {
106 117 let user = viewer.reader()?;
107 118 let user_id = user.id;
119 + let username = user.username.as_ref().to_owned();
108 120
109 - // Three round trips, each holding this blocking thread. The thing S3
110 - // exists to measure; see the module header on `super`.
111 121 let keys = viewer
112 122 .block_on(db::ssh_keys::list_keys_by_user(&viewer.app.db, user_id))
113 123 .map_err(|_| RouteError::internal("your keys could not be read"))?;
@@ -140,9 +150,14 @@
140 150 .collect();
141 151 let themes = crate::theming::console_theme_options(profile.console_theme.as_deref());
142 152
153 + Ok((username, keys, tokens, themes))
154 + }
155 +
156 + pub fn screen(viewer: &Viewer, _request: Request) -> Result<Response, RouteError> {
157 + let (username, keys, tokens, themes) = reading(viewer)?;
143 158 Ok(Response::fragment(
144 159 REGION,
145 - pane(user.username.as_ref(), &keys, &tokens, &themes),
160 + pane(&username, &keys, &tokens, &themes),
146 161 ))
147 162 }
148 163
@@ -202,7 +217,8 @@
202 217 /// Split from the handler so a test can build it without a database, which
203 218 /// is the same split `quasi_spike` used and the reason the description
204 219 /// layer is testable at all: the screen is a value.
205 - shape pane(
220 + #[staged]
221 + pub(crate) shape pane(
206 222 username: &str,
207 223 keys: &[KeyView],
208 224 tokens: &[TokenView],
@@ -213,7 +229,7 @@
213 229 section "SSH Keys";
214 230 text "Manage SSH keys for git clone and push access. \
215 231 Clone URL: git@makenot.work:{username}/{{repo}}.git";
216 - include keys_list(keys);
232 + include each keys_list(keys);
217 233 include add_key_form();
218 234
219 235 section "Console theme";
@@ -223,7 +239,7 @@
223 239 section "Access Tokens (HTTPS)";
224 240 text "Personal access tokens for git over HTTPS. Use a token as the password. \
225 241 Clone URL: https://<token>@makenot.work/{username}/{{repo}}.git";
226 - include tokens_list(tokens);
242 + include each tokens_list(tokens);
227 243 include add_token_form();
228 244 }
229 245 }
@@ -240,60 +256,60 @@
240 256 ///
241 257 /// No paging described here either: every one of these tables is a whole
242 258 /// set the handler already counted.
243 - shape keys_list(keys: &[KeyView]) -> Node;
259 + #[staged]
260 + shape keys_list(keys: &[KeyView]) -> Vec<Node>;
244 261
245 - given keys.is_empty() {
246 - true -> empty "No SSH keys registered.";
247 - otherwise -> table {
248 - column "Fingerprint" {
249 - width Fill;
250 - priority Essential;
251 - }
252 - column "Label" {
253 - width Content;
254 - }
255 - column "Added" {
256 - width Content;
257 - priority Optional;
258 - }
259 - column "" {
260 - width Content;
261 - priority Essential;
262 - }
262 + empty "No SSH keys registered." when keys.is_empty();
263 263
264 - for key in keys.iter() {
265 - cells {
266 - // `19d7602d`. A SHA256 fingerprint is a run of characters a
267 - // reader compares against another one, and a proportional
268 - // face makes that harder than it has to be. Plain rather
269 - // than classified: nothing lexed it and nothing should, so
270 - // what this buys is the monospace and not a colour.
271 - cell "" {
272 - literal key.fingerprint.clone();
273 - }
274 - cell key.label.clone();
275 - cell "Added {key.added}";
276 - cell "" {
277 - // This screen's own route, under its own nest, so the
278 - // answer is the pane it changed. It addressed the API
279 - // route until 2026-08-11 and swapped a whole Askama
280 - // table into this button; see `remove_key`. `awaiting`
281 - // for the same reason the forms carry it: the answer is
282 - // the whole pane rebuilt, so there is a wait with
283 - // nothing on screen saying so. The confirm gates the
284 - // first press, not the second one after it.
285 - act "Remove" to delete "{PATH}/keys/{key.id}" awaiting {
286 - // The template asked with hx-confirm. Said here, a
287 - // terminal host asks in its own way and no host can
288 - // forget to ask.
289 - confirm "Remove this SSH key?";
290 - tone Danger;
291 - }
264 + table {
265 + column "Fingerprint" {
266 + width Fill;
267 + priority Essential;
268 + }
269 + column "Label" {
270 + width Content;
271 + }
272 + column "Added" {
273 + width Content;
274 + priority Optional;
275 + }
276 + column "" {
277 + width Content;
278 + priority Essential;
279 + }
280 +
281 + for key in keys.iter() {
282 + cells {
283 + // `19d7602d`. A SHA256 fingerprint is a run of characters a
284 + // reader compares against another one, and a proportional
285 + // face makes that harder than it has to be. Plain rather
286 + // than classified: nothing lexed it and nothing should, so
287 + // what this buys is the monospace and not a colour.
288 + cell "" {
289 + literal key.fingerprint.clone();
290 + }
291 + cell key.label.clone();
292 + cell "Added {key.added}";
293 + cell "" {
294 + // This screen's own route, under its own nest, so the
295 + // answer is the pane it changed. It addressed the API
296 + // route until 2026-08-11 and swapped a whole Askama
297 + // table into this button; see `remove_key`. `awaiting`
298 + // for the same reason the forms carry it: the answer is
299 + // the whole pane rebuilt, so there is a wait with
300 + // nothing on screen saying so. The confirm gates the
301 + // first press, not the second one after it.
302 + act "Remove" to delete "{PATH}/keys/{key.id}" awaiting {
303 + // The template asked with hx-confirm. Said here, a
304 + // terminal host asks in its own way and no host can
305 + // forget to ask.
306 + confirm "Remove this SSH key?";
307 + tone Danger;
292 308 }
293 309 }
294 310 }
295 311 }
296 - }
312 + } unless keys.is_empty();
297 313 }
298 314
299 315 declare! {
@@ -304,6 +320,8 @@
304 320 /// as one word here and the renderer locks the submit button from it, which
305 321 /// is what `frontend/src/core/loading.ts` was written to do by hand and is
306 322 /// losing ground against.
323 + #[staged]
324 + #[constant]
307 325 shape add_key_form() -> Node;
308 326
309 327 form post "/api/users/me/ssh-keys" awaiting {
@@ -316,26 +334,9 @@
316 334 }
317 335 }
318 336
319 - /// Which theme the reader is on, or nothing.
320 - ///
321 - /// A supplier because `find` takes a closure. It hands back a `String`, which
322 - /// is the smallest type that works and keeps it out of the population, and
323 - /// [`a_theme_is_chosen`] is the predicate that decides whether it is asked for.
324 - fn chosen_theme(themes: &[ThemeOption]) -> String {
325 - themes
326 - .iter()
327 - .find(|theme| theme.selected)
328 - .map(|theme| theme.id.clone())
329 - .unwrap_or_default()
330 - }
331 -
332 - /// Whether any of them is marked.
333 - fn a_theme_is_chosen(themes: &[ThemeOption]) -> bool {
334 - themes.iter().any(|theme| theme.selected)
335 - }
336 -
337 337 declare! {
338 338 /// The console-theme picker.
339 + #[staged]
339 340 shape theme_form(themes: &[ThemeOption]) -> Node;
340 341
341 342 // A PUT, so a second submit overwrites rather than duplicating. Marked
@@ -347,10 +348,17 @@
347 348 hint "Following the terminal picks a light or dark palette from what your \
348 349 terminal reports. Separate from your profile theme, which is what \
349 350 visitors see.";
351 + // Marked per option rather than by a `value` the renderer compares
352 + // against each one. `ThemeOption` already knows per row, so the
353 + // old spelling collapsed that to one string for makeover-webview
354 + // to re-derive, which is one fact stated twice -- and it is the
355 + // form a residual cannot hold, since a compiled loop body cannot
356 + // carry "exactly one row differs". quasicoherent `c32bb877`.
350 357 for theme in themes.iter() {
351 - option Choice::new(theme.id.clone(), theme.name.clone());
358 + option Choice::new(theme.id.clone(), theme.name.clone()) {
359 + chosen when theme.selected;
360 + }
352 361 }
353 - value chosen_theme(themes) when a_theme_is_chosen(themes);
354 362 }
355 363 }
356 364 }
@@ -368,47 +376,47 @@
368 376 ///
369 377 /// No paging described here either: every one of these tables is a whole
370 378 /// set the handler already counted.
371 - shape tokens_list(tokens: &[TokenView]) -> Node;
379 + #[staged]
380 + shape tokens_list(tokens: &[TokenView]) -> Vec<Node>;
372 381
373 - given tokens.is_empty() {
374 - true -> empty "No access tokens.";
375 - otherwise -> table {
376 - column "Name" {
377 - width Fill;
378 - priority Essential;
379 - }
380 - column "Scope" {
381 - width Content;
382 - }
383 - column "Expires" {
384 - width Content;
385 - priority Optional;
386 - }
387 - column "Last used" {
388 - width Content;
389 - priority Optional;
390 - }
391 - column "" {
392 - width Content;
393 - priority Essential;
394 - }
382 + empty "No access tokens." when tokens.is_empty();
395 383
396 - for token in tokens.iter() {
397 - cells {
398 - cell token.name.clone();
399 - cell token.scope;
400 - cell token.expires.clone();
401 - cell token.last_used.clone();
402 - cell "" {
403 - act "Revoke" to delete "{PATH}/tokens/{token.id}" awaiting {
404 - confirm "Revoke this token?";
405 - tone Danger;
406 - }
384 + table {
385 + column "Name" {
386 + width Fill;
387 + priority Essential;
388 + }
389 + column "Scope" {
390 + width Content;
391 + }
392 + column "Expires" {
393 + width Content;
394 + priority Optional;
395 + }
396 + column "Last used" {
397 + width Content;
398 + priority Optional;
399 + }
400 + column "" {
401 + width Content;
402 + priority Essential;
403 + }
404 +
405 + for token in tokens.iter() {
406 + cells {
407 + cell token.name.clone();
408 + cell token.scope;
409 + cell token.expires.clone();
410 + cell token.last_used.clone();
411 + cell "" {
412 + act "Revoke" to delete "{PATH}/tokens/{token.id}" awaiting {
413 + confirm "Revoke this token?";
414 + tone Danger;
407 415 }
408 416 }
409 417 }
410 418 }
411 - }
419 + } unless tokens.is_empty();
412 420 }
413 421
414 422 declare! {
@@ -421,6 +429,8 @@
421 429 /// it at makeover-layout 0.15.0. It did become one word, and the hint came
422 430 /// out with it: the format is the description's now, `layout::DATE_FORMAT`,
423 431 /// so saying it again here would be a second place for it to drift.
432 + #[staged]
433 + #[constant]
424 434 shape add_token_form() -> Node;
425 435
426 436 // Creates a record, and unlike an SSH key the answer is a secret shown
@@ -450,25 +460,53 @@
450 460 Webview::new().with_shell(viewer.shell())
451 461 }
452 462
463 + /// One key as the tests draw it.
464 + ///
465 + /// Module-level rather than inside `mod tests` because `quasi::residuals` needs
466 + /// one too, and `KeyView` is this module's own type. Test-only.
467 + #[cfg(test)]
468 + pub(crate) fn sample_key(id: &str, fingerprint: &str) -> KeyView {
469 + KeyView {
470 + id: id.into(),
471 + fingerprint: fingerprint.into(),
472 + label: "laptop".into(),
473 + added: "Aug 10, 2026".into(),
474 + }
475 + }
476 +
477 + /// One token as the tests draw it. See [`sample_key`].
478 + #[cfg(test)]
479 + pub(crate) fn sample_token(id: &str, name: &str) -> TokenView {
480 + TokenView {
481 + id: id.into(),
482 + name: name.into(),
483 + scope: "Read + push",
484 + expires: "Never".into(),
485 + last_used: "Never".into(),
486 + }
487 + }
488 +
453 489 #[cfg(test)]
454 490 mod tests {
455 491 use super::*;
456 492 use quasi_axum::Serves;
457 493 use quasi_router::Node;
458 494
459 - fn key(id: &str, fingerprint: &str) -> KeyView {
460 - KeyView {
461 - id: id.into(),
462 - fingerprint: fingerprint.into(),
463 - label: "laptop".into(),
464 - added: "Aug 10, 2026".into(),
465 - }
466 - }
495 + use super::{sample_key as key, sample_token};
467 496
468 497 fn render(node: &Node) -> String {
469 498 Webview::new().fragment(node)
470 499 }
471 500
501 + /// The same, for a shape answering a run of nodes rather than one.
502 + ///
503 + /// `keys_list` and `tokens_list` each say a sentence or a table, which is
504 + /// two emissions and therefore a `Vec<Node>`; the pane splices them with
505 + /// `include each`. Rendered here the way the pane's region renders them.
506 + fn render_all(nodes: &[Node]) -> String {
507 + nodes.iter().map(render).collect()
508 + }
509 +
472 510 #[test]
473 511 fn the_region_matches_what_the_strip_draws() {
474 512 // The router says what it changed, through HX-Retarget. If this and the
@@ -515,7 +553,7 @@
515 553
516 554 #[test]
517 555 fn removing_a_key_asks_first_and_every_key_asks_about_itself() {
518 - let html = render(&keys_list(&[
556 + let html = render_all(&keys_list(&[
519 557 key("k1", "SHA256:aaa"),
520 558 key("k2", "SHA256:bbb"),
521 559 ]));
@@ -537,13 +575,7 @@
537 575 let html = render(&pane(
538 576 "max",
539 577 &[key("k1", "SHA256:aaa")],
540 - &[TokenView {
541 - id: "t1".into(),
542 - name: "laptop".into(),
543 - scope: "read",
544 - expires: "Never".into(),
545 - last_used: "Never".into(),
546 - }],
578 + &[sample_token("t1", "laptop")],
547 579 &[],
548 580 ));
549 581
@@ -473,4 +473,73 @@
473 473 ])),
474 474 ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"creators-anchored\" data-menu=\"anchored\" hidden></div></div>")),
475 475 ]);
476 +
477 + pub static SSH_KEYS: ::quasi_router::stage::Residual =
478 + ::quasi_router::stage::Residual::compiled(&[
479 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div id=\"settings-ssh-keys\" class=\"region pane\"><h2 class=\"heading\">SSH Keys</h2><p class=\"text\">Manage SSH keys for git clone and push access. Clone URL: git@makenot.work:")),
480 + ::quasi_router::stage::Op::Hole { scope: 0, id: 0 },
481 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("/{repo}.git</p>")),
482 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
483 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"placeholder\" data-state=\"empty\" role=\"status\" aria-live=\"polite\"><p class=\"placeholder-text\">No SSH keys registered.</p></div>")),
484 +
485 + ])),
486 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
487 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"table\" class=\"table\"><div role=\"row\" class=\"table-head\"><span role=\"columnheader\" class=\"table-heading col-Fingerprint cell-fill cell-keeps\">Fingerprint</span><span role=\"columnheader\" class=\"table-heading col-Label cell-content cell-drops-next\">Label</span><span role=\"columnheader\" class=\"table-heading col-Added cell-content cell-drops-first\">Added</span><span role=\"columnheader\" class=\"table-heading col- cell-content cell-keeps\"></span></div>")),
488 + ::quasi_router::stage::Op::Loop(::std::borrow::Cow::Borrowed(&[
489 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Fingerprint cell-fill cell-keeps\"><code class=\"code\">")),
490 + ::quasi_router::stage::Op::Hole { scope: 1, id: 0 },
491 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</code></div><div class=\"cell col-Label cell-content cell-drops-next cell-value\">")),
492 + ::quasi_router::stage::Op::Hole { scope: 1, id: 1 },
493 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Added cell-content cell-drops-first cell-value\">Added ")),
494 + ::quasi_router::stage::Op::Hole { scope: 1, id: 2 },
495 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col- cell-content cell-keeps\"><span class=\"cell-actions\"><button type=\"button\" class=\"button\" data-tone=\"danger\" data-act hx-delete=\"/dashboard/tabs/ssh-keys/keys/")),
496 + ::quasi_router::stage::Op::Hole { scope: 1, id: 3 },
497 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" hx-confirm=\"Remove this SSH key?\" hx-swap=\"outerMorph\" hx-disable=\"this\" data-awaiting=\"indeterminate\">Remove</button></span></div></div>")),
498 +
499 + ])),
500 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")),
501 +
502 + ])),
503 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<form class=\"form\" hx-post=\"/api/users/me/ssh-keys\" hx-swap=\"outerMorph\" hx-disable=\"find button[type='submit']\" data-awaiting=\"indeterminate\"><div class=\"form-group\"><label class=\"form-label\" for=\"public_key\">Public Key</label><textarea class=\"field\" id=\"public_key\" name=\"public_key\" required aria-describedby=\"public_key-hint\"></textarea><div class=\"form-hint\" id=\"public_key-hint\">Paste the contents of your ~/.ssh/id_ed25519.pub or similar public key file</div></div><div class=\"form-group\"><label class=\"form-label\" for=\"label\">Label</label><input type=\"text\" class=\"field\" id=\"label\" name=\"label\" value=\"\"></div><button type=\"submit\" class=\"button act-submit\">Add SSH Key</button></form><h2 class=\"heading\">Console theme</h2><p class=\"text\">Color palette for your terminal dashboard over ssh makenot.work.</p><form class=\"form\" hx-put=\"/api/users/me/console-theme\" hx-swap=\"outerMorph\" hx-disable=\"find button[type='submit']\" data-awaiting=\"indeterminate\"><div class=\"form-group\"><label class=\"form-label\" for=\"theme_id\">Console theme</label><select class=\"field\" id=\"theme_id\" name=\"theme_id\" aria-describedby=\"theme_id-hint\">")),
504 + ::quasi_router::stage::Op::Loop(::std::borrow::Cow::Borrowed(&[
505 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<option value=\"")),
506 + ::quasi_router::stage::Op::Hole { scope: 2, id: 0 },
507 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\"")),
508 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
509 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(" selected")),
510 +
511 + ])),
512 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed(">")),
513 + ::quasi_router::stage::Op::Hole { scope: 2, id: 1 },
514 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</option>")),
515 +
516 + ])),
517 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</select><div class=\"form-hint\" id=\"theme_id-hint\">Following the terminal picks a light or dark palette from what your terminal reports. Separate from your profile theme, which is what visitors see.</div></div><button type=\"submit\" class=\"button act-submit\">Save Theme</button></form><h2 class=\"heading\">Access Tokens (HTTPS)</h2><p class=\"text\">Personal access tokens for git over HTTPS. Use a token as the password. Clone URL: https://&lt;token&gt;@makenot.work/")),
518 + ::quasi_router::stage::Op::Hole { scope: 0, id: 1 },
519 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("/{repo}.git</p>")),
520 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
521 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"placeholder\" data-state=\"empty\" role=\"status\" aria-live=\"polite\"><p class=\"placeholder-text\">No access tokens.</p></div>")),
522 +
523 + ])),
524 + ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[
525 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"table\" class=\"table\"><div role=\"row\" class=\"table-head\"><span role=\"columnheader\" class=\"table-heading col-Name cell-fill cell-keeps\">Name</span><span role=\"columnheader\" class=\"table-heading col-Scope cell-content cell-drops-next\">Scope</span><span role=\"columnheader\" class=\"table-heading col-Expires cell-content cell-drops-first\">Expires</span><span role=\"columnheader\" class=\"table-heading col-Last-used cell-content cell-drops-first\">Last used</span><span role=\"columnheader\" class=\"table-heading col- cell-content cell-keeps\"></span></div>")),
526 + ::quasi_router::stage::Op::Loop(::std::borrow::Cow::Borrowed(&[
527 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"row\" class=\"table-row\" data-row><div class=\"cell col-Name cell-fill cell-keeps cell-value\">")),
528 + ::quasi_router::stage::Op::Hole { scope: 3, id: 0 },
529 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Scope cell-content cell-drops-next cell-value\">")),
530 + ::quasi_router::stage::Op::Hole { scope: 3, id: 1 },
531 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Expires cell-content cell-drops-first cell-value\">")),
532 + ::quasi_router::stage::Op::Hole { scope: 3, id: 2 },
533 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Last-used cell-content cell-drops-first cell-value\">")),
534 + ::quasi_router::stage::Op::Hole { scope: 3, id: 3 },
535 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col- cell-content cell-keeps\"><span class=\"cell-actions\"><button type=\"button\" class=\"button\" data-tone=\"danger\" data-act hx-delete=\"/dashboard/tabs/ssh-keys/tokens/")),
536 + ::quasi_router::stage::Op::Hole { scope: 3, id: 4 },
537 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\" hx-confirm=\"Revoke this token?\" hx-swap=\"outerMorph\" hx-disable=\"this\" data-awaiting=\"indeterminate\">Revoke</button></span></div></div>")),
538 +
539 + ])),
540 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")),
541 +
542 + ])),
543 + ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<form class=\"form\" hx-post=\"/api/users/me/git-tokens\" hx-swap=\"outerMorph\" hx-disable=\"find button[type='submit']\" data-awaiting=\"indeterminate\"><div class=\"form-group\"><label class=\"form-label\" for=\"name\">Name</label><input type=\"text\" class=\"field\" id=\"name\" name=\"name\" required value=\"\"></div><div class=\"form-group\"><label class=\"form-label\" for=\"expires_on\">Expires (optional)</label><input type=\"date\" class=\"field\" id=\"expires_on\" name=\"expires_on\" aria-describedby=\"expires_on-hint\" value=\"\"><div class=\"form-hint\" id=\"expires_on-hint\">Leave blank for a token that does not expire.</div></div><div class=\"form-group\"><label class=\"form-checkbox-label\"><input type=\"checkbox\" id=\"can_push\" name=\"can_push\"><span>Allow push (write access)</span></label></div><button type=\"submit\" class=\"button act-submit\">Create Token</button></form><div class=\"anchored\" id=\"settings-ssh-keys-anchored\" data-menu=\"anchored\" hidden></div></div>")),
544 + ]);
476 545