299 lines
10 KiB
Python
299 lines
10 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Phase 1-3 Fix for Node-RED Stop Prompt Issues
|
|
|
|
This script fixes:
|
|
1. Stop modal nested inside scrap modal (extracts and relocates)
|
|
2. Missing cancelStopReason() function
|
|
3. Incomplete submitStopReason() function
|
|
|
|
Author: Claude Code
|
|
Date: 2025-11-23
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
|
|
def find_modal_boundaries(content, modal_id):
|
|
"""Find the exact start and end positions of a modal div."""
|
|
start_pattern = f'<div id="{modal_id}"'
|
|
start_pos = content.find(start_pattern)
|
|
|
|
if start_pos == -1:
|
|
return None, None
|
|
|
|
# Count divs to find matching close tag
|
|
pos = start_pos
|
|
div_count = 0
|
|
|
|
while pos < len(content):
|
|
if content[pos:pos+4] == '<div':
|
|
div_count += 1
|
|
elif content[pos:pos+6] == '</div>':
|
|
div_count -= 1
|
|
if div_count == 0:
|
|
return start_pos, pos + 6
|
|
pos += 1
|
|
|
|
return start_pos, None
|
|
|
|
def extract_and_fix_home_template(flows_data):
|
|
"""Extract stop modal from scrap modal and add missing functions."""
|
|
|
|
# Find the Home Template node
|
|
home_node = None
|
|
home_index = None
|
|
|
|
for idx, node in enumerate(flows_data):
|
|
if node.get('id') == '1821c4842945ecd8':
|
|
home_node = node
|
|
home_index = idx
|
|
break
|
|
|
|
if not home_node:
|
|
print("❌ ERROR: Home Template node not found!")
|
|
return False
|
|
|
|
print(f"✅ Found Home Template node at index {home_index}")
|
|
|
|
format_content = home_node.get('format', '')
|
|
original_length = len(format_content)
|
|
|
|
print(f"📏 Original format length: {original_length:,} characters")
|
|
|
|
# =========================================================================
|
|
# PHASE 1: Extract Stop Modal from Scrap Modal
|
|
# =========================================================================
|
|
|
|
print("\n🔧 PHASE 1: Extracting stop modal from scrap modal...")
|
|
|
|
# Find scrap modal boundaries
|
|
scrap_start, scrap_end = find_modal_boundaries(format_content, 'scrap-modal')
|
|
if scrap_start is None or scrap_end is None:
|
|
print("❌ ERROR: Could not find scrap modal boundaries")
|
|
return False
|
|
|
|
print(f" Scrap modal: positions {scrap_start} to {scrap_end}")
|
|
|
|
# Find stop modal boundaries
|
|
stop_start, stop_end = find_modal_boundaries(format_content, 'stopReasonModal')
|
|
if stop_start is None or stop_end is None:
|
|
print("❌ ERROR: Could not find stop modal boundaries")
|
|
return False
|
|
|
|
print(f" Stop modal: positions {stop_start} to {stop_end}")
|
|
|
|
# Verify stop modal is nested inside scrap modal
|
|
if not (scrap_start < stop_start < scrap_end):
|
|
print("⚠️ WARNING: Stop modal doesn't appear to be nested inside scrap modal")
|
|
print(f" This might already be fixed, but continuing anyway...")
|
|
else:
|
|
print(" ✅ Confirmed: Stop modal IS nested inside scrap modal")
|
|
|
|
# Extract the stop modal HTML
|
|
stop_modal_html = format_content[stop_start:stop_end]
|
|
print(f" 📦 Extracted {len(stop_modal_html):,} characters of stop modal HTML")
|
|
|
|
# Remove stop modal from its current position (inside scrap modal)
|
|
format_content_without_stop = format_content[:stop_start] + format_content[stop_end:]
|
|
|
|
# Find new scrap modal end (after removing stop modal)
|
|
scrap_start_new, scrap_end_new = find_modal_boundaries(format_content_without_stop, 'scrap-modal')
|
|
|
|
if scrap_end_new is None:
|
|
print("❌ ERROR: Could not find scrap modal end after extraction")
|
|
return False
|
|
|
|
print(f" ✅ New scrap modal end: position {scrap_end_new}")
|
|
|
|
# Insert stop modal AFTER scrap modal
|
|
# Add a newline for readability
|
|
insertion_point = scrap_end_new
|
|
format_content_fixed = (
|
|
format_content_without_stop[:insertion_point] +
|
|
'\n\n' +
|
|
stop_modal_html +
|
|
'\n' +
|
|
format_content_without_stop[insertion_point:]
|
|
)
|
|
|
|
print(f" ✅ Stop modal relocated to position {insertion_point}")
|
|
print(f" 📏 New format length: {len(format_content_fixed):,} characters")
|
|
|
|
# =========================================================================
|
|
# PHASE 2: Add Missing cancelStopReason() Function
|
|
# =========================================================================
|
|
|
|
print("\n🔧 PHASE 2: Adding cancelStopReason() function...")
|
|
|
|
# Check if function already exists
|
|
if 'scope.cancelStopReason' in format_content_fixed:
|
|
print(" ⚠️ cancelStopReason() already exists, skipping...")
|
|
else:
|
|
# Find where to insert (after selectStopReason function)
|
|
select_stop_pattern = r'(scope\.selectStopReason\s*=\s*function[^}]*\};\s*)'
|
|
match = re.search(select_stop_pattern, format_content_fixed, re.DOTALL)
|
|
|
|
if match:
|
|
insertion_pos = match.end()
|
|
|
|
cancel_function = '''
|
|
scope.cancelStopReason = function() {
|
|
console.log('[STOP] Cancel clicked - closing modal');
|
|
scope.stopPrompt.show = false;
|
|
scope.stopPrompt.selectedCategory = null;
|
|
scope.stopPrompt.selectedReason = null;
|
|
scope.stopPrompt.notes = '';
|
|
};
|
|
|
|
'''
|
|
|
|
format_content_fixed = (
|
|
format_content_fixed[:insertion_pos] +
|
|
cancel_function +
|
|
format_content_fixed[insertion_pos:]
|
|
)
|
|
|
|
print(" ✅ Added cancelStopReason() function")
|
|
else:
|
|
print(" ⚠️ WARNING: Could not find selectStopReason function to insert after")
|
|
|
|
# =========================================================================
|
|
# PHASE 3: Fix submitStopReason() Function
|
|
# =========================================================================
|
|
|
|
print("\n🔧 PHASE 3: Fixing submitStopReason() to hide modal...")
|
|
|
|
# Find the submitStopReason function
|
|
submit_pattern = r'(scope\.submitStopReason\s*=\s*function\(\)\s*\{)'
|
|
match = re.search(submit_pattern, format_content_fixed)
|
|
|
|
if match:
|
|
# Find the closing brace of this function
|
|
func_start = match.start()
|
|
brace_count = 0
|
|
pos = match.end()
|
|
func_end = None
|
|
|
|
while pos < len(format_content_fixed):
|
|
if format_content_fixed[pos] == '{':
|
|
brace_count += 1
|
|
elif format_content_fixed[pos] == '}':
|
|
if brace_count == 0:
|
|
func_end = pos
|
|
break
|
|
brace_count -= 1
|
|
pos += 1
|
|
|
|
if func_end:
|
|
current_function = format_content_fixed[func_start:func_end+1]
|
|
|
|
# Check if it already hides the modal
|
|
if 'scope.stopPrompt.show = false' in current_function:
|
|
print(" ✅ submitStopReason() already hides modal, skipping...")
|
|
else:
|
|
# Find the scope.send() call
|
|
send_pattern = r'(scope\.send\(\{[^}]*\}\);)'
|
|
send_match = re.search(send_pattern, current_function, re.DOTALL)
|
|
|
|
if send_match:
|
|
# Add cleanup code after scope.send()
|
|
cleanup_code = '''
|
|
|
|
// Hide modal and reset form
|
|
scope.stopPrompt.show = false;
|
|
scope.stopPrompt.selectedCategory = null;
|
|
scope.stopPrompt.selectedReason = null;
|
|
scope.stopPrompt.notes = '';'''
|
|
|
|
# Calculate position in full content
|
|
send_end_in_function = send_match.end()
|
|
send_end_in_content = func_start + send_end_in_function
|
|
|
|
format_content_fixed = (
|
|
format_content_fixed[:send_end_in_content] +
|
|
cleanup_code +
|
|
format_content_fixed[send_end_in_content:]
|
|
)
|
|
|
|
print(" ✅ Added modal hide and form reset to submitStopReason()")
|
|
else:
|
|
print(" ⚠️ WARNING: Could not find scope.send() call in submitStopReason")
|
|
else:
|
|
print(" ⚠️ WARNING: Could not find end of submitStopReason function")
|
|
else:
|
|
print(" ⚠️ WARNING: Could not find submitStopReason function")
|
|
|
|
# =========================================================================
|
|
# Update the node with fixed content
|
|
# =========================================================================
|
|
|
|
flows_data[home_index]['format'] = format_content_fixed
|
|
|
|
print(f"\n📊 SUMMARY:")
|
|
print(f" Original length: {original_length:,} characters")
|
|
print(f" Final length: {len(format_content_fixed):,} characters")
|
|
print(f" Difference: {len(format_content_fixed) - original_length:+,} characters")
|
|
|
|
return True
|
|
|
|
def main():
|
|
"""Main execution function."""
|
|
|
|
flows_path = '/home/mdares/.node-red/flows.json'
|
|
|
|
print("=" * 70)
|
|
print("Node-RED Stop Modal Fix - Phases 1-3")
|
|
print("=" * 70)
|
|
|
|
# Load flows.json
|
|
print(f"\n📂 Loading {flows_path}...")
|
|
try:
|
|
with open(flows_path, 'r') as f:
|
|
flows_data = json.load(f)
|
|
print(f" ✅ Loaded {len(flows_data)} nodes")
|
|
except Exception as e:
|
|
print(f" ❌ ERROR: {e}")
|
|
return 1
|
|
|
|
# Apply fixes
|
|
if not extract_and_fix_home_template(flows_data):
|
|
print("\n❌ FAILED to apply fixes")
|
|
return 1
|
|
|
|
# Validate JSON
|
|
print("\n🔍 Validating JSON structure...")
|
|
try:
|
|
json_output = json.dumps(flows_data, indent=2)
|
|
print(" ✅ JSON structure is valid")
|
|
except Exception as e:
|
|
print(f" ❌ ERROR: Invalid JSON structure: {e}")
|
|
return 1
|
|
|
|
# Write back to file
|
|
print(f"\n💾 Writing fixed flows to {flows_path}...")
|
|
try:
|
|
with open(flows_path, 'w') as f:
|
|
f.write(json_output)
|
|
print(" ✅ Successfully wrote flows.json")
|
|
except Exception as e:
|
|
print(f" ❌ ERROR: {e}")
|
|
return 1
|
|
|
|
print("\n" + "=" * 70)
|
|
print("✅ ALL PHASES COMPLETE!")
|
|
print("=" * 70)
|
|
print("\n📋 Next steps:")
|
|
print(" 1. Restart Node-RED: node-red-restart or sudo systemctl restart nodered")
|
|
print(" 2. Test STOP button on dashboard")
|
|
print(" 3. Verify Cancel button works")
|
|
print(" 4. Verify Submit button works and closes modal")
|
|
print(" 5. Check that both scrap and stop modals work independently")
|
|
print("\n💡 Backup saved as: flows.json.backup_YYYYMMDD_HHMMSS")
|
|
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|