OXYGENHUB

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