//! The description layer: screens served through quasi rather than Askama. //! //! Wiki note `mnw-server-conversion-plan`, step S3 onward. `crate::shell` //! already put the document head under quasi's renderer (S1); this is where //! screens themselves start moving. //! //! Each of the seven converted screens shipped one at a time behind a //! `QUASI_SCREENS` switch, so a conversion went out when its own test was green //! and reverted by editing an env var. All seven are flipped and the switch is //! deleted (`64b33b26`): [`mounts`] is unconditional, the route tables register //! no Askama counterpart, and there is nothing left to revert to. A screen added //! here is live the moment it is mounted. //! //! # Why a per-request state and not a per-request handler argument //! //! `quasi_router::Handler = fn(&S, Request)`. `S` is the app, and on every //! other host in the tree there is one app and one viewer for the life of the //! process. A server has one process and many viewers, and that is the only //! assumption it breaks, so the fix is that the adapter builds `S` per request //! rather than that the handler grows a parameter. `Adapter::per_viewer` is //! that: the factory runs in async context, where the session lookup already //! lives, and hands the sync router a [`Viewer`] with the answer already in it. //! //! # The cost this exists to measure //! //! The router is sync (quasi's decision 6, taken for hosts with no runtime), so //! quasi-axum dispatches on `spawn_blocking` and a handler reaching sqlx does it //! through `Handle::block_on`. Every described request therefore holds a //! blocking-pool thread for the length of its database round trips. That is //! fine on a desktop app over rusqlite and is an open question on the one host //! in the tree with many concurrent readers. It is not arguable, only //! measurable: see `tests/load` and the S3 numbers in the wiki note. use quasi_declare::declare; use tokio::runtime::Handle; use axum::extract::FromRef; use crate::AppState; use crate::auth::SessionUser; pub mod auth_pages; pub mod buyer_contacts; pub mod cart_act; pub mod clip_acts; pub mod collections; pub mod creators; pub mod custom_page; pub mod discover_search; pub mod discover_typeahead; pub mod embeds; pub mod export_act; pub mod export_portal; pub mod fan_plus; pub mod feeds; pub mod follow; pub mod forum_memberships; pub mod git_blame; pub mod git_browse; pub mod git_commit; pub mod git_explore; pub mod git_repos; pub mod item_files; pub mod item_sales; pub mod item_tabs; pub mod library_acts; pub mod library_contacts; pub mod library_tabs; pub mod license_key_act; pub mod link_remove_act; pub mod literal; pub mod media_picker; pub mod payout_summary; pub mod policy; pub mod pricing; pub mod project; pub mod project_analytics; pub mod project_blog; pub mod project_content; pub mod project_members; pub mod project_overview; pub mod project_tabs; pub mod promo_code_acts; pub mod repo_acts; pub mod residuals; pub mod rich_field; pub mod schedule_field; pub mod session_acts; pub mod settings_tabs; pub mod shortcuts; pub mod ssh_keys; pub mod team; pub mod tip; pub mod upload_field; pub mod use_cases; pub mod user; pub mod user_analytics; pub mod user_projects; pub mod user_support; pub mod user_tabs; pub mod version_delete_act; pub mod widgets; /// The state one request is answered against. /// /// Built per request by the adapter's factory, which is the whole of quasi's /// answer to a server: everything resolvable from the request head is loaded /// before the sync router runs, and a handler reads it off `&S` the way every /// other host's handler reads its app. pub struct Viewer { /// The long-lived application state. Cloning it clones handles, not data. pub app: AppState, /// Who is asking, when anybody is. /// /// Resolved and revocation-checked by [`crate::auth::authenticate`], the /// same path the extractor takes. /// /// `None` only on a mount built with [`Audience::Anyone`], which is the /// public documents: a screen a reader can reach with no session, whose /// header and controls differ by whether one is held. Every other mount /// refuses before the handler runs, so a screen behind [`mount`], /// [`writes_only`] or a served document mount can read it through /// [`reader`](Self::reader) and never see the refusal that method can /// return. /// /// The field is `pub` and the accessor exists beside it because both /// readings are legitimate: a gated screen wants the user and treats /// absence as impossible, and a public screen wants the option and treats /// absence as an ordinary state. pub user: Option, /// The runtime the request arrived on, so a sync handler can reach the /// async database. Captured in the factory rather than read inside the /// handler: `Handle::current` works on a blocking thread today, and /// depending on that is depending on where quasi-axum happens to dispatch. pub runtime: Handle, /// This session's CSRF token, for the shell to hand to the document. /// /// Resolved in the factory rather than in a renderer because minting one is /// an async session write and a renderer is sync. It is the same token the /// Askama pages carry: [`crate::csrf::get_or_create_token`] is /// get-or-create, so a described page and a templated one in the same /// session agree, and validation is one comparison either way. pub csrf: String, /// This request's session-tracking id, when it has one. /// /// The same class of fact as [`csrf`](Self::csrf) and resolved the same /// way: reading it is an async session lookup, so the factory does it and /// the sync handler reads the answer off `&S`. It is what lets a screen /// tell the reader's own row apart from the rest, which /// `user_sessions` needs twice over: the `Current` badge, and the one row /// that offers no `Sign out`. /// /// `None` is a real state rather than a failure. A session predating /// `crate::auth::SESSION_TRACKING_KEY` carries no tracking id, and a /// screen answering for one marks no row as current. pub session_id: Option, } impl Viewer { /// Run a database future from inside a sync handler. /// /// The blocking hop, named in one place so the thing being measured is /// countable rather than spread across every handler. Every call holds this /// blocking thread until the query answers. pub fn block_on(&self, future: F) -> F::Output { self.runtime.block_on(future) } /// Who is asking, on a mount that guarantees somebody is. /// /// The gated mounts resolve the session before the handler runs and refuse /// without one, so this cannot fail there. It returns a `Result` rather /// than unwrapping because that guarantee lives in the mount rather than in /// the type: if a screen is ever moved onto a public mount, the failure is /// a refusal the reader can read instead of a panic in a blocking thread. /// /// A screen that genuinely serves both audiences reads /// [`user`](Self::user) directly instead. pub fn reader(&self) -> Result<&SessionUser, quasi_router::RouteError> { self.user .as_ref() .ok_or_else(|| quasi_router::RouteError::denied("sign in to continue")) } /// The shell every described screen is drawn in. /// /// Here rather than in each screen's `renderer` because of what it carries: /// a described page whose shell does not declare the session token has /// every write on it refused, and the five screens that wrote /// `Shell::under("/static")` out by hand were five chances to forget. A /// screen that wants more says so on top of this; a screen that says /// nothing gets the token anyway. /// /// The layer order matches `crate::shell`, which is the Askama half of the /// same document: `makeover` is prepended by the renderer, and the site's /// own sheets live in `components`. #[must_use] pub fn shell(&self) -> quasi_webview::Shell { quasi_webview::Shell::under("/static") .layered(["base", "components", "responsive"]) // Every described write is an htmx request, and htmx inherits this // from the body, so one declaration covers the whole document. .sending("X-CSRF-Token", &self.csrf) } /// The shell a described screen that owns its whole DOCUMENT is drawn in. /// /// [`shell`](Self::shell) is right for a fragment landing inside an Askama /// page, which already has the head, the tail and the token meta. A /// document owes all three itself: [`crate::shell::described`] is the same /// builder `base.html` renders through, [`crate::shell::body_last`] is the /// toast container and the classic shims, and the token meta is what the /// pre-module scripts read. /// /// The meta is not redundant with `Shell::sending`. That covers htmx, and /// `frontend/src/core/net.ts`, `frontend/src/core/htmx-glue.ts`, /// `static/passkey.js` and `static/project-sections.js` all read /// `meta[name=csrf-token]` instead. `/pricing` needed none of it because it /// holds no session. #[must_use] pub fn document_shell(&self) -> quasi_webview::Shell { document_shell(&self.csrf) } } /// The shell a described document is drawn in, by the token it carries. /// /// [`Viewer::document_shell`] is this with the token already in hand. It is a /// free function as well because a screen whose address carries a wildcard /// segment cannot be a quasi route at all -- `quasi_router`'s matcher takes /// `{name}` and nothing else -- so the git file and blame views are built here /// and served from their own axum handlers, which hold a token and no /// [`Viewer`]. #[must_use] pub fn document_shell(csrf: &str) -> quasi_webview::Shell { crate::shell::described() .sending("X-CSRF-Token", csrf) .with_body_last(crate::shell::body_last()) .with_chrome(crate::quasi::shortcuts::chrome()) .with_head(format!( "", crate::helpers::escape_html(csrf) )) } /// Who a mount is willing to answer. /// /// The signed-out question `b5cbb646` left open, answered here rather than by a /// second state type. Two mounts, one `Viewer`: what differs between a panel /// behind a login and a public page is whether a missing session ends the /// request, and that is one branch in the factory rather than a parallel /// hierarchy of states, factories and renderer signatures. #[derive(Clone, Copy, PartialEq, Eq)] enum Audience { /// A session is required, and its absence ends the request. /// /// Every panel and every document behind a login. The screens built on this /// read [`Viewer::reader`] and never see it fail. Reader, /// A session is read when there is one, and its absence is an ordinary /// state the screen describes. /// /// The public documents: `/team` and the rest of the `pages/` screens that /// read the same to a visitor and to a reader, and differ only in the /// header they carry. A screen here still gets a CSRF token, because the /// header's own controls post. Anyone, } /// Build the state factory the adapter calls per request. /// /// On [`Audience::Reader`] it refuses with `Unauthorized` when there is no /// session to resolve, which the adapter turns into a bare status with no body. /// That is the right shape there and not a shortcut: a store that will not /// answer is not a signed-out reader, and rendering a sign-in notice would need /// the renderer that is built from the state that could not be resolved. /// /// On [`Audience::Anyone`] a failed `authenticate` is not a refusal, it is a /// visitor: the viewer is built with `user: None` and the request goes on. The /// CSRF token is minted either way, since it is the session's rather than the /// user's and a sessionless form still needs one. /// /// The absent session layer stays an internal error on both, because that is /// wiring rather than an audience. fn viewer_factory( app: AppState, audience: Audience, ) -> impl Fn(&http::request::Parts) -> quasi_axum::StateFuture + Send + Sync + 'static { move |parts| { let app = app.clone(); let runtime = Handle::current(); // Taken out of the head first, so the future owns what it needs. let session = parts.extensions.get::().cloned(); Box::pin(async move { let Some(session) = session else { // The session layer runs in front of this. Its absence is a // wiring mistake rather than a signed-out reader. return Err(quasi_router::RouteError::internal("no session layer")); }; let user = match crate::auth::authenticate(&session, &app).await { Ok(user) => Some(user), Err(_) if audience == Audience::Anyone => None, Err(_) => { return Err(quasi_router::RouteError::denied("sign in to continue")); } }; // Before the handler runs, because the write that mints a token has // to finish on the session this request holds. A failure here is // the session store, not the reader. let csrf = crate::csrf::get_or_create_token(&session) .await .map_err(|_| quasi_router::RouteError::internal("csrf token"))?; // Same reason as `csrf`: an async read a sync handler cannot do. // Absent on a legacy session, which is a state the screens describe // rather than an error. let session_id = session .get::(crate::auth::SESSION_TRACKING_KEY) .await .ok() .flatten(); Ok(Viewer { app, user, runtime, csrf, session_id, }) }) } } /// Every converted screen that is switched on, with the address it answers. /// /// One mount per screen rather than one router for all of them, because axum /// strips a nest's prefix before the inner service sees the request: a single /// nest covering both would have to sit at a prefix the Askama routes also live /// under, and matchit refuses to hold a wildcard beside the parameterised routes /// already there. Measured, not assumed: nesting at `/dashboard/project` panics /// at startup against `/dashboard/project/{slug}/tabs/overview`. /// /// The list is empty when nothing is switched on, so the caller registers its /// Askama routes exactly as before and the adapter is not in the stack at all. A /// conversion is a startup-time choice: config is read once, and a per-request /// branch would pay for a switch that never moves. /// Every address a described screen can claim, switched on or not. /// /// `mounts` returns only what is currently on, which depends on config. This is /// the whole set, and it exists for the CSRF coverage test: the manifest that /// test reads is a process-global, so a test that switches a screen on leaves an /// entry behind for a path the default router does not serve, and the probe /// reads that as a route that lost its protection. The list lets it skip exactly /// those and nothing else. /// /// Checked against `mounts` below rather than trusted, since a screen added to /// one and not the other is the obvious way for this to rot. pub const PATHS: &[&str] = &[ ssh_keys::PATH, library_contacts::PATH, buyer_contacts::PATH, user_analytics::PATH, payout_summary::PATH, forum_memberships::LIBRARY_PATH, forum_memberships::SETTINGS_PATH, ]; /// Every described screen that owns its whole document, with the address it /// answers. /// /// Its own list rather than an entry in [`PATHS`], for the same reason /// [`public_mounts`] keeps its own: what a nest answers with decides what a /// test can assert about it. A panel screen answers `Response::Fragment` and /// names the region it changed, which `tests/workflows/described_screens.rs` /// checks by reading `HX-Retarget` off every entry in [`PATHS`]. A document /// answers `Outcome::Screen`, which sets no such header and is not a defect. /// /// The CSRF probe reads [`PATHS`] as its skip list, and a document screen /// registers no mutating route, so it has nothing to skip here either. pub const DOCUMENT_PATHS: &[&str] = &[feeds::PATH, export_portal::PATH]; /// Every described document a reader with no session can reach, with the /// address it answers. /// /// Its own list beside [`DOCUMENT_PATHS`] for the same reason that one sits /// beside [`PATHS`]: what a mount answers with decides what a test can assert /// about it. These answer `Outcome::Screen` like the gated documents, and /// differ in that a signed-out request is a render rather than a 401, which is /// what `tests/workflows/pages.rs` presses them for. /// /// Not in [`public_mounts`], which is the other public list and a different /// mechanism: those screens resolve nothing per request and take a state built /// once at startup. These resolve a session when there is one, so they carry a /// per-request viewer and can mint a CSRF token for the form on them. pub const PUBLIC_DOCUMENT_PATHS: &[&str] = &[ team::PATH, use_cases::PATH, policy::PATH, fan_plus::PATH, creators::PATH, collections::PATH, git_explore::PATH, git_repos::PATH, ]; /// Every screen's switch name, in the same order as [`PATHS`]. /// /// Test-only: the switches themselves are read from each screen's own `SCREEN` /// in `mounts`, and this exists so the consistency check below has both halves /// to compare. Kept beside `PATHS` rather than inside the test module, because /// the pairing is the thing being asserted and splitting them is how they drift. #[cfg(test)] const SCREENS: &[&str] = &[ ssh_keys::SCREEN, library_contacts::SCREEN, buyer_contacts::SCREEN, user_analytics::SCREEN, payout_summary::SCREEN, forum_memberships::LIBRARY_SCREEN, forum_memberships::SETTINGS_SCREEN, ]; /// One window an analytics panel offers. /// /// Named members rather than a tuple, for `policy`'s reason: a description names /// what it draws, and `.1` is not a name. pub(super) struct Range { /// What the address carries. pub value: &'static str, /// What the heading calls it. pub label: &'static str, } /// The four windows, and what each is called. /// /// Shared by `project_analytics` and `user_analytics`, which draw the same /// selector against different addresses. It was duplicated between them, along /// with `Range`, `range_heading`, `is_shown` and a `chip` supplier each; the /// supplier went when `chip` became a node member (quasicoherent `e2030032`) /// and the rest is here. pub(super) const RANGES: &[Range] = &[ Range { value: "7d", label: "Last 7 days", }, Range { value: "30d", label: "Last 30 days", }, Range { value: "90d", label: "Last 90 days", }, Range { value: "all", label: "All time", }, ]; /// What the current range is called. pub(super) fn range_heading(range: &str) -> &'static str { RANGES .iter() .find(|window| window.value == range) .map_or("All time", |window| window.label) } /// Whether this window is the one being shown. /// /// A supplier because a comparison is an expression and the form admits none in /// an argument. It hands back a `bool`, which is the smallest type that works. pub(super) fn is_shown(window: &Range, range: &str) -> bool { window.value == range } /// Every described screen, mounted. /// /// Unconditional since `64b33b26`. Each entry used to be gated on /// `described(app, ..)` reading `QUASI_SCREENS`, so a screen could be switched /// off and its Askama rendering registered instead. There is no Askama /// rendering to fall back to any more, and the route tables no longer register /// one, so a screen missing from this list is an address nothing answers. pub fn mounts(app: &AppState) -> Vec<(&'static str, axum::Router)> { vec![ ( ssh_keys::PATH, served_panel_mount( app, ssh_keys::REGION, ssh_keys::WRITES, ssh_keys::renderer, |viewer, _| { let (username, keys, tokens, themes) = ssh_keys::reading(viewer)?; Ok(ssh_keys::pane_serve( &residuals::SSH_KEYS, &username, &keys, &tokens, &themes, )) }, ), ), ( library_contacts::PATH, served_panel_mount( app, library_contacts::REGION, library_contacts::WRITES, library_contacts::renderer, |viewer, _| { let (buyers, shared) = library_contacts::reading(viewer)?; Ok(library_contacts::pane_serve( &residuals::LIBRARY_CONTACTS, &buyers, &shared, )) }, ), ), // One module, two screens: the library tab and the settings section are // the same table under different chrome. ( forum_memberships::LIBRARY_PATH, served_panel_mount( app, forum_memberships::LIBRARY_REGION, &[], forum_memberships::renderer, |viewer, _| { let (memberships, base) = forum_memberships::reading(viewer)?; Ok(forum_memberships::library_pane_serve( &residuals::FORUMS_LIBRARY, &memberships, &base, )) }, ), ), ( buyer_contacts::PATH, served_panel_mount( app, buyer_contacts::REGION, &[], buyer_contacts::renderer, |viewer, _| { Ok(buyer_contacts::pane_serve( &residuals::BUYER_CONTACTS, &buyer_contacts::reading(viewer)?, )) }, ), ), ( user_analytics::PATH, mount(app, user_analytics::screen, &[], user_analytics::renderer), ), ( payout_summary::PATH, served_panel_mount( app, payout_summary::REGION, &[], payout_summary::renderer, |viewer, _| { let (balance, payouts_enabled) = payout_summary::reading(viewer)?; Ok(payout_summary::card_serve( &residuals::PAYOUT_SUMMARY, balance.as_ref(), payouts_enabled, )) }, ), ), // A nest that answers no address of its own: the Members panel is read // through its Askama route, which keeps a conditional GET, and only its // writes are described. `03c0977b`; see `writes_only`. ( project_members::NEST, writes_only(app, project_members::WRITES, project_members::renderer), ), // The item dashboard's Sales panel: a parameterized read address, so // the read stays on Askama and only the Refund write is described. // `b25dd957`; see `writes_only`. ( item_sales::NEST, writes_only(app, item_sales::WRITES, item_sales::renderer), ), ( forum_memberships::SETTINGS_PATH, served_panel_mount( app, forum_memberships::SETTINGS_REGION, &[], forum_memberships::renderer, |viewer, _| { let (memberships, base) = forum_memberships::settings_reading(viewer)?; Ok(forum_memberships::settings_pane_serve( &residuals::FORUMS_SETTINGS, &memberships, &base, )) }, ), ), ] } /// Every described screen that owns its whole document, mounted. /// /// See [`DOCUMENT_PATHS`] for why these are not in [`mounts`]. pub fn document_mounts(app: &AppState) -> Vec<(&'static str, axum::Router)> { vec![ ( feeds::PATH, served_gated_mount(app, feeds::PATH, feeds::renderer, |viewer, carried| { // One read, borrowed twice: the document says which page it is // and the markup fills from the same rows. let loaded = feeds::reading(viewer, carried)?; Ok(Served { screen: feeds::page_screen(&loaded.page()), markup: feeds::page_region_serve(&residuals::FEED, &loaded.page()).into(), }) }), ), ( export_portal::PATH, served_gated_mount( app, export_portal::PATH, export_portal::renderer, |viewer, _| { let page = export_portal::reading(viewer)?; Ok(Served { screen: export_portal::page_screen(&page), markup: export_portal::page_region_serve(&residuals::EXPORT_PORTAL, &page) .into(), }) }, ), ), ] } /// The gate in front of every document nest: a reader or a branded refusal. async fn signed_in( axum::extract::State(app): axum::extract::State, request: axum::extract::Request, next: axum::middleware::Next, ) -> axum::response::Response { use axum::response::IntoResponse as _; let Some(session) = request .extensions() .get::() .cloned() else { // The session layer runs in front of this. Its absence is wiring. return crate::error::AppError::Internal(anyhow::anyhow!("no session layer")) .into_response(); }; match crate::auth::authenticate(&session, &app).await { Ok(_) => next.run(request).await, Err(refusal) => refusal.into_response(), } } /// Every described document a reader with no session can reach, mounted. /// /// See [`PUBLIC_DOCUMENT_PATHS`] for why these are neither in /// [`document_mounts`] nor in [`public_mounts`]. pub fn public_document_mounts(app: &AppState) -> Vec<(&'static str, axum::Router)> { vec![ ( team::PATH, served_document_mount(app, team::PATH, team::renderer, |_, _| { Ok(Served { screen: team::page_screen(), markup: residuals::settled(&residuals::TEAM), }) }), ), ( use_cases::PATH, served_document_mount(app, use_cases::PATH, use_cases::renderer, |viewer, _| { // One read of the prices, stating the document and filling the // nine holes. let prices = use_cases::prices(viewer); Ok(Served { screen: use_cases::page_screen(&prices), markup: use_cases::page_region_serve(&residuals::USE_CASES, &prices).into(), }) }), ), ( policy::PATH, served_document_mount(app, policy::PATH, policy::renderer, |_, _| { Ok(Served { screen: policy::page_screen(), markup: residuals::settled(&residuals::POLICY), }) }), ), ( fan_plus::PATH, served_document_mount( app, fan_plus::PATH, fan_plus::renderer, |viewer, carried| { // One read of the membership, deciding both the document and // which branches are filled. Read twice, the two could disagree // and the page would state one reader and draw another. let (standing, just_subscribed) = fan_plus::reading(viewer, carried)?; Ok(Served { screen: fan_plus::page_screen(&standing, just_subscribed), markup: fan_plus::page_region_serve( &residuals::FAN_PLUS, &standing, just_subscribed, ) .into(), }) }, ), ), ( creators::PATH, served_document_mount(app, creators::PATH, creators::renderer, |viewer, _| { // One read of the standing, the count and the prices, stating // the document and filling the holes. let (standing, total_creators, prices) = creators::reading(viewer)?; Ok(Served { screen: creators::page_screen(&standing, total_creators, &prices), markup: creators::page_region_serve( &residuals::CREATORS, &standing, total_creators, &prices, ) .into(), }) }), ), ( collections::PATH, served_document_mount( app, collections::PATH, collections::renderer, |viewer, carried| { // The address's captures and the row they resolve to, read // once. `Carried` carries them because a screen on the seam // never reaches the adapter and so has no `Request`. let loaded = collections::reading(viewer, carried)?; Ok(Served { screen: collections::page_screen(&loaded), markup: collections::page_region_serve(&residuals::COLLECTIONS, &loaded) .into(), }) }, ), ), // The git browse tree carries a per-IP cap on every read, and a // described document taking one of its addresses has to carry it too: // these routes walk bare repositories on disk. The mount is an // `axum::Router`, so the layer goes on here rather than through a // parameter, and the limiter is rebuilt from the same constants // `routes::git` reads. See `git_repos`'s module header. Both git // listings mount here, and a module that is declared but not mounted is // an address nothing answers: `git_explore` shipped that way and its own // tests did not run either, because an undeclared module is not compiled. ( git_explore::PATH, served_document_mount( app, git_explore::PATH, git_explore::renderer, |viewer, carried| { let loaded = git_explore::reading(viewer, carried)?; Ok(Served { screen: git_explore::page_screen(&loaded), markup: git_explore::page_region_serve(&residuals::GIT_EXPLORE, &loaded) .into(), }) }, ) .layer(tower_governor::GovernorLayer::new( crate::helpers::rate_limiter_ms( crate::constants::GIT_BROWSE_RATE_LIMIT_MS, crate::constants::GIT_BROWSE_RATE_LIMIT_BURST, ), )), ), // The first screen on the seam behind a rate limiter, and the layer // goes on here exactly as it does for the adapted mounts beside it: // `served_document_mount` answers an `axum::Router` too. ( git_repos::PATH, served_document_mount( app, git_repos::PATH, git_repos::renderer, |viewer, carried| { let loaded = git_repos::reading(viewer, carried)?; Ok(Served { screen: git_repos::page_screen(&loaded), markup: git_repos::page_region_serve(&residuals::GIT_REPOS, &loaded).into(), }) }, ) .layer(tower_governor::GovernorLayer::new( crate::helpers::rate_limiter_ms( crate::constants::GIT_BROWSE_RATE_LIMIT_MS, crate::constants::GIT_BROWSE_RATE_LIMIT_BURST, ), )), ), ] } /// A panel whose region is already written, mounted. /// /// The seam's mount for a screen in [`PATHS`], which is a different answer from /// a document's: a panel is fetched by a page that already exists, so what it /// returns is a fragment plus the `HX-Retarget` naming the region it changed. /// There is no document, no shell and no `Screen` -- which is why this is not /// [`served_document_mount`] with a flag. /// /// # The nest is still the adapter's, and the read is merged in front /// /// A panel screen's writes stay on the adapter: they answer a `Response` the /// router builds per request and there is nothing to derive about a deletion. /// So the nest is [`nest`] with no read registered, and the seam's `GET /` goes /// on the router in front of it. The adapter mounts as a fallback, deliberately /// (see `quasi_axum::Adapter::into_router`), so a route added here wins without /// anything being taken away. /// /// # The headers are the ones `quasi_http` would have set /// /// `Content-Type: text/html; charset=utf-8` and `HX-Retarget: #region`, which is /// exactly what `respond` emits for `Outcome::Fragment`. Written out rather than /// reached through, because reaching through means building the `Response` this /// path exists to avoid building. fn served_panel_mount( app: &AppState, region: &'static str, writes: &[(quasi_router::Method, &'static str, Screen)], renderer: fn(&Viewer) -> quasi_webview::Webview, answer: fn(&Viewer, &Carried) -> Result, ) -> axum::Router { let held = app.clone(); nest(app, None, writes, renderer).route( "/", axum::routing::get(move |mut parts: axum::http::request::Parts| { let app = held.clone(); async move { use axum::response::IntoResponse as _; // The gate is `Audience::Reader`, the same as the nest beside // it, so a signed-out request is refused before the handler // runs. A panel is fetched by a page that already checked, so // the adapter's bare refusal is the right answer here and the // branded 401 is the document mounts'. let viewer = match viewer_factory(app, Audience::Reader)(&parts).await { Ok(viewer) => viewer, Err(refusal) => { return axum::http::StatusCode::from_u16(refusal.class.http_status()) .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) .into_response(); } }; let carried = Carried::of(&mut parts).await; // On a blocking thread. See `served` for why. let Ok(answered) = tokio::task::spawn_blocking(move || answer(&viewer, &carried)).await else { // A panic in the answer. Reported as ours, because it is. return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(); }; match answered { Ok(markup) => ( [(quasi_axum::htmx::RETARGET, format!("#{region}"))], axum::response::Html(markup), ) .into_response(), Err(refusal) => axum::http::StatusCode::from_u16(refusal.class.http_status()) .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) .into_response(), } } }), ) } /// A public document whose regions are already written, mounted. /// /// The seam's mount, shared by every screen on it. It replaced an adapter mount /// that built a `quasi_axum::Adapter`, whose whole job is to call a handler that /// answers a `Screen` and render it, and there is no `Screen` here to answer /// with -- so the two could never have been one, and once `/git` moved across on /// 2026-09-08 there was no public document left on the adapter and that mount /// was deleted, and so was the handler it called. The /// markup was derived on a build machine, so answering is writing the document /// around it (quasicoherent `793d99dd`). /// /// `body` is where the two kinds of screen differ, and it is the only place /// they do. A screen that reads nothing has a residual of one literal, so its /// `body` hands back `residual.settled()` and copies nothing. A screen that /// reads something has holes, so its `body` calls the generated filler, which /// walks the residual and writes the request's values into the gaps. Neither /// builds a `Node`, which is the property the seam exists for; a `Cow` is what /// lets the first path stay a borrow while the second returns a `String`. /// /// `answer` produces the document and its markup **together, from one read**. /// They are not two jobs: a screen states its own title and measure from the /// same values that fill its holes, and `/fan-plus` reads a subscription to /// decide both. Two closures would read it twice per request and could disagree /// between the two reads, which is a worse bug than the one this mount exists to /// avoid. /// /// It takes the viewer because a document is not derivable: the shell carries /// whoever is looking. It takes what the address carried because a screen may /// read it: `/fan-plus` shows a welcome banner only on the way back from /// checkout, which is `?subscribed=true` and is carried nowhere else. /// /// It is fallible for the reason every other screen is. A screen here answers no /// `Screen`, so it never reaches the adapter and cannot use the adapter's /// refusal path; answering something plausible instead would be worse than /// answering nothing, because a member whose subscription could not be read /// would be shown the page that asks them to subscribe. The status comes off the /// `RouteError` exactly as `quasi_axum` takes it, and the body is empty for /// `quasi_axum::unresolved`'s reason: nothing was reached, so there is nothing to /// say that the status does not. /// /// Pairing a body with the wrong screen serves one page's markup inside /// another's document, which nothing here can catch; what catches it is /// `residuals::tests`, which produces each screen's markup both ways and /// compares them. fn served_document_mount( app: &AppState, path: &'static str, renderer: fn(&Viewer) -> quasi_webview::Webview, answer: fn(&Viewer, &Carried) -> Result, ) -> axum::Router { served(app, path, Audience::Anyone, renderer, answer) } /// A gated document whose regions are already written, mounted. /// /// [`served_document_mount`] with the two things a public mount leaves out put /// back: the factory is built on [`Audience::Reader`], so a signed-out request /// is refused rather than answered, and [`signed_in`] runs in front so that /// refusal is the branded 401 with its way back in rather than a bare 403. The /// two are written out separately rather than parameterised, because what a /// mount refuses is what a test can press it for. fn served_gated_mount( app: &AppState, path: &'static str, renderer: fn(&Viewer) -> quasi_webview::Webview, answer: fn(&Viewer, &Carried) -> Result, ) -> axum::Router { served(app, path, Audience::Reader, renderer, answer) } /// The seam's mount, under either audience. fn served( app: &AppState, path: &'static str, audience: Audience, renderer: fn(&Viewer) -> quasi_webview::Webview, answer: fn(&Viewer, &Carried) -> Result, ) -> axum::Router { let gated = matches!(audience, Audience::Reader); let held = app.clone(); let router = axum::Router::new().route( path, axum::routing::get(move |mut parts: axum::http::request::Parts| { let app = held.clone(); async move { use axum::response::IntoResponse as _; // Under `Audience::Anyone` a reader is never refused for being // signed out, so the error arm there is the session layer or // the CSRF store failing, which is this server being broken // rather than this page being unreachable. Under // `Audience::Reader` the gate in front has already refused a // visitor, so it is the same failure. Same answer the adapter // gives every other screen for it. let Ok(viewer) = viewer_factory(app.clone(), audience)(&parts).await else { return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(); }; let carried = Carried::of(&mut parts).await; // On a blocking thread, for the reason `quasi_axum` dispatches // every other screen on one: `answer` reaches the database // through `Viewer::block_on`, and `block_on` from a runtime // worker panics with "Cannot start a runtime from within a // runtime". This mount is the one dispatch in the tree that is // ours rather than the adapter's, so it is the one place that // has to say so. let held = (viewer, carried); let Ok((answered, held)) = tokio::task::spawn_blocking(move || { let outcome = answer(&held.0, &held.1); (outcome, held) }) .await else { // A panic in the answer. Reported as ours, because it is, // and the same way the adapter reports one. return axum::http::StatusCode::INTERNAL_SERVER_ERROR.into_response(); }; let viewer = held.0; match answered { Ok(Served { screen, markup }) => { axum::response::Html(renderer(&viewer).served(&screen, &markup)) .into_response() } // `http_status` answers a bare `u16`, and a status this // server cannot spell is this server being broken rather // than the reader being refused. Err(refusal) => axum::http::StatusCode::from_u16(refusal.class.http_status()) .unwrap_or(axum::http::StatusCode::INTERNAL_SERVER_ERROR) .into_response(), } } }), ); if gated { router.layer(axum::middleware::from_fn_with_state(app.clone(), signed_in)) } else { router } } /// What a screen on the seam answers with: its document, and the markup to draw /// inside it. /// /// The pair rather than two returns, because they have to come from one read. /// `Webview::served` carries the caveat worth keeping next to the call: the /// markup has to have come from this renderer, which is what `residuals::tests` /// asserts and nothing at this level can. pub struct Served { /// The document: the title, the measure, the shell. pub screen: quasi_router::Screen, /// The regions, already written. Borrowed when the screen reads nothing. pub markup: std::borrow::Cow<'static, str>, } /// What the address carried, for a screen on the seam that reads it. /// /// The adapter builds a `quasi_router::Request` and hands every other screen /// one; a screen here answers no `Screen`, so it never reaches the adapter and /// there is no `Request` to read. This is that much of one: the query, parsed /// the same way, and nothing else. Kept to what a served document actually /// needs rather than growing into a second `Request` nobody asked for. pub struct Carried { /// The query the reader arrived with. query: std::collections::HashMap, /// What the address pattern caught, for a screen at a parameterised /// address. captures: std::collections::HashMap, } impl Carried { /// What this request carried, out of the parts the adapter never sees. /// /// # The captures are axum's, and they are percent-decoded /// /// A screen on the seam answers no `Screen`, so its address is matched by /// the axum route this mount registers rather than by /// `quasi_router::Router`. The two agree about which requests match and /// differ in one thing: axum decodes `%2f`-style escapes in a capture and /// quasi hands the raw segment over. So `/c/ada/a%2Db` reaches the seam as /// slug `a-b` and reaches the described twin as `a%2Db`. /// /// Stated rather than reconciled. The difference is a widening toward the /// spelling RFC 3986 says the address means, it lands on the same row, and /// the alternative is a second path matcher in this crate. async fn of(parts: &mut axum::http::request::Parts) -> Self { use axum::extract::FromRequestParts as _; let query = parts .uri .query() .map(|query| { url::form_urlencoded::parse(query.as_bytes()) .map(|(key, value)| (key.into_owned(), value.into_owned())) .collect() }) .unwrap_or_default(); // An address with no captures in it has no `UrlParams` extension at // all, which is a refusal here and an empty map rather than a failure: // `/policy` carries none and asks for none. let captures = axum::extract::RawPathParams::from_request_parts(parts, &()) .await .map(|params| { params .iter() .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect() }) .unwrap_or_default(); Self { query, captures } } /// Whether the address carried `key` set to exactly `value`. #[must_use] pub fn says(&self, key: &str, value: &str) -> bool { self.query.get(key).is_some_and(|held| held.trim() == value) } /// What the query carried under `key`, if anything. #[must_use] pub fn asked(&self, key: &str) -> Option<&str> { self.query.get(key).map(String::as_str) } /// What the address pattern caught under `name`. /// /// Fallible with the refusal a missing capture deserves rather than an /// `Option`: a capture the pattern declares is always present, so absence /// is this mount being registered at an address that does not name it, and /// every caller would write the same `ok_or_else`. pub fn capture(&self, name: &str) -> Result<&str, quasi_router::RouteError> { self.captures .get(name) .map(String::as_str) .ok_or_else(|| quasi_router::RouteError::not_found("no such page")) } } /// Every described screen a reader with no session can reach, mounted. /// /// Separate from [`mounts`] because of the factory, not because of the address: /// [`viewer_factory`] resolves a session and refuses without one, which is the /// right answer for every screen behind a login and the wrong one for a /// marketing page. A public screen takes a state resolved once at startup, so /// its adapter is `Adapter::new` rather than `Adapter::per_viewer`. /// /// Deliberately not in [`PATHS`]. That list is what the CSRF probe skips and /// what `tests/workflows/described_screens.rs` presses with a signed-in /// fixture, and both readings are about screens that hold a session. A public /// screen registers no mutating route, so the probe has nothing to skip. pub fn public_mounts(app: &AppState) -> Vec<(&'static str, axum::Router)> { vec![ (pricing::PATH, pricing_mount(app)), (shortcuts::PATH, shortcuts_mount()), ] } /// The keyboard-shortcuts overlay, on the same shell every described screen /// gets so the listing looks like the site it is drawn over. /// /// Its own mount rather than a route inside the pricing nest: an overlay /// reachable from every screen is not one screen's route, and the binding's /// address is absolute in the emitted markup (`e0c0d991`). fn shortcuts_mount() -> axum::Router { quasi_axum::Adapter::new( shortcuts::router(), std::sync::Arc::new(()), std::sync::Arc::new(pricing::renderer()), ) .into_router() } /// The fee calculator's own nest: the page and the recompute it answers. fn pricing_mount(app: &AppState) -> axum::Router { let state = std::sync::Arc::new(pricing::Pricing { billing: crate::Billing::from_ref(app), founder_window_open: app.config.creator_pricing.founder_window_open, changelog_published: crate::changelog::is_published(), }); let router = quasi_router::Router::::new() .get("/", pricing::screen) .get("/compare", pricing::compare); quasi_axum::Adapter::new(router, state, std::sync::Arc::new(pricing::renderer())).into_router() } declare! { /// This server's own copy, as markdown. /// /// `Node::rich` means "markdown somebody else wrote": quasi hardens it, so /// its links carry `nofollow`, its raw markup is dropped and fetchable /// schemes are filtered. That is right for a forum post and wrong for a /// page's own sentence, and until quasi grew the trust axis every described /// page here was telling crawlers not to follow its own links /// (quasicoherent `24a3b1df`). /// /// The two axes stay separate in quasi -- `Richness` is what the format may /// express, `Trust` is who wrote it -- and this is the one combination this /// server reaches for often enough to name: a sentence, ours. A screen /// wanting tables says so with `Node::richness`, and a screen carrying a /// reader's markdown keeps `Node::rich`. /// /// **The test for using it is authorship, not tidiness.** The string has to /// be a literal in this repository, or interpolated from a value that /// cannot carry markup. A creator's description reaching a screen through /// the database is `Node::rich` however well-behaved it has been. /// # Constant, and deliberately not staged /// /// An `include` of this with a literal folds to the `#[constant]` shim and /// is derived once, which is what `/policy` uses six times over. /// /// `#[staged]` was tried and is wrong. The parameter is the **markdown /// source**, so staging replaces the whole document with one sentinel: the /// derivation renders `

