Fix 7 bugs: async proxies, mode default, JS key serialization, validation

- Use asyncio.to_thread for proxy endpoints to avoid blocking event loop
- Add mode to DEFAULTS so it doesn't silently insert 0
- Use JSON serialization for keys in project_dynamic.js (with comma fallback)
- Validate path exists in change_path, friendly error on duplicate rename
- Remove unused exp param from rename closure
- Use deepcopy for DEFAULTS consistently

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 20:29:24 +01:00
parent 993fc86070
commit 497e6b06fb
5 changed files with 30 additions and 15 deletions

View File

@@ -1,3 +1,4 @@
import asyncio
import json import json
import logging import logging
import urllib.parse import urllib.parse
@@ -88,7 +89,7 @@ if PromptServer is not None:
async def list_projects_proxy(request): async def list_projects_proxy(request):
manager_url = request.query.get("url", "http://localhost:8080") manager_url = request.query.get("url", "http://localhost:8080")
url = f"{manager_url.rstrip('/')}/api/projects" url = f"{manager_url.rstrip('/')}/api/projects"
data = _fetch_json(url) data = await asyncio.to_thread(_fetch_json, url)
return web.json_response(data) return web.json_response(data)
@PromptServer.instance.routes.get("/json_manager/list_project_files") @PromptServer.instance.routes.get("/json_manager/list_project_files")
@@ -96,7 +97,7 @@ if PromptServer is not None:
manager_url = request.query.get("url", "http://localhost:8080") manager_url = request.query.get("url", "http://localhost:8080")
project = urllib.parse.quote(request.query.get("project", ""), safe='') project = urllib.parse.quote(request.query.get("project", ""), safe='')
url = f"{manager_url.rstrip('/')}/api/projects/{project}/files" url = f"{manager_url.rstrip('/')}/api/projects/{project}/files"
data = _fetch_json(url) data = await asyncio.to_thread(_fetch_json, url)
return web.json_response(data) return web.json_response(data)
@PromptServer.instance.routes.get("/json_manager/list_project_sequences") @PromptServer.instance.routes.get("/json_manager/list_project_sequences")
@@ -105,7 +106,7 @@ if PromptServer is not None:
project = urllib.parse.quote(request.query.get("project", ""), safe='') project = urllib.parse.quote(request.query.get("project", ""), safe='')
file_name = urllib.parse.quote(request.query.get("file", ""), safe='') file_name = urllib.parse.quote(request.query.get("file", ""), safe='')
url = f"{manager_url.rstrip('/')}/api/projects/{project}/files/{file_name}/sequences" url = f"{manager_url.rstrip('/')}/api/projects/{project}/files/{file_name}/sequences"
data = _fetch_json(url) data = await asyncio.to_thread(_fetch_json, url)
return web.json_response(data) return web.json_response(data)
@PromptServer.instance.routes.get("/json_manager/get_project_keys") @PromptServer.instance.routes.get("/json_manager/get_project_keys")
@@ -117,7 +118,7 @@ if PromptServer is not None:
seq = int(request.query.get("seq", "1")) seq = int(request.query.get("seq", "1"))
except (ValueError, TypeError): except (ValueError, TypeError):
seq = 1 seq = 1
data = _fetch_keys(manager_url, project, file_name, seq) data = await asyncio.to_thread(_fetch_keys, manager_url, project, file_name, seq)
if data.get("error") in ("http_error", "network_error", "parse_error"): if data.get("error") in ("http_error", "network_error", "parse_error"):
status = data.get("status", 502) status = data.get("status", 502)
return web.json_response(data, status=status) return web.json_response(data, status=status)

View File

