max / makenotwork
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
- Claude-Session
- https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
6 files changed,
+242 insertions,
-154 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.105" } | |
| 166 | + | quasi-router = { git = "https://makenot.work/git/max/quasi.git", version = "0.106" } | |
| 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.105" } | |
| 187 | - | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.105" } | |
| 186 | + | quasi-axum = { git = "https://makenot.work/git/max/quasi.git", version = "0.106" } | |
| 187 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.106" } | |
| 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.105" } | |
| 192 | + | quasi-basics = { git = "https://makenot.work/git/max/quasi.git", version = "0.106" } | |
| 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.105" } | |
| 291 | + | quasi-webview = { git = "https://makenot.work/git/max/quasi.git", version = "0.106" } | |
| 292 | 292 | ||
| 293 | 293 | [dev-dependencies] | |
| 294 | 294 | tower = { version = "0.5.3", features = ["util"] } |
| @@ -432,8 +432,18 @@ | |||
| 432 | 432 | if page.current_page < page.total_pages { | |
| 433 | 433 | rest = rest.forward(surface.address(page.current_page + 1)); | |
| 434 | 434 | } | |
| 435 | + | // Which page the reader is on is carried per jump rather than compared | |
| 436 | + | // against the paging inside each renderer. `Jump::here`'s reason is | |
| 437 | + | // `Choice::chosen`'s: a strip is a loop, and a residual holds one compiled | |
| 438 | + | // body per loop. | |
| 435 | 439 | for jump in page.pagination_range { | |
| 436 | - | rest = rest.jumping(*jump as usize, surface.address(*jump)); | |
| 440 | + | let at = *jump as usize; | |
| 441 | + | let jumping = quasi_router::screen::Jump::new(at, surface.address(*jump)); | |
| 442 | + | rest = rest.jumping(if at == page.current_page as usize { | |
| 443 | + | jumping.here() | |
| 444 | + | } else { | |
| 445 | + | jumping | |
| 446 | + | }); | |
| 437 | 447 | } | |
| 438 | 448 | ||
| 439 | 449 | rest | |
| @@ -568,17 +578,23 @@ | |||
| 568 | 578 | } | |
| 569 | 579 | ||
| 570 | 580 | /// A first page offers no way back and a last page no way forward, and the | |
| 571 | - | /// renderer draws each as disabled rather than absent. | |
| 581 | + | /// renderer draws neither rather than drawing one it has to disable. | |
| 582 | + | /// | |
| 583 | + | /// Ruled by Max 2026-09-08, against the disabled control that stood here: a | |
| 584 | + | /// Prev that cannot go back is a control that answers nothing. See | |
| 585 | + | /// `quasi-webview`'s `rest_html`. | |
| 572 | 586 | #[test] | |
| 573 | 587 | fn the_ends_of_the_set_say_so() { | |
| 574 | 588 | let first = library(1, 4); | |
| 575 | - | assert!(first.contains("<button type=\"button\""), "{first}"); | |
| 576 | - | assert!(first.contains("disabled>Prev"), "{first}"); | |
| 577 | - | assert!(!first.contains("disabled>Next"), "{first}"); | |
| 589 | + | assert!(!first.contains("rest-previous"), "{first}"); | |
| 590 | + | assert!(first.contains("rest-next"), "{first}"); | |
| 578 | 591 | ||
| 579 | 592 | let last = library(4, 4); | |
| 580 | - | assert!(last.contains("disabled>Next"), "{last}"); | |
| 581 | - | assert!(!last.contains("disabled>Prev"), "{last}"); | |
| 593 | + | assert!(last.contains("rest-previous"), "{last}"); | |
| 594 | + | assert!(!last.contains("rest-next"), "{last}"); | |
| 595 | + | ||
| 596 | + | assert!(!first.contains("disabled>"), "{first}"); | |
| 597 | + | assert!(!last.contains("disabled>"), "{last}"); | |
| 582 | 598 | } | |
| 583 | 599 | ||
| 584 | 600 | /// One page is no rest at all, which is what `{% if total_pages > 1 %}` |
| @@ -30,7 +30,7 @@ | |||
| 30 | 30 | use makeover_layout as layout; | |
| 31 | 31 | use quasi_declare::declare; | |
| 32 | 32 | use quasi_router::screen::Rest; | |
| 33 | - | use quasi_router::{Action, Document, Request, Response, RouteError}; | |
| 33 | + | use quasi_router::{Action, Document, RouteError}; | |
| 34 | 34 | use quasi_webview::Webview; | |
| 35 | 35 | ||
| 36 | 36 | use crate::{constants, db}; | |
| @@ -44,7 +44,7 @@ | |||
| 44 | 44 | const MEASURE: layout::Measure = layout::Measure::Wide; | |
| 45 | 45 | ||
| 46 | 46 | /// Everything the screen draws, resolved before it is drawn. | |
| 47 | - | struct Loaded { | |
| 47 | + | pub(crate) struct Loaded { | |
| 48 | 48 | repos: Vec<Repo>, | |
| 49 | 49 | page: usize, | |
| 50 | 50 | has_more: bool, | |
| @@ -52,6 +52,26 @@ | |||
| 52 | 52 | signed_in: bool, | |
| 53 | 53 | } | |
| 54 | 54 | ||
| 55 | + | impl Loaded { | |
| 56 | + | /// Where this page starts, which is what a `Rest` counts from. | |
| 57 | + | /// | |
| 58 | + | /// A supplier because the declared form has no arithmetic, and it is the | |
| 59 | + | /// same sum [`load`] makes to ask the database. | |
| 60 | + | fn offset(&self) -> usize { | |
| 61 | + | (self.page - 1).saturating_mul(constants::GIT_REPOS_PER_PAGE) | |
| 62 | + | } | |
| 63 | + | ||
| 64 | + | /// The page back, and the page on. Suppliers for [`offset`](Self::offset)'s | |
| 65 | + | /// reason. | |
| 66 | + | fn previous(&self) -> usize { | |
| 67 | + | self.page.saturating_sub(1) | |
| 68 | + | } | |
| 69 | + | ||
| 70 | + | fn next(&self) -> usize { | |
| 71 | + | self.page + 1 | |
| 72 | + | } | |
| 73 | + | } | |
| 74 | + | ||
| 55 | 75 | /// One repository in the listing. | |
| 56 | 76 | struct Repo { | |
| 57 | 77 | owner: String, | |
| @@ -59,21 +79,21 @@ | |||
| 59 | 79 | description: String, | |
| 60 | 80 | } | |
| 61 | 81 | ||
| 62 | - | /// The page. | |
| 63 | - | pub fn screen(viewer: &super::Viewer, request: Request) -> Result<Response, RouteError> { | |
| 64 | - | // Moved out of the request rather than borrowed: the signature is quasi's. | |
| 65 | - | let carried = request.carried; | |
| 66 | - | // Clamped exactly as the shipped handler clamped it: a page number out of a | |
| 67 | - | // query string is reader input, and the offset it becomes is multiplied. | |
| 82 | + | /// The one read this page makes, for the mount that serves it from a residual. | |
| 83 | + | /// | |
| 84 | + | /// The clamp is the shipped handler's, unchanged: a page number out of a query | |
| 85 | + | /// string is reader input, and the offset it becomes is multiplied. | |
| 86 | + | pub(crate) fn reading( | |
| 87 | + | viewer: &super::Viewer, | |
| 88 | + | carried: &super::Carried, | |
| 89 | + | ) -> Result<Loaded, RouteError> { | |
| 68 | 90 | let page = carried | |
| 69 | - | .get("page") | |
| 91 | + | .asked("page") | |
| 70 | 92 | .and_then(|value| value.trim().parse::<usize>().ok()) | |
| 71 | 93 | .unwrap_or(1) | |
| 72 | 94 | .clamp(1, 10_000); | |
| 73 | 95 | ||
| 74 | - | let loaded = load(viewer, page)?; | |
| 75 | - | ||
| 76 | - | Ok(page_screen(&loaded).into()) | |
| 96 | + | load(viewer, page) | |
| 77 | 97 | } | |
| 78 | 98 | ||
| 79 | 99 | /// Read one page of public repositories, plus one row to learn whether there is | |
| @@ -110,38 +130,44 @@ | |||
| 110 | 130 | ||
| 111 | 131 | declare! { | |
| 112 | 132 | /// The whole document: the title, the measure, the body. | |
| 113 | - | shape page_screen(loaded: &Loaded) -> Screen; | |
| 133 | + | pub(crate) shape page_screen(loaded: &Loaded) -> Screen; | |
| 114 | 134 | ||
| 115 | 135 | screen single "Repositories - Git - Makenotwork" { | |
| 116 | 136 | measured MEASURE; | |
| 117 | 137 | documented Document::default().classed(crate::shell::body_class(MEASURE, &[])); | |
| 118 | 138 | summarised "Public repositories on Makenotwork, with git notes rendered on every commit."; | |
| 119 | 139 | ||
| 120 | - | region PAGE_REGION as Pane { | |
| 121 | - | page "Repositories"; | |
| 140 | + | include page_region(loaded); | |
| 141 | + | } | |
| 142 | + | } | |
| 122 | 143 | ||
| 123 | - | // Notes are the one thing this browser does that no other forge | |
| 124 | - | // does, and nothing on a repository page says so to somebody who | |
| 125 | - | // has never seen one. The landing page is where that sentence | |
| 126 | - | // reaches everybody. | |
| 127 | - | include super::own_prose( | |
| 128 | - | "Every repository here renders [git notes](/docs/git-notes): annotation attached \ | |
| 129 | - | to a commit without rewriting it, stored in the repository and carried by a clone." | |
| 130 | - | ); | |
| 144 | + | declare! { | |
| 145 | + | /// The page's one region, split out so it can be staged. | |
| 146 | + | #[staged] | |
| 147 | + | pub(crate) shape page_region(loaded: &Loaded) -> Slot; | |
| 131 | 148 | ||
| 132 | - | // The only route to an annotation whose target repository is gone: | |
| 133 | - | // nothing else links to it once there is no commit page to link | |
| 134 | - | // from. | |
| 135 | - | include super::own_prose( | |
| 136 | - | "[Your annotations](/git/my-annotations), private to you, across every repository \ | |
| 137 | - | you have read here." | |
| 138 | - | ) when loaded.signed_in; | |
| 149 | + | region PAGE_REGION as Pane { | |
| 150 | + | page "Repositories"; | |
| 139 | 151 | ||
| 140 | - | given loaded.repos.is_empty() { | |
| 141 | - | true -> empty "No public repositories yet."; | |
| 142 | - | otherwise -> include listing(loaded); | |
| 143 | - | } | |
| 144 | - | } | |
| 152 | + | // Notes are the one thing this browser does that no other forge | |
| 153 | + | // does, and nothing on a repository page says so to somebody who | |
| 154 | + | // has never seen one. The landing page is where that sentence | |
| 155 | + | // reaches everybody. | |
| 156 | + | include super::own_prose( | |
| 157 | + | "Every repository here renders [git notes](/docs/git-notes): annotation attached \ | |
| 158 | + | to a commit without rewriting it, stored in the repository and carried by a clone." | |
| 159 | + | ); | |
| 160 | + | ||
| 161 | + | // The only route to an annotation whose target repository is gone: | |
| 162 | + | // nothing else links to it once there is no commit page to link | |
| 163 | + | // from. | |
| 164 | + | include super::own_prose( | |
| 165 | + | "[Your annotations](/git/my-annotations), private to you, across every repository \ | |
| 166 | + | you have read here." | |
| 167 | + | ) when loaded.signed_in; | |
| 168 | + | ||
| 169 | + | empty "No public repositories yet." when loaded.repos.is_empty(); | |
| 170 | + | include listing(loaded) unless loaded.repos.is_empty(); | |
| 145 | 171 | } | |
| 146 | 172 | } | |
| 147 | 173 | ||
| @@ -151,6 +177,7 @@ | |||
| 151 | 177 | /// Two columns, two cells, all four written here, so the row stays | |
| 152 | 178 | /// positional: naming would buy nothing a reader cannot already check by | |
| 153 | 179 | /// looking up four lines. | |
| 180 | + | #[staged] | |
| 154 | 181 | shape listing(loaded: &Loaded) -> Node; | |
| 155 | 182 | ||
| 156 | 183 | table { | |
| @@ -171,35 +198,43 @@ | |||
| 171 | 198 | } | |
| 172 | 199 | ||
| 173 | 200 | // One page of one is the whole listing, and the pager is what says so. | |
| 174 | - | // The condition is `rest`'s own, said here rather than answered with an | |
| 175 | - | // `Option`: a table either shows what it has not shown or does not, and | |
| 176 | - | // that is what a guard is for. | |
| 177 | - | more rest(loaded) when loaded.page over 1 or loaded.has_more; | |
| 201 | + | // A table either shows what it has not shown or does not, and that is | |
| 202 | + | // what a guard is for. | |
| 203 | + | // | |
| 204 | + | // Described rather than supplied. A `Rest` has no sentinel and a | |
| 205 | + | // supplier of one takes a struct, so a pager handed over whole was a | |
| 206 | + | // hole the residual could not hold; everything it carries is a number | |
| 207 | + | // or an address, which are the two things a residual already has. | |
| 208 | + | // The directions are written back-then-forward because that is the | |
| 209 | + | // order they draw in, which `quasi-declare` holds this to. | |
| 210 | + | // quasicoherent `cbb63155`. | |
| 211 | + | more Rest::page(loaded.offset(), constants::GIT_REPOS_PER_PAGE) { | |
| 212 | + | back Action::get("{PATH}?page={loaded.previous()}").navigating() | |
| 213 | + | when loaded.page over 1; | |
| 214 | + | forward Action::get("{PATH}?page={loaded.next()}").navigating() | |
| 215 | + | when loaded.has_more; | |
| 216 | + | } when loaded.page over 1 or loaded.has_more; | |
| 178 | 217 | } | |
| 179 | 218 | } | |
| 180 | 219 | ||
| 181 | - | /// What the reader has not been shown. | |
| 220 | + | /// One page of repositories as the tests draw it. | |
| 182 | 221 | /// | |
| 183 | - | /// Prev/next only. The template drew `Newer` and `Older` and no numbers, and | |
| 184 | - | /// `Rest` draws numbers only for a screen that calls `jumping`, so this is a | |
| 185 | - | /// parity conversion rather than a reduction. | |
| 186 | - | /// | |
| 187 | - | /// Whether there is anything to show is [`listing`]'s guard rather than an | |
| 188 | - | /// `Option` here: built for a page that offers neither direction this is a | |
| 189 | - | /// `Rest` with no controls, and nothing asks for it. | |
| 190 | - | fn rest(loaded: &Loaded) -> Rest { | |
| 191 | - | let per = constants::GIT_REPOS_PER_PAGE; | |
| 192 | - | let from = (loaded.page - 1) * per; | |
| 193 | - | ||
| 194 | - | let mut rest = Rest::page(from, per); | |
| 195 | - | if loaded.page > 1 { | |
| 196 | - | rest = rest.back(Action::get(format!("{PATH}?page={}", loaded.page - 1)).navigating()); | |
| 222 | + | /// Module-level rather than inside `mod tests` because `quasi::residuals` needs | |
| 223 | + | /// one too, and `Loaded` is this module's own type. Test-only. | |
| 224 | + | #[cfg(test)] | |
| 225 | + | pub(crate) fn loaded(count: usize, page: usize, has_more: bool, signed_in: bool) -> Loaded { | |
| 226 | + | Loaded { | |
| 227 | + | repos: (0..count) | |
| 228 | + | .map(|n| Repo { | |
| 229 | + | owner: "ada".into(), | |
| 230 | + | name: format!("repo{n}"), | |
| 231 | + | description: format!("Number {n}"), | |
| 232 | + | }) | |
| 233 | + | .collect(), | |
| 234 | + | page, | |
| 235 | + | has_more, | |
| 236 | + | signed_in, | |
| 197 | 237 | } | |
| 198 | - | if loaded.has_more { | |
| 199 | - | rest = rest.forward(Action::get(format!("{PATH}?page={}", loaded.page + 1)).navigating()); | |
| 200 | - | } | |
| 201 | - | ||
| 202 | - | rest | |
| 203 | 238 | } | |
| 204 | 239 | ||
| 205 | 240 | /// The document this screen is drawn in. | |
| @@ -216,21 +251,6 @@ | |||
| 216 | 251 | mod tests { | |
| 217 | 252 | use super::*; | |
| 218 | 253 | ||
| 219 | - | fn loaded(count: usize, page: usize, has_more: bool, signed_in: bool) -> Loaded { | |
| 220 | - | Loaded { | |
| 221 | - | repos: (0..count) | |
| 222 | - | .map(|n| Repo { | |
| 223 | - | owner: "ada".into(), | |
| 224 | - | name: format!("repo{n}"), | |
| 225 | - | description: format!("Number {n}"), | |
| 226 | - | }) | |
| 227 | - | .collect(), | |
| 228 | - | page, | |
| 229 | - | has_more, | |
| 230 | - | signed_in, | |
| 231 | - | } | |
| 232 | - | } | |
| 233 | - | ||
| 234 | 254 | fn html(loaded: &Loaded) -> String { | |
| 235 | 255 | use quasi_axum::Serves as _; | |
| 236 | 256 |
| @@ -835,11 +835,18 @@ | |||
| 835 | 835 | // tests did not run either, because an undeclared module is not compiled. | |
| 836 | 836 | ( | |
| 837 | 837 | git_explore::PATH, | |
| 838 | - | public_document_mount( | |
| 838 | + | served_document_mount( | |
| 839 | 839 | app, | |
| 840 | 840 | git_explore::PATH, | |
| 841 | - | git_explore::screen, | |
| 842 | 841 | git_explore::renderer, | |
| 842 | + | |viewer, carried| { | |
| 843 | + | let loaded = git_explore::reading(viewer, carried)?; | |
| 844 | + | Ok(Served { | |
| 845 | + | screen: git_explore::page_screen(&loaded), | |
| 846 | + | markup: git_explore::page_region_serve(&residuals::GIT_EXPLORE, &loaded) | |
| 847 | + | .into(), | |
| 848 | + | }) | |
| 849 | + | }, | |
| 843 | 850 | ) | |
| 844 | 851 | .layer(tower_governor::GovernorLayer::new( | |
| 845 | 852 | crate::helpers::rate_limiter_ms( | |
| @@ -952,54 +959,14 @@ | |||
| 952 | 959 | ) | |
| 953 | 960 | } | |
| 954 | 961 | ||
| 955 | - | /// A described document a reader NAVIGATES to with or without a session. | |
| 956 | - | /// | |
| 957 | - | /// [`document_mount`] with the two things that make it gated removed: the | |
| 958 | - | /// factory is built on [`Audience::Anyone`], so a signed-out request builds a | |
| 959 | - | /// viewer rather than being refused, and there is no [`signed_in`] layer in | |
| 960 | - | /// front of it, so nothing turns that into a 401. What is left is identical, | |
| 961 | - | /// including the exact-address registration -- see [`document_mount`] for why a | |
| 962 | - | /// document is not mounted as a nest. | |
| 963 | - | /// | |
| 964 | - | /// The screens here are the sessionless pages: the ones whose whole purpose is | |
| 965 | - | /// to be reachable by somebody who cannot sign in. A page that merely *reads* | |
| 966 | - | /// better when signed in is still gated; the test is whether refusing a visitor | |
| 967 | - | /// is the right answer. | |
| 968 | - | /// | |
| 969 | - | /// # Layers go on the returned router, at the call site | |
| 970 | - | /// | |
| 971 | - | /// This returns an `axum::Router`, so a mount that needs middleware takes it | |
| 972 | - | /// with `.layer(..)` where it is registered rather than through a parameter | |
| 973 | - | /// here. That matters because the middleware is per-address rather than per | |
| 974 | - | /// mount kind: the git browse addresses carry a per-IP cap and the marketing | |
| 975 | - | /// pages carry none, and threading an `Option<Layer>` through every call would | |
| 976 | - | /// make the mount know about a policy that belongs to the route. | |
| 977 | - | /// | |
| 978 | - | /// **Check what the address carried before moving it.** A route lifted out of a | |
| 979 | - | /// router with a `route_layer` silently loses that layer, and for the git tree | |
| 980 | - | /// that would be a rate limit removed from a route that walks repositories on | |
| 981 | - | /// disk. See `git_repos`. | |
| 982 | - | fn public_document_mount( | |
| 983 | - | app: &AppState, | |
| 984 | - | path: &'static str, | |
| 985 | - | screen: Screen, | |
| 986 | - | renderer: fn(&Viewer) -> quasi_webview::Webview, | |
| 987 | - | ) -> axum::Router { | |
| 988 | - | let router = quasi_router::Router::<Viewer>::new().get(path, screen); | |
| 989 | - | quasi_axum::Adapter::per_viewer( | |
| 990 | - | router, | |
| 991 | - | viewer_factory(app.clone(), Audience::Anyone), | |
| 992 | - | move |viewer, _, _| renderer(viewer), | |
| 993 | - | ) | |
| 994 | - | .into_router() | |
| 995 | - | } | |
| 996 | - | ||
| 997 | 962 | /// A public document whose regions are already written, mounted. | |
| 998 | 963 | /// | |
| 999 | - | /// The seam's mount, shared by every screen on it. It is not | |
| 1000 | - | /// [`public_document_mount`] and cannot be: that one builds a | |
| 1001 | - | /// `quasi_axum::Adapter`, whose whole job is to call a handler that answers a | |
| 1002 | - | /// `Screen` and render it, and there is no `Screen` here to answer with. The | |
| 964 | + | /// The seam's mount, shared by every screen on it. It replaced an adapter mount | |
| 965 | + | /// that built a `quasi_axum::Adapter`, whose whole job is to call a handler that | |
| 966 | + | /// answers a `Screen` and render it, and there is no `Screen` here to answer | |
| 967 | + | /// with -- so the two could never have been one, and once `/git` moved across on | |
| 968 | + | /// 2026-09-08 there was no public document left on the adapter and that mount | |
| 969 | + | /// was deleted, and so was the handler it called. The | |
| 1003 | 970 | /// markup was derived on a build machine, so answering is writing the document | |
| 1004 | 971 | /// around it (quasicoherent `793d99dd`). | |
| 1005 | 972 | /// | |
| @@ -1047,13 +1014,12 @@ | |||
| 1047 | 1014 | ||
| 1048 | 1015 | /// A gated document whose regions are already written, mounted. | |
| 1049 | 1016 | /// | |
| 1050 | - | /// [`served_document_mount`] with the two things [`public_document_mount`] | |
| 1051 | - | /// removes put back: the factory is built on [`Audience::Reader`], so a | |
| 1052 | - | /// signed-out request is refused rather than answered, and [`signed_in`] runs in | |
| 1053 | - | /// front so that refusal is the branded 401 with its way back in rather than a | |
| 1054 | - | /// bare 403. The relationship is exactly [`document_mount`]'s to | |
| 1055 | - | /// [`public_document_mount`], and it is written twice for the same reason those | |
| 1056 | - | /// are: what a mount refuses is what a test can press it for. | |
| 1017 | + | /// [`served_document_mount`] with the two things a public mount leaves out put | |
| 1018 | + | /// back: the factory is built on [`Audience::Reader`], so a signed-out request | |
| 1019 | + | /// is refused rather than answered, and [`signed_in`] runs in front so that | |
| 1020 | + | /// refusal is the branded 401 with its way back in rather than a bare 403. The | |
| 1021 | + | /// two are written out separately rather than parameterised, because what a | |
| 1022 | + | /// mount refuses is what a test can press it for. | |
| 1057 | 1023 | fn served_gated_mount( | |
| 1058 | 1024 | app: &AppState, | |
| 1059 | 1025 | path: &'static str, | |
| @@ -1545,13 +1511,13 @@ | |||
| 1545 | 1511 | .split_once("\n}") | |
| 1546 | 1512 | .expect("public_document_mounts ends") | |
| 1547 | 1513 | .0; | |
| 1548 | - | // Two mount styles, and both are public documents. Most screens go | |
| 1549 | - | // through the adapter, which calls a handler that answers a `Screen` | |
| 1550 | - | // and renders it. A screen on the residual seam has no `Screen` to | |
| 1551 | - | // answer with, so it takes [`served_document_mount`] instead | |
| 1552 | - | // (quasicoherent `793d99dd`). Counting only the first style would let a | |
| 1553 | - | // screen leave `PUBLIC_DOCUMENT_PATHS` unnoticed by moving onto the | |
| 1554 | - | // seam, which is the drift this test exists to catch. | |
| 1514 | + | // Both mount styles, though only one of them has a caller left. Every | |
| 1515 | + | // public document is on the residual seam as of 2026-09-08 and takes | |
| 1516 | + | // `served_document_mount`, which answers no `Screen`; the adapter mount | |
| 1517 | + | // that answered one is gone. The first count stays because what this | |
| 1518 | + | // test exists to catch is a screen leaving `PUBLIC_DOCUMENT_PATHS` | |
| 1519 | + | // unnoticed by changing how it is mounted, and a count that only knows | |
| 1520 | + | // today's style would not catch it changing back. | |
| 1555 | 1521 | let adapted = mounted.matches("public_document_mount(").count(); | |
| 1556 | 1522 | let served = mounted.matches("served_document_mount(").count(); | |
| 1557 | 1523 | assert_eq!( |
| @@ -102,6 +102,9 @@ | |||
| 102 | 102 | Node::Region(super::creators::page_region_staged(plan)) | |
| 103 | 103 | }), | |
| 104 | 104 | ("SSH_KEYS", |plan| super::ssh_keys::pane_staged(plan)), | |
| 105 | + | ("GIT_EXPLORE", |plan| { | |
| 106 | + | Node::Region(super::git_explore::page_region_staged(plan)) | |
| 107 | + | }), | |
| 105 | 108 | ] | |
| 106 | 109 | } | |
| 107 | 110 | ||
| @@ -771,6 +774,38 @@ | |||
| 771 | 774 | } | |
| 772 | 775 | } | |
| 773 | 776 | ||
| 777 | + | /// The git listing fills to what the renderer builds, at every page shape. | |
| 778 | + | /// | |
| 779 | + | /// The first residual holding a **described pager** (quasicoherent | |
| 780 | + | /// `cbb63155`). A pager used to be a `Rest` the screen supplied whole, | |
| 781 | + | /// which has no sentinel, so a paged screen could not reach the seam at | |
| 782 | + | /// all. Said as a description it is a `Rest` settled by its own body, one | |
| 783 | + | /// guard per direction, and everything it carries is a number or an | |
| 784 | + | /// address. | |
| 785 | + | /// | |
| 786 | + | /// Four page shapes, because a pager has four and each drops a different | |
| 787 | + | /// branch: the first page, a middle one, the last, and the single page that | |
| 788 | + | /// draws no pager. Crossed against an empty listing and a signed-out | |
| 789 | + | /// reader, which are the region's other two guards. | |
| 790 | + | #[test] | |
| 791 | + | fn the_git_listing_residual_fills_to_what_the_renderer_builds() { | |
| 792 | + | use crate::quasi::git_explore::{loaded, page_region, page_region_serve}; | |
| 793 | + | use quasi_axum::Serves as _; | |
| 794 | + | ||
| 795 | + | for (page, has_more) in [(1, true), (2, true), (3, false), (1, false)] { | |
| 796 | + | for count in [0, 3] { | |
| 797 | + | for signed_in in [true, false] { | |
| 798 | + | let held = loaded(count, page, has_more, signed_in); | |
| 799 | + | assert_eq!( | |
| 800 | + | page_region_serve(&GIT_EXPLORE, &held), | |
| 801 | + | Webview::new().fragment(&Node::Region(page_region(&held))), | |
| 802 | + | "page {page}, has_more {has_more}, {count} repos, signed in {signed_in}", | |
| 803 | + | ); | |
| 804 | + | } | |
| 805 | + | } | |
| 806 | + | } | |
| 807 | + | } | |
| 808 | + | ||
| 774 | 809 | /// The payout card fills to what the renderer builds. | |
| 775 | 810 | /// | |
| 776 | 811 | /// The first residual holding a **figure strip**, which is the other half | |
| @@ -872,12 +907,12 @@ | |||
| 872 | 907 | /// is to describe the chart rather than to compile around it, so this | |
| 873 | 908 | /// screen stays here until a chart is in the vocabulary. | |
| 874 | 909 | /// quasicoherent `7d6ad166`. | |
| 875 | - | /// - `/git` and `/feed`: both page, and a pager is a supplied `Rest` | |
| 876 | - | /// with no sentinel. The answer is to describe `more` rather than | |
| 877 | - | /// supply it. quasicoherent `cbb63155`. | |
| 910 | + | /// - `/feed`: pages with a numbered strip, which prints each page | |
| 911 | + | /// number and marks the one being read by comparing it against the | |
| 912 | + | /// paging -- "exactly one row differs" inside a loop, which takes the | |
| 913 | + | /// Select's answer. quasicoherent `cbb63155`. | |
| 878 | 914 | const OFF_THE_SEAM: &[&str] = &[ | |
| 879 | 915 | crate::quasi::user_analytics::PATH, | |
| 880 | - | crate::quasi::git_explore::PATH, | |
| 881 | 916 | crate::quasi::feeds::PATH, | |
| 882 | 917 | ]; | |
| 883 | 918 | ||
| @@ -920,8 +955,8 @@ | |||
| 920 | 955 | /// Screens with their own filling test: `/use-cases`, `/fan-plus`, | |
| 921 | 956 | /// `/c/{username}/{slug}`, `/dashboard/export`, `/git/{owner}`, and the | |
| 922 | 957 | /// two forum panes, the two contact panes, the payout card, | |
| 923 | - | /// `/creators` and `/dashboard/tabs/ssh-keys`. | |
| 924 | - | const HOLED: usize = 12; | |
| 958 | + | /// `/creators`, `/dashboard/tabs/ssh-keys` and `/git`. | |
| 959 | + | const HOLED: usize = 13; | |
| 925 | 960 | ||
| 926 | 961 | assert_eq!( | |
| 927 | 962 | roster().len(), |
| @@ -542,4 +542,55 @@ | |||
| 542 | 542 | ])), | |
| 543 | 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 | 544 | ]); | |
| 545 | + | ||
| 546 | + | pub static GIT_EXPLORE: ::quasi_router::stage::Residual = | |
| 547 | + | ::quasi_router::stage::Residual::compiled(&[ | |
| 548 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div id=\"git-explore\" class=\"region pane\"><h1 class=\"heading\">Repositories</h1><div class=\"rich\" data-disable-scripting><p>Every repository here renders <a href=\"/docs/git-notes\" rel=\"noopener noreferrer\">git notes</a>: annotation attached to a commit without rewriting it, stored in the repository and carried by a clone.</p>\n</div>")), | |
| 549 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 550 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"rich\" data-disable-scripting><p><a href=\"/git/my-annotations\" rel=\"noopener noreferrer\">Your annotations</a>, private to you, across every repository you have read here.</p>\n</div>")), | |
| 551 | + | ||
| 552 | + | ])), | |
| 553 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 554 | + | ::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 public repositories yet.</p></div>")), | |
| 555 | + | ||
| 556 | + | ])), | |
| 557 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 558 | + | ::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-Repository cell-content cell-keeps\">Repository</span><span role=\"columnheader\" class=\"table-heading col-Description cell-fill cell-drops-next\">Description</span></div>")), | |
| 559 | + | ::quasi_router::stage::Op::Loop(::std::borrow::Cow::Borrowed(&[ | |
| 560 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div role=\"row\" class=\"table-row\" data-row href=\"/git/")), | |
| 561 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 3 }, | |
| 562 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("/")), | |
| 563 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 4 }, | |
| 564 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\"><div class=\"cell col-Repository cell-content cell-keeps cell-value\">")), | |
| 565 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 0 }, | |
| 566 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("/")), | |
| 567 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 1 }, | |
| 568 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div><div class=\"cell col-Description cell-fill cell-drops-next cell-value\">")), | |
| 569 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 2 }, | |
| 570 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div></div>")), | |
| 571 | + | ||
| 572 | + | ])), | |
| 573 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")), | |
| 574 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 575 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"rest\">")), | |
| 576 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 577 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<a class=\"button rest-previous\" href=\"/git?page=")), | |
| 578 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 6 }, | |
| 579 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\">Prev</a>")), | |
| 580 | + | ||
| 581 | + | ])), | |
| 582 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<span class=\"rest-position\">Show more</span>")), | |
| 583 | + | ::quasi_router::stage::Op::Branch(::std::borrow::Cow::Borrowed(&[ | |
| 584 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<a class=\"button rest-next\" href=\"/git?page=")), | |
| 585 | + | ::quasi_router::stage::Op::Hole { scope: 1, id: 7 }, | |
| 586 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("\">Next</a>")), | |
| 587 | + | ||
| 588 | + | ])), | |
| 589 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("</div>")), | |
| 590 | + | ||
| 591 | + | ])), | |
| 592 | + | ||
| 593 | + | ])), | |
| 594 | + | ::quasi_router::stage::Op::Lit(::std::borrow::Cow::Borrowed("<div class=\"anchored\" id=\"git-explore-anchored\" data-menu=\"anchored\" hidden></div></div>")), | |
| 595 | + | ]); | |
| 545 | 596 |