Added run monitoring and fixed some focus settings

Added a way to monitor success or error and run a custom script
This commit is contained in:
HAL-XP
2025-12-03 12:10:47 +01:00
parent 3eceaea385
commit 4ece0021ae
4 changed files with 323 additions and 141 deletions
+15 -4
View File
@@ -2,9 +2,9 @@
![Version](https://img.shields.io/badge/version-1.0.0-blue) ![ComfyUI](https://img.shields.io/badge/ComfyUI-Compatible-green)
**HALXP-Comfy** (High Availability & Layout eXtension Pack) is a professional UI suite for ComfyUI designed to enhance workflow management.
**HALXP-Comfy** is a professional UX suite for ComfyUI designed to enhance workflow management.
This extension is the starting point for a growing collection of UI tools. Currently, it features **HALXP Focus**, a smart navigation utility, with more enhancements currently in development.
This extension is the starting point for a growing collection of tools. Currently, it features **HALXP Focus**, a way to auto-focus the current running node and , **HALXP RunMonitor** taht lets you run a script on success or error with more enhancements currently in development.
**Verified working on:** Legacy UI, ComfyUI V1 (Nodes 2.0), and Portable Distributions.
@@ -21,7 +21,18 @@ A smart "Focus" button injected into your main menu bar.
* **Visual Feedback:** Features a status indicator (Green dot = Active) so you always know when auto-follow is engaged.
* **V1 Compatible:** Works natively with the new Vue.js frontend and PrimeVue menus.
### 2. More Coming Soon
### 2. HALXP RunMonitor
A way to run a custom script on workflow success or failure
* Go in the Settings under HALXP-Comfy and set a batch file to run or anything you want
* (play a wav, open a website, etc.) separately for sucess and errors - doesn't cover static validation on launch
* ex: With my phone provider (Free Mobile in France), I have an API to send SMS so I can go do other things and get notified when my workflow ends
* batch file content example:
* start "" "https://smsapi.free-mobile.fr/sendmsg?user=XXXXXXXXXX&pass=YYYYYYYYYYY&msg=Comfy%%20Error%%20!"
* **Smart Docking:** Automatically detects the ComfyUI Manager button and injects itself seamlessly beside it.
* **Visual Feedback:** Features a status indicator (Green dot = Active) so you always know when Monitor is engaged.
* **V1 Compatible:** Works natively with the new Vue.js frontend and PrimeVue menus.
### 3. More Coming Soon
HALXP is built as a modular suite. Expect future updates to include additional layout and debugging tools to further improve your ComfyUI experience.
## 📦 Installation
@@ -35,4 +46,4 @@ HALXP is built as a modular suite. Expect future updates to include additional l
1. Navigate to your `ComfyUI/custom_nodes/` directory.
2. Clone this repository:
```bash
git clone [https://github.com/HAL-XP/halxp-comfy](https://github.com/HAL-XP/halxp-comfy)
git clone [https://github.com/YOUR_USERNAME/halxp-comfyui](https://github.com/YOUR_USERNAME/halxp-comfyui)
+76 -8
View File
@@ -1,12 +1,80 @@
# HALXP - High Availability & Layout eXtension Pack
# Registers the web directory for JS extensions
import server
import subprocess
import asyncio
from aiohttp import web
# --- CONFIG STORAGE ---
halxp_config = {
"enabled": False,
"success_path": "",
"error_path": ""
}
# --- STATE TRACKING ---
run_has_error = False
# --- 1. API ROUTE ---
@server.PromptServer.instance.routes.post("/halxp/update_config")
async def update_halxp_config(request):
try:
data = await request.json()
halxp_config.update(data)
return web.json_response({"status": "ok"})
except Exception as e:
print(f"[HALXP-Core] Error updating config: {e}")
return web.json_response({"status": "error"}, status=500)
# --- 2. MESSAGE HOOK ---
original_send_json = server.PromptServer.instance.send_json
async def intercepted_send_json(self, event, data, sid=None):
global run_has_error
# 1. Execute Original Behavior
await original_send_json(event, data, sid)
# 2. Check RunMonitor Logic
if halxp_config.get("enabled"):
try:
# --- START: SAFETY RESET ---
# Just in case the last run didn't clear the flag, we clear it on start too.
if event == "execution_start":
run_has_error = False
# --- DETECT ERROR ---
elif event == "execution_error":
run_has_error = True
cmd = halxp_config.get("error_path")
if cmd and cmd.strip():
print(f"[HALXP-RunMonitor] ❌ Workflow Error. Running: {cmd}")
subprocess.Popen(cmd, shell=True)
# --- DETECT FINISH (SUCCESS or FAIL) ---
# 'executing' with node=None means the prompt is done.
elif event == "executing" and data.get("node") is None:
# Only run success script if NO error occurred
if not run_has_error:
cmd = halxp_config.get("success_path")
if cmd and cmd.strip():
print(f"[HALXP-RunMonitor] ✅ Workflow Finished. Running: {cmd}")
subprocess.Popen(cmd, shell=True)
# --- CRITICAL FIX: ALWAYS RESET STATE HERE ---
# Once the run is finished, we must clear the error flag
# so the NEXT run starts fresh.
if run_has_error:
print(f"[HALXP-RunMonitor] 🔄 Run finished with errors. Resetting state for next run.")
run_has_error = False
except Exception as e:
print(f"[HALXP-RunMonitor] ⚠️ Failed to execute monitoring script: {e}")
# Apply the hook
server.PromptServer.instance.send_json = intercepted_send_json.__get__(server.PromptServer.instance, server.PromptServer)
# --- STANDARD NODE MAPPING ---
WEB_DIRECTORY = "./js"
# Export empty dictionaries so ComfyUI doesn't warn about missing mappings
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
__all__ = ["WEB_DIRECTORY", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
print("\033[34m[HALXP-ComfyUI] JS Extensions Loaded successfully.\033[0m")
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"]
+66 -129
View File
@@ -1,183 +1,115 @@
// \ComfyUI\custom_nodes\better-focus\js\universal_focus.js
// console.log("%c[HALXP-Comfy-Focus] Loaded", "background: purple; color: white; font-weight: bold;");
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "Comfy.HALXP-ComfyUI.Focus",
name: "Comfy.HALXP.Focus",
async setup() {
// --- CONFIG ---
// --- CONSTANTS ---
const SMOOTHNESS = 0.15;
const SETTING_ID = "HALXP-Comfy.Focus.DisplayButton";
// Clean IDs
const SETTING_DISPLAY_ID = "HALXP.Focus.DisplayButton";
const SETTING_AUTO_ID = "HALXP.Focus.AutoEnable";
// --- STATE ---
let isFocusEnabled = false;
let isWorkflowRunning = false;
let activeNode = null;
let animationFrameId = null;
let focusBtn = null;
let dot = null;
// --- 0. REGISTER SETTING ---
// --- 1. REGISTER SETTINGS ---
app.ui.settings.addSetting({
id: SETTING_ID,
id: SETTING_DISPLAY_ID,
name: "Display Focus Button",
type: "boolean",
defaultValue: true,
// MUST be an array.
// Element 0 = Tab Name (HALXP-Comfy)
// Element 1 = Section Header (Focus)
category: ['HALXP-Comfy', 'Focus'],
// 3rd element 'Display' ensures uniqueness
category: ['HALXP-Comfy', 'Focus', 'Display'],
tooltip: "Show or hide the HALXP Focus button in the top menu.",
onChange: (value) => {
if (value) {
// Try to inject immediately if turned on
injectNextToManager();
} else {
// Remove button and disable focus if turned off
if (focusBtn) {
focusBtn.remove();
// Also force stop focus mode if it was active
if (isFocusEnabled) setFocusState(false);
}
if (value) injectNextToManager();
else {
if (focusBtn) focusBtn.remove();
if (isFocusEnabled) setFocusState(false);
}
}
});
// --- 1. CREATE BUTTON ---
app.ui.settings.addSetting({
id: SETTING_AUTO_ID,
name: "Auto-Focus on Run",
type: "boolean",
defaultValue: true,
// 3rd element 'Auto' ensures uniqueness
category: ['HALXP-Comfy', 'Focus', 'Auto'],
tooltip: "Automatically enable Focus mode when a workflow starts executing.",
});
// --- 2. BUTTON LOGIC ---
function createButton() {
const btn = document.createElement("button");
btn.textContent = "Focus";
btn.id = "halxp-focus-btn";
// CSS: Flex-safe, visible, matches Manager style
btn.style.cssText = `
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0 10px;
margin: 0 4px; /* Space between Focus and Manager */
background-color: var(--comfy-input-bg, #333);
color: var(--fg-color, #fff);
border: 1px solid var(--border-color, #555);
border-radius: 4px;
cursor: pointer;
font-family: sans-serif;
font-size: 13px;
font-weight: 600;
/* CRITICAL VISIBILITY SETTINGS */
flex-shrink: 0;
white-space: nowrap;
z-index: 9999;
pointer-events: auto;
/* DEBUG: REMOVE THIS RED BORDER LATER IF YOU WANT */
position: relative; display: inline-flex; align-items: center; justify-content: center;
padding: 0 10px; margin: 0 4px; background-color: var(--comfy-input-bg, #333);
color: var(--fg-color, #fff); border: 1px solid var(--border-color, #555);
border-radius: 4px; cursor: pointer; font-family: sans-serif; font-size: 13px;
font-weight: 600; flex-shrink: 0; white-space: nowrap; z-index: 9999; pointer-events: auto;
border: 1px solid #d3c1c1ff;
`;
const d = document.createElement("span");
d.style.cssText = `
width: 8px; height: 8px; border-radius: 50%;
background-color: #666; margin-right: 8px;
display: inline-block;
flex-shrink: 0;
`;
d.style.cssText = `width: 8px; height: 8px; border-radius: 50%; background-color: #666; margin-right: 8px; display: inline-block; flex-shrink: 0;`;
btn.prepend(d);
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
toggleFocus();
};
btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); toggleFocus(); };
dot = d;
return btn;
}
// --- 2. FIND MANAGER & INJECT ---
function injectNextToManager() {
// Check Settings first. If setting is false, do not inject.
const shouldDisplay = app.ui.settings.getSettingValue(SETTING_ID, true);
if (!shouldDisplay) return;
// Stop if we already exist
if (document.getElementById("halxp-focus-btn")) return;
// 1. Find the Manager Button
// We look for any button that strictly contains the text "Manager"
const allButtons = Array.from(document.querySelectorAll("button"));
const shouldDisplay = app.ui.settings.getSettingValue(SETTING_DISPLAY_ID, true);
if (!shouldDisplay || document.getElementById("halxp-focus-btn")) return;
const allButtons = Array.from(document.querySelectorAll("button"));
const managerBtn = allButtons.find(b => {
// Must be visible
if (!b.offsetParent) return false;
const text = b.innerText.trim();
return text === "Manager" || text === "ComfyUI Manager";
});
// 2. Inject
if (managerBtn && managerBtn.parentNode) {
// console.log("[Visual Assist] Found Manager Button. Attaching...");
if (!focusBtn) focusBtn = createButton();
// Match the height of the Manager button exactly
const h = managerBtn.offsetHeight;
if (h > 0) focusBtn.style.height = `${h}px`;
else focusBtn.style.height = "28px";
// Insert BEFORE the Manager button (to its left)
focusBtn.style.height = h > 0 ? `${h}px` : "28px";
managerBtn.parentNode.insertBefore(focusBtn, managerBtn);
// Restore state appearance if we were somehow already enabled
if (isFocusEnabled) {
dot.style.backgroundColor = "#4CAF50";
dot.style.boxShadow = "0 0 6px #4CAF50";
focusBtn.style.color = "#fff";
focusBtn.style.borderColor = "#4CAF50";
}
return;
if (isFocusEnabled) updateButtonStyle(true);
}
}
// --- 3. LOOP ---
// We poll every 500ms because the Manager itself loads slowly.
setInterval(injectNextToManager, 500);
// --- 4. FOCUS LOGIC (Standard) ---
function toggleFocus() {
setFocusState(!isFocusEnabled);
}
// --- 3. FOCUS CORE LOGIC ---
function toggleFocus() { setFocusState(!isFocusEnabled); }
function setFocusState(enabled) {
isFocusEnabled = enabled;
// Guard: If button doesn't exist (setting off), we can't style it.
if (!focusBtn || !dot) return;
if (focusBtn && dot) updateButtonStyle(enabled);
if (enabled && activeNode) requestAnimationFrame(smoothFollow);
else if (animationFrameId) { cancelAnimationFrame(animationFrameId); animationFrameId = null; }
}
if (isFocusEnabled) {
dot.style.backgroundColor = "#4CAF50";
dot.style.boxShadow = "0 0 6px #4CAF50";
focusBtn.style.color = "#fff";
focusBtn.style.borderColor = "#4CAF50";
if (activeNode) requestAnimationFrame(smoothFollow);
function updateButtonStyle(enabled) {
if (!dot || !focusBtn) return;
if (enabled) {
dot.style.backgroundColor = "#4CAF50"; dot.style.boxShadow = "0 0 6px #4CAF50";
focusBtn.style.color = "#fff"; focusBtn.style.borderColor = "#4CAF50";
} else {
dot.style.backgroundColor = "#666";
dot.style.boxShadow = "none";
focusBtn.style.color = "var(--fg-color, #ccc)";
focusBtn.style.borderColor = "#d3c1c1ff"; // Back to disabled
if (animationFrameId) {
cancelAnimationFrame(animationFrameId);
animationFrameId = null;
}
dot.style.backgroundColor = "#666"; dot.style.boxShadow = "none";
focusBtn.style.color = "var(--fg-color, #ccc)"; focusBtn.style.borderColor = "#d3c1c1ff";
}
}
// Listen for canvas drag
// Disable on manual drag
const waitForCanvas = setInterval(() => {
if (app.canvas && app.canvas.canvas) {
clearInterval(waitForCanvas);
@@ -187,13 +119,21 @@ app.registerExtension({
}
}, 500);
// --- 4. EXECUTION LOOP ---
api.addEventListener("executing", (e) => {
if (e.detail) {
if (!isWorkflowRunning) {
isWorkflowRunning = true;
if (app.ui.settings.getSettingValue(SETTING_AUTO_ID, true)) setFocusState(true);
}
activeNode = app.graph.getNodeById(e.detail);
if (isFocusEnabled && activeNode) {
app.canvas.setDirty(true, true);
if (!animationFrameId) requestAnimationFrame(smoothFollow);
}
} else {
isWorkflowRunning = false;
activeNode = null;
}
});
@@ -201,14 +141,11 @@ app.registerExtension({
if (isFocusEnabled && activeNode) {
const canvas = app.canvas;
const ds = canvas.ds;
const screenCenterX = canvas.canvas.clientWidth / 2;
const screenCenterY = canvas.canvas.clientHeight / 2;
const nodeCenterX = activeNode.pos[0] + activeNode.size[0] / 2;
const nodeCenterY = activeNode.pos[1] + activeNode.size[1] / 2;
const targetOffsetX = (screenCenterX / ds.scale) - nodeCenterX;
const targetOffsetY = (screenCenterY / ds.scale) - nodeCenterY;
ds.offset[0] += (targetOffsetX - ds.offset[0]) * SMOOTHNESS;
ds.offset[1] += (targetOffsetY - ds.offset[1]) * SMOOTHNESS;
const center = [canvas.canvas.clientWidth / 2, canvas.canvas.clientHeight / 2];
const nodeCenter = [activeNode.pos[0] + activeNode.size[0] / 2, activeNode.pos[1] + activeNode.size[1] / 2];
const target = [(center[0] / ds.scale) - nodeCenter[0], (center[1] / ds.scale) - nodeCenter[1]];
ds.offset[0] += (target[0] - ds.offset[0]) * SMOOTHNESS;
ds.offset[1] += (target[1] - ds.offset[1]) * SMOOTHNESS;
canvas.setDirty(true, true);
animationFrameId = requestAnimationFrame(smoothFollow);
}
+166
View File
@@ -0,0 +1,166 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "Comfy.HALXP.RunMonitor",
async setup() {
console.log("[HALXP-RunMonitor] Initializing...");
// --- CONSTANTS ---
const ID_ENABLE = "HALXP.RunMonitor.Enable";
const ID_SUCCESS = "HALXP.RunMonitor.SuccessPath";
const ID_ERROR = "HALXP.RunMonitor.ErrorPath";
// --- STATE & UI ELEMENTS ---
let monitorBtn = null;
let monitorDot = null;
// --- 1. CORE LOGIC: SYNC & UPDATE ---
// Updates the visuals of the button (Green/Gray) based on state
function updateButtonVisuals(enabled) {
if (!monitorBtn || !monitorDot) return;
if (enabled) {
// ACTIVE STATE (Green)
monitorDot.style.backgroundColor = "#4CAF50";
monitorDot.style.boxShadow = "0 0 6px #4CAF50";
monitorBtn.style.color = "#fff";
monitorBtn.style.borderColor = "#4CAF50";
monitorBtn.title = "Monitor: ON";
} else {
// INACTIVE STATE (Gray/Default)
monitorDot.style.backgroundColor = "#666";
monitorDot.style.boxShadow = "none";
monitorBtn.style.color = "var(--fg-color, #ccc)";
monitorBtn.style.borderColor = "#d3c1c1ff"; // Matches Focus default border
monitorBtn.title = "Monitor: OFF";
}
}
// Sends config to Python backend AND updates UI
function syncConfigToBackend() {
const enabled = app.ui.settings.getSettingValue(ID_ENABLE, false);
const config = {
enabled: enabled,
success_path: app.ui.settings.getSettingValue(ID_SUCCESS, ""),
error_path: app.ui.settings.getSettingValue(ID_ERROR, "")
};
// 1. Update the UI Button immediately
updateButtonVisuals(enabled);
// 2. Send to Backend
api.fetchApi("/halxp/update_config", {
method: "POST",
body: JSON.stringify(config)
});
}
// --- 2. REGISTER SETTINGS ---
app.ui.settings.addSetting({
id: ID_ENABLE,
name: "Enable External Monitoring",
type: "boolean",
defaultValue: false,
category: ['HALXP-Comfy', 'Run Monitor', 'Enable'],
tooltip: "Enable to run scripts on workflow status change.",
onChange: syncConfigToBackend // Triggers when changed via Menu OR Button
});
app.ui.settings.addSetting({
id: ID_SUCCESS,
name: "Success Command Path",
type: "text",
defaultValue: "",
category: ['HALXP-Comfy', 'Run Monitor', 'Success'],
tooltip: "Full path to a .bat/.sh to run on success.",
onChange: syncConfigToBackend
});
app.ui.settings.addSetting({
id: ID_ERROR,
name: "Error Command Path",
type: "text",
defaultValue: "",
category: ['HALXP-Comfy', 'Run Monitor', 'Error'],
tooltip: "Full path to a .bat/.sh to run on error.",
onChange: syncConfigToBackend
});
// --- 3. UI BUTTON CREATION (Copied from Focus) ---
function createButton() {
const btn = document.createElement("button");
btn.textContent = "Monitor";
btn.id = "halxp-monitor-btn";
// EXACT STYLE COPY FROM FOCUS.JS
btn.style.cssText = `
position: relative; display: inline-flex; align-items: center; justify-content: center;
padding: 0 10px; margin: 0 4px; background-color: var(--comfy-input-bg, #333);
color: var(--fg-color, #fff); border: 1px solid var(--border-color, #555);
border-radius: 4px; cursor: pointer; font-family: sans-serif; font-size: 13px;
font-weight: 600; flex-shrink: 0; white-space: nowrap; z-index: 9999; pointer-events: auto;
border: 1px solid #d3c1c1ff;
`;
// Create the Status Dot
const d = document.createElement("span");
d.style.cssText = `width: 8px; height: 8px; border-radius: 50%; background-color: #666; margin-right: 8px; display: inline-block; flex-shrink: 0;`;
btn.prepend(d);
monitorDot = d;
// Handle Click: Toggle the setting (which triggers onChange -> syncConfigToBackend)
btn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const current = app.ui.settings.getSettingValue(ID_ENABLE, false);
app.ui.settings.setSettingValue(ID_ENABLE, !current);
};
return btn;
}
function injectButton() {
// Prevent duplicates
if (document.getElementById("halxp-monitor-btn")) return;
// Find the "Manager" button or any existing button to latch onto
const allButtons = Array.from(document.querySelectorAll("button"));
const targetBtn = allButtons.find(b => {
if (!b.offsetParent) return false;
const text = b.innerText.trim();
// Try to place it near Manager, or Focus if it loaded first
return text === "Manager" || text === "ComfyUI Manager" || text === "Focus";
});
if (targetBtn && targetBtn.parentNode) {
monitorBtn = createButton();
// Match height of the neighbor
const h = targetBtn.offsetHeight;
monitorBtn.style.height = h > 0 ? `${h}px` : "28px";
// Insert BEFORE the target (Standardizes menu flow)
// If you want it specifically AFTER Focus but BEFORE Manager, this generally works
// because extensions load in order.
targetBtn.parentNode.insertBefore(monitorBtn, targetBtn);
// Set initial visual state
const isEnabled = app.ui.settings.getSettingValue(ID_ENABLE, false);
updateButtonVisuals(isEnabled);
}
}
// --- 4. INITIALIZATION ---
// Attempt to inject button periodically until UI is ready
setInterval(injectButton, 500);
// Sync backend on load (wait slightly for settings to load)
setTimeout(syncConfigToBackend, 1000);
}
});