Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ tracing-wasm = "0.2"
console_error_panic_hook = "0.1"
web-sys = { version = "0.3", features = ["Window", "Location"] }
js-sys = "0.3"
futures = "0.3"
1 change: 1 addition & 0 deletions crates/app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ js-sys.workspace = true
chrono.workspace = true
thiserror.workspace = true
serde.workspace = true
futures.workspace = true
12 changes: 12 additions & 0 deletions crates/app/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
<meta name="description" content="Lepo — monitor your GitHub repositories in one place." />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#fbfbf9" />

<!--
Content Security Policy. Cloudflare Pages injects its Web Analytics beacon
(static.cloudflareinsights.com/beacon.min.js) at runtime; the script-src
below explicitly allows it alongside our own wasm/inline scripts so the
browser stops reporting CSP violations. Adjust the host whitelist if you
change analytics providers.
-->
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval' 'unsafe-inline' https://static.cloudflareinsights.com; connect-src 'self' https://api.github.com https://static.cloudflareinsights.com; img-src 'self' data: https://avatars.githubusercontent.com https://static.cloudflareinsights.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; frame-src 'self';"
/>
<title>Lepo — GitHub Repo Monitor</title>

<!-- Favicon -->
Expand Down
83 changes: 40 additions & 43 deletions crates/app/src/pages/dashboard.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
//! Dashboard page: monitor many repos via a sortable table or compact cards
//! (AGENTS.md §5.1, §5.2; DESIGN.md "Dashboard Pattern").

use futures::future::join_all;
use github_api::{GithubApi, GithubClient, IssueParams, PullParams};
use leptos::prelude::*;
use leptos::reactive::callback::Callable;
Expand Down Expand Up @@ -84,18 +85,23 @@ pub fn DashboardPage() -> impl IntoView {
Some(c) => c,
None => return Vec::new(),
};
let mut out = Vec::with_capacity(repos.len());
for r in repos.iter() {
let (repo, issues, pulls, ci) = fetch_bundle(&client, r).await;
rate_limit.update(&client);
out.push(RepoCardData {
r#ref: r.clone(),
// Fetch every repo's bundle concurrently instead of one-at-a-time.
// Each bundle already fans out its 4 sub-requests internally, so the
// whole dashboard now completes in roughly one repo's worth of latency
// rather than N repos × 4 sequential requests.
let out: Vec<RepoCardData> = join_all(repos.iter().map(|r| fetch_bundle(&client, r)))
.await
.into_iter()
.enumerate()
.map(|(i, (repo, issues, pulls, ci))| RepoCardData {
r#ref: repos[i].clone(),
repo,
issues,
pulls,
ci,
});
}
})
.collect();
rate_limit.update(&client);
out
}
});
Expand Down Expand Up @@ -439,7 +445,8 @@ fn repo_table(
}

/// Fetches metadata, issues, pulls, and latest CI run for one repo,
/// tolerating partial failure on any single piece.
/// tolerating partial failure on any single piece. The four sub-requests run
/// concurrently so a single repo's bundle costs ~1 round-trip of latency.
async fn fetch_bundle(
client: &GithubClient,
r: &RepoRef,
Expand All @@ -449,40 +456,30 @@ async fn fetch_bundle(
Vec<PullRequest>,
Option<WorkflowRun>,
) {
let repo = client.get_repo(&r.owner, &r.name).await.ok();
let issues = client
.list_issues(
&r.owner,
&r.name,
&IssueParams {
state: "open".into(),
labels: vec![],
sort: String::new(),
per_page: 30,
},
)
.await
.map(|(v, _)| v)
.unwrap_or_default();
let pulls = client
.list_pulls(
&r.owner,
&r.name,
&PullParams {
state: "open".into(),
sort: String::new(),
per_page: 30,
},
)
.await
.map(|(v, _)| v)
.unwrap_or_default();
let ci = client
.latest_workflow_run(&r.owner, &r.name)
.await
.ok()
.flatten();
(repo, issues, pulls, ci)
let repo_fut = client.get_repo(&r.owner, &r.name);
let issue_params = IssueParams {
state: "open".into(),
labels: vec![],
sort: String::new(),
per_page: 30,
};
let issues_fut = client.list_issues(&r.owner, &r.name, &issue_params);
let pull_params = PullParams {
state: "open".into(),
sort: String::new(),
per_page: 30,
};
let pulls_fut = client.list_pulls(&r.owner, &r.name, &pull_params);
let ci_fut = client.latest_workflow_run(&r.owner, &r.name);

let (repo, issues, pulls, ci) = futures::join!(repo_fut, issues_fut, pulls_fut, ci_fut,);

(
repo.ok(),
issues.map(|(v, _)| v).unwrap_or_default(),
pulls.map(|(v, _)| v).unwrap_or_default(),
ci.ok().flatten(),
)
}

/// Compact card grid of monitored repos (used when the user prefers cards).
Expand Down