1717CONFIGS_DIR = os .path .join (folder_paths .get_output_directory (), "ultimate-configs" )
1818os .makedirs (CONFIGS_DIR , exist_ok = True )
1919
20- # --- API: CONFIG MANAGEMENT ---
20+ # --- CONFIG BUILDER JS DIRECTORY ---
21+ EXTENSION_DIR = os .path .dirname (os .path .abspath (__file__ ))
22+ CONFIG_BUILDER_JS_DIR = os .path .join (EXTENSION_DIR , "js" )
23+
24+ # =============================================================================
25+ # SERVE CONFIG BUILDER JS FILES (NOT AUTO-LOADED)
26+ # =============================================================================
27+
28+ @server .PromptServer .instance .routes .get ("/ultimate_config_sampler/js/{filename:.*}" )
29+ async def serve_config_builder_js (request ):
30+ r"""
31+ Serve JavaScript files from /js/ directory with cache-busting headers.
32+ Supports subdirectories with both / and \ (e.g., config-builder/utilities.js)
33+ """
34+ try :
35+ filename = request .match_info ['filename' ]
36+ print (f"[ConfigBuilder] Request for: { filename } " )
37+
38+ # Security: prevent directory traversal with ..
39+ if '..' in filename or filename .startswith ('/' ) or filename .startswith ('\\ ' ):
40+ print (f"[ConfigBuilder] FORBIDDEN: { filename } " )
41+ return web .Response (status = 403 , text = "Forbidden" )
42+
43+ # Normalize path separators (convert backslashes to forward slashes)
44+ filename = filename .replace ('\\ ' , '/' )
45+
46+ # Build full path and normalize it
47+ file_path = os .path .normpath (os .path .join (CONFIG_BUILDER_JS_DIR , filename ))
48+
49+ # Double-check the resolved path is still within our JS directory
50+ normalized_base = os .path .normpath (CONFIG_BUILDER_JS_DIR )
51+
52+ if not file_path .startswith (normalized_base ):
53+ print (f"[ConfigBuilder] FORBIDDEN - Outside base dir: { file_path } " )
54+ return web .Response (status = 403 , text = "Forbidden - path outside base directory" )
55+
56+ if not os .path .exists (file_path ):
57+ print (f"[ConfigBuilder] NOT FOUND: { file_path } " )
58+ return web .Response (status = 404 , text = f"File not found: { filename } " )
59+
60+ print (f"[ConfigBuilder] Reading file: { file_path } " )
61+ with open (file_path , 'r' , encoding = 'utf-8' ) as f :
62+ content = f .read ()
63+
64+ print (f"[ConfigBuilder] File read successfully, { len (content )} bytes" )
65+
66+ # Return with cache-busting headers
67+ return web .Response (
68+ text = content ,
69+ content_type = 'application/javascript' ,
70+ headers = {
71+ 'Cache-Control' : 'no-cache, no-store, must-revalidate' ,
72+ 'Pragma' : 'no-cache' ,
73+ 'Expires' : '0'
74+ }
75+ )
76+
77+ except Exception as e :
78+ print (f"[ConfigBuilder] ERROR: { e } " )
79+ import traceback
80+ traceback .print_exc ()
81+ return web .Response (status = 500 , text = f"Server error: { str (e )} " )
82+
83+
84+ # =============================================================================
85+ # API: CONFIG MANAGEMENT
86+ # =============================================================================
87+
2188@server .PromptServer .instance .routes .get ("/configbuilder/list_configs" )
2289async def list_configs (request ):
2390 try :
@@ -82,7 +149,10 @@ async def load_config(request):
82149 return web .Response (status = 500 , text = str (e ))
83150
84151
85- # --- API: DELETE SESSION ---
152+ # =============================================================================
153+ # API: DELETE SESSION
154+ # =============================================================================
155+
86156@server .PromptServer .instance .routes .post ("/config_tester/delete_session" )
87157async def delete_session (request ):
88158 try :
@@ -108,7 +178,10 @@ async def delete_session(request):
108178 except Exception as e :
109179 return web .Response (status = 500 , text = str (e ))
110180
111- # --- API: SAVE CHANGES (Optimized - Only Changed Items) ---
181+ # =============================================================================
182+ # API: SAVE CHANGES (Optimized - Only Changed Items)
183+ # =============================================================================
184+
112185@server .PromptServer .instance .routes .post ("/config_tester/save_changes" )
113186async def save_changes (request ):
114187 """
@@ -175,7 +248,10 @@ async def save_changes(request):
175248 traceback .print_exc ()
176249 return web .Response (status = 500 , text = str (e ))
177250
178- # --- API: SAVE MANIFEST (Legacy - Full Save) ---
251+ # =============================================================================
252+ # API: SAVE MANIFEST (Legacy - Full Save)
253+ # =============================================================================
254+
179255@server .PromptServer .instance .routes .post ("/config_tester/save_manifest" )
180256async def save_manifest (request ):
181257 """
@@ -197,72 +273,85 @@ async def save_manifest(request):
197273 return web .Response (status = 400 , text = "Missing session_name or manifest" )
198274
199275 base_dir = os .path .join (folder_paths .get_output_directory (), "benchmarks" , session_name )
200- os .makedirs (base_dir , exist_ok = True )
201276 manifest_path = os .path .join (base_dir , "manifest.json" )
202277
203- # CRITICAL FIX: Merge with disk version to preserve generation's new items
204- try :
205- if os .path .exists (manifest_path ):
206- # Load what's currently on disk (may have new items from generation)
278+ # --- MERGE STRATEGY: Preserve server data ---
279+ # 1. Load server manifest (has newest images)
280+ server_manifest = None
281+ if os .path .exists (manifest_path ):
282+ try :
207283 with open (manifest_path , "r" ) as f :
208- disk_manifest = json .load (f )
209-
210- # Create lookup of dashboard items by ID
211- dashboard_items_dict = {
212- item .get ("id" ): item
213- for item in manifest_data .get ("items" , [])
214- if "id" in item
215- }
216-
217- # Find items on disk that aren't in dashboard (newly generated)
218- new_items = []
219- for disk_item in disk_manifest .get ("items" , []):
220- item_id = disk_item .get ("id" )
221- if item_id and item_id not in dashboard_items_dict :
222- # This item was generated after dashboard loaded
223- new_items .append (disk_item )
224-
225- # Add new items to dashboard's manifest
226- if new_items :
227- print (f"[ConfigTester] 📄 Preserving { len (new_items )} newly generated items not in dashboard" )
228- manifest_data ["items" ] = new_items + manifest_data .get ("items" , [])
229-
230- # Preserve meta from disk (has latest settings)
231- if "meta" in disk_manifest :
232- # Keep user's changes but preserve generation settings
233- manifest_data ["meta" ] = disk_manifest ["meta" ]
234-
235- except Exception as e :
236- print (f"[ConfigTester] ⚠️ Could not merge with disk manifest: { e } " )
237- # Continue with save anyway - dashboard data is more important than merge
238-
239- # Save the merged manifest
284+ server_manifest = json .load (f )
285+ except :
286+ pass
287+
288+ if server_manifest :
289+ # Build lookup of items by ID from dashboard
290+ dashboard_items = {item .get ("id" ): item for item in manifest_data .get ("items" , [])}
291+
292+ # Merge: Update existing items, keep new items
293+ merged_items = []
294+ for server_item in server_manifest .get ("items" , []):
295+ item_id = server_item .get ("id" )
296+ if item_id in dashboard_items :
297+ # Item exists in dashboard: merge updates
298+ dashboard_item = dashboard_items [item_id ]
299+ # Preserve server's metadata but update user actions
300+ merged_item = server_item .copy ()
301+ merged_item ["favorited" ] = dashboard_item .get ("favorited" , False )
302+ merged_item ["rejected" ] = dashboard_item .get ("rejected" , False )
303+ merged_item ["note" ] = dashboard_item .get ("note" , "" )
304+ merged_items .append (merged_item )
305+ else :
306+ # NEW item from server (generation added it): keep as-is
307+ merged_items .append (server_item )
308+
309+ # Update manifest
310+ manifest_data ["items" ] = merged_items
311+
312+ # Preserve server's meta
313+ if "meta" in server_manifest :
314+ manifest_data ["meta" ] = server_manifest ["meta" ]
315+
316+ # Save merged manifest
317+ os .makedirs (base_dir , exist_ok = True )
240318 with open (manifest_path , "w" ) as f :
241319 json .dump (manifest_data , f , indent = 4 )
242-
243- print ("Save Manifest at init" )
320+
244321 return web .Response (status = 200 , text = "Saved" )
322+
245323 except Exception as e :
246324 print (f"[ConfigTester] Error saving manifest: { e } " )
325+ import traceback
326+ traceback .print_exc ()
247327 return web .Response (status = 500 , text = str (e ))
248328
249- # --- API: FETCH SESSION HTML ---
329+ # =============================================================================
330+ # API: GET SESSION HTML
331+ # =============================================================================
332+
250333@server .PromptServer .instance .routes .post ("/config_tester/get_session_html" )
251334async def get_session_html (request ):
335+ """
336+ Dynamically generate HTML for a session.
337+ This allows the dashboard to load without triggering a workflow execution.
338+ """
252339 try :
253340 data = await request .json ()
254341 session_name = data .get ("session_name" )
255- node_id = data .get ("node_id" , "0" ) # Fallback ID
342+ node_id = data .get ("node_id" )
256343
257- # --- sanitize ---
258344 if session_name :
259345 session_name = re .sub (r'[^\w\-]' , '' , session_name )
260-
346+
347+ if not session_name :
348+ return web .Response (status = 400 , text = "Missing session_name" )
349+
261350 base_dir = os .path .join (folder_paths .get_output_directory (), "benchmarks" , session_name )
262351 manifest_path = os .path .join (base_dir , "manifest.json" )
263352
264353 if not os .path .exists (manifest_path ):
265- return web .Response (status = 404 , text = f"Session '{ session_name } ' not found. " )
354+ return web .Response (status = 404 , text = f"Session '{ session_name } ' not found" )
266355
267356 with open (manifest_path , "r" ) as f :
268357 manifest = json .load (f )
@@ -274,7 +363,10 @@ async def get_session_html(request):
274363 except Exception as e :
275364 return web .Response (status = 500 , text = str (e ))
276365
277- # --- API: EXPORT FAVORITES ---
366+ # =============================================================================
367+ # API: EXPORT FAVORITES
368+ # =============================================================================
369+
278370@server .PromptServer .instance .routes .post ("/config_tester/export_favorites" )
279371async def export_favorites (request ):
280372 """
@@ -442,7 +534,10 @@ async def export_favorites(request):
442534 traceback .print_exc ()
443535 return web .Response (status = 500 , text = str (e ))
444536
445- # --- MAPPINGS ---
537+ # =============================================================================
538+ # NODE MAPPINGS
539+ # =============================================================================
540+
446541NODE_CLASS_MAPPINGS = {
447542 "UltimateSamplerGrid" : SamplerGridTester ,
448543 "UltimateGridDashboard" : SamplerConfigDashboardViewer ,
@@ -453,7 +548,7 @@ async def export_favorites(request):
453548NODE_DISPLAY_NAME_MAPPINGS = {
454549 "UltimateSamplerGrid" : "Ultimate Sampler Grid (Generator)" ,
455550 "UltimateGridDashboard" : "Ultimate Grid Dashboard (Viewer)" ,
456- "UltimateConfigBuilder" : "Ultimate Config Builder (WIP) " ,
551+ "UltimateConfigBuilder" : "Ultimate Config Builder" ,
457552 "SmartJSONText" : "Smart JSON Text" ,
458553}
459554
0 commit comments