fabrica

hanna/fabrica

feat(web): paginate dashboard activity; label + language-bar fixes

d1603bb · hanna committed on 2026-07-25

Paginate the dashboard "Recent activity" feed (retain up to 100 commits, 10 per
page with prev/next + a page-size selector that preserves the repo-search term).
Lay label descriptions to the right of the chip instead of below. Fully hide the
language-bar <details> disclosure triangle (display:block on the summary) so it
no longer shows or reserves left indent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed by hannaSSH key fingerprint: SHA256:4g9cWhkAAw8gwqhJUcVbSnGEvdGwklW+1Aa/fMUu59k
3 files changed · +37 −11UnifiedSplit
assets/base.css +13 −3
@@ -342,9 +342,13 @@ body.drawer-open .drawer-backdrop {
342}342}
343.label-list .entry-row-body {343.label-list .entry-row-body {
344 display: flex;344 display: flex;
345 flex-direction: column;345 flex-direction: row;
346 gap: 0.25rem;346 align-items: center;
347 align-items: flex-start;347 gap: 0.6rem;
348 flex-wrap: wrap;
349}
350.label-list .entry-row-meta {
351 margin: 0;
348}352}
349.label-add {353.label-add {
350 display: flex;354 display: flex;
@@ -606,12 +610,18 @@ body.drawer-open .drawer-backdrop {
606 margin: 0 0 1rem;610 margin: 0 0 1rem;
607}611}
608.lang-bar-summary {612.lang-bar-summary {
613 /* `display: block` drops the default disclosure triangle (and the indent it
614 reserves) in Chromium; the list-style/marker resets cover other engines. */
615 display: block;
609 list-style: none;616 list-style: none;
610 cursor: pointer;617 cursor: pointer;
611}618}
612.lang-bar-summary::-webkit-details-marker {619.lang-bar-summary::-webkit-details-marker {
613 display: none;620 display: none;
614}621}
622.lang-bar-summary::marker {
623 content: "";
624}
615.lang-bar {625.lang-bar {
616 display: flex;626 display: flex;
617 height: 0.6rem;627 height: 0.6rem;
crates/web/src/activity.rs +20 −5
@@ -25,8 +25,8 @@ use crate::repo::git_read;
25const WEEKS: usize = 53;25const WEEKS: usize = 53;
26/// Commits scanned per repo on its default branch — bounds dashboard cost.26/// Commits scanned per repo on its default branch — bounds dashboard cost.
27const SCAN_PER_REPO: usize = 400;27const SCAN_PER_REPO: usize = 400;
28/// Recent commits shown in the activity feed.28/// The most recent commits retained for the (paginated) activity feed.
29const FEED_LIMIT: usize = 20;29const FEED_LIMIT: usize = 100;
3030
31/// One commit in the activity feed.31/// One commit in the activity feed.
32pub struct FeedItem {32pub struct FeedItem {
@@ -219,8 +219,20 @@ impl Activity {
219 }219 }
220 }220 }
221221
222 /// Render the recent-activity commit feed.222 /// Render the recent-activity commit feed, paginated. `path`/`query` are the
223 pub fn feed_section(&self, now_ms: i64) -> Markup {223 /// dashboard URL and its query (so the page-size selector and prev/next keep
224 /// any repo-search term).
225 pub fn feed_section(
226 &self,
227 now_ms: i64,
228 paging: crate::repo::Pagination,
229 path: &str,
230 query: Option<&str>,
231 ) -> Markup {
232 let start = paging.offset().min(self.feed.len());
233 let end = start.saturating_add(paging.per_page).min(self.feed.len());
234 let page = &self.feed[start..end];
235 let has_next = end < self.feed.len();
224 html! {236 html! {
225 section class="listing" {237 section class="listing" {
226 h2 { "Recent activity" }238 h2 { "Recent activity" }
@@ -228,7 +240,7 @@ impl Activity {
228 div class="card" { p class="muted" { "No recent activity." } }240 div class="card" { p class="muted" { "No recent activity." } }
229 } @else {241 } @else {
230 ul class="activity-feed card" {242 ul class="activity-feed card" {
231 @for item in &self.feed {243 @for item in page {
232 @let base = format!("/{}/{}", item.owner, item.repo);244 @let base = format!("/{}/{}", item.owner, item.repo);
233 li class="activity-item" {245 li class="activity-item" {
234 div class="activity-head muted" {246 div class="activity-head muted" {
@@ -246,6 +258,9 @@ impl Activity {
246 }258 }
247 }259 }
248 }260 }
261 @if has_next || paging.page > 1 {
262 (crate::repo::pagination_nav(path, query, paging, has_next))
263 }
249 }264 }
250 }265 }
251 }266 }
crates/web/src/pages.rs +4 −3
@@ -70,7 +70,7 @@ pub async fn home(
70) -> AppResult<Response> {70) -> AppResult<Response> {
71 let (jar, chrome) = build_chrome(&state, jar, user.clone(), uri.path().to_string());71 let (jar, chrome) = build_chrome(&state, jar, user.clone(), uri.path().to_string());
72 if let Some(u) = user {72 if let Some(u) = user {
73 let body = dashboard_body(&state, &u, &lq.q.unwrap_or_default()).await?;73 let body = dashboard_body(&state, &u, &lq.q.unwrap_or_default(), &uri).await?;
74 Ok((jar, page(&chrome, "Dashboard", body)).into_response())74 Ok((jar, page(&chrome, "Dashboard", body)).into_response())
75 } else {75 } else {
76 let body = explore_repos_body(&state, "").await?;76 let body = explore_repos_body(&state, "").await?;
@@ -217,9 +217,10 @@ fn user_card(users: &[User]) -> Markup {
217217
218/// The dashboard: the viewer's contribution activity and recent commits, beside218/// The dashboard: the viewer's contribution activity and recent commits, beside
219/// a filterable list of their repositories.219/// a filterable list of their repositories.
220async fn dashboard_body(state: &AppState, user: &User, q: &str) -> AppResult<Markup> {220async fn dashboard_body(state: &AppState, user: &User, q: &str, uri: &Uri) -> AppResult<Markup> {
221 let now = now_ms();221 let now = now_ms();
222 let activity = crate::activity::compute(state, user, now).await?;222 let activity = crate::activity::compute(state, user, now).await?;
223 let paging = repo::Pagination::from_query(uri.query());
223224
224 let mine = state.store.repos_by_owner(&user.id).await?;225 let mine = state.store.repos_by_owner(&user.id).await?;
225 let needle = q.trim().to_lowercase();226 let needle = q.trim().to_lowercase();
@@ -233,7 +234,7 @@ async fn dashboard_body(state: &AppState, user: &User, q: &str) -> AppResult<Mar
233 div class="dashboard" {234 div class="dashboard" {
234 div class="dashboard-main" {235 div class="dashboard-main" {
235 (activity.heatmap())236 (activity.heatmap())
236 (activity.feed_section(now))237 (activity.feed_section(now, paging, uri.path(), uri.query()))
237 }238 }
238 aside class="dashboard-side" {239 aside class="dashboard-side" {
239 form class="search-row" method="get" {240 form class="search-row" method="get" {