feat(web): the hub's sky on Explore and the signed-out home (closes #165) 0a3b244b748f
The same engine and density as Your sky, from the public repositories the page lists (up to 40, the most recently active; a filter narrows the sky with the list), clusters beside the list. Never a private repository, whoever looks: tested signed out and for the private repository's owner. The loader moves to a shared include, backdrop.html.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 19:54 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
0a3b244b748fdcfbd077e9a5c447efe1a9cfe7f6530601976d1e2201e5f60bbb- Depends on
- 1553bfc9ae53 17ca6cddc534 18e381ea88c2 25ab8c73514a 2f14c5f39e81 33bd0bd45f8a 61ecbd299f22 b5668108487f d23464c0ac60 df5c69cbd047
- Depended on by
- 4dfa4599b3f7 a7aa76ac52d7 da8f7826a645 6246d1e51f4f
- In bookmarks
- api-unknown-fields cancel-landed-runs diffusion-pin-bookmark draft-ready main oh-comment-body-file proposal-list-fast
- In tags
- v0.5.0 v0.5.1 v0.5.2 v0.6.0 v0.6.1 v0.7.0 v0.8.0 v0.9.0 v0.9.1 v0.9.11 v0.9.15 v0.9.16 v0.9.19 v0.9.2 v0.9.20 v0.9.21 v0.9.22 v0.9.23 v0.9.24 v0.9.3 v0.9.4 v0.9.5 v0.9.6 v0.9.7 v0.9.8 v0.9.9
- Edits
- 17 atoms, 1 tree operation, 7 files
Browse the tree at this change
added templates/backdrop.html +23 −0
| 1 | + | {# A backdrop (docs/backgrounds.md, #157): the page's `backdrop` data drawn |
|
| 2 | + | by the engine, Rust compiled to WebAssembly. The viewer's choice (Settings › Appearance) is a cookie; |
|
| 3 | + | unset, a touch-only device or Save-Data gets a still sky. Any failure |
|
| 4 | + | leaves no backdrop and nothing else changed. #} |
|
| 5 | + | <canvas id="backdrop" aria-hidden="true"></canvas> |
|
| 6 | + | <script type="application/json" id="backdrop-data">{{ backdrop|safe }}</script> |
|
| 7 | + | <script type="module"> |
|
| 8 | + | const canvas = document.getElementById("backdrop"); |
|
| 9 | + | const chosen = document.cookie.match(/(?:^|; )backdrop=(moving|still|off)/); |
|
| 10 | + | const touch = matchMedia("(pointer: coarse)").matches && !matchMedia("(pointer: fine)").matches; |
|
| 11 | + | const pick = chosen ? chosen[1] : (touch || (navigator.connection && navigator.connection.saveData) ? "still" : "moving"); |
|
| 12 | + | if (pick === "off") { |
|
| 13 | + | canvas.remove(); |
|
| 14 | + | } else { |
|
| 15 | + | try { |
|
| 16 | + | const bd = await import("/static/backdrop/backdrop.js?v={{ shell.assets() }}"); |
|
| 17 | + | await bd.default({ module_or_path: "/static/backdrop/backdrop_bg.wasm?v={{ shell.assets() }}" }); |
|
| 18 | + | bd.start(canvas, JSON.parse(document.getElementById("backdrop-data").textContent), pick === "still"); |
|
| 19 | + | } catch (e) { |
|
| 20 | + | canvas.remove(); |
|
| 21 | + | } |
|
| 22 | + | } |
|
| 23 | + | </script> |
modified CHANGELOG.md +1 −0
| … | 6 unchanged lines |
||
| 7 | 7 | - **Diffusion (#148, docs/diffusion.md).** When a repository that others pin as a subrepo moves on, each consumer that follows it gets a proposal moving the pin, signed by the hub, tested by the consumer's own pipeline, with the upstream's changes listed. One proposal per consumer and subrepo, kept up to date; never landed by itself. Turned on per repository (Settings › Diffusion, `PUT /repos/{owner}/{name}/diffusion`, or `follow` in `.oxymodules`), off by default; `green` waits for the upstream's run to pass. A repository's page shows who uses it and how far behind each is (Diffusion › Diffuses to, `GET /repos/{owner}/{name}/consumers`). Consumers are bumped on the bookmark their work lands on, learned from where their proposals land. |
|
| 8 | 8 | - **Diffusion in the API (#153):** `GET /repos/{owner}/{name}/diffusion` gives what a repository takes from and what it diffuses to, each with its pill, for the VS Code extension. |
|
| 9 | 9 | - **The card layout everywhere (#159–#164):** the About card's counts in its heading and its buttons centred; wider repository cards with each Diffusion row on one line; the Code button and + New project in the accent blue; proposal and issue sidebars, profile and organisation pages, search results and the wiki's sidebar as cards; every panel's and form card's heading a shaded bar. |
|
| 10 | + | - **The hub's sky (#165):** Explore and the signed-out home draw the same sky from the public repositories listed (a filter narrows it), never a private one, whoever looks. |
|
| 10 | 11 | - **Your sky (#157, docs/backgrounds.md):** a quiet, moving backdrop behind the dashboard, drawn in Rust compiled to WebAssembly: your repositories as clusters and their last 30 days (every change pushed, proposal and issue events, releases, Flow runs) as marks around them; the cursor lights and links them, and names the cluster it rests by. Settings › Appearance: Moving, Still or Off; still under reduced motion. |
|
| 11 | 12 | - **Signed-in pages are private (#158):** HTML for a signed-in viewer says `Cache-Control: private, no-store`, so no proxy or CDN serves one viewer's page to another. |
|
| 12 | 13 | - **The dashboard in cards (#155):** each section a rounded card with its heading and count in a shaded bar, your repositories a sidebar card with New in its heading. |
|
| … | 166 unchanged lines |
modified crates/hub/src/web/home.rs +14 −1
| … | 21 unchanged lines |
||
| 22 | 22 | sort: String, |
|
| 23 | 23 | topic: String, |
|
| 24 | 24 | stars: std::collections::HashMap<i64, i64>, |
|
| 25 | + | /// The hub's sky (docs/backgrounds.md §2, #165) on Explore and the |
|
| 26 | + | /// signed-out home: the public repositories listed; empty elsewhere. |
|
| 27 | + | backdrop: String, |
|
| 25 | 28 | } |
|
| 26 | 29 | ||
| 27 | 30 | impl HomePage { |
|
| … | 33 unchanged lines |
||
| 61 | 64 | const FEED_LIMIT: i64 = 40; |
|
| 62 | 65 | /// The most events Your sky reads, and how far back (docs/backgrounds.md §5). |
|
| 63 | 66 | const SKY_EVENTS: i64 = 1500; |
|
| 67 | + | /// The most repositories the hub's sky draws, the most recently active. |
|
| 68 | + | const SKY_REPOS: usize = 40; |
|
| 64 | 69 | const SKY_DAYS: i64 = 30; |
|
| 65 | 70 | ||
| 66 | 71 | /// Your sky's data (docs/backgrounds.md §5): the viewer's repositories and |
|
| … | 66 unchanged lines |
||
| 133 | 138 | "name" => repos.sort_by_key(|r| (r.name.to_lowercase(), r.owner.to_lowercase())), |
|
| 134 | 139 | _ => repos.sort_by_key(|r| std::cmp::Reverse(r.updated_ms)), |
|
| 135 | 140 | } |
|
| 136 | - | Ok(Page(HomePage { description: app.config.site.description.clone(), shell, repos, owner: None, q: q.q, sort, topic, stars })) |
|
| 141 | + | // The hub's sky: the public repositories listed (a filter narrows it), |
|
| 142 | + | // the most recently active first; never a private one, whoever looks. |
|
| 143 | + | let public: Vec<db::repos::RepoRow> = repos.iter().filter(|r| r.visibility == "public").take(SKY_REPOS).cloned().collect(); |
|
| 144 | + | let ids: Vec<i64> = public.iter().map(|r| r.id).collect(); |
|
| 145 | + | let now = db::now_ms(); |
|
| 146 | + | let sky = db::activity::sky(&app.db, &ids, now - SKY_DAYS * 86_400_000, SKY_EVENTS).await.map_err(db_err)?; |
|
| 147 | + | let backdrop = sky_json(&public, &sky, now); |
|
| 148 | + | Ok(Page(HomePage { description: app.config.site.description.clone(), shell, repos, owner: None, q: q.q, sort, topic, stars, backdrop })) |
|
| 137 | 149 | } |
|
| 138 | 150 | ||
| 139 | 151 | /// A person's page (hub spec §6): avatar, names, bio, linked signing |
|
| … | 82 unchanged lines |
||
| 222 | 234 | sort: "activity".into(), |
|
| 223 | 235 | topic: String::new(), |
|
| 224 | 236 | stars, |
|
| 237 | + | backdrop: String::new(), |
|
| 225 | 238 | }) |
|
| 226 | 239 | .into_response()) |
|
| 227 | 240 | } |
|
| … | 89 unchanged lines |
modified crates/hub/tests/issues.rs +14 −2
| … | 388 unchanged lines |
||
| 389 | 389 | .await |
|
| 390 | 390 | .unwrap(); |
|
| 391 | 391 | assert_eq!(res.headers()[header::CACHE_CONTROL], "private, no-store"); |
|
| 392 | - | // Signed out there is no sky at all. |
|
| 393 | - | assert!(!anon.body.contains("backdrop-data")); |
|
| 392 | + | // Signed out, `/` has the hub's sky (#165): public repositories only. |
|
| 393 | + | let sky_of = |body: &str| { |
|
| 394 | + | let start = body.find(r#"id="backdrop-data">"#).unwrap() + r#"id="backdrop-data">"#.len(); |
|
| 395 | + | body[start..start + body[start..].find("</script>").unwrap()].to_string() |
|
| 396 | + | }; |
|
| 397 | + | let hub_sky = sky_of(&anon.body); |
|
| 398 | + | assert!(hub_sky.contains("andy/demo") && !hub_sky.contains("andy/secret"), "{hub_sky}"); |
|
| 399 | + | // Explore's sky is the hub's, public only, even for the owner of a |
|
| 400 | + | // private repository; an owner's listing has none. |
|
| 401 | + | let explore = get(app, "/explore", Some(&andy)).await; |
|
| 402 | + | assert!(explore.body.contains("andy/secret"), "the list shows him his private repository"); |
|
| 403 | + | let hub_sky = sky_of(&explore.body); |
|
| 404 | + | assert!(hub_sky.contains("andy/demo") && !hub_sky.contains("andy/secret"), "{hub_sky}"); |
|
| 405 | + | assert!(!get(app, "/andy", None).await.body.contains("backdrop-data")); |
|
| 394 | 406 | // The engine is served as WebAssembly. |
|
| 395 | 407 | let res = oxygenhub::router(app.clone()) |
|
| 396 | 408 | .oneshot(Request::builder().uri("/static/backdrop/backdrop_bg.wasm").body(Body::empty()).unwrap()) |
|
| … | 569 unchanged lines |
modified docs/backgrounds.md +4 −3
| 1 | 1 | # Backdrops: living backgrounds for OxygenHub |
|
| 2 | 2 | ||
| 3 | - | Status: the engine and the dashboard's Your sky are built (oxygenhub#157, |
|
| 4 | - | 2026-10-13); the other scenes are proposed (oxygenhub#156). |
|
| 3 | + | Status: the engine, the dashboard's Your sky (oxygenhub#157) and the hub's |
|
| 4 | + | sky on Explore and the signed-out home (oxygenhub#165) are built |
|
| 5 | + | (2026-10-13); the other scenes are proposed (oxygenhub#156). |
|
| 5 | 6 | ||
| 6 | 7 | ## 1. What a backdrop is |
|
| 7 | 8 | ||
| … | 22 unchanged lines |
||
| 30 | 31 | | Page | Scene | Shows | |
|
| 31 | 32 | |---|---|---| |
|
| 32 | 33 | | Dashboard | **Your sky** (§5) | Your repositories as clusters; their activity in the last 30 days as the marks around them | |
|
| 33 | - | | Signed-out home, Explore | **The hub's sky** | The same scene from public repositories only | |
|
| 34 | + | | Signed-out home, Explore | **The hub's sky** | The same scene from the public repositories listed, up to 40, the most recently active; a filter narrows it. Never a private repository, whoever looks | |
|
| 34 | 35 | | Sign in, register, device sign-in | **The mark** | The Oxygen mark's two rings with marks orbiting them; brand only, no data | |
|
| 35 | 36 | | A repository's Diffusion tab | **Ripples** | The upstream and its consumers as the Diffusion mark draws them: a pulse goes out on a bump, green or red as its run ends | |
|
| 36 | 37 | | Admin › Runners › Images | **The swarm** | Runners as nodes; chunks moving between them while an image is fetched, from the swarm tracker's live state | |
|
| … | 163 unchanged lines |
modified templates/dashboard.html +1 −23
| 1 | 1 | {% extends "base.html" %} |
|
| 2 | 2 | {% block title %}Dashboard{% endblock %} |
|
| 3 | 3 | {% block content %} |
|
| 4 | - | {# Your sky (docs/backgrounds.md §5, #157): the engine is Rust compiled to |
|
| 5 | - | WebAssembly. The viewer's choice (Settings › Appearance) is a cookie; |
|
| 6 | - | unset, a touch-only device or Save-Data gets a still sky. Any failure |
|
| 7 | - | leaves no backdrop and nothing else changed. #} |
|
| 8 | - | <canvas id="backdrop" aria-hidden="true"></canvas> |
|
| 9 | - | <script type="application/json" id="backdrop-data">{{ backdrop|safe }}</script> |
|
| 10 | - | <script type="module"> |
|
| 11 | - | const canvas = document.getElementById("backdrop"); |
|
| 12 | - | const chosen = document.cookie.match(/(?:^|; )backdrop=(moving|still|off)/); |
|
| 13 | - | const touch = matchMedia("(pointer: coarse)").matches && !matchMedia("(pointer: fine)").matches; |
|
| 14 | - | const pick = chosen ? chosen[1] : (touch || (navigator.connection && navigator.connection.saveData) ? "still" : "moving"); |
|
| 15 | - | if (pick === "off") { |
|
| 16 | - | canvas.remove(); |
|
| 17 | - | } else { |
|
| 18 | - | try { |
|
| 19 | - | const bd = await import("/static/backdrop/backdrop.js?v={{ shell.assets() }}"); |
|
| 20 | - | await bd.default({ module_or_path: "/static/backdrop/backdrop_bg.wasm?v={{ shell.assets() }}" }); |
|
| 21 | - | bd.start(canvas, JSON.parse(document.getElementById("backdrop-data").textContent), pick === "still"); |
|
| 22 | - | } catch (e) { |
|
| 23 | - | canvas.remove(); |
|
| 24 | - | } |
|
| 25 | - | } |
|
| 26 | - | </script> |
|
| 4 | + | {% include "backdrop.html" %} |
|
| 27 | 5 | <div class="head"> |
|
| 28 | 6 | <h1>Dashboard <small>signed in as {% if let Some(u) = shell.user %}{{ u }}{% endif %}</small></h1> |
|
| 29 | 7 | </div> |
|
| … | 90 unchanged lines |
modified templates/home.html +2 −1
| 1 | 1 | {% extends "base.html" %} |
|
| 2 | 2 | {% block title %}{% if let Some(o) = owner %}{{ o }}{% else %}Repositories{% endif %}{% endblock %} |
|
| 3 | 3 | {% block content %} |
|
| 4 | + | {% if !backdrop.is_empty() %}{% include "backdrop.html" %}{% endif %} |
|
| 4 | 5 | <div class="head"> |
|
| 5 | 6 | {% if let Some(o) = owner %}<h1>{{ o }} <small>{{ repos.len()|plural("repository", "repositories") }}</small></h1> |
|
| 6 | 7 | {% else %}<h1>Repositories <small>{{ description }}</small></h1>{% endif %} |
|
| … | 13 unchanged lines |
||
| 20 | 21 | {% if repos.is_empty() %} |
|
| 21 | 22 | <p class="empty">No repositories yet. Create one with <code>oxygenhub admin repo create OWNER/NAME</code>, then <code>oxy push</code> into it.</p> |
|
| 22 | 23 | {% else %} |
|
| 23 | - | <ul class="rows repos"> |
|
| 24 | + | <ul class="rows repos" data-backdrop-avoid> |
|
| 24 | 25 | {% for r in repos %} |
|
| 25 | 26 | <li> |
|
| 26 | 27 | <div> |
|
| … | 11 unchanged lines |