|
|
@@ -419,11 +419,14 @@ $("#theme").onclick = () => {
|
|
|
// Vanilla win manager: absolute windows, drag by titlebar, corner resize,
|
|
|
// minimize/close to the dock (chip to restore), click-to-front z-order.
|
|
|
let ztop = 20;
|
|
|
-const wins = {}; // managed windows: { state, jobs }
|
|
|
+const wins = {}; // named windows: { topo, state, jobs }
|
|
|
+// every window ever created (named, logs, raw, recipes) — the dock lists
|
|
|
+// minimized ones from here, so a window can't be minimized into nowhere
|
|
|
+const winRegistry = new Set();
|
|
|
const logWins = new Map(); // attach_id -> { win, pre, buf, paused, ... }
|
|
|
const front = (el) => { el.style.zIndex = ++ztop; };
|
|
|
|
|
|
-function makeWin({ id, title, x, y, w, h, bodyHtml, onClosed, extraCls, removeOnClose }) {
|
|
|
+function makeWin({ id, title, short, x, y, w, h, bodyHtml, onClosed, extraCls, removeOnClose }) {
|
|
|
const win = document.createElement("section");
|
|
|
win.className = "win" + (extraCls ? " " + extraCls : "");
|
|
|
if (id) win.id = id;
|
|
|
@@ -431,6 +434,7 @@ function makeWin({ id, title, x, y, w, h, bodyHtml, onClosed, extraCls, removeOn
|
|
|
const bar = document.createElement("div");
|
|
|
bar.className = "win-bar";
|
|
|
bar.innerHTML = `<span class="wtitle">${esc(title)}</span>
|
|
|
+ <button class="wbtn" data-act="max" title="maximize">▢</button>
|
|
|
<button class="wbtn" data-act="min" title="minimize">–</button>
|
|
|
<button class="wbtn" data-act="close" title="close">×</button>`;
|
|
|
const body = document.createElement("div");
|
|
|
@@ -442,10 +446,32 @@ function makeWin({ id, title, x, y, w, h, bodyHtml, onClosed, extraCls, removeOn
|
|
|
$("#winlayer").appendChild(win);
|
|
|
const closed = { value: false };
|
|
|
bar.querySelector('[data-act="min"]').onclick = () => { win.style.display = "none"; renderDock(); };
|
|
|
+ // maximize = fill the window layer (minus the dock strip, which hosts the
|
|
|
+ // + menu and holds the chips of minimized windows)
|
|
|
+ const maxBtn = bar.querySelector('[data-act="max"]');
|
|
|
+ let geom = null;
|
|
|
+ maxBtn.onclick = () => {
|
|
|
+ front(win);
|
|
|
+ if (geom) {
|
|
|
+ Object.assign(win.style, geom);
|
|
|
+ geom = null;
|
|
|
+ maxBtn.textContent = "▢";
|
|
|
+ maxBtn.title = "maximize";
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ geom = { left: win.style.left, top: win.style.top, width: win.style.width, height: win.style.height };
|
|
|
+ win.style.left = "0px";
|
|
|
+ win.style.top = "0px";
|
|
|
+ win.style.width = $("#winlayer").clientWidth + "px";
|
|
|
+ win.style.height = Math.max(200, $("#winlayer").clientHeight - 40) + "px";
|
|
|
+ maxBtn.textContent = "❐";
|
|
|
+ maxBtn.title = "restore size";
|
|
|
+ };
|
|
|
bar.querySelector('[data-act="close"]').onclick = () => {
|
|
|
if (removeOnClose) win.remove(); // log windows die with their attach
|
|
|
else win.style.display = "none";
|
|
|
closed.value = true;
|
|
|
+ winRegistry.delete(handle);
|
|
|
renderDock();
|
|
|
if (onClosed) onClosed();
|
|
|
};
|
|
|
@@ -470,10 +496,13 @@ function makeWin({ id, title, x, y, w, h, bodyHtml, onClosed, extraCls, removeOn
|
|
|
win.addEventListener("pointerdown", () => front(win));
|
|
|
const handle = {
|
|
|
win, body, title,
|
|
|
+ label: short || title,
|
|
|
+ unmaximize() { if (geom) maxBtn.onclick(); },
|
|
|
restore() { win.style.display = ""; closed.value = false; front(win); renderDock(); },
|
|
|
isMinimized: () => win.style.display === "none",
|
|
|
isClosed: () => closed.value,
|
|
|
};
|
|
|
+ winRegistry.add(handle);
|
|
|
return handle;
|
|
|
}
|
|
|
|
|
|
@@ -495,13 +524,9 @@ function renderDock() {
|
|
|
b.onclick = fn;
|
|
|
dock.appendChild(b);
|
|
|
};
|
|
|
- for (const h of Object.values(wins)) {
|
|
|
- if (h.isMinimized()) chip((h.isClosed() ? "+ " : "– ") + h.title, h.restore);
|
|
|
- }
|
|
|
- for (const w of logWins.values()) {
|
|
|
- if (w.win.style.display === "none") chip("– logs: " + w.node, () => { w.win.style.display = ""; front(w.win); renderDock(); });
|
|
|
- }
|
|
|
- if (!rawWin) chip("+ raw config / recipe / effective", openRaw);
|
|
|
+ const min = [...winRegistry].filter((h) => h.isMinimized() && h.win.isConnected);
|
|
|
+ for (const h of min) chip("– " + h.label, h.restore);
|
|
|
+ if (!min.length && !rawWin) chip("+ raw config / recipe / effective", openRaw);
|
|
|
// New-window menu (plan): State / Logs / Jobs / Raw.
|
|
|
const plus = document.createElement("button");
|
|
|
plus.className = "dock-chip plus";
|
|
|
@@ -1243,6 +1268,7 @@ async function openLog(cluster, node, recipe) {
|
|
|
recipe = recipe || (n.containers[0] ? n.containers[0].recipe : "");
|
|
|
const win = makeWin({
|
|
|
title: "logs: " + node + (recipe ? " · " + recipe : ""),
|
|
|
+ short: "logs: " + node,
|
|
|
x: 20 + (logWins.size * 26) % 240, y: 20 + (logWins.size * 26) % 160,
|
|
|
w: Math.min(620, innerWidth - 40), h: Math.min(420, innerHeight * 0.6),
|
|
|
bodyHtml: `<div class="logbar">
|
|
|
@@ -1562,22 +1588,24 @@ function initManagedWindows() {
|
|
|
// topology map: the primary window (left); state/jobs stack on the right
|
|
|
const tw = Math.max(560, Math.floor(W * 0.58));
|
|
|
const rx = tw + 30, rw = Math.max(340, W - rx - 12);
|
|
|
- const rh = Math.max(240, Math.floor(L * 0.48));
|
|
|
+ // the map is a fixed-aspect drawing: size its window to the drawing, or
|
|
|
+ // the SVG letterboxes itself with big empty bands above and below
|
|
|
+ const TH = Math.min(Math.round(tw * (TG_H / TG_W)) + 34, L - 260);
|
|
|
wins.topo = makeWin({
|
|
|
id: "win-topo", title: "Topology", x: 12, y: 12,
|
|
|
- w: tw, h: L - 12,
|
|
|
+ w: tw, h: TH,
|
|
|
bodyHtml: `<div id="topo"><div class="topo-empty">waiting for the first fleet scan…</div></div>`,
|
|
|
});
|
|
|
+ wins.jobs = makeWin({
|
|
|
+ id: "win-jobs", title: "Jobs", x: 12, y: TH + 24,
|
|
|
+ w: tw, h: L - TH - 24,
|
|
|
+ bodyHtml: `<div class="jobs" id="jobs"><div class="jobs-empty">no sm121 containers running, no actions yet — serve / stop / power started from this dashboard appear here</div></div>`,
|
|
|
+ });
|
|
|
wins.state = makeWin({
|
|
|
id: "win-state", title: "Cluster state", x: rx, y: 12,
|
|
|
- w: rw, h: rh,
|
|
|
+ w: rw, h: L - 12,
|
|
|
bodyHtml: `<div id="clusters"><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div></div>`,
|
|
|
});
|
|
|
- wins.jobs = makeWin({
|
|
|
- id: "win-jobs", title: "Jobs", x: rx, y: rh + 24,
|
|
|
- w: rw, h: L - rh - 24,
|
|
|
- bodyHtml: `<div class="jobs" id="jobs"><div class="jobs-empty">no sm121 containers running, no actions yet — serve / stop / power started from this dashboard appear here</div></div>`,
|
|
|
- });
|
|
|
renderDock();
|
|
|
}
|
|
|
async function boot() {
|