ZQH...HQZ

` and the filler writes the request's /// markdown into that paragraph unparsed. `git_repos` is where it showed -- /// a fenced code block came back as a literal ``` inside a `

`. /// /// # A staged screen that wants a value inside prose writes the `rich` /// /// Not because prose cannot be staged, but because the hole has to be in /// the **source** rather than around it. `rich "... [name]({base}) ..."` /// written inside the staged shape puts a sentinel in the markdown, so /// docengine parses it and the residual holds the markup with the value's /// own gap in it; the same sentence formatted first and handed here does /// not. `git_repos` and `forum_memberships` are the two sites, and each /// inlines this shape's two-line body with a note saying so. /// /// What a staged `rich` then requires is that the value cannot change how /// the source parses -- which is the rule this shape already carries for a /// different reason. A value that could alter the parse could carry markup, /// and such a value wants `Node::rich`, untrusted and not on the seam. #[must_use] #[constant] pub shape own_prose(source: impl Into) -> Node; rich source { trust quasi_router::Trust::Trusted; } } /// A handler, spelled once so the screens and the mount agree about it. pub type Screen = fn(&Viewer, quasi_router::Request) -> Result; /// One screen behind the adapter, answering the root of its own nest. /// /// The tab is `/` because the nest has already taken the address off: a screen /// mounted at its own tab endpoint sees one route and never has to agree with /// the prefix twice. /// /// # Why a screen serves its own writes /// /// `writes` registers routes under the same nest, and a destructive control on /// a described screen should address one of them rather than the API route the /// Askama version used. Decision 7 is the reason: a described route answers with /// a `Response::Fragment` naming the region it changed, so the answer lands where /// it belongs and carries the screen's own markup. /// /// An API route can do neither, and both described screens that called one were /// wrong in different ways. `DELETE /api/users/me/ssh-keys/{id}` answers an htmx /// request with the whole re-rendered Askama list, and with no target htmx put /// that table inside the button that was pressed. `DELETE /api/contacts/{id}` /// answers 204, which htmx is configured never to swap, so the row stays on /// screen after a successful revoke. Neither is visible to a test that checks /// only that the address exists. /// /// The write still goes through the same CSRF envelope: `crate::csrf` nests this /// service under an Auto posture, and the core module attaches the token to /// every htmx request on the page. fn mount( app: &AppState, screen: Screen, writes: &[(quasi_router::Method, &'static str, Screen)], renderer: fn(&Viewer) -> quasi_webview::Webview, ) -> axum::Router { nest(app, Some(screen), writes, renderer) } /// A nest that serves writes and answers no address of its own. /// /// The hole it fills: a panel whose route answers a conditional GET cannot be a /// mounted screen, because /// [`mount`] has no way to say "304 if the cache generation has not moved". So /// it stays a fill on its Askama handler -- and a fill has no nest, so it had /// nowhere to put its writes, so its controls kept addressing API routes that /// answer 200 or 204 and cannot name the region they changed. The patch for /// that was `data-after`, the private dispatcher vocabulary in /// `frontend/src/core/dispatch.ts` that this conversion exists to retire. /// /// This is the other half of such a panel: the read keeps its Askama route and /// its ETag, and the writes get described routes that answer /// `Response::Fragment` naming the panel's region, exactly as a mounted /// screen's do. /// /// # The cost, stated once rather than per panel /// /// One panel is then served by two routers, and the read and the write are no /// longer visible in one place. That is the trade: the alternative was nine /// tabs converting their markup while keeping their JS, which lowers no seal /// and is not what S4 is for. /// /// # The address is a fixed prefix, and the ids go inside it /// /// A nest is mounted at a fixed path, which is also why `project_analytics` is /// a fill rather than a mounted screen. Path parameters live in the inner /// router, which does support them: `ssh_keys` already registers `/keys/{id}` /// and `/tokens/{id}` under its own nest. So a writes-only nest carries its ids /// in the inner paths and does not reuse the API route's address. That API /// route stays for API consumers, exactly as `/api/users/me/ssh-keys/{id}` did /// when `ssh_keys` moved its controls off it. fn writes_only( app: &AppState, writes: &[(quasi_router::Method, &'static str, Screen)], renderer: fn(&Viewer) -> quasi_webview::Webview, ) -> axum::Router { nest(app, None, writes, renderer) } /// The router both of the above build, with or without a root GET. fn nest( app: &AppState, screen: Option, writes: &[(quasi_router::Method, &'static str, Screen)], renderer: fn(&Viewer) -> quasi_webview::Webview, ) -> axum::Router { let mut quasi = quasi_router::Router::::new(); if let Some(screen) = screen { quasi = quasi.get("/", screen); } for (method, path, handler) in writes { quasi = match method { quasi_router::Method::Delete => quasi.delete(path, *handler), quasi_router::Method::Put => quasi.put(path, *handler), quasi_router::Method::Get => quasi.get(path, *handler), quasi_router::Method::Post => quasi.post(path, *handler), }; } quasi_axum::Adapter::per_viewer( quasi, viewer_factory(app.clone(), Audience::Reader), move |viewer, _, _| renderer(viewer), ) .into_router() } #[cfg(test)] mod tests { use super::*; /// The whole of what `own_prose` is for: a sentence this server wrote is /// trusted, so quasi stops telling crawlers not to follow its own links. /// `Node::rich` alone is untrusted and is the right default for a forum /// post; the two are one call apart and read the same at a glance. #[test] fn our_own_sentence_is_trusted_and_a_readers_is_not() { let ours = own_prose("Read the [docs](/docs)."); let theirs = quasi_router::Node::rich("Read the [docs](/docs)."); assert!(matches!( ours, quasi_router::Node::Rich { trust: quasi_router::Trust::Trusted, .. } )); assert!(matches!( theirs, quasi_router::Node::Rich { trust: quasi_router::Trust::Untrusted, .. } )); } #[test] fn every_screen_is_listed_in_paths() { // The two lists are written by hand and read by two different things, // so the check is that adding a screen to `mounts` and forgetting // `PATHS` fails here rather than silently weakening the CSRF coverage // probe's skip list. assert_eq!( PATHS.len(), SCREENS.len(), "PATHS and SCREENS describe the same screens" ); let source = include_str!("mod.rs"); let mounted = source .split_once("pub fn mounts(") .expect("mounts exists") .1 .split_once("\n}") .expect("mounts ends") .0; // Counted as `mount(` since `64b33b26`. `mounts` used to push // conditionally into a Vec, one `mounted.push((` per screen the switch // had on; it returns a `vec![..]` literal now because every screen is // mounted unconditionally, so the thing to count is the adapter call // each entry makes. Not `mount(app,`: rustfmt breaks the longer entries // across lines and that token then finds three of the six. // // `served_panel_mount(` ends in the same token and is counted with // them, which is what this wants: a panel that moves onto the residual // seam is still a screen `PATHS` has to list. `writes_only(` does not, // which is also right -- those nests answer no address of their own. let registered = mounted.matches("mount(").count(); assert_eq!( registered, PATHS.len(), "mounts registers {registered} screens, PATHS lists {}", PATHS.len() ); } #[test] fn every_document_screen_is_listed_in_document_paths() { let source = include_str!("mod.rs"); let mounted = source .split_once("pub fn document_mounts(") .expect("document_mounts exists") .1 .split_once("\n}") .expect("document_mounts ends") .0; // Two mount styles, and both are gated documents, for the same // reason `every_public_document_is_listed_in_public_document_paths` // counts two: a screen on the residual seam has no `Screen` to answer // with, so it takes [`served_gated_mount`] rather than the adapter. // Counting only the first would let a screen leave `DOCUMENT_PATHS` // unnoticed by moving onto the seam. let adapted = mounted.matches("document_mount(").count(); let served = mounted.matches("served_gated_mount(").count(); assert_eq!( adapted + served, DOCUMENT_PATHS.len(), "document_mounts and DOCUMENT_PATHS describe the same screens" ); } #[test] fn every_public_document_is_listed_in_public_document_paths() { let source = include_str!("mod.rs"); let mounted = source .split_once("pub fn public_document_mounts(") .expect("public_document_mounts exists") .1 .split_once("\n}") .expect("public_document_mounts ends") .0; // Both mount styles, though only one of them has a caller left. Every // public document is on the residual seam as of 2026-09-08 and takes // `served_document_mount`, which answers no `Screen`; the adapter mount // that answered one is gone. The first count stays because what this // test exists to catch is a screen leaving `PUBLIC_DOCUMENT_PATHS` // unnoticed by changing how it is mounted, and a count that only knows // today's style would not catch it changing back. let adapted = mounted.matches("public_document_mount(").count(); let served = mounted.matches("served_document_mount(").count(); assert_eq!( adapted + served, PUBLIC_DOCUMENT_PATHS.len(), "public_document_mounts and PUBLIC_DOCUMENT_PATHS describe the same screens" ); } /// Every git address carries the browse limiter, checked in the source /// because there is nothing to ask an `axum::Router` about afterwards. /// /// This is the check the whole `/git` conversion turns on. The tree's reads /// sit under one `route_layer` in `routes::git`, so an address lifted out of /// it and mounted here loses that layer silently: nothing fails to compile, /// no test fails, and a route that walks bare repositories on disk stops /// being capped. A conversion that forgets it fails here instead. #[test] fn every_described_git_address_keeps_the_browse_limiter() { let source = include_str!("mod.rs"); let mounted = source .split_once("pub fn public_document_mounts(") .expect("public_document_mounts exists") .1 .split_once("\n}") .expect("public_document_mounts ends") .0; let git_paths = PUBLIC_DOCUMENT_PATHS .iter() .filter(|path| path.starts_with("/git")) .count(); assert!(git_paths > 0, "no git document is mounted yet"); assert_eq!( mounted.matches("GIT_BROWSE_RATE_LIMIT_MS").count(), git_paths, "a described /git address is mounted without the browse limiter" ); } /// The two document lists differ by exactly one thing, and it is the one /// that matters: a gated document refuses a visitor, a public one renders to /// them. Nothing else about the mount changes, so a screen in the wrong list /// is a page that 401s or a page that leaks, depending on the direction. #[test] fn no_document_is_in_both_lists() { for path in PUBLIC_DOCUMENT_PATHS { assert!( !DOCUMENT_PATHS.contains(path), "{path} is mounted both gated and public" ); } } #[test] fn a_path_is_claimed_by_exactly_one_screen() { // Two screens on one address is an axum panic at startup, and the two // forum-memberships screens are the near miss: one module, two paths. let mut seen = PATHS.to_vec(); seen.extend_from_slice(DOCUMENT_PATHS); seen.extend_from_slice(PUBLIC_DOCUMENT_PATHS); seen.sort_unstable(); let before = seen.len(); seen.dedup(); assert_eq!( before, seen.len(), "two screens claim one address: {seen:?}" ); } }