Sfoglia il codice sorgente

web: recipes panel window — the report --serve page mounted at /recipes (one implementation, prefix-rewritten paths/SSE/viewer) + iframe theme inheritance; fix stale raw-window responses overwriting a newer selection; docs + playwright checks

Alexander Korolev 1 mese fa
parent
commit
07055b199f
4 ha cambiato i file con 39 aggiunte e 2 eliminazioni
  1. 4 1
      AGENTS.md
  2. 5 0
      specs/tech-architecture/web-dashboard.md
  3. 4 1
      src/web/ui.html
  4. 26 0
      src/web/ui.playwright.mjs

+ 4 - 1
AGENTS.md

@@ -58,7 +58,10 @@ each run shows a provenance chip (`⚙ img@<buildsum12>·rcp@<sha12>`, ⚠ when
 the recipe has since drifted from the copy) and the markdown table gets a
 Provenance column. Old runs without a package show `—` / no chip.
 
-`sm121 web` — local axum + SSE dashboard: topology map (physical layer:
+`sm121 web` — local axum + SSE dashboard: **recipes panel** window (the
+`sm121 report --serve` page mounted at `/recipes` — one implementation, its
+own SSE stream and `/r/` viewer prefix-rewritten server-side), topology map
+(physical layer:
 8 GX10 boxes on a 2×4 grid, 200G fabric switch + passive 10G switch + DAC
 direct links derived from config `ib_ip`/`extra_entities`, HA glyphs with
 live states, cluster frames — click a frame chip to activate the cluster),

+ 5 - 0
specs/tech-architecture/web-dashboard.md

@@ -147,6 +147,11 @@ UI outgrows ~2–3k lines.)
     `sm121.recipe` label) with the node list — a container is reported by
     every cluster sharing its node, so a per-node×cluster listing made the
     whole board look busy.
+  - `recipes` — the `sm121 report --serve` panel (table of tested recipes,
+    bench history, stars, the `/r/...` viewer) embedded in an iframe; the
+    dashboard **nests that page at `/recipes`** (`report::html_page_at`
+    rewrites the page's own absolute paths, its SSE stream and viewer links
+    move with it) so there is one implementation of the panel, not two.
   - `raw {kind: config|recipe:name|effective}` — verbatim content, monospace,
     copy button, JSON pretty for `effective`.
 - **LED grid:** rows = clusters → nodes; each node row: power LED,

+ 4 - 1
src/web/ui.html

@@ -1386,19 +1386,22 @@ async function openRaw() {
     sel.value = cur || "config";
     load();
   };
+  let seq = 0;
   async function load() {
     const kind = sel.value;
+    const mine = ++seq; // responses can land out of order — last one wins
     note.textContent = "";
     if (!kind) return;
     const path = kind === "config" ? "/api/raw/config" : kind === "effective" ? "/api/raw/effective" : "/api/raw/recipe/" + encodeURIComponent(kind.slice("recipe:".length));
     try {
       const res = await fetch(path);
       const text = await res.text();
+      if (mine !== seq) return; // a newer selection already answered
       if (!res.ok) { note.textContent = "error: " + text; return; }
       if (kind === "effective") {
         try { pre.textContent = JSON.stringify(JSON.parse(text), null, 2); } catch { pre.textContent = text; }
       } else pre.textContent = text;
-    } catch (e) { note.textContent = "error: " + e.message; }
+    } catch (e) { if (mine === seq) note.textContent = "error: " + e.message; }
   }
   sel.onchange = load;
   fill(); // config/effective are always available; recipes fill in later

+ 26 - 0
src/web/ui.playwright.mjs

@@ -281,6 +281,7 @@ ok("raw window opens via the new-window menu", (await page.locator(".rawwin").co
 await page.selectOption(".rawwin select", "effective");
 await page.waitForFunction(
   () => (document.querySelector(".rawwin .rawpre").textContent || "").includes('"clusters"'),
+  null,
   { timeout: 5000 },
 );
 ok("raw: effective shows the merged config JSON", true);
@@ -317,6 +318,31 @@ await page.waitForFunction(
 );
 ok("topo: frame chip click activates cluster", true);
 
+// --- 14. recipes panel: the `sm121 report --serve` page, mounted at /recipes -----
+await page.click("#dock .plus");
+await page.click('.dockmenu .dock-chip:text-is("recipes")');
+await page.waitForSelector("#win-recipes iframe", { timeout: 10000 });
+const recipes = page.frameLocator("#win-recipes iframe");
+await recipes.locator("table").first().waitFor({ timeout: 10000 });
+ok("recipes: panel page loads in its window", (await page.locator("#win-recipes iframe").count()) === 1);
+ok("recipes: table rendered", (await recipes.locator("tr.row").count()) > 0);
+ok(
+  "recipes: its own SSE stream is live behind the prefix",
+  (await recipes.locator("#live").innerText()).includes("live"),
+);
+// the iframe inherits the dashboard's colour scheme (report page is
+// `light dark` and would otherwise follow the OS)
+const scheme = await page.evaluate(() =>
+  getComputedStyle(document.querySelector("#win-recipes iframe")).colorScheme,
+);
+ok("recipes: iframe colour-scheme follows the theme", scheme === "light" || scheme === "dark", scheme);
+// the report page's own paths move with the mount (dashboard owns /events)
+ok(
+  "recipes: iframe loads the mounted panel",
+  (await page.locator("#win-recipes iframe").getAttribute("src")) === "/recipes",
+);
+await page.evaluate(() => document.querySelector("#win-recipes").style.display = "none");
+
 // --- no page JS errors across the whole flow -------------------------------------
 ok("no page JS errors", pageErrors.length === 0, pageErrors.join("; "));