Floating monitor: magnet docking and admin memory actions
The magnet docks the monitor bottom-right: beside the minimap in the graph, in the corner of the App view; dragging undocks it. Admins get Unload models and Free models & cache. /free now needs admin.
This commit is contained in:
@@ -4,10 +4,13 @@
|
||||
|
||||
### Added
|
||||
|
||||
- Floating server monitor: a magnet docks it in the bottom-right corner and keeps it there, beside the minimap in the graph, in the corner of the App view. Dragging it undocks it
|
||||
- Floating server monitor: **Unload models** and **Free models & cache** buttons for admins, like ComfyUI Manager's
|
||||
- Admin option *ComfyUI Manager in its own sidebar tab*: a **Manager** tab in the left bar (Open Manager, Custom Nodes Manager) replaces the top-bar button, for accounts with the *Manager* permission
|
||||
|
||||
### Changed
|
||||
|
||||
- Freeing models and cache (ComfyUI's `/free`) needs *Admin* instead of *Queue*: it affects everyone's runs
|
||||
- Other plugins' top-bar buttons (rgthree-comfy, Crystools...) are hidden without the *Manager* permission, like ComfyUI Manager's. Their settings kept on the server, which apply to everyone, need *Manager* too
|
||||
- Pixaroma's Workflows panel is hidden along with ComfyUI's Workflows and Apps tabs (admin setting now called *Hide other workflow browsers*), and its shortcut opens Files. Its server routes are refused to everyone but admins: they read the workflows folder straight from disk, private folders included
|
||||
|
||||
|
||||
+2
-2
@@ -6,9 +6,9 @@
|
||||
|---|---|
|
||||
| *(none)*, a viewer | Watch shared workflows, chat, follow people, open snapshots as private copies |
|
||||
| **Edit** | Change shared workflows, save or rename workflow files, take and restore snapshots, upload files |
|
||||
| **Queue** | Queue prompts, stop or remove your own runs, free memory. Stopping or removing someone else's, or clearing the queue, needs *Admin* |
|
||||
| **Queue** | Queue prompts, stop or remove your own runs. Stopping or removing someone else's, or clearing the queue, needs *Admin* |
|
||||
| **Manager** | Open ComfyUI Manager: install, update or remove custom nodes and models, snapshots, restart. Also other plugins' top-bar buttons and their server-wide settings (rgthree-comfy, Crystools) |
|
||||
| **Admin** | Everything, plus deleting workflow files, accounts and server settings |
|
||||
| **Admin** | Everything, plus deleting workflow files, freeing models and cache, accounts and server settings |
|
||||
|
||||
Queueing, uploads, and saving or deleting workflow files are checked by the server too, when *Enforce permissions on the ComfyUI API* is on (the default). A viewer cannot get around it with a script.
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ PROTECTED = [
|
||||
("POST", "/prompt", "queue"),
|
||||
("POST", "/queue", "queue"),
|
||||
("POST", "/interrupt", "queue"),
|
||||
("POST", "/free", "queue"),
|
||||
("POST", "/free", "admin"), # unloading models affects everyone's runs
|
||||
("POST", "/upload/", "edit|queue"),
|
||||
("POST", "/api/jobs", "queue"),
|
||||
]
|
||||
|
||||
@@ -122,6 +122,7 @@ def comfy_routes(user_root, queue=None):
|
||||
table.route("*", prefix + "/prompt")(prompt)
|
||||
table.post(prefix + "/queue")(queue_route)
|
||||
table.post(prefix + "/interrupt")(ok)
|
||||
table.post(prefix + "/free")(ok)
|
||||
table.route("*", prefix + "/manager/{tail:.*}")(ok)
|
||||
table.route("*", prefix + "/customnode/{tail:.*}")(ok)
|
||||
return table
|
||||
|
||||
@@ -592,3 +592,14 @@ def test_manager_tab_option(run):
|
||||
assert (await sock.until("server"))["server"]["manager_tab"] is True
|
||||
await sock.close()
|
||||
run(scenario())
|
||||
|
||||
|
||||
def test_freeing_memory_is_admin_only(run):
|
||||
async def scenario():
|
||||
async with rig_server() as rig:
|
||||
admin = await rig.setup_admin()
|
||||
queuer = await rig.add_user("queuer", queue=True, manager=True)
|
||||
body = {"unload_models": True, "free_memory": True}
|
||||
assert (await rig.http.post(rig.url("/api/free"), json=body, headers=queuer)).status == 403, "affects everyone's runs"
|
||||
assert (await rig.http.post(rig.url("/api/free"), json=body, headers=admin)).status == 200
|
||||
run(scenario())
|
||||
|
||||
@@ -38,7 +38,7 @@ export class RigSharePanel {
|
||||
this.historyOpen = false;
|
||||
this.newKey = null;
|
||||
|
||||
this.widget = new ServerWidget(client, () => this.render());
|
||||
this.widget = new ServerWidget(client, () => this.render(), { inAppMode: () => sync.inAppMode });
|
||||
|
||||
this.header = h("div", { class: "rs-header" });
|
||||
this.nav = h("div", { class: "rs-nav", role: "tablist" });
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
// Optional floating server monitor: a small draggable card over the canvas.
|
||||
//
|
||||
// The magnet docks it in the bottom-right corner and keeps it there: beside the
|
||||
// minimap in the graph, in the corner of the App view. Dragging it undocks it.
|
||||
// Admins also get ComfyUI's memory actions (unload models, free models & cache).
|
||||
|
||||
import { h, storage } from "./ui.js";
|
||||
|
||||
const KEY_ON = "rigshare.serverFloat";
|
||||
const KEY_POS = "rigshare.serverFloatPos";
|
||||
const KEY_MAGNET = "rigshare.serverFloatMagnet";
|
||||
const GAP = 10;
|
||||
|
||||
// Lucide "magnet" (ISC), inline: neither PrimeIcons nor ComfyUI ships one.
|
||||
const MAGNET_SVG = `<svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.2"
|
||||
stroke-linecap="round" stroke-linejoin="round"><path d="m6 15-4-4 6.75-6.77a7.79 7.79 0 0 1 11 11L13 22l-4-4 6.39-6.36a2.14 2.14 0 0 0-3-3L6 15"/>
|
||||
<path d="m5 8 4 4"/><path d="m12 15 4 4"/></svg>`;
|
||||
|
||||
function mini(label, pct, text) {
|
||||
const p = Math.max(0, Math.min(100, pct || 0));
|
||||
@@ -14,13 +25,26 @@ function mini(label, pct, text) {
|
||||
h("span", { class: "rs-mini-text" }, text));
|
||||
}
|
||||
|
||||
/** Visible element's box, or null. */
|
||||
function box(selector) {
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
return r.width > 0 && r.height > 0 ? r : null;
|
||||
}
|
||||
|
||||
export class ServerWidget {
|
||||
constructor(client, onChange) {
|
||||
constructor(client, onChange, { inAppMode } = {}) {
|
||||
this.client = client;
|
||||
this.onChange = onChange;
|
||||
this.inAppMode = inAppMode ?? (() => false);
|
||||
this.enabled = storage.get(KEY_ON) === "1";
|
||||
this.magnet = storage.get(KEY_MAGNET) !== "0"; // docked unless the user moved it
|
||||
this.el = null;
|
||||
client.on("stats", () => this.render());
|
||||
client.on("self", () => this.render());
|
||||
window.addEventListener("resize", () => this.dock());
|
||||
setInterval(() => this.dock(), 500); // panels open and close, the minimap toggles...
|
||||
if (this.enabled) this.show();
|
||||
}
|
||||
|
||||
@@ -34,16 +58,25 @@ export class ServerWidget {
|
||||
show() {
|
||||
if (this.el) return;
|
||||
this.body = h("div", { class: "rs-float-body" });
|
||||
this.magnetBtn = h("button", {
|
||||
class: "rs-float-close rs-float-magnet", title: "Dock in the bottom-right corner",
|
||||
onclick: () => this.setMagnet(!this.magnet),
|
||||
});
|
||||
this.magnetBtn.innerHTML = MAGNET_SVG;
|
||||
const head = h("div", { class: "rs-float-head" },
|
||||
h("i", { class: "pi pi-server" }), h("span", { class: "rs-grow" }, "Server"),
|
||||
this.magnetBtn,
|
||||
h("button", { class: "rs-float-close", title: "Close", onclick: () => this.toggle() }, h("i", { class: "pi pi-times" })));
|
||||
this.el = h("div", { class: "rs-float rs-panel-vars" }, head, this.body);
|
||||
this.actions = h("div", { class: "rs-float-actions" });
|
||||
this.el = h("div", { class: "rs-float rs-panel-vars" }, head, this.body, this.actions);
|
||||
let pos = null;
|
||||
try { pos = JSON.parse(storage.get(KEY_POS)); } catch { /* default */ }
|
||||
this.place(pos?.x ?? window.innerWidth - 300, pos?.y ?? 70);
|
||||
document.body.append(this.el);
|
||||
this.place(pos?.x ?? window.innerWidth - 300, pos?.y ?? 70);
|
||||
this.drag(head);
|
||||
this.render();
|
||||
this.paintMagnet();
|
||||
this.dock();
|
||||
}
|
||||
|
||||
hide() {
|
||||
@@ -51,13 +84,48 @@ export class ServerWidget {
|
||||
this.el = null;
|
||||
}
|
||||
|
||||
setMagnet(on) {
|
||||
this.magnet = on;
|
||||
storage.set(KEY_MAGNET, on ? null : "0");
|
||||
this.paintMagnet();
|
||||
this.dock();
|
||||
}
|
||||
|
||||
paintMagnet() {
|
||||
this.magnetBtn?.classList.toggle("active", this.magnet);
|
||||
if (this.magnetBtn) this.magnetBtn.title = this.magnet ? "Docked: drag to undock" : "Dock in the bottom-right corner";
|
||||
}
|
||||
|
||||
/** The docked position: beside the minimap in the graph, the corner of the App view. */
|
||||
dockTarget() {
|
||||
const w = this.el.offsetWidth;
|
||||
const hgt = this.el.offsetHeight;
|
||||
if (this.inAppMode()) {
|
||||
const area = box("#linearCenterPanel") ?? { right: window.innerWidth, bottom: window.innerHeight };
|
||||
return { x: area.right - w - GAP, y: area.bottom - hgt - GAP };
|
||||
}
|
||||
const minimap = box('[data-testid="minimap-container"]');
|
||||
if (minimap) return { x: minimap.left - w - GAP, y: minimap.bottom - hgt };
|
||||
// No minimap: the canvas corner, above ComfyUI's zoom controls.
|
||||
const canvas = box("#graph-canvas-container") ?? box("#graph-canvas") ?? { right: window.innerWidth, bottom: window.innerHeight };
|
||||
const controls = box('[data-testid="toggle-minimap-button"]');
|
||||
const bottom = controls ? Math.min(canvas.bottom, controls.top) : canvas.bottom;
|
||||
return { x: canvas.right - w - GAP, y: bottom - hgt - GAP };
|
||||
}
|
||||
|
||||
dock() {
|
||||
if (!this.el || !this.magnet) return;
|
||||
const { x, y } = this.dockTarget();
|
||||
this.place(x, y);
|
||||
}
|
||||
|
||||
place(x, y) {
|
||||
const w = this.el.offsetWidth || 260;
|
||||
const hgt = this.el.offsetHeight || 120;
|
||||
x = Math.max(4, Math.min(window.innerWidth - w - 4, x));
|
||||
y = Math.max(4, Math.min(window.innerHeight - hgt - 4, y));
|
||||
this.el.style.left = `${x}px`;
|
||||
this.el.style.top = `${y}px`;
|
||||
this.el.style.left = `${Math.round(x)}px`;
|
||||
this.el.style.top = `${Math.round(y)}px`;
|
||||
return { x, y };
|
||||
}
|
||||
|
||||
@@ -67,20 +135,48 @@ export class ServerWidget {
|
||||
e.preventDefault();
|
||||
const startX = e.clientX - this.el.offsetLeft;
|
||||
const startY = e.clientY - this.el.offsetTop;
|
||||
const move = (ev) => this.place(ev.clientX - startX, ev.clientY - startY);
|
||||
let moved = false;
|
||||
const move = (ev) => {
|
||||
if (!moved && this.magnet) this.setMagnet(false); // moving it by hand undocks it
|
||||
moved = true;
|
||||
this.place(ev.clientX - startX, ev.clientY - startY);
|
||||
};
|
||||
const up = () => {
|
||||
window.removeEventListener("pointermove", move);
|
||||
window.removeEventListener("pointerup", up);
|
||||
storage.set(KEY_POS, JSON.stringify({ x: this.el.offsetLeft, y: this.el.offsetTop }));
|
||||
if (moved) storage.set(KEY_POS, JSON.stringify({ x: this.el.offsetLeft, y: this.el.offsetTop }));
|
||||
};
|
||||
window.addEventListener("pointermove", move);
|
||||
window.addEventListener("pointerup", up);
|
||||
});
|
||||
}
|
||||
|
||||
async free(options, done) {
|
||||
try {
|
||||
const res = await this.client.request("POST", "/api/free", options);
|
||||
this.client.emit("toast", { severity: "success", summary: "RigShare", detail: done });
|
||||
return res;
|
||||
} catch (e) {
|
||||
this.client.emit("toast", { severity: "error", summary: "RigShare", detail: e.message || String(e) });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.el) return;
|
||||
const st = this.client.stats;
|
||||
const admin = !!this.client.perms?.admin;
|
||||
this.actions.style.display = admin ? "" : "none";
|
||||
if (admin && !this.actions.childElementCount) {
|
||||
this.actions.append(
|
||||
h("button", {
|
||||
class: "rs-btn rs-btn-sm", title: "Unload the models ComfyUI is holding in memory",
|
||||
onclick: () => this.free({ unload_models: true }, "Models unloaded"),
|
||||
}, "Unload models"),
|
||||
h("button", {
|
||||
class: "rs-btn rs-btn-sm", title: "Unload the models and clear the node cache (the next run starts from scratch)",
|
||||
onclick: () => this.free({ unload_models: true, free_memory: true }, "Models and cache freed"),
|
||||
}, "Free models & cache"));
|
||||
}
|
||||
if (!st) {
|
||||
this.body.replaceChildren(h("div", { class: "rs-muted rs-small" }, "Waiting for data…"));
|
||||
return;
|
||||
|
||||
@@ -244,6 +244,10 @@ body.rs-no-manager [aria-label="ComfyUI Manager"] { display: none !important; }
|
||||
.rs-float-head:active { cursor: grabbing; }
|
||||
.rs-float-close { border: 0; background: none; color: var(--rs-muted); cursor: pointer; padding: 2px; }
|
||||
.rs-float-close:hover { color: var(--rs-fg); }
|
||||
.rs-float-magnet { display: grid; place-items: center; border-radius: 4px; }
|
||||
.rs-float-magnet.active { color: var(--rs-accent); }
|
||||
.rs-float-actions { display: flex; gap: 4px; padding: 0 8px 8px; flex-wrap: wrap; }
|
||||
.rs-float-actions .rs-btn { flex: 1; justify-content: center; font-size: 11px; padding: 3px 6px; }
|
||||
.rs-float-body { padding: 6px 8px 8px; display: flex; flex-direction: column; gap: 4px; }
|
||||
.rs-mini { display: grid; grid-template-columns: 46px 1fr 62px; align-items: center; gap: 6px; }
|
||||
.rs-mini-label { color: var(--rs-muted); }
|
||||
|
||||
Reference in New Issue
Block a user