feat(web): the dashboard in cards, like the repository page (closes #155) f8cf54e99879
Waiting for your review, Your proposals, Issues assigned to you and Recent activity are each a rounded card with the heading and count in a shaded bar and the list flush inside (card-box, the main column's counterpart of side-card); Your repositories is a sidebar card with New in its heading.
- Author
- Andy McCall @andymccall <[email protected]>
- When
- · 2026-10-13 17:05 UTC
- Signature
- ✔ signed by Andy McCall
- Hash
f8cf54e9987922e4f4ffa0c3dcbfa5118129edd5c3ac3d2a0f75ea51f36eb1fc- Depends on
- 1656dc14c2ac 18e381ea88c2 25ab8c73514a 749d9a5fcd9f b5668108487f c9aacc0e7dc1 d23464c0ac60 eb2a3502601d
- Depended on by
- 8a48dd830178 7f283f8e8b97 6907a989e82a 33bd0bd45f8a 17ca6cddc534
- 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
- 22 atoms, 0 tree operations, 5 files
Browse the tree at this change
modified CHANGELOG.md +1 −0
| … | 5 unchanged lines |
||
| 6 | 6 | ||
| 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 | + | - **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. |
|
| 9 | 10 | - **Tidier pages (#152).** The repository sidebar is a column of cards with shaded headings: Diffusion in one card (Takes from and Diffuses to, each with its pill), the ten most recently moved bookmarks with a link to the rest, and the hub's delegate key shown with the OxygenHub mark. The code tree has a shaded head with the latest change and the history, and folder and file icons; the site bar has icons; the Diffusion tables share one column grid; Help › Coming from GitHub covers submodules and Renovate. |
|
| 10 | 11 | - **Admin › Runners › Images (#146):** every runner image, where it is and what uses it. |
|
| 11 | 12 | - **Faster pages after a restart (#145):** bookmark summaries are kept on disk; slow requests are logged. |
|
| … | 163 unchanged lines |
modified assets/github.css +2 −0
| … | 196 unchanged lines |
||
| 197 | 197 | :root[data-look="github"] .tree-box .tree-head + table.tree { border-top-left-radius: 0; border-top-right-radius: 0; } |
|
| 198 | 198 | :root[data-look="github"] .side-card > h3 { font-size: 14px; } |
|
| 199 | 199 | :root[data-look="github"] table.tree td.g.ic-dir { color: #54aeff; } |
|
| 200 | + | /* Lists inside a card (#155) take the card's border, not their own. */ |
|
| 201 | + | :root[data-look="github"] .card-box > ul.rows, :root[data-look="github"] .card-box > ul.feed { border: 0; border-radius: 0; } |
modified crates/hub/tests/issues.rs +4 −4
| … | 367 unchanged lines |
||
| 368 | 368 | // Andy owns both repositories: both proposals wait for his review. |
|
| 369 | 369 | let page = get(app, "/", Some(&andy)).await; |
|
| 370 | 370 | assert_eq!(page.status, StatusCode::OK); |
|
| 371 | - | assert!(page.body.contains("Waiting for your review <small>2</small>"), "{}", page.body); |
|
| 371 | + | assert!(page.body.contains("Waiting for your review</h2><span class=\"count-chip\">2</span>"), "{}", page.body); |
|
| 372 | 372 | assert!(page.body.contains("/andy/demo/proposal/1") && page.body.contains("/andy/secret/proposal/1")); |
|
| 373 | 373 | assert!(page.body.contains("Your repositories")); |
|
| 374 | 374 | assert!(page.body.contains("opened proposal"), "activity feed: {}", page.body); |
|
| … | 2 unchanged lines |
||
| 377 | 377 | let andy_id = db::accounts::account_by_name(&app.db, "andy").await.unwrap().unwrap().id; |
|
| 378 | 378 | let p = db::proposals::get(&app.db, demo.id, 1).await.unwrap().unwrap(); |
|
| 379 | 379 | db::proposals::add_review(&app.db, p.id, andy_id, "andy", "approve", "").await.unwrap(); |
|
| 380 | - | assert!(get(app, "/", Some(&andy)).await.body.contains("Waiting for your review <small>1</small>")); |
|
| 380 | + | assert!(get(app, "/", Some(&andy)).await.body.contains("Waiting for your review</h2><span class=\"count-chip\">1</span>")); |
|
| 381 | 381 | ||
| 382 | 382 | // Bob sees his own proposal on the public repository, never the private one's. |
|
| 383 | 383 | let page = get(app, "/", Some(&bob)).await; |
|
| 384 | - | assert!(page.body.contains("Your proposals <small>1</small>"), "{}", page.body); |
|
| 384 | + | assert!(page.body.contains("Your proposals</h2><span class=\"count-chip\">1</span>"), "{}", page.body); |
|
| 385 | 385 | assert!(!page.body.contains("Hidden work")); |
|
| 386 | - | assert!(page.body.contains("Waiting for your review <small>0</small>")); |
|
| 386 | + | assert!(page.body.contains("Waiting for your review</h2><span class=\"count-chip\">0</span>")); |
|
| 387 | 387 | assert!(page.body.contains("Faster startup")); |
|
| 388 | 388 | let feed = &page.body[page.body.find("Recent activity").unwrap()..]; |
|
| 389 | 389 | assert!(!feed.contains("andy/secret"), "private activity leaked: {feed}"); |
|
| … | 520 unchanged lines |
modified templates/dashboard.html +18 −9
| … | 3 unchanged lines |
||
| 4 | 4 | <div class="head"> |
|
| 5 | 5 | <h1>Dashboard <small>signed in as {% if let Some(u) = shell.user %}{{ u }}{% endif %}</small></h1> |
|
| 6 | 6 | </div> |
|
| 7 | - | <div class="side-layout dash"> |
|
| 7 | + | <div class="side-layout rev dash"> |
|
| 8 | 8 | <div> |
|
| 9 | - | <h2>Waiting for your review <small>{{ reviews.len() }}</small></h2> |
|
| 9 | + | <section class="card-box"> |
|
| 10 | + | <div class="card-head"><h2>Waiting for your review</h2><span class="count-chip">{{ reviews.len() }}</span></div> |
|
| 10 | 11 | {% if reviews.is_empty() %} |
|
| 11 | 12 | <p class="empty small">Nothing waits on you. Proposals by others in your repositories land here until you review them.</p> |
|
| 12 | 13 | {% else %} |
|
| … | 10 unchanged lines |
||
| 23 | 24 | {% endfor %} |
|
| 24 | 25 | </ul> |
|
| 25 | 26 | {% endif %} |
|
| 27 | + | </section> |
|
| 26 | 28 | ||
| 27 | 29 | {% if !authored.is_empty() %} |
|
| 28 | - | <h2>Your proposals <small>{{ authored.len() }}</small></h2> |
|
| 30 | + | <section class="card-box"> |
|
| 31 | + | <div class="card-head"><h2>Your proposals</h2><span class="count-chip">{{ authored.len() }}</span></div> |
|
| 29 | 32 | <ul class="rows"> |
|
| 30 | 33 | {% for p in authored %} |
|
| 31 | 34 | <li> |
|
| … | 6 unchanged lines |
||
| 38 | 41 | </li> |
|
| 39 | 42 | {% endfor %} |
|
| 40 | 43 | </ul> |
|
| 44 | + | </section> |
|
| 41 | 45 | {% endif %} |
|
| 42 | 46 | ||
| 43 | 47 | {% if !assigned.is_empty() %} |
|
| 44 | - | <h2>Issues assigned to you <small>{{ assigned.len() }}</small></h2> |
|
| 48 | + | <section class="card-box"> |
|
| 49 | + | <div class="card-head"><h2>Issues assigned to you</h2><span class="count-chip">{{ assigned.len() }}</span></div> |
|
| 45 | 50 | <ul class="rows"> |
|
| 46 | 51 | {% for i in assigned %} |
|
| 47 | 52 | <li> |
|
| … | 2 unchanged lines |
||
| 50 | 55 | </li> |
|
| 51 | 56 | {% endfor %} |
|
| 52 | 57 | </ul> |
|
| 58 | + | </section> |
|
| 53 | 59 | {% endif %} |
|
| 54 | 60 | ||
| 55 | - | <h2>Recent activity</h2> |
|
| 61 | + | <section class="card-box"> |
|
| 62 | + | <div class="card-head"><h2>Recent activity</h2></div> |
|
| 56 | 63 | {% if feed.is_empty() %} |
|
| 57 | 64 | <p class="empty small">No activity yet. Push to a repository and it shows here.</p> |
|
| 58 | 65 | {% else %} |
|
| … | 11 unchanged lines |
||
| 70 | 77 | {% endfor %} |
|
| 71 | 78 | </ul> |
|
| 72 | 79 | {% endif %} |
|
| 80 | + | </section> |
|
| 73 | 81 | </div> |
|
| 74 | - | <aside class="side"> |
|
| 75 | - | <div class="bar"><h3>Your repositories</h3><a class="go small" href="/new">new</a></div> |
|
| 82 | + | <aside class="side cards"> |
|
| 83 | + | <section class="side-card"><h3>Your repositories <span class="count-chip">{{ mine.len() }}</span><a class="more" href="/new">+ New</a></h3><div class="body"> |
|
| 76 | 84 | {% if mine.is_empty() %} |
|
| 77 | 85 | <p class="small mute">None yet. <a href="/new">Create one</a>, or <a href="/explore">explore</a>.</p> |
|
| 78 | 86 | {% else %} |
|
| 79 | - | <ul class="plain"> |
|
| 87 | + | <ul class="plain repo-list"> |
|
| 80 | 88 | {% for r in mine %} |
|
| 81 | - | <li><a href="/{{ r.owner }}/{{ r.name }}"><span class="mute">{{ r.owner }} /</span> {{ r.name }}</a>{% if r.visibility != "public" %} <span class="tag">{{ r.visibility }}</span>{% endif %}</li> |
|
| 89 | + | <li><a href="/{{ r.owner }}/{{ r.name }}" title="{{ r.owner }}/{{ r.name }}"><span class="mute">{{ r.owner }} /</span> {{ r.name }}</a>{% if r.visibility != "public" %}<span class="tag">{{ r.visibility }}</span>{% endif %}</li> |
|
| 82 | 90 | {% endfor %} |
|
| 83 | 91 | </ul> |
|
| 84 | 92 | {% endif %} |
|
| 93 | + | </div></section> |
|
| 85 | 94 | </aside> |
|
| 86 | 95 | </div> |
|
| 87 | 96 | {% endblock %} |
modified templates/style.css +14 −0
| … | 1090 unchanged lines |
||
| 1091 | 1091 | ul.df-list .df-line { display: flex; align-items: center; gap: 8px; margin-top: 3px; font-size: 12px; } |
|
| 1092 | 1092 | ul.bookmarks li { display: flex; align-items: center; gap: 6px; } |
|
| 1093 | 1093 | ul.bookmarks li > a { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
|
| 1094 | + | ||
| 1095 | + | /* A main-column section as a card (#155), the counterpart of the |
|
| 1096 | + | sidebar's side-card: a rounded box, its heading and count in a shaded |
|
| 1097 | + | bar, its list flush inside. */ |
|
| 1098 | + | .card-box { border: 1px solid var(--line); border-radius: 6px; background: var(--panel); overflow: hidden; margin-bottom: 16px; } |
|
| 1099 | + | .card-box > .card-head { display: flex; align-items: center; gap: 8px; padding: 8px 16px; background: var(--soft); border-bottom: 1px solid var(--line); } |
|
| 1100 | + | .card-box > .card-head h2, .card-box > .card-head h3 { margin: 0; font-size: 14px; font-weight: 600; } |
|
| 1101 | + | .card-box > .card-head .more { margin-left: auto; font-size: 12px; } |
|
| 1102 | + | .card-box > ul.rows, .card-box > ul.feed { border: 0; border-radius: 0; margin: 0; } |
|
| 1103 | + | .card-box > ul.rows li { margin: 0; border: 0; border-top: 1px solid var(--line); } |
|
| 1104 | + | .card-box > ul.rows li:first-child, .card-box > ul.feed li:first-child { border-top: 0; } |
|
| 1105 | + | .card-box > .empty { margin: 0; padding: 12px 16px; } |
|
| 1106 | + | ul.repo-list li { display: flex; align-items: center; gap: 6px; margin: 3px 0; } |
|
| 1107 | + | ul.repo-list li > a { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |