|
|
@@ -72,13 +72,22 @@
|
|
|
.actions select, .actions input[type=checkbox] { font: inherit; }
|
|
|
.actions select {
|
|
|
background: var(--btn); color: var(--fg); border: 1px solid var(--border);
|
|
|
- border-radius: 6px; padding: 3px 6px; font-size: 12.5px; max-width: 260px;
|
|
|
+ border-radius: 6px; padding: 2px 6px; font-size: 12.5px; max-width: 260px; height: 26px;
|
|
|
}
|
|
|
- .actions label { font-size: 12px; color: var(--muted); display: flex; align-items: center; gap: 4px; }
|
|
|
+ .actions label { font-size: 12px; color: var(--muted); display: inline-flex; align-items: center; gap: 4px; height: 26px; }
|
|
|
+ .actions label input { margin: 0; }
|
|
|
+ .strip { display: flex; gap: 6px; flex-wrap: wrap; margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--border); }
|
|
|
+ .chip {
|
|
|
+ display: inline-flex; align-items: center; gap: 6px; cursor: pointer; user-select: none;
|
|
|
+ border: 1px solid var(--border); border-radius: 999px; padding: 3px 10px; font-size: 12px;
|
|
|
+ color: var(--muted); background: var(--btn); transition: border-color .15s, color .15s;
|
|
|
+ }
|
|
|
+ .chip .m { opacity: .7; }
|
|
|
+ .chip.active { color: var(--fg); border-color: var(--accent); }
|
|
|
.abtn {
|
|
|
background: var(--btn); color: var(--fg); border: 1px solid var(--border);
|
|
|
- border-radius: 6px; padding: 3px 12px; cursor: pointer; font: inherit; font-size: 12.5px;
|
|
|
- transition: background .15s;
|
|
|
+ border-radius: 6px; padding: 0 12px; cursor: pointer; font: inherit; font-size: 12.5px;
|
|
|
+ height: 26px; transition: background .15s;
|
|
|
}
|
|
|
.abtn:hover:not(:disabled) { background: var(--btn-hover); }
|
|
|
.abtn:disabled { opacity: .45; cursor: default; }
|
|
|
@@ -132,7 +141,7 @@
|
|
|
</header>
|
|
|
<main>
|
|
|
<section class="card" id="state-card"><h2>Cluster state</h2><div id="clusters"></div></section>
|
|
|
- <section class="card"><h2>Jobs</h2><div class="jobs" id="jobs"><div class="jobs-empty">no jobs yet</div></div></section>
|
|
|
+ <section class="card"><h2>Jobs</h2><div class="jobs" id="jobs"><div class="jobs-empty">no actions yet — serve / stop / power started from this dashboard appear here</div></div></section>
|
|
|
</main>
|
|
|
<div class="dlg-back" id="dlg" hidden>
|
|
|
<div class="dlg">
|
|
|
@@ -148,7 +157,7 @@
|
|
|
"use strict";
|
|
|
const $ = (s) => document.querySelector(s);
|
|
|
const esc = (s) => String(s ?? "").replace(/[&<>"']/g, (c) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c]));
|
|
|
-const state = { snap: null, jobs: new Map(), recipes: [] };
|
|
|
+const state = { snap: null, jobs: new Map(), recipes: [], active: localStorage.getItem("sm121-active") || "" };
|
|
|
|
|
|
// --- theme ---------------------------------------------------------------
|
|
|
const root = document.documentElement;
|
|
|
@@ -248,9 +257,27 @@ function onState(snap) {
|
|
|
const prev = state.snap;
|
|
|
state.snap = snap;
|
|
|
state.jobs = new Map(snap.jobs.map((j) => [j.id, j]));
|
|
|
+ if (!snap.clusters.find((c) => c.name === state.active)) {
|
|
|
+ state.active = snap.clusters[0] ? snap.clusters[0].name : "";
|
|
|
+ }
|
|
|
const box = $("#clusters");
|
|
|
box.innerHTML = "";
|
|
|
- for (const c of snap.clusters) box.appendChild(renderCluster(c, prev ? prev.clusters.find((p) => p.name === c.name) : null));
|
|
|
+ const active = snap.clusters.find((c) => c.name === state.active);
|
|
|
+ if (active) {
|
|
|
+ box.appendChild(renderCluster(active, prev ? prev.clusters.find((p) => p.name === active.name) : null));
|
|
|
+ }
|
|
|
+ // compact fleet strip: one chip per cluster, click to make it active
|
|
|
+ const strip = document.createElement("div");
|
|
|
+ strip.className = "strip";
|
|
|
+ for (const c of snap.clusters) {
|
|
|
+ const chip = document.createElement("div");
|
|
|
+ chip.className = "chip" + (c.name === state.active ? " active" : "");
|
|
|
+ const dot = c.free > 0 ? "on" : c.nodes.some((n) => n.occupied) ? "b" : "off";
|
|
|
+ chip.innerHTML = `<span class="led ${dot}"></span>${esc(c.name)}<span class="m">${c.free} free</span>`;
|
|
|
+ chip.onclick = () => { state.active = c.name; localStorage.setItem("sm121-active", c.name); onState(state.snap); };
|
|
|
+ strip.appendChild(chip);
|
|
|
+ }
|
|
|
+ box.appendChild(strip);
|
|
|
renderJobs();
|
|
|
}
|
|
|
|
|
|
@@ -259,7 +286,7 @@ function renderJobs() {
|
|
|
const box = $("#jobs");
|
|
|
box.innerHTML = "";
|
|
|
const js = [...state.jobs.values()].sort((a, b) => b.started - a.started);
|
|
|
- if (!js.length) { box.innerHTML = '<div class="jobs-empty">no jobs yet</div>'; return; }
|
|
|
+ if (!js.length) { box.innerHTML = '<div class="jobs-empty">no actions yet — serve / stop / power started from this dashboard appear here</div>'; return; }
|
|
|
for (const j of js) {
|
|
|
const el = document.createElement("div");
|
|
|
el.className = "job " + (j.status === "ok" ? "ok" : j.status === "failed" ? "failed" : "jjob");
|
|
|
@@ -441,7 +468,7 @@ async function boot() {
|
|
|
const res = await fetch("/api/recipes");
|
|
|
if (res.ok) {
|
|
|
const d = await res.json();
|
|
|
- state.recipes = d.recipes || [];
|
|
|
+ state.recipes = (d.recipes || []).sort((a, b) => a.name.localeCompare(b.name));
|
|
|
if (state.snap) onState(state.snap); // re-render with recipes
|
|
|
}
|
|
|
} catch {}
|