Files
2025-12-02 16:27:21 +00:00

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())