@@ -267,7 +267,7 @@ def render_batch_processor(state: AppState):
with ui.row().classes('q-mt-sm'): with ui.row().classes('q-mt-sm'):
def add_empty(): def add_empty():
_add_sequence(DEFAULTS.copy()) _add_sequence(copy.deepcopy(DEFAULTS))
def add_from_source(): def add_from_source():
item = copy.deepcopy(DEFAULTS) item = copy.deepcopy(DEFAULTS)
@@ -383,7 +383,7 @@ def _render_sequence_card(i, seq, batch_list, data, file_path, state,
# --- Action row --- # --- Action row ---
with ui.row().classes('w-full q-gutter-sm action-row'): with ui.row().classes('w-full q-gutter-sm action-row'):
# Rename # Rename
async def rename(idx=i, s=seq, exp=expansion): async def rename(s=seq):
result = await ui.run_javascript( result = await ui.run_javascript(
f'prompt("Rename sequence:", {json.dumps(s.get("name", ""))})', f'prompt("Rename sequence:", {json.dumps(s.get("name", ""))})',
timeout=30.0, timeout=30.0,

View File

@@ -1,5 +1,6 @@
import json import json
import logging import logging
import sqlite3
from pathlib import Path from pathlib import Path
from nicegui import ui from nicegui import ui
@@ -127,6 +128,9 @@ def render_projects_tab(state: AppState):
state.config) state.config)
ui.notify(f'Renamed to "{new_name}"', type='positive') ui.notify(f'Renamed to "{new_name}"', type='positive')
render_project_list.refresh() render_project_list.refresh()
except sqlite3.IntegrityError:
ui.notify(f'A project named "{new_name}" already exists',
type='warning')
except Exception as e: except Exception as e:
ui.notify(f'Error: {e}', type='negative') ui.notify(f'Error: {e}', type='negative')
@@ -140,6 +144,9 @@ def render_projects_tab(state: AppState):
) )
if new_path and new_path.strip() and new_path.strip() != path: if new_path and new_path.strip() and new_path.strip() != path:
new_path = new_path.strip() new_path = new_path.strip()
if not Path(new_path).is_dir():
ui.notify(f'Warning: "{new_path}" does not exist',
type='warning')
state.db.update_project_path(name, new_path) state.db.update_project_path(name, new_path)
ui.notify(f'Path updated to "{new_path}"', type='positive') ui.notify(f'Path updated to "{new_path}"', type='positive')
render_project_list.refresh() render_project_list.refresh()

View File

@@ -30,6 +30,7 @@ DEFAULTS = {
"cfg": 1.5, "cfg": 1.5,
# --- Settings --- # --- Settings ---
"mode": 0,
"camera": "static", "camera": "static",
"flf": 0.0, "flf": 0.0,

View File

@@ -117,11 +117,11 @@ app.registerExtension({
return; return;
} }
// Store keys and types in hidden widgets for persistence (comma-separated) // Store keys and types in hidden widgets for persistence (JSON)
const okWidget = this.widgets?.find(w => w.name === "output_keys"); const okWidget = this.widgets?.find(w => w.name === "output_keys");
if (okWidget) okWidget.value = keys.join(","); if (okWidget) okWidget.value = JSON.stringify(keys);
const otWidget = this.widgets?.find(w => w.name === "output_types"); const otWidget = this.widgets?.find(w => w.name === "output_types");
if (otWidget) otWidget.value = types.join(","); if (otWidget) otWidget.value = JSON.stringify(types);
// Slot 0 is always total_sequences (INT) — ensure it exists // Slot 0 is always total_sequences (INT) — ensure it exists
if (this.outputs.length === 0 || this.outputs[0].name !== "total_sequences") { if (this.outputs.length === 0 || this.outputs[0].name !== "total_sequences") {
@@ -198,12 +198,18 @@ app.registerExtension({
const okWidget = this.widgets?.find(w => w.name === "output_keys"); const okWidget = this.widgets?.find(w => w.name === "output_keys");
const otWidget = this.widgets?.find(w => w.name === "output_types"); const otWidget = this.widgets?.find(w => w.name === "output_types");
const keys = okWidget?.value let keys = [];
? okWidget.value.split(",").filter(k => k.trim()) let types = [];
: []; if (okWidget?.value) {
const types = otWidget?.value try { keys = JSON.parse(okWidget.value); } catch (_) {
? otWidget.value.split(",") keys = okWidget.value.split(",").filter(k => k.trim());
: []; }
}
if (otWidget?.value) {
try { types = JSON.parse(otWidget.value); } catch (_) {
types = otWidget.value.split(",");
}
}
// Ensure slot 0 is total_sequences (INT) // Ensure slot 0 is total_sequences (INT)
if (this.outputs.length === 0 || this.outputs[0].name !== "total_sequences") { if (this.outputs.length === 0 || this.outputs[0].name !== "total_sequences") {