diff --git a/.gitignore b/.gitignore index a47ba7c..8569c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -44,4 +44,25 @@ test_*.json # Backup files *.bak -*.backup \ No newline at end of file +*.backup + +# Workflow backup directories (created during renaming) +workflow_backups/ + +# Database files (SQLite) +*.db +*.sqlite +*.sqlite3 +workflows.db + +# Rename logs +workflow_rename_log.json + +# Node.js artifacts (if using npm) +node_modules/ +package-lock.json + +# Python package files +*.egg-info/ +dist/ +build/ \ No newline at end of file diff --git a/comprehensive_workflow_renamer.py b/comprehensive_workflow_renamer.py new file mode 100644 index 0000000..eda5f59 --- /dev/null +++ b/comprehensive_workflow_renamer.py @@ -0,0 +1,396 @@ +#!/usr/bin/env python3 +""" +Comprehensive N8N Workflow Renamer +Complete standardization of all 2053+ workflows with uniform naming convention. +""" + +import json +import os +import glob +import re +import shutil +from typing import Dict, List, Any, Optional, Tuple +from pathlib import Path + +class ComprehensiveWorkflowRenamer: + """Renames ALL workflows to uniform 0001-9999 standard with intelligent analysis.""" + + def __init__(self, workflows_dir: str = "workflows"): + self.workflows_dir = workflows_dir + self.rename_log = [] + self.errors = [] + self.backup_dir = "workflow_backups" + + def analyze_all_workflows(self) -> Dict[str, Any]: + """Analyze all workflow files and generate comprehensive rename plan.""" + if not os.path.exists(self.workflows_dir): + print(f"β Workflows directory '{self.workflows_dir}' not found.") + return {'workflows': [], 'total': 0, 'errors': []} + + json_files = glob.glob(os.path.join(self.workflows_dir, "*.json")) + + if not json_files: + print(f"β No JSON files found in '{self.workflows_dir}' directory.") + return {'workflows': [], 'total': 0, 'errors': []} + + print(f"π Analyzing {len(json_files)} workflow files...") + + workflows = [] + for file_path in json_files: + try: + workflow_data = self._analyze_workflow_file(file_path) + if workflow_data: + workflows.append(workflow_data) + except Exception as e: + error_msg = f"Error analyzing {file_path}: {str(e)}" + print(f"β {error_msg}") + self.errors.append(error_msg) + continue + + # Sort by current filename for consistent numbering + workflows.sort(key=lambda x: x['current_filename']) + + # Assign new sequential numbers + for i, workflow in enumerate(workflows, 1): + workflow['new_number'] = f"{i:04d}" + workflow['new_filename'] = self._generate_new_filename(workflow, i) + + return { + 'workflows': workflows, + 'total': len(workflows), + 'errors': self.errors + } + + def _analyze_workflow_file(self, file_path: str) -> Optional[Dict[str, Any]]: + """Analyze a single workflow file and extract metadata for renaming.""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + data = json.load(f) + except (json.JSONDecodeError, UnicodeDecodeError) as e: + print(f"β Error reading {file_path}: {str(e)}") + return None + + filename = os.path.basename(file_path) + + # Extract workflow metadata + workflow = { + 'current_filename': filename, + 'current_path': file_path, + 'name': data.get('name', filename.replace('.json', '')), + 'workflow_id': data.get('id', ''), + 'active': data.get('active', False), + 'nodes': data.get('nodes', []), + 'connections': data.get('connections', {}), + 'tags': data.get('tags', []), + 'created_at': data.get('createdAt', ''), + 'updated_at': data.get('updatedAt', '') + } + + # Analyze nodes for intelligent naming + node_count = len(workflow['nodes']) + workflow['node_count'] = node_count + + # Determine complexity + if node_count <= 5: + complexity = 'Simple' + elif node_count <= 15: + complexity = 'Standard' + else: + complexity = 'Complex' + workflow['complexity'] = complexity + + # Find services and trigger type + services, trigger_type = self._analyze_nodes(workflow['nodes']) + workflow['services'] = list(services) + workflow['trigger_type'] = trigger_type + + # Determine purpose from name and nodes + workflow['purpose'] = self._determine_purpose(workflow['name'], workflow['nodes']) + + return workflow + + def _analyze_nodes(self, nodes: List[Dict]) -> Tuple[set, str]: + """Analyze nodes to determine services and trigger type.""" + services = set() + trigger_type = 'Manual' + + for node in nodes: + node_type = node.get('type', '') + node_name = node.get('name', '') + + # Determine trigger type + if any(x in node_type.lower() for x in ['webhook', 'http']): + trigger_type = 'Webhook' + elif any(x in node_type.lower() for x in ['cron', 'schedule', 'interval']): + trigger_type = 'Scheduled' + elif 'trigger' in node_type.lower() and trigger_type == 'Manual': + trigger_type = 'Triggered' + + # Extract service names + if node_type.startswith('n8n-nodes-base.'): + service = node_type.replace('n8n-nodes-base.', '') + service = service.replace('Trigger', '').replace('trigger', '') + + # Clean up service names + service_mapping = { + 'webhook': 'Webhook', + 'httpRequest': 'HTTP', + 'cron': 'Cron', + 'gmail': 'Gmail', + 'slack': 'Slack', + 'googleSheets': 'GoogleSheets', + 'airtable': 'Airtable', + 'notion': 'Notion', + 'telegram': 'Telegram', + 'discord': 'Discord', + 'twitter': 'Twitter', + 'github': 'GitHub', + 'hubspot': 'HubSpot', + 'salesforce': 'Salesforce', + 'stripe': 'Stripe', + 'shopify': 'Shopify', + 'trello': 'Trello', + 'asana': 'Asana', + 'clickup': 'ClickUp', + 'calendly': 'Calendly', + 'zoom': 'Zoom', + 'mattermost': 'Mattermost', + 'microsoftTeams': 'Teams', + 'googleCalendar': 'GoogleCalendar', + 'googleDrive': 'GoogleDrive', + 'dropbox': 'Dropbox', + 'onedrive': 'OneDrive', + 'aws': 'AWS', + 'azure': 'Azure', + 'googleCloud': 'GCP' + } + + clean_service = service_mapping.get(service, service.title()) + + # Skip utility nodes + if clean_service not in ['Set', 'Function', 'If', 'Switch', 'Merge', 'StickyNote', 'NoOp']: + services.add(clean_service) + + return services, trigger_type + + def _determine_purpose(self, name: str, nodes: List[Dict]) -> str: + """Determine workflow purpose from name and node analysis.""" + name_lower = name.lower() + + # Purpose keywords mapping + purpose_keywords = { + 'create': ['create', 'add', 'new', 'generate', 'build'], + 'update': ['update', 'modify', 'change', 'edit', 'patch'], + 'sync': ['sync', 'synchronize', 'mirror', 'replicate'], + 'send': ['send', 'email', 'message', 'notify', 'alert'], + 'import': ['import', 'load', 'fetch', 'get', 'retrieve'], + 'export': ['export', 'save', 'backup', 'archive'], + 'monitor': ['monitor', 'check', 'watch', 'track', 'status'], + 'process': ['process', 'transform', 'convert', 'parse'], + 'automate': ['automate', 'workflow', 'bot', 'automation'] + } + + for purpose, keywords in purpose_keywords.items(): + if any(keyword in name_lower for keyword in keywords): + return purpose.title() + + # Default purpose based on node analysis + return 'Automation' + + def _generate_new_filename(self, workflow: Dict, number: int) -> str: + """Generate new standardized filename.""" + # Format: 0001_Service1_Service2_Purpose_Trigger.json + + services = workflow['services'][:2] # Max 2 services in filename + purpose = workflow['purpose'] + trigger = workflow['trigger_type'] + + # Build filename components + parts = [f"{number:04d}"] + + # Add services + if services: + parts.extend(services) + + # Add purpose + parts.append(purpose) + + # Add trigger if not Manual + if trigger != 'Manual': + parts.append(trigger) + + # Join and clean filename + filename = '_'.join(parts) + filename = re.sub(r'[^\w\-_]', '', filename) # Remove special chars + filename = re.sub(r'_+', '_', filename) # Collapse multiple underscores + filename = filename.strip('_') # Remove leading/trailing underscores + + return f"{filename}.json" + + def create_backup(self) -> bool: + """Create backup of current workflows directory.""" + try: + if os.path.exists(self.backup_dir): + shutil.rmtree(self.backup_dir) + + shutil.copytree(self.workflows_dir, self.backup_dir) + print(f"β Backup created at: {self.backup_dir}") + return True + except Exception as e: + print(f"β Failed to create backup: {e}") + return False + + def execute_rename_plan(self, rename_plan: Dict[str, Any], dry_run: bool = True) -> bool: + """Execute the comprehensive rename plan.""" + if not rename_plan['workflows']: + print("β No workflows to rename.") + return False + + print(f"\n{'π DRY RUN - ' if dry_run else 'π EXECUTING - '}Renaming {rename_plan['total']} workflows") + + if not dry_run: + if not self.create_backup(): + print("β Cannot proceed without backup.") + return False + + success_count = 0 + + for workflow in rename_plan['workflows']: + old_path = workflow['current_path'] + new_filename = workflow['new_filename'] + new_path = os.path.join(self.workflows_dir, new_filename) + + # Check for filename conflicts + if os.path.exists(new_path) and old_path != new_path: + print(f"β οΈ Conflict: {new_filename} already exists") + continue + + if dry_run: + print(f"π {workflow['current_filename']} β {new_filename}") + else: + try: + os.rename(old_path, new_path) + self.rename_log.append({ + 'old': workflow['current_filename'], + 'new': new_filename, + 'services': workflow['services'], + 'purpose': workflow['purpose'], + 'trigger': workflow['trigger_type'] + }) + success_count += 1 + print(f"β {workflow['current_filename']} β {new_filename}") + except Exception as e: + error_msg = f"β Failed to rename {workflow['current_filename']}: {e}" + print(error_msg) + self.errors.append(error_msg) + + if not dry_run: + print(f"\nπ Rename complete: {success_count}/{rename_plan['total']} workflows renamed") + self._save_rename_log() + + return True + + def _save_rename_log(self): + """Save detailed rename log to file.""" + log_data = { + 'timestamp': os.popen('date').read().strip(), + 'total_renamed': len(self.rename_log), + 'errors': self.errors, + 'renames': self.rename_log + } + + with open('workflow_rename_log.json', 'w', encoding='utf-8') as f: + json.dump(log_data, f, indent=2, ensure_ascii=False) + + print(f"π Rename log saved to: workflow_rename_log.json") + + def generate_report(self, rename_plan: Dict[str, Any]) -> str: + """Generate comprehensive rename report.""" + workflows = rename_plan['workflows'] + total = rename_plan['total'] + + # Statistics + services_count = {} + purposes_count = {} + triggers_count = {} + + for workflow in workflows: + for service in workflow['services']: + services_count[service] = services_count.get(service, 0) + 1 + + purposes_count[workflow['purpose']] = purposes_count.get(workflow['purpose'], 0) + 1 + triggers_count[workflow['trigger_type']] = triggers_count.get(workflow['trigger_type'], 0) + 1 + + report = f""" +# π― Comprehensive Workflow Rename Plan + +## π Overview +- **Total workflows**: {total} +- **Naming convention**: 0001-{total:04d}_Service1_Service2_Purpose_Trigger.json +- **Quality improvement**: 100% standardized naming + +## π·οΈ Service Distribution +""" + + for service, count in sorted(services_count.items(), key=lambda x: x[1], reverse=True)[:10]: + report += f"- **{service}**: {count} workflows\n" + + report += f"\n## π― Purpose Distribution\n" + for purpose, count in sorted(purposes_count.items(), key=lambda x: x[1], reverse=True): + report += f"- **{purpose}**: {count} workflows\n" + + report += f"\n## β‘ Trigger Distribution\n" + for trigger, count in sorted(triggers_count.items(), key=lambda x: x[1], reverse=True): + report += f"- **{trigger}**: {count} workflows\n" + + report += f""" +## π Naming Examples +""" + + for i, workflow in enumerate(workflows[:10]): + report += f"- `{workflow['current_filename']}` β `{workflow['new_filename']}`\n" + + if len(workflows) > 10: + report += f"... and {len(workflows) - 10} more workflows\n" + + return report + + +def main(): + """Main execution function.""" + import argparse + + parser = argparse.ArgumentParser(description='Comprehensive N8N Workflow Renamer') + parser.add_argument('--analyze', action='store_true', help='Analyze all workflows and create rename plan') + parser.add_argument('--execute', action='store_true', help='Execute the rename plan (requires --analyze first)') + parser.add_argument('--dry-run', action='store_true', help='Show rename plan without executing') + parser.add_argument('--report', action='store_true', help='Generate comprehensive report') + + args = parser.parse_args() + + renamer = ComprehensiveWorkflowRenamer() + + if args.analyze or args.dry_run or args.report: + print("π Analyzing all workflows...") + rename_plan = renamer.analyze_all_workflows() + + if args.report: + report = renamer.generate_report(rename_plan) + print(report) + + if args.dry_run: + renamer.execute_rename_plan(rename_plan, dry_run=True) + + if args.execute: + confirm = input(f"\nβ οΈ This will rename {rename_plan['total']} workflows. Continue? (yes/no): ") + if confirm.lower() == 'yes': + renamer.execute_rename_plan(rename_plan, dry_run=False) + else: + print("β Rename cancelled.") + + else: + parser.print_help() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/static/index.html b/static/index.html index a251942..b2d1cbc 100644 --- a/static/index.html +++ b/static/index.html @@ -108,27 +108,24 @@ position: sticky; top: 0; z-index: 100; - backdrop-filter: blur(10px); } .search-section { display: flex; - gap: 1rem; align-items: center; - flex-wrap: wrap; + gap: 1rem; margin-bottom: 1rem; } .search-input { flex: 1; - min-width: 300px; padding: 0.75rem 1rem; - border: 2px solid var(--border); + border: 1px solid var(--border); border-radius: 0.5rem; - background: var(--bg-secondary); + background: var(--bg); color: var(--text); font-size: 1rem; - transition: all 0.2s ease; + min-width: 300px; } .search-input:focus { @@ -139,8 +136,8 @@ .filter-section { display: flex; - gap: 1rem; align-items: center; + gap: 1rem; flex-wrap: wrap; } @@ -154,37 +151,29 @@ font-size: 0.875rem; font-weight: 500; color: var(--text-secondary); - white-space: nowrap; } - select, input[type="checkbox"] { - padding: 0.5rem 0.75rem; + .filter-group select { + padding: 0.5rem; border: 1px solid var(--border); border-radius: 0.375rem; - background: var(--bg-secondary); + background: var(--bg); color: var(--text); font-size: 0.875rem; } .theme-toggle { - padding: 0.5rem 1rem; + background: var(--bg-tertiary); border: 1px solid var(--border); - border-radius: 0.375rem; - background: var(--bg-secondary); - color: var(--text); + border-radius: 0.5rem; + padding: 0.5rem 1rem; cursor: pointer; - transition: all 0.2s ease; + font-size: 1rem; margin-left: auto; } - .theme-toggle:hover { - background: var(--bg-tertiary); - } - .results-info { - display: flex; - justify-content: space-between; - align-items: center; + margin-top: 1rem; font-size: 0.875rem; color: var(--text-secondary); } @@ -192,7 +181,6 @@ /* Main Content */ .main { padding: 2rem 0; - min-height: 50vh; } /* States */ @@ -201,42 +189,31 @@ padding: 4rem 2rem; } + .state .icon { + font-size: 4rem; + margin-bottom: 1rem; + } + .state h3 { + font-size: 1.5rem; margin-bottom: 0.5rem; color: var(--text); } .state p { color: var(--text-secondary); - } - - .loading .icon { - font-size: 3rem; - margin-bottom: 1rem; - animation: pulse 2s infinite; - } - - @keyframes pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } - } - - .error .icon { - font-size: 3rem; - margin-bottom: 1rem; - color: var(--error); + margin-bottom: 2rem; } .retry-btn { - margin-top: 1rem; - padding: 0.75rem 1.5rem; background: var(--primary); color: white; border: none; + padding: 0.75rem 1.5rem; border-radius: 0.5rem; cursor: pointer; + font-size: 1rem; font-weight: 500; - transition: all 0.2s ease; } .retry-btn:hover { @@ -246,7 +223,7 @@ /* Workflow Grid */ .workflow-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem; } @@ -255,59 +232,46 @@ border: 1px solid var(--border); border-radius: 0.75rem; padding: 1.5rem; + box-shadow: var(--shadow); transition: all 0.2s ease; cursor: pointer; position: relative; } .workflow-card:hover { - border-color: var(--primary); box-shadow: var(--shadow-lg); + border-color: var(--primary); transform: translateY(-2px); } .workflow-header { display: flex; + align-items: center; justify-content: space-between; - align-items: flex-start; margin-bottom: 1rem; - gap: 1rem; } .workflow-meta { display: flex; align-items: center; - gap: 0.75rem; - flex-wrap: wrap; - flex: 1; + gap: 0.5rem; + font-size: 0.875rem; + color: var(--text-secondary); } .status-dot { - width: 0.75rem; - height: 0.75rem; + width: 8px; + height: 8px; border-radius: 50%; - flex-shrink: 0; } - .status-active { - background: var(--success); - animation: pulse-green 2s infinite; - } - - .status-inactive { - background: var(--text-muted); - } - - @keyframes pulse-green { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.6; } - } + .status-active { background: var(--success); } + .status-inactive { background: var(--text-muted); } .complexity-dot { - width: 0.5rem; - height: 0.5rem; + width: 8px; + height: 8px; border-radius: 50%; - flex-shrink: 0; } .complexity-low { background: var(--success); } @@ -317,85 +281,216 @@ .trigger-badge { background: var(--primary); color: white; - padding: 0.25rem 0.75rem; - border-radius: 1rem; + padding: 0.25rem 0.5rem; + border-radius: 0.375rem; font-size: 0.75rem; font-weight: 500; - text-transform: uppercase; - letter-spacing: 0.05em; - white-space: nowrap; } .workflow-title { - font-size: 1.125rem; + font-size: 1.25rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text); line-height: 1.4; - overflow-wrap: break-word; } .workflow-description { color: var(--text-secondary); - font-size: 0.875rem; - line-height: 1.5; margin-bottom: 1rem; - overflow-wrap: break-word; + line-height: 1.5; } .workflow-integrations { margin-top: 1rem; - padding-top: 1rem; - border-top: 1px solid var(--border); } .integrations-title { - font-size: 0.75rem; - font-weight: 600; + font-size: 0.875rem; + font-weight: 500; color: var(--text-secondary); margin-bottom: 0.5rem; - text-transform: uppercase; - letter-spacing: 0.05em; } .integrations-list { display: flex; flex-wrap: wrap; - gap: 0.5rem; + gap: 0.25rem; } .integration-tag { background: var(--bg-tertiary); - color: var(--text); - padding: 0.25rem 0.5rem; + color: var(--text-secondary); + padding: 0.125rem 0.5rem; border-radius: 0.25rem; font-size: 0.75rem; + border: 1px solid var(--border); + } + + .workflow-actions { + display: flex; + gap: 0.5rem; + margin-top: 1rem; + padding-top: 1rem; + border-top: 1px solid var(--border); + } + + .action-btn { + padding: 0.5rem 1rem; + border: 1px solid var(--border); + border-radius: 0.375rem; + background: var(--bg); + color: var(--text); + text-decoration: none; + font-size: 0.875rem; font-weight: 500; - } - - .load-more { - text-align: center; - padding: 2rem 0; - } - - .load-more-btn { - padding: 0.75rem 2rem; - background: var(--primary); - color: white; - border: none; - border-radius: 0.5rem; cursor: pointer; - font-weight: 500; transition: all 0.2s ease; } - .load-more-btn:hover:not(:disabled) { + .action-btn:hover { + background: var(--bg-tertiary); + border-color: var(--primary); + } + + .action-btn.primary { + background: var(--primary); + color: white; + border-color: var(--primary); + } + + .action-btn.primary:hover { background: var(--primary-dark); } - .load-more-btn:disabled { - opacity: 0.5; - cursor: not-allowed; + /* Load More */ + .load-more { + text-align: center; + margin-top: 2rem; + } + + .load-more-btn { + background: var(--primary); + color: white; + border: none; + padding: 0.75rem 2rem; + border-radius: 0.5rem; + cursor: pointer; + font-size: 1rem; + font-weight: 500; + } + + .load-more-btn:hover { + background: var(--primary-dark); + } + + /* Modal */ + .modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + padding: 1rem; + } + + .modal-content { + background: var(--bg); + border-radius: 0.75rem; + max-width: 800px; + width: 100%; + max-height: 90vh; + overflow-y: auto; + position: relative; + } + + .modal-header { + padding: 1.5rem; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + justify-content: space-between; + } + + .modal-title { + font-size: 1.25rem; + font-weight: 600; + } + + .modal-close { + background: none; + border: none; + font-size: 1.5rem; + cursor: pointer; + padding: 0.25rem; + color: var(--text-secondary); + } + + .modal-body { + padding: 1.5rem; + } + + .workflow-detail { + margin-bottom: 1rem; + } + + .workflow-detail h4 { + font-size: 0.875rem; + font-weight: 600; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + margin-bottom: 0.5rem; + } + + .section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.5rem; + } + + .copy-btn { + background: var(--primary); + color: white; + border: none; + padding: 0.25rem 0.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.25rem; + } + + .copy-btn:hover { + background: var(--primary-dark); + } + + .copy-btn.copied { + background: var(--success); + } + + .copy-btn.copied:hover { + background: var(--success); + } + + .json-viewer { + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 0.5rem; + padding: 1rem; + font-family: 'Courier New', monospace; + font-size: 0.875rem; + overflow-x: auto; + max-height: 400px; + white-space: pre-wrap; } .hidden { @@ -552,10 +647,65 @@ + + +
+