Ver código fonte

report: seed the panel from the repo cache at startup, scoped to configured repos

Follow-up to the previous commit, from watching it live: the merge only ran on
the keepalive tick, so a fresh dashboard was empty for up to 30 s, and it
merged the whole cache even when no repo is configured (a stale clone could
resurrect a recipe from an install that lists none). It now merges only the
cache of CONFIGURED repos, seeds the first collect at startup, and the panel
distinguishes 'N cached (none configured)' from 'no recipe repos configured'.

Verified live: config with [[recipe_repos]] → 117 rows immediately, ⟳ reports
'1 synced' with 139 recipes; a bogus URL → 'ok':false with the git error text
('repository … not found') in the panel chip.
Alexander Korolev 18 horas atrás
pai
commit
0d9cdda2cb
4 arquivos alterados com 50 adições e 11 exclusões
  1. 2 0
      src/report/mod.rs
  2. 6 1
      src/report/page.html
  3. 32 0
      src/report/repo_cache.rs
  4. 10 10
      src/report/serve.rs

+ 2 - 0
src/report/mod.rs

@@ -11,6 +11,8 @@
 //! recipe's `reports/` folder (`<YYYY-MM-DD>_<label>.md|.log`, the
 //! `sm121 publish`-style campaign reports).
 
+pub(crate) mod repo_cache;
+
 use crate::bench_artifacts::sha256_hex;
 use crate::config::{Config, Recipe};
 use anyhow::Result;

+ 6 - 1
src/report/page.html

@@ -745,7 +745,12 @@ function showRepoStat(text, bad) {
 async function loadRepoStatus() {
   try {
     const j = await (await fetch("/api/repos")).json();
-    if (!j.configured) { showRepoStat("⚠ no recipe repos configured", true); return; }
+    if (!j.configured) {
+      // Cached clones still serve recipes even with an empty [[recipe_repos]].
+      if (j.cached_roots) showRepoStat(`repos: ${j.cached_roots} cached (none configured)`, false);
+      else showRepoStat("⚠ no recipe repos configured", true);
+      return;
+    }
     const arch = (j.repos || []).filter(r => r.archived).length;
     const bad = (j.repos || []).filter(r => !r.cached && !r.archived).length;
     if (bad) { showRepoStat(`⚠ ${bad}/${j.configured} repo(s) not synced`, true); return; }

+ 32 - 0
src/report/repo_cache.rs

@@ -0,0 +1,32 @@
+//! The dashboard's view of the on-disk recipe-repo cache.
+//!
+//! The dashboard is long-lived: a repo cloned after it started (a manual
+//! sync, the background loop, a first boot whose sync failed) must not wait
+//! for a container restart. The CLI paths get the roots at process start,
+//! so this merge is private to the report refresher's own config copy.
+
+use crate::config::Config;
+
+/// Fold the on-disk recipe-repo cache into the dashboard's own config copy.
+/// Returns true when a new root appeared (the caller must re-collect).
+///
+/// The dashboard is long-lived: a repo cloned after it started must not wait
+/// for a container restart. The CLI paths get the roots at process start, so
+/// this copy is private to the refresher.
+pub(crate) fn merge_roots(cfg: &mut Config) -> bool {
+    // Only the cache of *configured* repos: an install that lists no repos
+    // must not resurrect stale clones (and the panel says so explicitly).
+    if cfg.recipe_repos.is_empty() {
+        return false;
+    }
+    let mut changed = false;
+    for root in crate::recipe_repos::cached_roots() {
+        let src = root.join("recipes").display().to_string();
+        if !cfg.recipe_sources.contains(&src) {
+            cfg.recipe_sources.push(src);
+            cfg.docker_image_sources.push(root.display().to_string());
+            changed = true;
+        }
+    }
+    changed
+}

+ 10 - 10
src/report/serve.rs

@@ -18,6 +18,7 @@
 
 use crate::config::Config;
 use crate::report::collect;
+use crate::report::repo_cache;
 use axum::{
     Router,
     extract::Path,
@@ -105,7 +106,14 @@ pub(crate) async fn refresh_loop(
         }
     }
     let mut last = String::new();
-    let mut rows: Vec<crate::report::ReportRow> = Vec::new();
+    // Seed: merge the cache and collect once up front, so a freshly started
+    // dashboard is correct immediately instead of after the first keepalive.
+    repo_cache::merge_roots(&mut cfg);
+    let mut rows: Vec<crate::report::ReportRow> = collect(&cfg, archived);
+    if !rows.is_empty() {
+        last = serde_json::to_string(&rows).unwrap_or_default();
+        let _ = tx.send(last.clone());
+    }
     let mut fleet: Option<crate::web::events::Snapshot> = None;
     // ts of the fleet snapshot the rows were last reconciled against
     let mut hull_ts: Option<u64> = None;
@@ -164,15 +172,7 @@ pub(crate) async fn refresh_loop(
                 // A repo cloned since the last tick (manual sync, the add-on's
                 // background loop, a first boot whose sync failed) must become
                 // visible without a restart: watch it and re-collect.
-                let mut dirs_changed = false;
-                for root in crate::recipe_repos::cached_roots() {
-                    let src = root.join("recipes").display().to_string();
-                    if !cfg.recipe_sources.contains(&src) {
-                        cfg.recipe_sources.push(src);
-                        cfg.docker_image_sources.push(root.display().to_string());
-                        dirs_changed = true;
-                    }
-                }
+                let mut dirs_changed = repo_cache::merge_roots(&mut cfg);
                 for dir in crate::config::watchable_recipe_dirs(&cfg) {
                     if watched.contains(&dir) {
                         continue;