//! Tier G1: one screen through the description layer, beside its Askama one. //! //! Wiki note `look-wave-2`, tier G. This exists to be measured and deleted, not //! to be built on. It serves `/spike/docs`, which answers the same thing //! `/docs` does, described rather than templated, so the two can be diffed and //! the cost of converting a screen here is a number instead of an argument. //! //! The docs index was chosen because it isolates the question. Its data is an //! in-memory `Arc`, so nothing about it is entangled with the async //! Postgres layer, and it is public, so nothing about it is entangled with the //! session. What is left over when both of those are held still is the part //! that is genuinely about describing a screen. What the choice *hides* is the //! two findings in the module docs below, which are the ones that decide G2. //! //! # Finding 1: a handler cannot await, and this server's data layer is async //! //! [`quasi_router::Handler`] is `fn(&S, Request) -> Result`, //! sync by quasi's decision 6, which was taken for egui-in-a-frame and //! terminal-in-an-event-loop and matches the desktop apps' rusqlite store. This //! server is async sqlx over Postgres end to end. `quasi-axum` runs the router //! on `spawn_blocking`, so a handler *can* reach a pool through //! `Handle::block_on`, but every described route then holds a blocking-pool //! thread for the length of a database round trip. That is a real cost on the //! one host in the tree that has many concurrent readers. //! //! # Finding 2: nothing carries identity into a handler //! //! `quasi_http::decode` builds the request and drops the header map. Its //! parameters are the path captures, the form body and the query string, which //! is to say things the route pattern fixed or the client chose. A handler receives `&S`, which is shared, and those //! params. There is no session, no cookie, no `AuthUser`, and no side channel: //! the doc comment on `Adapter::per_request` says so explicitly, and that //! factory feeds the *renderer*, after dispatch, not the handler. //! //! So an authenticated screen cannot be described today. That is most of this //! server: of the 105 conversion units phase 0 counted, the dashboard, project, //! item and library families are all behind auth. The public tier is what would //! convert as-is, which is roughly the batch-1 set. //! //! # Finding 3: an internal navigation is never a link //! //! Read off the emitted document, which the test below prints. A row that //! activates emits //! //! ```html //! //! ``` //! //! `quasi_webview::node::action_attrs` writes an `href` only for //! [`Destination::External`](quasi_router::Destination); an internal route is //! always a control carrying `hx-get`. So there is no href for a crawler to //! follow, no middle-click, no copy-link, and with JS off the page is inert. //! //! That collides head-on with finding 2. The only tier that can be described //! today is the public one, and the public one is the tier where this costs //! most: `/docs`, `/discover`, and every creator, project and item page is an //! SEO surface. The docs index today is `
  • `. //! //! # Finding 4: the site chrome has no home //! //! The Askama page opens with `{% include "partials/site_header.html" %}`, 35 //! lines of nav, sign-in state and the wordmark. [`Shell`] emits the `` //! and the `` wrapper and nothing inside it, so the header is either a //! `Region::Band` every screen redescribes, or a bespoke fill every screen //! carries. Either way it is per-screen work times 105, for markup that is the //! same on all of them, and `base.html` does it once today. //! //! # Finding 5: two vocabulary gaps this one screen already hits //! //! - **Disclosure.** The Guide section groups into `
    ` //! subsections. No node says that, so the described version flattens them. //! - **The search box.** The input, its results container and `docs-search.js` //! are bespoke, so this screen needs a `Region::Handover` fill even though //! phase 0 rated it furniture. Worth noting for the sizing: "furniture" was //! read off templates, and this one turned out to carry a fill. //! //! # What it deletes, measured on this screen //! //! `templates/pages/doc_index.html` is 45 lines and would go. The 45-line //! grouping pass in the Askama handler stays, near enough verbatim: the //! description replaces the markup, not the domain logic. The described screen //! is 14 lines of handler plus 7 of assembly. //! //! It does **not** delete the stylesheet. `style.css` carries 25 `.docs-*` //! rules; of those the `.docs-search-*` set survives with the fill, and the //! `.docs-index` / `.docs-section` set has nothing to attach to any more, //! because the emitted classes are the generated `.list` / `.row` / `.heading`. //! Note what that does to charter rule 13: both the page title and the section //! titles emit `class="heading"`, so the 13 named heading classes have no //! purchase on a described screen and any level distinction has to come from //! the element selector. use std::sync::Arc; use docengine::DocLoader; use quasi_axum::Adapter; use quasi_router::{Node, RegionKind, Request, Response, RouteError, Row, Screen, Slot}; use quasi_webview::{Shell, Webview}; /// The docs index, described. /// /// Compare against `routes::pages::public::docs::docs_index`, which is the same /// grouping over the same loader ending in `DocIndexTemplate`. fn docs_index(docs: &DocLoader, _request: Request) -> Result { // The grouping is the handler's either way. Describing a screen does not // remove the domain pass over the index, and this is a fair copy of it. let mut sections: Vec<(String, Vec)> = Vec::new(); for entry in docs.index() { let row = Row::new(entry.title.clone()) .activate(quasi_router::Action::get(format!("/docs/{}", entry.slug))); match sections.iter_mut().find(|(name, _)| name == &entry.section) { Some((_, rows)) => rows.push(row), None => sections.push((entry.section.clone(), vec![row])), } } Ok(screen_from(sections).into()) } /// Build the screen from an already-grouped index. /// /// Split out of the handler so a test can render it without standing up a /// [`DocLoader`]. The grouping above is the only part that touches the loader. fn screen_from(sections: Vec<(String, Vec)>) -> Screen { let mut pane = Slot::new("docs", RegionKind::Pane).with(Node::page("Documentation")); for (name, rows) in sections { pane = pane.with(Node::section(name)).with(Node::list(rows)); } Screen::sidebar_content("Documentation - Makenotwork").with(pane) } /// The renderer this spike serves with. fn renderer() -> Webview { // `Shell::under` points the asset paths at `/static`, which is what this // server already serves. `layered` states the cascade order the hand-written // head states today; without it the generated sheets would establish the // `makeover` layer by link order. let shell = Shell::under("/static").layered(["base", "components", "responsive"]); Webview::new().with_shell(shell) } /// The spike's route table and renderer, mounted under `/spike`. /// /// Mounted with `nest_service` rather than merged, because /// [`Adapter::into_router`] mounts as a fallback and this server already has /// one. The prefix is stripped, so the router sees `/docs` and the pattern /// below is the real one. pub fn router(docs: Arc) -> axum::Router { let quasi = quasi_router::Router::::new().get("/docs", docs_index); Adapter::new(quasi, docs, Arc::new(renderer())).into_router() } #[cfg(test)] mod tests { use super::*; use quasi_axum::Serves; /// Print what the description layer emits for this screen, so the G1 /// measurement is read off a real document rather than reasoned about. #[test] fn emits_the_docs_index() { let sections = vec![ ( "Guide".to_string(), vec![ Row::new("Getting started") .activate(quasi_router::Action::get("/docs/getting-started")), Row::new("Uploading files") .activate(quasi_router::Action::get("/docs/uploading")), ], ), ( "Reference".to_string(), vec![Row::new("API").activate(quasi_router::Action::get("/docs/api"))], ), ]; let html = renderer().screen(&screen_from(sections)); println!("{html}"); assert!(html.contains("Documentation")); } }