From 53d635c5bd84973f3a33a145953a2311adb05507 Mon Sep 17 00:00:00 2001 From: suradet-ps Date: Sat, 18 Jul 2026 07:55:12 +0700 Subject: [PATCH 1/2] perf: parallelize dashboard fetches and allow Cloudflare beacon in CSP - Fetch all repo bundles concurrently with futures::join_all instead of one-at-a-time sequentially (was N repos x 4 serial requests) - Fan out each repo's 4 sub-requests (repo/issues/pulls/CI) with futures::join! so a single bundle costs ~1 round-trip - Add Content-Security-Policy meta tag that whitelists static.cloudflareinsights.com, fixing the script-src violation from the Cloudflare Pages analytics beacon --- Cargo.lock | 1 + Cargo.toml | 1 + crates/app/Cargo.toml | 1 + crates/app/index.html | 12 ++++ crates/app/src/pages/dashboard.rs | 100 ++++++++++++++++-------------- 5 files changed, 68 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f494d15..c49e80a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,7 @@ version = "0.1.0" dependencies = [ "chrono", "console_error_panic_hook", + "futures", "github-api", "gloo-net", "gloo-storage", diff --git a/Cargo.toml b/Cargo.toml index 419429a..7770689 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 34596d9..baee2e3 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -29,3 +29,4 @@ js-sys.workspace = true chrono.workspace = true thiserror.workspace = true serde.workspace = true +futures.workspace = true diff --git a/crates/app/index.html b/crates/app/index.html index ad84949..3ad5e2d 100644 --- a/crates/app/index.html +++ b/crates/app/index.html @@ -6,6 +6,18 @@ + + + Lepo — GitHub Repo Monitor diff --git a/crates/app/src/pages/dashboard.rs b/crates/app/src/pages/dashboard.rs index b0f83ef..f45adbf 100644 --- a/crates/app/src/pages/dashboard.rs +++ b/crates/app/src/pages/dashboard.rs @@ -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; @@ -84,18 +85,27 @@ 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(), - repo, - issues, - pulls, - ci, - }); - } + // 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 = 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 } }); @@ -439,7 +449,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, @@ -449,40 +460,35 @@ async fn fetch_bundle( Vec, Option, ) { - 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). From 282a50e4de44ce593af75f9dc2724034e5f06f66 Mon Sep 17 00:00:00 2001 From: suradet-ps Date: Sat, 18 Jul 2026 08:05:40 +0700 Subject: [PATCH 2/2] style: run cargo fmt to satisfy formatting check --- crates/app/src/pages/dashboard.rs | 35 ++++++++++++------------------- 1 file changed, 13 insertions(+), 22 deletions(-) diff --git a/crates/app/src/pages/dashboard.rs b/crates/app/src/pages/dashboard.rs index f45adbf..93eb54e 100644 --- a/crates/app/src/pages/dashboard.rs +++ b/crates/app/src/pages/dashboard.rs @@ -89,22 +89,18 @@ pub fn DashboardPage() -> impl IntoView { // 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 = 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(); + let out: Vec = 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 } @@ -476,12 +472,7 @@ async fn fetch_bundle( 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, - ); + let (repo, issues, pulls, ci) = futures::join!(repo_fut, issues_fut, pulls_fut, ci_fut,); ( repo.ok(),