├── logs └── .gitkeep ├── requirements.txt ├── py_manager ├── __init__.py ├── favicon.svg ├── api_config.json ├── config.json ├── py_logger.py ├── py_manager.py ├── py_script_manager.py ├── py_manager.html ├── py_process.py ├── py_api.py ├── py_manager.css ├── py_interface.js └── socket.io.min.js ├── scripts ├── example.py ├── example2.py ├── data_processor.py ├── log_analyzer.py └── web_monitor.py ├── LICENSE ├── start_manager.py ├── CODE_OF_CONDUCT.md ├── CHANGELOG.md ├── download_socketio.py ├── CONTRIBUTING.md ├── .gitignore ├── deploy ├── create_package.py ├── DEPLOYMENT_GUIDE.md └── setup.py ├── allin1.py ├── GITHUB_SETUP.md ├── GITHUB_PUBLISHING_GUIDE.md └── README.md /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | # This file ensures the logs directory is included in git 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | flask==3.0.0 2 | flask-socketio==5.3.5 3 | flask-cors==4.0.0 4 | python-socketio==5.10.0 5 | requests==2.31.0 6 | psutil==5.9.6 -------------------------------------------------------------------------------- /py_manager/__init__.py: -------------------------------------------------------------------------------- 1 | """Python Manager Package""" 2 | 3 | # Import main components 4 | from . import py_process 5 | from . import py_logger 6 | 7 | __version__ = '1.0.0' -------------------------------------------------------------------------------- /scripts/example.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import os 4 | 5 | print(f"Example script started in: {os.getcwd()}", flush=True) 6 | print(f"Python executable: {sys.executable}", flush=True) 7 | 8 | vg_counter = 0 9 | while True: 10 | print(f"tick {vg_counter}", flush=True) 11 | vg_counter += 1 12 | time.sleep(5) -------------------------------------------------------------------------------- /scripts/example2.py: -------------------------------------------------------------------------------- 1 | import time 2 | import sys 3 | import os 4 | 5 | print(f"Example script started in: {os.getcwd()}", flush=True) 6 | print(f"Python executable: {sys.executable}", flush=True) 7 | 8 | vg_counter = 0 9 | while True: 10 | print(f"tick {vg_counter}", flush=True) 11 | vg_counter += 1 12 | time.sleep(5) -------------------------------------------------------------------------------- /py_manager/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /py_manager/api_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_settings": { 3 | "host": "127.0.0.1", 4 | "port": 5000, 5 | "debug": false, 6 | "cors_origins": ["http://localhost:8080", "http://127.0.0.1:8080", "http://localhost:*", "http://127.0.0.1:*"], 7 | "secret_key": "CHANGE-THIS-SECRET-KEY-IN-PRODUCTION", 8 | "auth_enabled": false, 9 | "auth_token": "CHANGE-THIS-TOKEN-IF-AUTH-ENABLED" 10 | }, 11 | "websocket_settings": { 12 | "ping_timeout": 60, 13 | "ping_interval": 25, 14 | "update_interval": 2 15 | } 16 | } -------------------------------------------------------------------------------- /scripts/data_processor.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | import json 4 | from datetime import datetime 5 | 6 | print("Data Processor started", flush=True) 7 | 8 | vg_processed_count = 0 9 | 10 | while True: 11 | # Simulate data processing 12 | vf_data = { 13 | 'timestamp': datetime.now().isoformat(), 14 | 'value': random.randint(100, 1000), 15 | 'status': 'processed' 16 | } 17 | 18 | vg_processed_count += 1 19 | 20 | print(f"Processed record #{vg_processed_count}: {json.dumps(vf_data)}", flush=True) 21 | 22 | # Random processing time between 2-5 seconds 23 | time.sleep(random.uniform(2, 5)) -------------------------------------------------------------------------------- /scripts/log_analyzer.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | from datetime import datetime 4 | 5 | print("Log Analyzer started", flush=True) 6 | 7 | ag_log_types = ['INFO', 'WARNING', 'ERROR', 'DEBUG'] 8 | ag_services = ['auth', 'api', 'database', 'cache'] 9 | 10 | vg_analyzed = 0 11 | 12 | while True: 13 | vg_analyzed += 1 14 | 15 | # Simulate log analysis 16 | vf_log_type = random.choice(ag_log_types) 17 | vf_service = random.choice(ag_services) 18 | vf_count = random.randint(1, 100) 19 | 20 | print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] Analyzed batch #{vg_analyzed}: Found {vf_count} {vf_log_type} logs from {vf_service} service", flush=True) 21 | 22 | # Process every 3 seconds 23 | time.sleep(3) -------------------------------------------------------------------------------- /scripts/web_monitor.py: -------------------------------------------------------------------------------- 1 | import time 2 | import random 3 | from datetime import datetime 4 | 5 | print("Web Monitor started", flush=True) 6 | 7 | ag_websites = [ 8 | 'example.com', 9 | 'test-site.org', 10 | 'monitor-this.net' 11 | ] 12 | 13 | vg_check_count = 0 14 | 15 | while True: 16 | for vf_site in ag_websites: 17 | vg_check_count += 1 18 | vf_status = random.choice(['UP', 'UP', 'UP', 'DOWN']) # 75% up 19 | vf_response_time = random.randint(50, 500) if vf_status == 'UP' else 0 20 | 21 | print(f"[{datetime.now().strftime('%H:%M:%S')}] Check #{vg_check_count}: {vf_site} - {vf_status} ({vf_response_time}ms)", flush=True) 22 | 23 | time.sleep(1) 24 | 25 | # Wait 10 seconds before next round 26 | time.sleep(10) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Prismatex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /start_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Easy startup script for Python Manager""" 3 | 4 | import sys 5 | import os 6 | 7 | print(""" 8 | ╔══════════════════════════════════════════════╗ 9 | ║ Python Manager Launcher ║ 10 | ╚══════════════════════════════════════════════╝ 11 | 12 | Choose startup mode: 13 | 1. All-in-One Server (Recommended) 14 | 2. Separate API + Web servers 15 | 3. CLI Manager only 16 | 0. Exit 17 | """) 18 | 19 | choice = input("Enter your choice (1-3): ").strip() 20 | 21 | if choice == '1': 22 | print("\nStarting All-in-One server...") 23 | os.system('python allin1.py') 24 | 25 | elif choice == '2': 26 | print("\nStarting separate servers...") 27 | print("Run these commands in separate terminals:") 28 | print(" Terminal 1: python api_server.py") 29 | print(" Terminal 2: python web_server.py") 30 | print("\nPress Enter to continue...") 31 | input() 32 | 33 | elif choice == '3': 34 | print("\nStarting CLI manager...") 35 | os.system('python manager.py') 36 | 37 | elif choice == '0': 38 | print("Goodbye!") 39 | sys.exit(0) 40 | 41 | else: 42 | print("Invalid choice!") -------------------------------------------------------------------------------- /py_manager/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "manager_settings": { 3 | "port": 8080, 4 | "log_retention_days": 7, 5 | "check_interval_seconds": 5, 6 | "auto_restart_max_attempts": 3, 7 | "auto_restart_delay_seconds": 10 8 | }, 9 | "scripts": [ 10 | { 11 | "id": "data_processor", 12 | "name": "Data Processor", 13 | "path": "scripts/data_processor.py", 14 | "args": [], 15 | "auto_restart": true, 16 | "enabled": true, 17 | "max_memory_mb": 512, 18 | "log_file": "data_processor.log" 19 | }, 20 | { 21 | "id": "web_monitor", 22 | "name": "Web Monitor", 23 | "path": "scripts/web_monitor.py", 24 | "args": [], 25 | "auto_restart": false, 26 | "enabled": true, 27 | "max_memory_mb": 256, 28 | "log_file": "web_monitor.log" 29 | }, 30 | { 31 | "id": "log_analyzer", 32 | "name": "Log Analyzer", 33 | "path": "scripts/log_analyzer.py", 34 | "args": [], 35 | "auto_restart": true, 36 | "enabled": true, 37 | "max_memory_mb": 256, 38 | "log_file": "log_analyzer.log" 39 | }, 40 | { 41 | "id": "script_1f6837bb", 42 | "name": "Example", 43 | "path": "D:\\xampp\\htdocs\\mpy0\\scripts\\example.py", 44 | "args": [], 45 | "auto_restart": true, 46 | "enabled": true, 47 | "max_memory_mb": 512, 48 | "log_file": "script_1f6837bb.log" 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to a positive environment: 15 | 16 | * Using welcoming and inclusive language 17 | * Being respectful of differing viewpoints and experiences 18 | * Gracefully accepting constructive criticism 19 | * Focusing on what is best for the community 20 | * Showing empathy towards other community members 21 | 22 | Examples of unacceptable behavior: 23 | 24 | * The use of sexualized language or imagery and unwelcome sexual attention 25 | * Trolling, insulting/derogatory comments, and personal or political attacks 26 | * Public or private harassment 27 | * Publishing others' private information without explicit permission 28 | * Other conduct which could reasonably be considered inappropriate 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Project maintainers are responsible for clarifying and enforcing our standards of 33 | acceptable behavior and will take appropriate and fair corrective action in 34 | response to any behavior that they deem inappropriate, threatening, offensive, 35 | or harmful. 36 | 37 | ## Enforcement 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 40 | reported to the project maintainers. All complaints will be reviewed and 41 | investigated promptly and fairly. 42 | 43 | ## Attribution 44 | 45 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 46 | version 2.0, available at 47 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 48 | 49 | [homepage]: https://www.contributor-covenant.org 50 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to Python Manager will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [1.0.0] - 2025-06-07 9 | 10 | ### Added 11 | - Initial release of Python Manager 12 | - Web-based dashboard for managing Python scripts 13 | - Support for scripts from any location on the system 14 | - Real-time process monitoring (CPU, memory usage) 15 | - Centralized logging system 16 | - Auto-restart capability for failed scripts 17 | - REST API for programmatic control 18 | - WebSocket support for real-time updates 19 | - Script management UI (add/remove scripts via web interface) 20 | - Directory browser for finding Python scripts 21 | - Multiple deployment options (setup.py, portable package) 22 | - Support for script arguments 23 | - Bulk operations (start all, stop all) 24 | 25 | ### Features 26 | - Multi-script management from single interface 27 | - Absolute and relative path support 28 | - Configurable process limits and restart attempts 29 | - Cross-platform compatibility (Windows, Linux, macOS) 30 | - Modern, responsive web interface 31 | - No database required (JSON configuration) 32 | 33 | ### Technical Details 34 | - Built with Flask and Flask-SocketIO 35 | - Process monitoring via psutil 36 | - Bootstrap-inspired custom CSS 37 | - Vanilla JavaScript (no heavy frameworks) 38 | - Following specific naming conventions for maintainability 39 | 40 | ## [Unreleased] 41 | 42 | ### Planned Features 43 | - Docker support 44 | - Script scheduling (cron-like functionality) 45 | - Resource usage graphs 46 | - Email/webhook notifications 47 | - Script dependencies management 48 | - Multi-user authentication 49 | - Dark mode theme improvements 50 | - Export/import configurations 51 | - Script grouping/categories 52 | - Performance metrics history 53 | 54 | --- 55 | 56 | To see what's being worked on, check out the [issues](https://github.com/yourusername/python-manager/issues) page. 57 | -------------------------------------------------------------------------------- /download_socketio.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Download Socket.IO client library for offline use 4 | """ 5 | 6 | import urllib.request 7 | import os 8 | import sys 9 | 10 | def vf_download_socketio(): 11 | """Download Socket.IO from CDN""" 12 | # Updated to use the reliable cdnjs URL 13 | vg_url = "https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.8.1/socket.io.min.js" 14 | vg_target_dir = os.path.join(os.path.dirname(__file__), 'py_manager') 15 | vg_target_file = os.path.join(vg_target_dir, 'socket.io.min.js') 16 | 17 | print("Downloading Socket.IO client library...") 18 | print(f"From: {vg_url}") 19 | print(f"To: {vg_target_file}") 20 | 21 | try: 22 | # Create directory if it doesn't exist 23 | os.makedirs(vg_target_dir, exist_ok=True) 24 | 25 | # Download the file 26 | urllib.request.urlretrieve(vg_url, vg_target_file) 27 | 28 | # Check file size 29 | vg_size = os.path.getsize(vg_target_file) 30 | print(f"\n✓ Downloaded successfully! ({vg_size:,} bytes)") 31 | print("\nSocket.IO is now available for offline use.") 32 | print("\nNote: Socket.IO is MIT licensed and can be freely distributed.") 33 | 34 | except Exception as e: 35 | print(f"\n✗ Error downloading Socket.IO: {e}") 36 | print("\nYou can manually download it from:") 37 | print(vg_url) 38 | print(f"\nAnd save it as: {vg_target_file}") 39 | return False 40 | 41 | return True 42 | 43 | if __name__ == "__main__": 44 | print("Python Manager - Socket.IO Downloader") 45 | print("="*40) 46 | print("\nThis will download Socket.IO for offline use.") 47 | print("The application will work without this, but WebSocket") 48 | print("connections will fall back to polling mode.\n") 49 | 50 | vf_confirm = input("Download Socket.IO? (y/n): ").lower() 51 | 52 | if vf_confirm == 'y': 53 | if vf_download_socketio(): 54 | print("\nSetup complete!") 55 | else: 56 | sys.exit(1) 57 | else: 58 | print("\nSkipped. The application will use Socket.IO from CDN.") 59 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Python Manager 2 | 3 | First off, thank you for considering contributing to Python Manager! It's people like you that make Python Manager such a great tool. 4 | 5 | ## Code of Conduct 6 | 7 | This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to the project maintainers. 8 | 9 | ## How Can I Contribute? 10 | 11 | ### Reporting Bugs 12 | 13 | Before creating bug reports, please check existing issues as you might find out that you don't need to create one. When you are creating a bug report, please include as many details as possible: 14 | 15 | * **Use a clear and descriptive title** 16 | * **Describe the exact steps to reproduce the problem** 17 | * **Provide specific examples to demonstrate the steps** 18 | * **Describe the behavior you observed after following the steps** 19 | * **Explain which behavior you expected to see instead and why** 20 | * **Include screenshots if possible** 21 | * **Include your Python version and OS** 22 | 23 | ### Suggesting Enhancements 24 | 25 | Enhancement suggestions are tracked as GitHub issues. When creating an enhancement suggestion, please include: 26 | 27 | * **Use a clear and descriptive title** 28 | * **Provide a step-by-step description of the suggested enhancement** 29 | * **Provide specific examples to demonstrate the steps** 30 | * **Describe the current behavior and explain which behavior you expected to see instead** 31 | * **Explain why this enhancement would be useful** 32 | 33 | ### Pull Requests 34 | 35 | * Fill in the required template 36 | * Do not include issue numbers in the PR title 37 | * Follow the Python style guide (PEP 8) 38 | * Include thoughtfully-worded, well-structured tests 39 | * Document new code 40 | * End all files with a newline 41 | 42 | ## Development Process 43 | 44 | 1. Fork the repo and create your branch from `main`. 45 | 2. If you've added code that should be tested, add tests. 46 | 3. If you've changed APIs, update the documentation. 47 | 4. Ensure the test suite passes. 48 | 5. Make sure your code follows the existing style. 49 | 6. Issue that pull request! 50 | 51 | ## Style Guide 52 | 53 | ### Python Code Style 54 | 55 | * Follow [PEP 8](https://www.python.org/dev/peps/pep-0008/) 56 | * Use meaningful variable names following the project's convention: 57 | * `vg_` prefix for global variables 58 | * `ag_` prefix for global arrays 59 | * `vf_` prefix for function variables 60 | * `af_` prefix for function arrays 61 | * Keep functions focused and under 50 lines when possible 62 | * Add docstrings to all functions 63 | * Avoid using classes unless necessary 64 | * Keep files under 1250 lines 65 | 66 | ### JavaScript Code Style 67 | 68 | * Use meaningful variable names with the same prefix convention as Python 69 | * Avoid using imports - keep code in single files 70 | * Add comments for complex logic 71 | * Use consistent indentation (2 spaces) 72 | 73 | ### Commit Messages 74 | 75 | * Use the present tense ("Add feature" not "Added feature") 76 | * Use the imperative mood ("Move cursor to..." not "Moves cursor to...") 77 | * Limit the first line to 72 characters or less 78 | * Reference issues and pull requests liberally after the first line 79 | 80 | Example: 81 | ``` 82 | Add script validation before starting 83 | 84 | - Check if script file exists 85 | - Validate Python syntax 86 | - Show meaningful error messages 87 | 88 | Fixes #123 89 | ``` 90 | 91 | ## Project Structure 92 | 93 | When contributing, please maintain the existing structure: 94 | 95 | ``` 96 | python-manager/ 97 | ├── py_manager/ # Core modules only 98 | ├── scripts/ # Example scripts only 99 | ├── logs/ # Log files (gitignored) 100 | ├── deploy/ # Deployment tools 101 | └── od/ # Old/archived files (gitignored) 102 | ``` 103 | 104 | ## Testing 105 | 106 | Before submitting a pull request: 107 | 108 | 1. Test all basic operations: 109 | - Starting/stopping scripts 110 | - Adding/removing scripts via UI 111 | - Viewing logs 112 | - API endpoints 113 | 114 | 2. Test on different platforms if possible: 115 | - Windows 116 | - Linux 117 | - macOS 118 | 119 | 3. Verify no regression in existing features 120 | 121 | ## Questions? 122 | 123 | Feel free to open an issue with your question or reach out to the maintainers directly. 124 | 125 | Thank you for contributing! 🎉 126 | -------------------------------------------------------------------------------- /py_manager/py_logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import datetime 4 | from pathlib import Path 5 | 6 | # Global variables 7 | vg_log_dir = os.path.join(os.path.dirname(__file__), 'logs') 8 | vg_max_log_size = 10 * 1024 * 1024 # 10MB 9 | vg_log_retention_days = 7 10 | 11 | def vf_ensure_log_dir(): 12 | """Ensure log directory exists""" 13 | os.makedirs(vg_log_dir, exist_ok=True) 14 | 15 | # Create subdirectories for organization 16 | vf_today = datetime.date.today().strftime('%Y-%m-%d') 17 | vf_date_dir = os.path.join(vg_log_dir, vf_today) 18 | os.makedirs(vf_date_dir, exist_ok=True) 19 | 20 | return vf_date_dir 21 | 22 | def vf_write_manager_log(vf_level, vf_message, vf_script_id=None): 23 | """Write to manager log file""" 24 | vf_ensure_log_dir() 25 | 26 | vf_timestamp = datetime.datetime.now().isoformat() 27 | vf_log_entry = { 28 | 'timestamp': vf_timestamp, 29 | 'level': vf_level, 30 | 'message': vf_message 31 | } 32 | 33 | if vf_script_id: 34 | vf_log_entry['script_id'] = vf_script_id 35 | 36 | vf_log_file = os.path.join(vg_log_dir, 'manager.log') 37 | 38 | with open(vf_log_file, 'a') as vf_file: 39 | vf_file.write(json.dumps(vf_log_entry) + '\n') 40 | 41 | # Check if rotation needed 42 | vf_rotate_log_if_needed(vf_log_file) 43 | 44 | def vf_rotate_log_if_needed(vf_log_path): 45 | """Rotate log file if it exceeds size limit""" 46 | if not os.path.exists(vf_log_path): 47 | return 48 | 49 | vf_size = os.path.getsize(vf_log_path) 50 | 51 | if vf_size > vg_max_log_size: 52 | vf_timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S') 53 | vf_rotated_name = f"{vf_log_path}.{vf_timestamp}" 54 | os.rename(vf_log_path, vf_rotated_name) 55 | 56 | # Log rotation event 57 | vf_write_manager_log('INFO', f'Log rotated: {os.path.basename(vf_rotated_name)}') 58 | 59 | def vf_cleanup_old_logs(): 60 | """Remove logs older than retention period""" 61 | vf_cutoff_date = datetime.date.today() - datetime.timedelta(days=vg_log_retention_days) 62 | 63 | for vf_root, vf_dirs, vf_files in os.walk(vg_log_dir): 64 | # Check date directories 65 | for vf_dir in vf_dirs: 66 | try: 67 | vf_dir_date = datetime.datetime.strptime(vf_dir, '%Y-%m-%d').date() 68 | if vf_dir_date < vf_cutoff_date: 69 | vf_dir_path = os.path.join(vf_root, vf_dir) 70 | # Remove old directory and contents 71 | for vf_file in os.listdir(vf_dir_path): 72 | os.remove(os.path.join(vf_dir_path, vf_file)) 73 | os.rmdir(vf_dir_path) 74 | vf_write_manager_log('INFO', f'Cleaned up old logs: {vf_dir}') 75 | except ValueError: 76 | # Not a date directory, skip 77 | pass 78 | 79 | def vf_read_recent_logs(vf_script_id=None, vf_lines=100): 80 | """Read recent log entries""" 81 | ag_logs = [] 82 | 83 | if vf_script_id: 84 | # Read specific script log 85 | vf_log_path = os.path.join(vg_log_dir, f"{vf_script_id}.log") 86 | if os.path.exists(vf_log_path): 87 | ag_logs = vf_read_last_lines(vf_log_path, vf_lines) 88 | else: 89 | # Read manager log 90 | vf_log_path = os.path.join(vg_log_dir, 'manager.log') 91 | if os.path.exists(vf_log_path): 92 | ag_logs = vf_read_last_lines(vf_log_path, vf_lines) 93 | 94 | return ag_logs 95 | 96 | def vf_read_last_lines(vf_file_path, vf_num_lines): 97 | """Read last N lines from a file efficiently""" 98 | ag_lines = [] 99 | 100 | with open(vf_file_path, 'rb') as vf_file: 101 | # Start from end of file 102 | vf_file.seek(0, 2) 103 | vf_file_size = vf_file.tell() 104 | 105 | vf_block_size = 1024 106 | vf_blocks = [] 107 | 108 | while len(ag_lines) < vf_num_lines and vf_file_size > 0: 109 | vf_read_size = min(vf_block_size, vf_file_size) 110 | vf_file_size -= vf_read_size 111 | vf_file.seek(vf_file_size) 112 | 113 | vf_block = vf_file.read(vf_read_size) 114 | vf_blocks.insert(0, vf_block) 115 | 116 | ag_lines = b''.join(vf_blocks).decode('utf-8', errors='ignore').splitlines() 117 | 118 | return ag_lines[-vf_num_lines:] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | 162 | # VS Code 163 | .vscode/ 164 | 165 | # Python Manager specific 166 | logs/*.log 167 | py_manager/logs/*.log 168 | scripts/*.log 169 | *.log 170 | 171 | # Keep log directories but not log files 172 | !logs/.gitkeep 173 | !py_manager/logs/.gitkeep 174 | 175 | # Old/archived files directory 176 | od/ 177 | 178 | # Development reference files (optional - remove these lines if you want to include them) 179 | airef1.txt 180 | airef2.txt 181 | filelist.txt 182 | 183 | # OS specific 184 | .DS_Store 185 | Thumbs.db 186 | desktop.ini 187 | 188 | # Temporary files 189 | *.tmp 190 | *.temp 191 | *.swp 192 | *~ 193 | 194 | # Backup files 195 | *.bak 196 | *.backup 197 | 198 | # Package files created by deployment scripts 199 | deploy/*.zip 200 | deploy/python_manager_portable_*.zip 201 | -------------------------------------------------------------------------------- /deploy/create_package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Create a portable package of Python Manager 4 | """ 5 | 6 | import os 7 | import sys 8 | import zipfile 9 | import json 10 | from datetime import datetime 11 | 12 | def vf_create_package(): 13 | """Create a portable zip package""" 14 | print("Creating portable Python Manager package...") 15 | 16 | # Get source directory 17 | vg_source_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | # Create package name with timestamp 20 | vg_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") 21 | vg_package_name = f"python_manager_portable_{vg_timestamp}.zip" 22 | vg_package_path = os.path.join(vg_source_dir, 'deploy', vg_package_name) 23 | 24 | # Files to include 25 | vg_files_to_include = { 26 | # Root files 27 | 'start_manager.py': 'start_manager.py', 28 | 'allin1.py': 'allin1.py', 29 | 'api_server.py': 'api_server.py', 30 | 'web_server.py': 'web_server.py', 31 | 'manager.py': 'manager.py', 32 | 'requirements.txt': 'requirements.txt', 33 | 'readme.md': 'README.md', 34 | 35 | # py_manager files 36 | 'py_manager/py_api.py': 'py_manager/py_api.py', 37 | 'py_manager/py_process.py': 'py_manager/py_process.py', 38 | 'py_manager/py_logger.py': 'py_manager/py_logger.py', 39 | 'py_manager/py_manager.py': 'py_manager/py_manager.py', 40 | 'py_manager/py_script_manager.py': 'py_manager/py_script_manager.py', 41 | 'py_manager/py_manager.html': 'py_manager/py_manager.html', 42 | 'py_manager/py_interface.js': 'py_manager/py_interface.js', 43 | 'py_manager/py_manager.css': 'py_manager/py_manager.css', 44 | 'py_manager/api_config.json': 'py_manager/api_config.json', 45 | 'py_manager/__init__.py': 'py_manager/__init__.py', 46 | 47 | # Deployment scripts 48 | 'deploy/setup.py': 'setup.py' 49 | } 50 | 51 | # Create zip file 52 | with zipfile.ZipFile(vg_package_path, 'w', zipfile.ZIP_DEFLATED) as vf_zip: 53 | # Add files 54 | for vf_source, vf_dest in vg_files_to_include.items(): 55 | vf_full_source = os.path.join(vg_source_dir, vf_source) 56 | if os.path.exists(vf_full_source): 57 | vf_zip.write(vf_full_source, vf_dest) 58 | print(f" ✓ Added: {vf_dest}") 59 | 60 | # Create empty config.json 61 | vg_empty_config = { 62 | "manager_settings": { 63 | "port": 8080, 64 | "log_retention_days": 7, 65 | "check_interval_seconds": 5, 66 | "auto_restart_max_attempts": 3, 67 | "auto_restart_delay_seconds": 10 68 | }, 69 | "scripts": [] 70 | } 71 | vf_zip.writestr('py_manager/config.json', json.dumps(vg_empty_config, indent=2)) 72 | print(" ✓ Added: py_manager/config.json (empty)") 73 | 74 | # Create directory placeholders 75 | vf_zip.writestr('scripts/.placeholder', '') 76 | vf_zip.writestr('logs/.placeholder', '') 77 | vf_zip.writestr('py_manager/logs/.placeholder', '') 78 | print(" ✓ Added: directory placeholders") 79 | 80 | # Add quick start guide 81 | vg_quickstart = """Python Manager - Quick Start Guide 82 | ================================= 83 | 84 | 1. Extract this archive to your desired location 85 | 2. Install requirements: pip install -r requirements.txt 86 | 3. Run: python start_manager.py 87 | 4. Open browser to: http://localhost:5000 88 | 89 | Or use the setup script for guided installation: 90 | python setup.py 91 | 92 | Features: 93 | - Add Python scripts from ANY folder on your system 94 | - Monitor and control multiple scripts from one interface 95 | - View centralized logs 96 | - Auto-restart on failure 97 | - REST API for automation 98 | 99 | For more information, see README.md 100 | """ 101 | vf_zip.writestr('QUICKSTART.txt', vg_quickstart) 102 | print(" ✓ Added: QUICKSTART.txt") 103 | 104 | print(f"\n✓ Package created: {vg_package_name}") 105 | print(f" Size: {os.path.getsize(vg_package_path) / 1024:.1f} KB") 106 | print(f" Location: {vg_package_path}") 107 | 108 | return vg_package_path 109 | 110 | def vf_create_installer_script(): 111 | """Create a simple installer script to include in the package""" 112 | vg_installer_content = '''#!/usr/bin/env python 113 | """ 114 | Quick installer for Python Manager 115 | """ 116 | 117 | import subprocess 118 | import sys 119 | import os 120 | 121 | print("Python Manager Quick Installer") 122 | print("="*30) 123 | 124 | # Install requirements 125 | print("\\nInstalling requirements...") 126 | subprocess.check_call([sys.executable, '-m', 'pip', 'install', '-r', 'requirements.txt']) 127 | 128 | print("\\n✓ Installation complete!") 129 | print("\\nTo start Python Manager, run:") 130 | print(" python start_manager.py") 131 | print("\\nThen open: http://localhost:5000") 132 | ''' 133 | 134 | vg_installer_path = os.path.join(os.path.dirname(__file__), 'quick_install.py') 135 | with open(vg_installer_path, 'w') as f: 136 | f.write(vg_installer_content) 137 | 138 | return vg_installer_path 139 | 140 | if __name__ == '__main__': 141 | # Create installer first 142 | vf_installer = vf_create_installer_script() 143 | 144 | # Create package 145 | vf_package = vf_create_package() 146 | 147 | # Clean up installer 148 | if os.path.exists(vf_installer): 149 | os.remove(vf_installer) 150 | 151 | print("\nPackage ready for distribution!") 152 | -------------------------------------------------------------------------------- /deploy/DEPLOYMENT_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Python Manager Deployment Guide 2 | 3 | ## Overview 4 | Python Manager is a flexible tool for managing multiple Python scripts from a centralized web interface. It supports scripts from any location on your system, making it perfect for managing distributed Python projects. 5 | 6 | ## Deployment Options 7 | 8 | ### Option 1: Quick Setup (Recommended) 9 | Use the interactive setup script for guided installation: 10 | 11 | ```bash 12 | python deploy/setup.py 13 | ``` 14 | 15 | This will: 16 | - Check and install required packages 17 | - Copy files to your chosen location 18 | - Create initial configuration 19 | - Generate convenient startup scripts 20 | 21 | ### Option 2: Manual Installation 22 | 23 | 1. **Copy files** to your project directory: 24 | - All root .py files 25 | - The entire `py_manager/` directory 26 | - Create `scripts/` and `logs/` directories 27 | 28 | 2. **Install requirements**: 29 | ```bash 30 | pip install flask flask-socketio flask-cors psutil 31 | ``` 32 | 33 | 3. **Run the manager**: 34 | ```bash 35 | python start_manager.py 36 | ``` 37 | 38 | ### Option 3: Portable Package 39 | Create a distributable package: 40 | 41 | ```bash 42 | python deploy/create_package.py 43 | ``` 44 | 45 | This creates a zip file containing everything needed to run Python Manager on any system. 46 | 47 | ## Configuration 48 | 49 | ### Adding Scripts 50 | Scripts can be added in two ways: 51 | 52 | 1. **Via Web Interface** (Recommended): 53 | - Click "Manage Scripts" button 54 | - Enter script path or browse directories 55 | - Scripts can be from ANY folder on your system 56 | - Configure display name, arguments, and auto-restart 57 | 58 | 2. **Via config.json**: 59 | ```json 60 | { 61 | "scripts": [ 62 | { 63 | "id": "my_script", 64 | "name": "My Script", 65 | "path": "C:/full/path/to/script.py", 66 | "args": ["--arg1", "value"], 67 | "auto_restart": true, 68 | "enabled": true, 69 | "max_memory_mb": 512, 70 | "log_file": "my_script.log" 71 | } 72 | ] 73 | } 74 | ``` 75 | 76 | ### Path Support 77 | - **Absolute paths**: `C:\Users\me\projects\script.py` or `/home/user/script.py` 78 | - **Relative paths**: `scripts/local_script.py` 79 | - Scripts can be located anywhere on your system 80 | 81 | ## Usage Scenarios 82 | 83 | ### Scenario 1: Project with Distributed Scripts 84 | You have Python scripts in multiple project folders: 85 | ``` 86 | C:\Projects\ 87 | ├── DataProcessor\ 88 | │ └── processor.py 89 | ├── WebScraper\ 90 | │ └── scraper.py 91 | └── ReportGenerator\ 92 | └── generator.py 93 | ``` 94 | 95 | Simply add each script using its full path via the web interface. 96 | 97 | ### Scenario 2: Microservices Management 98 | Managing multiple microservices: 99 | - Add each service's main.py 100 | - Configure different ports via arguments 101 | - Enable auto-restart for reliability 102 | - Monitor resource usage across services 103 | 104 | ### Scenario 3: Data Pipeline 105 | Managing ETL pipeline scripts: 106 | - Add scripts in execution order 107 | - Use the API to orchestrate execution 108 | - View centralized logs for debugging 109 | - Auto-restart failed steps 110 | 111 | ## Advanced Features 112 | 113 | ### REST API 114 | Automate script management: 115 | ```python 116 | import requests 117 | 118 | # Start a script 119 | requests.post('http://localhost:5000/api/scripts/my_script/start') 120 | 121 | # Get status 122 | response = requests.get('http://localhost:5000/api/scripts/status') 123 | print(response.json()) 124 | ``` 125 | 126 | ### Script Arguments 127 | Pass arguments to scripts: 128 | - Via UI: Enter space-separated arguments 129 | - Via config: Use the "args" array 130 | - Supports all Python command-line arguments 131 | 132 | ### Auto-Restart 133 | Configure automatic restart on failure: 134 | - Set max restart attempts 135 | - Configure restart delay 136 | - View restart history in logs 137 | 138 | ## Troubleshooting 139 | 140 | ### Scripts Not Starting 141 | - Check script path is correct (absolute paths recommended) 142 | - Verify Python can execute the script directly 143 | - Check logs for error messages 144 | 145 | ### Cannot Add Scripts 146 | - Ensure script exists and is readable 147 | - Use absolute paths for scripts outside the project 148 | - Check file has .py extension 149 | 150 | ### Socket.IO Connection Issues 151 | - The application uses Socket.IO from CDN by default 152 | - If you see connection errors but the app still works, it's using polling mode 153 | - For offline use, download socket.io.min.js manually from: https://cdn.socket.io/4.8.1/socket.io.min.js 154 | - Place it in the py_manager/ directory 155 | 156 | ### Performance Issues 157 | - Monitor CPU/memory usage in dashboard 158 | - Adjust max_memory_mb limits 159 | - Check scripts for resource leaks 160 | 161 | ## Best Practices 162 | 163 | 1. **Use Absolute Paths**: When adding scripts from other projects 164 | 2. **Set Memory Limits**: Prevent runaway scripts 165 | 3. **Enable Auto-Restart**: For critical services 166 | 4. **Regular Log Cleanup**: Configure log retention 167 | 5. **Use the API**: For automation and integration 168 | 169 | ## Security Considerations 170 | 171 | - API authentication can be enabled in api_config.json 172 | - Restrict network access if running on a server 173 | - Be cautious with script arguments 174 | - Regular security updates for dependencies 175 | 176 | ## Support 177 | 178 | For issues or questions: 179 | 1. Check the logs in `py_manager/logs/` 180 | 2. Verify all dependencies are installed 181 | 3. Ensure scripts work when run directly 182 | 4. Check file permissions 183 | 184 | Python Manager makes it easy to manage Python scripts across your entire system from one convenient interface! 185 | -------------------------------------------------------------------------------- /allin1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """All-in-one server for Python Manager - serves web interface and API together""" 3 | 4 | import os 5 | import sys 6 | import threading 7 | import time 8 | from flask import Flask, send_from_directory, jsonify, send_file 9 | 10 | # Add py_manager to path 11 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'py_manager')) 12 | 13 | # Import our modules 14 | import py_process 15 | import py_logger 16 | from py_api import vg_app, vg_socketio, vf_status_update_loop 17 | 18 | # Get the absolute path to py_manager directory 19 | vg_py_manager_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'py_manager') 20 | 21 | # Configure static file serving - BEFORE the API routes 22 | @vg_app.route('/') 23 | def serve_index(): 24 | """Serve the main interface""" 25 | return send_file(os.path.join(vg_py_manager_path, 'py_manager.html')) 26 | 27 | @vg_app.route('/py_manager.css') 28 | def serve_css(): 29 | """Serve CSS file""" 30 | return send_file(os.path.join(vg_py_manager_path, 'py_manager.css')) 31 | 32 | @vg_app.route('/py_interface.js') 33 | def serve_js(): 34 | """Serve JavaScript file""" 35 | return send_file(os.path.join(vg_py_manager_path, 'py_interface.js')) 36 | 37 | @vg_app.route('/socket.io.min.js') 38 | def serve_socketio(): 39 | """Serve Socket.IO if available locally""" 40 | vf_socketio_path = os.path.join(vg_py_manager_path, 'socket.io.min.js') 41 | if os.path.exists(vf_socketio_path): 42 | return send_file(vf_socketio_path) 43 | else: 44 | # Return empty script if not found 45 | return "// Socket.IO not found locally", 404 46 | 47 | # Health check for the all-in-one server 48 | @vg_app.route('/health') 49 | def health_check(): 50 | """Combined health check""" 51 | return jsonify({ 52 | 'status': 'healthy', 53 | 'version': '1.0.0', 54 | 'mode': 'all-in-one', 55 | 'api': 'available', 56 | 'interface': 'available', 57 | 'py_manager_path': vg_py_manager_path 58 | }) 59 | 60 | @vg_app.route('/') 61 | def serve_any_static(filename): 62 | """Catch-all for any static files""" 63 | vf_file_path = os.path.join(vg_py_manager_path, filename) 64 | if os.path.exists(vf_file_path): 65 | return send_file(vf_file_path) 66 | else: 67 | # Try without ./ prefix 68 | if filename.startswith('./'): 69 | vf_file_path = os.path.join(vg_py_manager_path, filename[2:]) 70 | if os.path.exists(vf_file_path): 71 | return send_file(vf_file_path) 72 | return f"File not found: {filename}", 404 73 | 74 | @vg_app.route('/debug') 75 | def debug_info(): 76 | """Debug endpoint to check paths""" 77 | import json 78 | return json.dumps({ 79 | 'py_manager_path': vg_py_manager_path, 80 | 'files': os.listdir(vg_py_manager_path), 81 | 'routes': [str(rule) for rule in vg_app.url_map.iter_rules()], 82 | 'current_dir': os.getcwd() 83 | }, indent=2) 84 | 85 | def vf_start_all_in_one(): 86 | """Start the all-in-one server""" 87 | 88 | print("="*60) 89 | print("Python Manager - All-in-One Server") 90 | print("="*60) 91 | 92 | # Check if py_manager directory exists 93 | if not os.path.exists(vg_py_manager_path): 94 | print(f"ERROR: py_manager directory not found at: {vg_py_manager_path}") 95 | return 96 | 97 | # Check if required files exist 98 | required_files = ['py_manager.html', 'py_manager.css', 'py_interface.js'] 99 | for file in required_files: 100 | file_path = os.path.join(vg_py_manager_path, file) 101 | if not os.path.exists(file_path): 102 | print(f"ERROR: Required file not found: {file_path}") 103 | else: 104 | print(f"✓ Found: {file}") 105 | 106 | # Load configurations 107 | if not py_process.vf_load_config(): 108 | print("Failed to load process configuration!") 109 | return 110 | 111 | print("✓ Configuration loaded") 112 | 113 | # Start the process monitor thread 114 | vf_monitor_thread = threading.Thread(target=py_process.vf_monitor_processes) 115 | vf_monitor_thread.daemon = True 116 | vf_monitor_thread.start() 117 | print("✓ Process monitor started") 118 | 119 | # Start status update thread for WebSocket 120 | vf_update_thread = threading.Thread(target=vf_status_update_loop) 121 | vf_update_thread.daemon = True 122 | vf_update_thread.start() 123 | print("✓ WebSocket updater started") 124 | 125 | # Log startup 126 | py_logger.vf_write_manager_log('INFO', 'All-in-one server started') 127 | 128 | # List all routes for debugging 129 | print("\nRegistered routes:") 130 | for rule in vg_app.url_map.iter_rules(): 131 | print(f" {rule.rule}") 132 | 133 | print("\n" + "="*60) 134 | print("Server is ready!") 135 | print("="*60) 136 | print(f"\n→ Web Interface: http://localhost:5000/") 137 | print(f"→ API Endpoint: http://localhost:5000/api/") 138 | print(f"→ Health Check: http://localhost:5000/health") 139 | print(f"\nPress Ctrl+C to stop the server") 140 | print("="*60 + "\n") 141 | 142 | # Run the Flask app with SocketIO 143 | try: 144 | vg_socketio.run( 145 | vg_app, 146 | host='0.0.0.0', 147 | port=5000, 148 | debug=False, 149 | use_reloader=False, # Disable reloader to prevent double startup 150 | log_output=True # Enable logging to see requests 151 | ) 152 | except KeyboardInterrupt: 153 | print("\n\nShutting down...") 154 | py_process.vf_cleanup() 155 | py_logger.vf_write_manager_log('INFO', 'All-in-one server stopped') 156 | print("Server stopped gracefully") 157 | 158 | if __name__ == '__main__': 159 | vf_start_all_in_one() -------------------------------------------------------------------------------- /GITHUB_SETUP.md: -------------------------------------------------------------------------------- 1 | # Uploading Python Manager to GitHub 2 | 3 | ## Pre-Upload Checklist ✅ 4 | 5 | ### 1. Clean Sensitive Data 6 | - [x] API keys use placeholder values 7 | - [x] No hardcoded passwords 8 | - [x] No personal file paths in config 9 | - [x] Log files excluded via .gitignore 10 | 11 | ### 2. Files Ready 12 | - [x] README.md is comprehensive 13 | - [x] LICENSE file present (MIT) 14 | - [x] .gitignore properly configured 15 | - [x] Requirements.txt up to date 16 | - [x] Documentation complete 17 | 18 | ### 3. Optional Cleanup 19 | Before uploading, you may want to: 20 | ```bash 21 | # Remove development reference files (already in .gitignore) 22 | rm airef1.txt airef2.txt filelist.txt 23 | 24 | # Clean up old files directory 25 | rm -rf od/ 26 | 27 | # Clear all log files 28 | find . -name "*.log" -type f -delete 29 | 30 | # Remove __pycache__ directories 31 | find . -type d -name __pycache__ -exec rm -rf {} + 32 | ``` 33 | 34 | ## GitHub Upload Steps 35 | 36 | ### 1. Create GitHub Repository 37 | 1. Go to https://github.com/new 38 | 2. Repository name: `python-manager` 39 | 3. Description: "Web-based Python script manager with real-time monitoring and control" 40 | 4. Public repository 41 | 5. DON'T initialize with README (we already have one) 42 | 6. Click "Create repository" 43 | 44 | ### 2. Initial Upload 45 | ```bash 46 | # Navigate to your project directory 47 | cd D:\xampp\htdocs\mpy0 48 | 49 | # Initialize git repository 50 | git init 51 | 52 | # Add all files 53 | git add . 54 | 55 | # Create initial commit 56 | git commit -m "Initial commit: Python Manager v1.0.0" 57 | 58 | # Add GitHub remote (replace YOUR_USERNAME) 59 | git remote add origin https://github.com/YOUR_USERNAME/python-manager.git 60 | 61 | # Push to GitHub 62 | git branch -M main 63 | git push -u origin main 64 | ``` 65 | 66 | ### 3. Add Repository Topics 67 | On GitHub, add topics to help people find your project: 68 | - python 69 | - flask 70 | - process-manager 71 | - script-manager 72 | - monitoring 73 | - web-dashboard 74 | - websocket 75 | - real-time 76 | 77 | ### 4. Update README 78 | Replace `yourusername` in README.md with your actual GitHub username: 79 | ```bash 80 | # Update all instances of yourusername 81 | sed -i 's/yourusername/YOUR_GITHUB_USERNAME/g' README.md 82 | ``` 83 | 84 | ### 5. Create Release 85 | 1. Go to your repository on GitHub 86 | 2. Click "Releases" → "Create a new release" 87 | 3. Tag version: `v1.0.0` 88 | 4. Release title: "Python Manager v1.0.0" 89 | 5. Description: 90 | ```markdown 91 | ## 🎉 Initial Release 92 | 93 | Python Manager is a web-based tool for managing multiple Python scripts with real-time monitoring and control. 94 | 95 | ### Features 96 | - Multi-script management 97 | - Real-time monitoring 98 | - Auto-restart capability 99 | - Centralized logging 100 | - REST API 101 | - WebSocket support 102 | - File browser with auto-complete paths 103 | 104 | ### Installation 105 | See the [README](README.md) for detailed installation instructions. 106 | 107 | ### Quick Start 108 | ```bash 109 | git clone https://github.com/YOUR_USERNAME/python-manager.git 110 | cd python-manager 111 | pip install -r requirements.txt 112 | python start_manager.py 113 | ``` 114 | ``` 115 | 116 | ### 6. Add GitHub Actions (Optional) 117 | Create `.github/workflows/python-app.yml`: 118 | ```yaml 119 | name: Python application 120 | 121 | on: 122 | push: 123 | branches: [ main ] 124 | pull_request: 125 | branches: [ main ] 126 | 127 | jobs: 128 | test: 129 | runs-on: ubuntu-latest 130 | strategy: 131 | matrix: 132 | python-version: [3.7, 3.8, 3.9, '3.10', '3.11'] 133 | 134 | steps: 135 | - uses: actions/checkout@v3 136 | - name: Set up Python ${{ matrix.python-version }} 137 | uses: actions/setup-python@v4 138 | with: 139 | python-version: ${{ matrix.python-version }} 140 | - name: Install dependencies 141 | run: | 142 | python -m pip install --upgrade pip 143 | pip install -r requirements.txt 144 | - name: Test imports 145 | run: | 146 | python -c "import py_manager.py_process" 147 | python -c "import py_manager.py_logger" 148 | python -c "import py_manager.py_api" 149 | ``` 150 | 151 | ## Post-Upload Tasks 152 | 153 | ### 1. Add Badges to README 154 | Add these badges at the top of your README: 155 | ```markdown 156 | [](https://GitHub.com/YOUR_USERNAME/python-manager/releases/) 157 | [](https://GitHub.com/YOUR_USERNAME/python-manager/stargazers/) 158 | [](https://GitHub.com/YOUR_USERNAME/python-manager/issues/) 159 | ``` 160 | 161 | ### 2. Create Issues for Future Features 162 | - Docker support 163 | - Script scheduling 164 | - Email notifications 165 | - Multi-user authentication 166 | - Resource usage graphs 167 | 168 | ### 3. Add Contributing Guidelines 169 | Already included in CONTRIBUTING.md! 170 | 171 | ### 4. Enable GitHub Pages (Optional) 172 | For project documentation website: 173 | 1. Settings → Pages 174 | 2. Source: Deploy from a branch 175 | 3. Branch: main, folder: /docs 176 | 177 | ## Promotion 178 | 179 | ### Share Your Project 180 | 1. Post on Reddit: r/Python, r/flask, r/selfhosted 181 | 2. Share on Twitter/X with hashtags: #Python #OpenSource #WebDev 182 | 3. Submit to: 183 | - Awesome Python: https://github.com/vinta/awesome-python 184 | - Awesome Flask: https://github.com/humiaozuzu/awesome-flask 185 | - Made with Flask: https://github.com/rochacbruno/flask-powered 186 | 187 | ### Write a Blog Post 188 | Consider writing about: 189 | - Why you built it 190 | - Technical challenges solved 191 | - Architecture decisions 192 | - Future plans 193 | 194 | Good luck with your open source project! 🚀 195 | -------------------------------------------------------------------------------- /py_manager/py_manager.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import threading 5 | 6 | # Import from same package 7 | import py_process 8 | import py_logger 9 | 10 | # Global variables 11 | vg_running = True 12 | vg_monitor_thread = None 13 | 14 | def vf_monitor_loop(): 15 | """Background thread for monitoring processes""" 16 | while vg_running: 17 | py_process.vf_monitor_processes() 18 | time.sleep(py_process.vg_config['manager_settings']['check_interval_seconds']) 19 | 20 | def vf_print_status(): 21 | """Print current status of all scripts""" 22 | print("\n" + "="*60) 23 | print("Python Script Manager - Status") 24 | print("="*60) 25 | 26 | ag_status = py_process.vf_get_all_status() 27 | 28 | for vf_script in ag_status: 29 | print(f"\nScript: {vf_script['name']} (ID: {vf_script['id']})") 30 | print(f" Status: {vf_script['status']}") 31 | print(f" Enabled: {vf_script['enabled']}") 32 | 33 | if vf_script['status'] == 'running': 34 | print(f" PID: {vf_script['pid']}") 35 | print(f" CPU: {vf_script['cpu_percent']:.1f}%") 36 | print(f" Memory: {vf_script['memory_mb']:.1f} MB") 37 | print(f" Start Time: {vf_script['start_time']}") 38 | elif vf_script.get('restart_attempts', 0) > 0: 39 | print(f" Restart Attempts: {vf_script['restart_attempts']}") 40 | 41 | print("\n" + "="*60) 42 | 43 | def vf_show_menu(): 44 | """Display interactive menu""" 45 | print("\nCommands:") 46 | print(" 1. Start script") 47 | print(" 2. Stop script") 48 | print(" 3. Restart script") 49 | print(" 4. Show status") 50 | print(" 5. View logs") 51 | print(" 6. Reload config") 52 | print(" 0. Exit") 53 | print("") 54 | 55 | def vf_select_script(): 56 | """Let user select a script from list""" 57 | ag_scripts = py_process.vg_config['scripts'] 58 | 59 | print("\nAvailable scripts:") 60 | for vf_idx, vf_script in enumerate(ag_scripts): 61 | print(f" {vf_idx + 1}. {vf_script['name']} ({vf_script['id']})") 62 | 63 | try: 64 | vf_choice = int(input("\nSelect script number: ")) - 1 65 | if 0 <= vf_choice < len(ag_scripts): 66 | return ag_scripts[vf_choice]['id'] 67 | except ValueError: 68 | pass 69 | 70 | print("Invalid selection") 71 | return None 72 | 73 | def vf_main(): 74 | """Main interactive loop""" 75 | global vg_running, vg_monitor_thread 76 | 77 | print("Python Script Manager Starting...") 78 | 79 | # Load configuration 80 | if not py_process.vf_load_config(): 81 | print("Failed to load configuration!") 82 | return 83 | 84 | # Start monitor thread 85 | vg_monitor_thread = threading.Thread(target=vf_monitor_loop) 86 | vg_monitor_thread.daemon = True 87 | vg_monitor_thread.start() 88 | 89 | # Log startup 90 | py_logger.vf_write_manager_log('INFO', 'Manager started') 91 | 92 | # Initial status 93 | vf_print_status() 94 | 95 | # Interactive loop 96 | while vg_running: 97 | vf_show_menu() 98 | 99 | try: 100 | vf_choice = input("Enter command: ").strip() 101 | 102 | if vf_choice == '0': 103 | vg_running = False 104 | break 105 | 106 | elif vf_choice == '1': # Start 107 | vf_script_id = vf_select_script() 108 | if vf_script_id: 109 | vf_result = py_process.vf_start_script(vf_script_id) 110 | print(f"Result: {vf_result}") 111 | py_logger.vf_write_manager_log('INFO', f"Start command: {vf_result}", vf_script_id) 112 | 113 | elif vf_choice == '2': # Stop 114 | vf_script_id = vf_select_script() 115 | if vf_script_id: 116 | vf_result = py_process.vf_stop_script(vf_script_id) 117 | print(f"Result: {vf_result}") 118 | py_logger.vf_write_manager_log('INFO', f"Stop command: {vf_result}", vf_script_id) 119 | 120 | elif vf_choice == '3': # Restart 121 | vf_script_id = vf_select_script() 122 | if vf_script_id: 123 | vf_result = py_process.vf_restart_script(vf_script_id) 124 | print(f"Result: {vf_result}") 125 | py_logger.vf_write_manager_log('INFO', f"Restart command: {vf_result}", vf_script_id) 126 | 127 | elif vf_choice == '4': # Status 128 | vf_print_status() 129 | 130 | elif vf_choice == '5': # View logs 131 | print("\nLog options:") 132 | print(" 1. Manager logs") 133 | print(" 2. Script logs") 134 | 135 | vf_log_choice = input("Select: ").strip() 136 | 137 | if vf_log_choice == '1': 138 | ag_logs = py_logger.vf_read_recent_logs(None, 20) 139 | print("\nRecent Manager Logs:") 140 | for vf_line in ag_logs: 141 | print(f" {vf_line}") 142 | 143 | elif vf_log_choice == '2': 144 | vf_script_id = vf_select_script() 145 | if vf_script_id: 146 | ag_logs = py_logger.vf_read_recent_logs(vf_script_id, 20) 147 | print(f"\nRecent logs for {vf_script_id}:") 148 | for vf_line in ag_logs: 149 | print(f" {vf_line}") 150 | 151 | elif vf_choice == '6': # Reload config 152 | if py_process.vf_load_config(): 153 | print("Configuration reloaded successfully") 154 | else: 155 | print("Failed to reload configuration") 156 | 157 | else: 158 | print("Invalid command") 159 | 160 | except KeyboardInterrupt: 161 | print("\nUse '0' to exit properly") 162 | except Exception as vf_error: 163 | print(f"Error: {vf_error}") 164 | py_logger.vf_write_manager_log('ERROR', str(vf_error)) 165 | 166 | # Cleanup 167 | print("\nShutting down...") 168 | py_process.vf_cleanup() 169 | py_logger.vf_write_manager_log('INFO', 'Manager stopped') 170 | 171 | if __name__ == "__main__": 172 | vf_main() -------------------------------------------------------------------------------- /GITHUB_PUBLISHING_GUIDE.md: -------------------------------------------------------------------------------- 1 | # Publishing Python Manager to GitHub 2 | 3 | Follow these steps to publish your Python Manager project to GitHub for others to use freely. 4 | 5 | ## Pre-publication Checklist 6 | 7 | ✅ **Files Created:** 8 | - `LICENSE` - MIT License for free use 9 | - `README.md` - Comprehensive project documentation 10 | - `.gitignore` - Excludes unnecessary files 11 | - `CONTRIBUTING.md` - Guidelines for contributors 12 | - `.gitkeep` files - Ensures empty directories are included 13 | 14 | ## Step-by-Step GitHub Publication 15 | 16 | ### 1. Create a GitHub Repository 17 | 18 | 1. Go to [GitHub.com](https://github.com) and sign in 19 | 2. Click the **"+"** icon in the top right → **"New repository"** 20 | 3. Configure your repository: 21 | - **Repository name:** `python-manager` (or your preferred name) 22 | - **Description:** "Web-based tool for managing multiple Python scripts with real-time monitoring and control" 23 | - **Public** repository (for free use by others) 24 | - **DON'T** initialize with README (we already have one) 25 | - **DON'T** add .gitignore (we already have one) 26 | - **DON'T** choose a license (we already have MIT) 27 | 4. Click **"Create repository"** 28 | 29 | ### 2. Prepare Your Local Repository 30 | 31 | Open a terminal/command prompt in your project directory (`D:\xampp\htdocs\mpy0`) and run: 32 | 33 | ```bash 34 | # Initialize git repository 35 | git init 36 | 37 | # Add all files 38 | git add . 39 | 40 | # Create your first commit 41 | git commit -m "Initial commit: Python Manager - Multi-script management tool" 42 | ``` 43 | 44 | ### 3. Connect to GitHub 45 | 46 | Replace `YOUR_USERNAME` with your GitHub username: 47 | 48 | ```bash 49 | # Add GitHub as remote origin 50 | git remote add origin https://github.com/YOUR_USERNAME/python-manager.git 51 | 52 | # Push to GitHub 53 | git branch -M main 54 | git push -u origin main 55 | ``` 56 | 57 | ### 4. Clean Sensitive Data 58 | 59 | Before pushing, ensure you: 60 | 61 | 1. **Remove any sensitive data** from config files: 62 | ```bash 63 | # Edit py_manager/api_config.json 64 | # Change the secret_key to a placeholder 65 | ``` 66 | 67 | 2. **Clear log files** (optional): 68 | ```bash 69 | # Windows 70 | del logs\*.log 71 | del py_manager\logs\*.log 72 | 73 | # Linux/Mac 74 | rm logs/*.log 75 | rm py_manager/logs/*.log 76 | ``` 77 | 78 | 3. **Update personal information**: 79 | - Edit `LICENSE` file - replace `[Your Name]` with your actual name or username 80 | - Update `README.md` - replace `yourusername` with your GitHub username in all URLs 81 | 82 | ### 5. Create a Release 83 | 84 | 1. Go to your repository on GitHub 85 | 2. Click **"Releases"** → **"Create a new release"** 86 | 3. Fill in: 87 | - **Tag version:** `v1.0.0` 88 | - **Release title:** `Python Manager v1.0.0` 89 | - **Description:** 90 | ``` 91 | Initial release of Python Manager 92 | 93 | Features: 94 | - Manage Python scripts from any location 95 | - Real-time monitoring and logging 96 | - Web-based dashboard 97 | - REST API for automation 98 | - Auto-restart capabilities 99 | ``` 100 | 4. Click **"Publish release"** 101 | 102 | ### 6. Enhance Your Repository 103 | 104 | After initial publication: 105 | 106 | 1. **Add Topics** (for discoverability): 107 | - Go to repository settings 108 | - Add topics: `python`, `process-manager`, `monitoring`, `flask`, `websocket`, `dashboard` 109 | 110 | 2. **Add Screenshots**: 111 | - Take screenshots of the web interface 112 | - Create an `images/` directory 113 | - Update README.md with actual screenshots 114 | 115 | 3. **Enable GitHub Pages** (optional - for documentation): 116 | - Settings → Pages → Source: main branch 117 | - Create a `docs/` folder for additional documentation 118 | 119 | 4. **Set up Issues Templates**: 120 | - Go to Settings → Features → Issues → Set up templates 121 | - Add bug report and feature request templates 122 | 123 | ### 7. Promote Your Project 124 | 125 | 1. **Share on Social Media**: 126 | - Twitter/X: "Just released Python Manager - a web-based tool for managing multiple Python scripts! 🐍 Check it out: [link]" 127 | - LinkedIn: Professional announcement 128 | - Reddit: r/Python, r/opensource 129 | 130 | 2. **Submit to Lists**: 131 | - [Awesome Python](https://github.com/vinta/awesome-python) 132 | - [Awesome Flask](https://github.com/humiaozuzu/awesome-flask) 133 | 134 | 3. **Write a Blog Post** or **Dev.to Article** about your project 135 | 136 | ## Repository Structure After Publication 137 | 138 | ``` 139 | python-manager/ 140 | ├── .git/ # Git repository data 141 | ├── .gitignore # Git ignore rules 142 | ├── LICENSE # MIT License 143 | ├── README.md # Main documentation 144 | ├── CONTRIBUTING.md # Contribution guidelines 145 | ├── requirements.txt # Python dependencies 146 | ├── start_manager.py # Entry point 147 | ├── py_manager/ # Core application 148 | ├── scripts/ # Example scripts 149 | ├── deploy/ # Deployment tools 150 | └── logs/ # Log directory (empty) 151 | ``` 152 | 153 | ## Maintenance Tips 154 | 155 | 1. **Respond to Issues**: Check regularly for bug reports and questions 156 | 2. **Review Pull Requests**: Test and merge community contributions 157 | 3. **Update Documentation**: Keep README current with new features 158 | 4. **Tag Releases**: Use semantic versioning (v1.0.0, v1.1.0, etc.) 159 | 5. **Add CI/CD**: Consider GitHub Actions for automated testing 160 | 161 | ## Example GitHub Actions (Optional) 162 | 163 | Create `.github/workflows/python-app.yml`: 164 | 165 | ```yaml 166 | name: Python application 167 | 168 | on: 169 | push: 170 | branches: [ main ] 171 | pull_request: 172 | branches: [ main ] 173 | 174 | jobs: 175 | build: 176 | runs-on: ubuntu-latest 177 | 178 | steps: 179 | - uses: actions/checkout@v2 180 | - name: Set up Python 181 | uses: actions/setup-python@v2 182 | with: 183 | python-version: '3.8' 184 | - name: Install dependencies 185 | run: | 186 | python -m pip install --upgrade pip 187 | pip install -r requirements.txt 188 | - name: Lint with flake8 189 | run: | 190 | pip install flake8 191 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 192 | ``` 193 | 194 | ## Success Metrics 195 | 196 | Track your project's success: 197 | - ⭐ Stars - Shows popularity 198 | - 🍴 Forks - Shows usage 199 | - 🐛 Issues - Shows engagement 200 | - 🔄 Pull Requests - Shows community contribution 201 | 202 | Good luck with your open-source journey! 🚀 203 | -------------------------------------------------------------------------------- /py_manager/py_script_manager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Script management functions for Python Manager""" 3 | 4 | import os 5 | import json 6 | import uuid 7 | from pathlib import Path 8 | 9 | # Global variable for config path 10 | vg_config_path = os.path.join(os.path.dirname(__file__), 'config.json') 11 | 12 | def vf_load_config(): 13 | """Load configuration from file""" 14 | try: 15 | with open(vg_config_path, 'r') as f: 16 | return json.load(f) 17 | except Exception as e: 18 | print(f"Error loading config: {e}") 19 | return None 20 | 21 | def vf_save_config(vf_config): 22 | """Save configuration to file""" 23 | try: 24 | with open(vg_config_path, 'w') as f: 25 | json.dump(vf_config, f, indent=2) 26 | return True 27 | except Exception as e: 28 | print(f"Error saving config: {e}") 29 | return False 30 | 31 | def vf_add_script(vf_script_path, vf_name=None, vf_args=None, vf_auto_restart=True): 32 | """Add a new script to the configuration""" 33 | # Load current config 34 | vf_config = vf_load_config() 35 | if not vf_config: 36 | return {"success": False, "error": "Failed to load configuration"} 37 | 38 | # Validate script path 39 | vf_abs_path = os.path.abspath(vf_script_path) 40 | if not os.path.exists(vf_abs_path): 41 | return {"success": False, "error": f"Script not found: {vf_abs_path}"} 42 | 43 | if not vf_abs_path.endswith('.py'): 44 | return {"success": False, "error": "Only Python (.py) files are supported"} 45 | 46 | # Generate script ID 47 | vf_script_id = f"script_{uuid.uuid4().hex[:8]}" 48 | 49 | # Use filename as name if not provided 50 | if not vf_name: 51 | vf_name = os.path.basename(vf_abs_path).replace('.py', '').replace('_', ' ').title() 52 | 53 | # Create script entry 54 | vf_new_script = { 55 | "id": vf_script_id, 56 | "name": vf_name, 57 | "path": vf_abs_path, # Use absolute path 58 | "args": vf_args or [], 59 | "auto_restart": vf_auto_restart, 60 | "enabled": True, 61 | "max_memory_mb": 512, 62 | "log_file": f"{vf_script_id}.log" 63 | } 64 | 65 | # Add to config 66 | vf_config['scripts'].append(vf_new_script) 67 | 68 | # Save config 69 | if vf_save_config(vf_config): 70 | return {"success": True, "script": vf_new_script} 71 | else: 72 | return {"success": False, "error": "Failed to save configuration"} 73 | 74 | def vf_remove_script(vf_script_id): 75 | """Remove a script from the configuration""" 76 | vf_config = vf_load_config() 77 | if not vf_config: 78 | return {"success": False, "error": "Failed to load configuration"} 79 | 80 | # Find and remove script 81 | vf_original_count = len(vf_config['scripts']) 82 | vf_config['scripts'] = [s for s in vf_config['scripts'] if s['id'] != vf_script_id] 83 | 84 | if len(vf_config['scripts']) == vf_original_count: 85 | return {"success": False, "error": "Script not found"} 86 | 87 | # Save config 88 | if vf_save_config(vf_config): 89 | return {"success": True} 90 | else: 91 | return {"success": False, "error": "Failed to save configuration"} 92 | 93 | def vf_update_script(vf_script_id, vf_updates): 94 | """Update script configuration""" 95 | vf_config = vf_load_config() 96 | if not vf_config: 97 | return {"success": False, "error": "Failed to load configuration"} 98 | 99 | # Find script 100 | vf_script_found = False 101 | for vf_script in vf_config['scripts']: 102 | if vf_script['id'] == vf_script_id: 103 | # Update allowed fields 104 | if 'name' in vf_updates: 105 | vf_script['name'] = vf_updates['name'] 106 | if 'args' in vf_updates: 107 | vf_script['args'] = vf_updates['args'] 108 | if 'auto_restart' in vf_updates: 109 | vf_script['auto_restart'] = vf_updates['auto_restart'] 110 | if 'enabled' in vf_updates: 111 | vf_script['enabled'] = vf_updates['enabled'] 112 | if 'max_memory_mb' in vf_updates: 113 | vf_script['max_memory_mb'] = vf_updates['max_memory_mb'] 114 | vf_script_found = True 115 | break 116 | 117 | if not vf_script_found: 118 | return {"success": False, "error": "Script not found"} 119 | 120 | # Save config 121 | if vf_save_config(vf_config): 122 | return {"success": True} 123 | else: 124 | return {"success": False, "error": "Failed to save configuration"} 125 | 126 | def vf_list_python_files(vf_directory): 127 | """List all Python files in a directory recursively""" 128 | vf_python_files = [] 129 | 130 | try: 131 | vf_path = Path(vf_directory) 132 | if not vf_path.exists(): 133 | return {"success": False, "error": f"Directory not found: {vf_directory}"} 134 | 135 | if not vf_path.is_dir(): 136 | return {"success": False, "error": f"Path is not a directory: {vf_directory}"} 137 | 138 | # Limit search depth to avoid very deep recursion 139 | vf_max_depth = 3 140 | 141 | # Find all .py files with limited depth 142 | for vf_file in vf_path.rglob("*.py"): 143 | # Skip __pycache__ directories and check depth 144 | if "__pycache__" not in str(vf_file): 145 | try: 146 | # Calculate depth 147 | vf_relative = vf_file.relative_to(vf_path) 148 | vf_depth = len(vf_relative.parts) - 1 149 | 150 | if vf_depth <= vf_max_depth: 151 | vf_python_files.append({ 152 | "path": str(vf_file), 153 | "name": vf_file.name, 154 | "relative": str(vf_relative) 155 | }) 156 | except Exception: 157 | # Skip files that cause issues 158 | pass 159 | 160 | # Sort by relative path for better display 161 | vf_python_files.sort(key=lambda x: x['relative']) 162 | 163 | return {"success": True, "files": vf_python_files} 164 | except PermissionError: 165 | return {"success": False, "error": f"Permission denied: Cannot access {vf_directory}"} 166 | except Exception as e: 167 | return {"success": False, "error": str(e)} 168 | 169 | def vf_validate_script_path(vf_path): 170 | """Validate if a path is a valid Python script""" 171 | try: 172 | vf_abs_path = os.path.abspath(vf_path) 173 | 174 | # Check if file exists 175 | if not os.path.exists(vf_abs_path): 176 | return {"valid": False, "error": "File not found"} 177 | 178 | # Check if it's a Python file 179 | if not vf_abs_path.endswith('.py'): 180 | return {"valid": False, "error": "Not a Python file"} 181 | 182 | # Check if readable 183 | if not os.access(vf_abs_path, os.R_OK): 184 | return {"valid": False, "error": "File not readable"} 185 | 186 | return { 187 | "valid": True, 188 | "absolute_path": vf_abs_path, 189 | "filename": os.path.basename(vf_abs_path), 190 | "directory": os.path.dirname(vf_abs_path) 191 | } 192 | except Exception as e: 193 | return {"valid": False, "error": str(e)} 194 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python Manager 🐍 2 | 3 | [](https://opensource.org/licenses/MIT) 4 | [](https://www.python.org/downloads/) 5 | [](https://flask.palletsprojects.com/) 6 | 7 | A web-based tool for managing multiple Python scripts with real-time monitoring, logging, and control. Perfect for managing microservices, data pipelines, background jobs, or any collection of Python scripts. 8 | 9 |  10 | 11 | 12 | ## ✨ Features 13 | 14 | - **🚀 Multi-Script Management**: Start, stop, and restart Python scripts from anywhere on your system 15 | - **📊 Real-time Monitoring**: Track CPU and memory usage for each script 16 | - **🔄 Auto-Restart**: Automatically restart scripts on failure with configurable retry limits 17 | - **📝 Centralized Logging**: View logs for all scripts in one place 18 | - **🌐 Web Interface**: Modern, responsive dashboard accessible from any browser 19 | - **🔌 REST API**: Full API for programmatic control and automation 20 | - **📁 Flexible Path Support**: Add scripts from ANY location on your system 21 | - **⚡ WebSocket Support**: Real-time status updates without page refresh 22 | - **🎯 Easy Deployment**: Multiple installation options for different use cases 23 | 24 | ## 🚀 Quick Start 25 | 26 | ### Option 1: Interactive Setup (Recommended) 27 | ```bash 28 | git clone https://github.com/prismatex/python-manager.git 29 | cd python-manager 30 | python deploy/setup.py 31 | ``` 32 | 33 | ### Option 2: Direct Run 34 | ```bash 35 | # Clone the repository 36 | git clone https://github.com/prismatex/python-manager.git 37 | cd python-manager 38 | 39 | # Install requirements 40 | pip install -r requirements.txt 41 | 42 | # (Optional) Download Socket.IO for offline use 43 | python download_socketio.py 44 | 45 | # Start the manager 46 | python start_manager.py 47 | ``` 48 | 49 | Then open your browser to: **http://localhost:5000** 50 | 51 | ### 📦 Socket.IO Setup 52 | 53 | Python Manager uses Socket.IO for real-time updates. By default, it loads from CDN (cdnjs.cloudflare.com). For offline use or better reliability: 54 | 55 | ```bash 56 | python download_socketio.py 57 | ``` 58 | 59 | This downloads Socket.IO locally (MIT licensed, freely redistributable). The application automatically falls back to the local file if CDN is unavailable. 60 | 61 | ## 📋 Requirements 62 | 63 | - Python 3.7 or higher 64 | - pip (Python package manager) 65 | 66 | Required packages (automatically installed): 67 | - Flask 68 | - Flask-SocketIO 69 | - Flask-CORS 70 | - psutil 71 | 72 | ## 🎯 Usage 73 | 74 | ### Adding Scripts 75 | 76 | 1. **Via Web Interface** (Easy): 77 | - Click the "⚙ Manage Scripts" button 78 | - Enter the full path to your Python script or browse for it 79 | - Configure display name, arguments, and auto-restart options 80 | - Click "Add Script" 81 | 82 | 2. **Via Configuration File**: 83 | Edit `py_manager/config.json`: 84 | ```json 85 | { 86 | "scripts": [ 87 | { 88 | "id": "my_script", 89 | "name": "My Awesome Script", 90 | "path": "C:/path/to/your/script.py", 91 | "args": ["--arg1", "value"], 92 | "auto_restart": true, 93 | "enabled": true, 94 | "max_memory_mb": 512, 95 | "log_file": "my_script.log" 96 | } 97 | ] 98 | } 99 | ``` 100 | 101 | ### Managing Scripts 102 | 103 | - **Start/Stop**: Click the respective buttons on each script card 104 | - **View Logs**: Click "View Logs" to see real-time output 105 | - **Bulk Actions**: Use "Start All" or "Stop All" for multiple scripts 106 | - **Auto-restart**: Enable to automatically restart failed scripts 107 | 108 | ## 🛠️ API Usage 109 | 110 | Python Manager provides a REST API for automation: 111 | 112 | ```python 113 | import requests 114 | 115 | # Base URL 116 | base_url = "http://localhost:5000/api" 117 | 118 | # Start a script 119 | response = requests.post(f"{base_url}/scripts/my_script/start") 120 | 121 | # Stop a script 122 | response = requests.post(f"{base_url}/scripts/my_script/stop") 123 | 124 | # Get all scripts status 125 | response = requests.get(f"{base_url}/scripts/status") 126 | print(response.json()) 127 | 128 | # Get logs 129 | response = requests.get(f"{base_url}/scripts/my_script/logs?lines=50") 130 | ``` 131 | 132 | ### API Endpoints 133 | 134 | | Method | Endpoint | Description | 135 | |--------|----------|-------------| 136 | | GET | `/api/health` | Health check | 137 | | GET | `/api/scripts` | List all scripts | 138 | | GET | `/api/scripts/status` | Get status of all scripts | 139 | | POST | `/api/scripts/{id}/start` | Start a script | 140 | | POST | `/api/scripts/{id}/stop` | Stop a script | 141 | | POST | `/api/scripts/{id}/restart` | Restart a script | 142 | | GET | `/api/scripts/{id}/logs` | Get script logs | 143 | | POST | `/api/scripts/add` | Add new script | 144 | | DELETE | `/api/scripts/{id}/remove` | Remove script | 145 | 146 | ## 📦 Deployment Options 147 | 148 | ### For Development Projects 149 | ```bash 150 | python deploy/setup.py 151 | ``` 152 | Follow the interactive prompts to set up Python Manager in your project. 153 | 154 | ### For Production 155 | 1. Clone to your server 156 | 2. Set up as a systemd service (Linux) or Windows Service 157 | 3. Configure authentication in `api_config.json` 158 | 4. Use a reverse proxy (nginx/Apache) for HTTPS 159 | 160 | ### Create Portable Package 161 | ```bash 162 | python deploy/create_package.py 163 | ``` 164 | This creates a distributable ZIP file with everything needed. 165 | 166 | ## 🏗️ Project Structure 167 | 168 | ``` 169 | python-manager/ 170 | ├── start_manager.py # Main entry point 171 | ├── allin1.py # All-in-one server 172 | ├── requirements.txt # Python dependencies 173 | ├── py_manager/ # Core modules 174 | │ ├── py_process.py # Process management 175 | │ ├── py_logger.py # Logging system 176 | │ ├── py_api.py # REST API 177 | │ ├── py_manager.html # Web interface 178 | │ └── ... 179 | ├── scripts/ # Example scripts 180 | ├── logs/ # Log files 181 | └── deploy/ # Deployment tools 182 | ``` 183 | 184 | ## 🤝 Contributing 185 | 186 | Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 187 | 188 | 1. Fork the repository 189 | 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 190 | 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 191 | 4. Push to the branch (`git push origin feature/AmazingFeature`) 192 | 5. Open a Pull Request 193 | 194 | ## 📝 License 195 | 196 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 197 | 198 | ## 🙏 Acknowledgments 199 | 200 | - Built with Flask and Flask-SocketIO 201 | - Process monitoring powered by psutil 202 | - UI inspired by modern dashboard designs 203 | 204 | ## 📧 Support 205 | 206 | - Create an [Issue](https://github.com/prismatex/python-manager/issues) for bug reports or feature requests 207 | - Check the [Wiki](https://github.com/prismatex/python-manager/wiki) for detailed documentation 208 | - Join our [Discussions](https://github.com/prismatex/python-manager/discussions) for community support 209 | 210 | ## 🚧 Roadmap 211 | 212 | - [ ] Docker support 213 | - [ ] Script scheduling (cron-like functionality) 214 | - [ ] Resource usage graphs 215 | - [ ] Script dependencies management 216 | - [ ] Email/webhook notifications 217 | - [ ] Dark mode theme 218 | - [ ] Multi-user support with authentication 219 | 220 | --- 221 | 222 | Made with ❤️ by the Python community 223 | -------------------------------------------------------------------------------- /py_manager/py_manager.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Python Manager - Process Control Dashboard 8 | 9 | 10 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Python Manager Dashboard 25 | 26 | 27 | Disconnected 28 | 29 | 30 | 🌓 Toggle Theme 31 | 32 | 33 | 34 | 35 | 36 | 37 | Total Scripts: 38 | 0 39 | 40 | 41 | Running: 42 | 0 43 | 44 | 45 | Stopped: 46 | 0 47 | 48 | 49 | CPU Total: 50 | 0% 51 | 52 | 53 | Memory Total: 54 | 0 MB 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | Managed Scripts 64 | 65 | 66 | ▶ Start All 67 | 68 | 69 | ■ Stop All 70 | 71 | 72 | ↻ Reload Config 73 | 74 | 75 | ⚙ Manage Scripts 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Logs 89 | 90 | 91 | Manager Logs 92 | 93 | 94 | Refresh 95 | 96 | 97 | Clear View 98 | 99 | 100 | 101 | 102 | 103 | Select a script to view logs 104 | 105 | 106 | 107 | 108 | 109 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | Confirm Action 122 | × 123 | 124 | 125 | 126 | 127 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | Script Manager 139 | × 140 | 141 | 142 | 143 | 144 | Add New Script 145 | 146 | Script Path: 147 | 148 | 149 | Browse 150 | Browse auto-completes to scripts directory. Edit path if your script is elsewhere. 151 | 152 | 153 | Display Name: 154 | 155 | 156 | 157 | Arguments: 158 | 159 | 160 | 161 | 162 | 163 | Auto-restart on failure 164 | 165 | 166 | Add Script 167 | 168 | 169 | 170 | 171 | Current Scripts 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | -------------------------------------------------------------------------------- /py_manager/py_process.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import psutil 3 | import json 4 | import os 5 | import sys 6 | import time 7 | import signal 8 | from datetime import datetime 9 | 10 | # Global variables 11 | vg_processes = {} # Dictionary to store running processes 12 | vg_config = None # Configuration data 13 | vg_restart_attempts = {} # Track restart attempts 14 | 15 | # Fix path for script execution 16 | vg_base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | def vf_load_config(): 19 | """Load configuration from config.json""" 20 | global vg_config 21 | vf_config_path = os.path.join(os.path.dirname(__file__), 'config.json') 22 | 23 | try: 24 | with open(vf_config_path, 'r') as vf_file: 25 | vg_config = json.load(vf_file) 26 | return True 27 | except Exception as vf_error: 28 | print(f"Error loading config: {vf_error}") 29 | return False 30 | 31 | def vf_start_script(vf_script_id): 32 | """Start a Python script by its ID""" 33 | global vg_processes 34 | 35 | # Find script configuration 36 | vf_script_config = None 37 | for vf_script in vg_config['scripts']: 38 | if vf_script['id'] == vf_script_id: 39 | vf_script_config = vf_script 40 | break 41 | 42 | if not vf_script_config: 43 | return {"success": False, "error": "Script ID not found"} 44 | 45 | if not vf_script_config['enabled']: 46 | return {"success": False, "error": "Script is disabled"} 47 | 48 | # Check if already running 49 | if vf_script_id in vg_processes and vf_is_process_running(vf_script_id): 50 | return {"success": False, "error": "Script already running"} 51 | 52 | # Prepare command with proper path handling 53 | vf_script_path = vf_script_config['path'] 54 | 55 | # Check if it's an absolute path 56 | if not os.path.isabs(vf_script_path): 57 | # If relative, join with base path 58 | vf_script_path = os.path.join(vg_base_path, vf_script_path) 59 | 60 | # Check if script exists 61 | if not os.path.exists(vf_script_path): 62 | return {"success": False, "error": f"Script not found: {vf_script_path}"} 63 | 64 | vf_cmd = [sys.executable, vf_script_path] 65 | if vf_script_config.get('args'): 66 | vf_cmd.extend(vf_script_config['args']) 67 | 68 | # Prepare log file 69 | vf_log_dir = os.path.join(os.path.dirname(__file__), 'logs') 70 | os.makedirs(vf_log_dir, exist_ok=True) 71 | 72 | vf_log_path = os.path.join(vf_log_dir, vf_script_config['log_file']) 73 | vf_log_file = open(vf_log_path, 'a') 74 | 75 | try: 76 | # Start process with proper working directory 77 | # Use script's directory as working directory for absolute paths 78 | vf_working_dir = os.path.dirname(vf_script_path) if os.path.isabs(vf_script_config['path']) else vg_base_path 79 | 80 | vf_process = subprocess.Popen( 81 | vf_cmd, 82 | stdout=vf_log_file, 83 | stderr=subprocess.STDOUT, 84 | cwd=vf_working_dir # Set working directory based on path type 85 | ) 86 | 87 | # Store process info 88 | vg_processes[vf_script_id] = { 89 | 'process': vf_process, 90 | 'pid': vf_process.pid, 91 | 'start_time': datetime.now().isoformat(), 92 | 'log_file': vf_log_file, 93 | 'config': vf_script_config 94 | } 95 | 96 | # Reset restart attempts 97 | vg_restart_attempts[vf_script_id] = 0 98 | 99 | return { 100 | "success": True, 101 | "pid": vf_process.pid, 102 | "message": f"Script {vf_script_id} started successfully" 103 | } 104 | 105 | except Exception as vf_error: 106 | vf_log_file.close() 107 | return {"success": False, "error": str(vf_error)} 108 | 109 | def vf_stop_script(vf_script_id, vf_timeout=10): 110 | """Stop a running Python script""" 111 | global vg_processes 112 | 113 | if vf_script_id not in vg_processes: 114 | return {"success": False, "error": "Script not in process list"} 115 | 116 | vf_process_info = vg_processes[vf_script_id] 117 | vf_process = vf_process_info['process'] 118 | 119 | try: 120 | # Try graceful termination first 121 | vf_process.terminate() 122 | 123 | # Wait for process to terminate 124 | try: 125 | vf_process.wait(timeout=vf_timeout) 126 | except subprocess.TimeoutExpired: 127 | # Force kill if timeout 128 | vf_process.kill() 129 | vf_process.wait() 130 | 131 | # Close log file 132 | vf_process_info['log_file'].close() 133 | 134 | # Remove from process list 135 | del vg_processes[vf_script_id] 136 | 137 | return {"success": True, "message": f"Script {vf_script_id} stopped"} 138 | 139 | except Exception as vf_error: 140 | return {"success": False, "error": str(vf_error)} 141 | 142 | def vf_restart_script(vf_script_id): 143 | """Restart a Python script""" 144 | vf_stop_result = vf_stop_script(vf_script_id) 145 | 146 | if vf_stop_result['success'] or 'not in process list' in str(vf_stop_result.get('error', '')): 147 | time.sleep(1) # Brief pause before restart 148 | return vf_start_script(vf_script_id) 149 | 150 | return vf_stop_result 151 | 152 | def vf_is_process_running(vf_script_id): 153 | """Check if a process is still running""" 154 | if vf_script_id not in vg_processes: 155 | return False 156 | 157 | vf_process = vg_processes[vf_script_id]['process'] 158 | return vf_process.poll() is None 159 | 160 | def vf_get_process_info(vf_script_id): 161 | """Get detailed information about a running process""" 162 | if vf_script_id not in vg_processes: 163 | return None 164 | 165 | if not vf_is_process_running(vf_script_id): 166 | return None 167 | 168 | vf_process_info = vg_processes[vf_script_id] 169 | vf_pid = vf_process_info['pid'] 170 | 171 | try: 172 | vf_psutil_process = psutil.Process(vf_pid) 173 | 174 | return { 175 | 'pid': vf_pid, 176 | 'status': 'running', 177 | 'start_time': vf_process_info['start_time'], 178 | 'cpu_percent': vf_psutil_process.cpu_percent(interval=0.1), 179 | 'memory_mb': vf_psutil_process.memory_info().rss / 1024 / 1024, 180 | 'num_threads': vf_psutil_process.num_threads() 181 | } 182 | except psutil.NoSuchProcess: 183 | return None 184 | 185 | def vf_monitor_processes(): 186 | """Monitor all processes and handle auto-restarts""" 187 | global vg_restart_attempts 188 | 189 | ag_dead_processes = [] 190 | 191 | # Check each process 192 | for vf_script_id in list(vg_processes.keys()): 193 | if not vf_is_process_running(vf_script_id): 194 | ag_dead_processes.append(vf_script_id) 195 | 196 | # Handle dead processes 197 | for vf_script_id in ag_dead_processes: 198 | vf_config = vg_processes[vf_script_id]['config'] 199 | 200 | # Clean up dead process entry 201 | vg_processes[vf_script_id]['log_file'].close() 202 | del vg_processes[vf_script_id] 203 | 204 | # Check if auto-restart is enabled 205 | if vf_config.get('auto_restart', False): 206 | vf_max_attempts = vg_config['manager_settings']['auto_restart_max_attempts'] 207 | vf_current_attempts = vg_restart_attempts.get(vf_script_id, 0) 208 | 209 | if vf_current_attempts < vf_max_attempts: 210 | print(f"Auto-restarting {vf_script_id} (attempt {vf_current_attempts + 1})") 211 | vg_restart_attempts[vf_script_id] = vf_current_attempts + 1 212 | 213 | # Wait before restart 214 | time.sleep(vg_config['manager_settings']['auto_restart_delay_seconds']) 215 | vf_start_script(vf_script_id) 216 | else: 217 | print(f"Max restart attempts reached for {vf_script_id}") 218 | 219 | def vf_get_all_status(): 220 | """Get status of all configured scripts""" 221 | ag_status = [] 222 | 223 | for vf_script in vg_config['scripts']: 224 | vf_script_id = vf_script['id'] 225 | vf_status = { 226 | 'id': vf_script_id, 227 | 'name': vf_script['name'], 228 | 'enabled': vf_script['enabled'], 229 | 'path': vf_script['path'] 230 | } 231 | 232 | if vf_is_process_running(vf_script_id): 233 | vf_process_info = vf_get_process_info(vf_script_id) 234 | vf_status.update(vf_process_info) 235 | else: 236 | vf_status['status'] = 'stopped' 237 | vf_status['restart_attempts'] = vg_restart_attempts.get(vf_script_id, 0) 238 | 239 | ag_status.append(vf_status) 240 | 241 | return ag_status 242 | 243 | def vf_cleanup(): 244 | """Clean up all processes before exit""" 245 | print("Cleaning up processes...") 246 | 247 | for vf_script_id in list(vg_processes.keys()): 248 | vf_stop_script(vf_script_id) 249 | 250 | # Signal handler for clean shutdown 251 | def vf_signal_handler(vf_signum, vf_frame): 252 | vf_cleanup() 253 | exit(0) 254 | 255 | # Register signal handlers 256 | signal.signal(signal.SIGINT, vf_signal_handler) 257 | signal.signal(signal.SIGTERM, vf_signal_handler) 258 | 259 | # Initialize configuration on module load 260 | vf_load_config() -------------------------------------------------------------------------------- /deploy/setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Python Manager Setup Script 4 | Easily deploy Python Manager to your project 5 | """ 6 | 7 | import os 8 | import sys 9 | import shutil 10 | import json 11 | import subprocess 12 | 13 | def vf_print_header(): 14 | """Print setup header""" 15 | print(""" 16 | ╔══════════════════════════════════════════════╗ 17 | ║ Python Manager Setup & Deployment ║ 18 | ╚══════════════════════════════════════════════╝ 19 | """) 20 | 21 | def vf_check_requirements(): 22 | """Check if required packages are installed""" 23 | vg_required = ['flask', 'flask-socketio', 'flask-cors', 'psutil'] 24 | vg_missing = [] 25 | 26 | for vf_package in vg_required: 27 | try: 28 | __import__(vf_package.replace('-', '_')) 29 | except ImportError: 30 | vg_missing.append(vf_package) 31 | 32 | if vg_missing: 33 | print(f"Missing required packages: {', '.join(vg_missing)}") 34 | vf_install = input("Install missing packages? (y/n): ").lower() 35 | if vf_install == 'y': 36 | subprocess.check_call([sys.executable, '-m', 'pip', 'install'] + vg_missing) 37 | else: 38 | print("Setup cancelled. Please install required packages manually.") 39 | sys.exit(1) 40 | 41 | print("✓ All requirements satisfied") 42 | 43 | def vf_get_deployment_path(): 44 | """Get the deployment path from user""" 45 | print("\nWhere would you like to install Python Manager?") 46 | print("1. Current directory") 47 | print("2. Custom path") 48 | 49 | vf_choice = input("\nChoice (1-2): ").strip() 50 | 51 | if vf_choice == '1': 52 | return os.getcwd() 53 | elif vf_choice == '2': 54 | vf_path = input("Enter path: ").strip() 55 | if not os.path.exists(vf_path): 56 | vf_create = input(f"Path '{vf_path}' doesn't exist. Create it? (y/n): ").lower() 57 | if vf_create == 'y': 58 | os.makedirs(vf_path, exist_ok=True) 59 | else: 60 | print("Setup cancelled.") 61 | sys.exit(1) 62 | return vf_path 63 | else: 64 | print("Invalid choice.") 65 | sys.exit(1) 66 | 67 | def vf_copy_files(vf_source_dir, vf_target_dir): 68 | """Copy Python Manager files to target directory""" 69 | print(f"\nCopying files to {vf_target_dir}...") 70 | 71 | # Create py_manager directory 72 | vf_py_manager_target = os.path.join(vf_target_dir, 'py_manager') 73 | os.makedirs(vf_py_manager_target, exist_ok=True) 74 | 75 | # Files to copy 76 | vg_root_files = [ 77 | 'start_manager.py', 78 | 'allin1.py', 79 | 'api_server.py', 80 | 'web_server.py', 81 | 'manager.py', 82 | 'requirements.txt' 83 | ] 84 | 85 | vg_py_manager_files = [ 86 | 'py_api.py', 87 | 'py_process.py', 88 | 'py_logger.py', 89 | 'py_manager.py', 90 | 'py_script_manager.py', 91 | 'py_manager.html', 92 | 'py_interface.js', 93 | 'py_manager.css', 94 | 'api_config.json', 95 | '__init__.py' 96 | ] 97 | 98 | # Note: socket.io.min.js is loaded from CDN in HTML 99 | 100 | # Copy root files 101 | for vf_file in vg_root_files: 102 | vf_source = os.path.join(vf_source_dir, vf_file) 103 | vf_dest = os.path.join(vf_target_dir, vf_file) 104 | if os.path.exists(vf_source): 105 | shutil.copy2(vf_source, vf_dest) 106 | print(f" ✓ {vf_file}") 107 | 108 | # Copy py_manager files 109 | for vf_file in vg_py_manager_files: 110 | vf_source = os.path.join(vf_source_dir, 'py_manager', vf_file) 111 | vf_dest = os.path.join(vf_py_manager_target, vf_file) 112 | if os.path.exists(vf_source): 113 | shutil.copy2(vf_source, vf_dest) 114 | print(f" ✓ py_manager/{vf_file}") 115 | 116 | # Create necessary directories 117 | os.makedirs(os.path.join(vf_target_dir, 'scripts'), exist_ok=True) 118 | os.makedirs(os.path.join(vf_target_dir, 'logs'), exist_ok=True) 119 | os.makedirs(os.path.join(vf_py_manager_target, 'logs'), exist_ok=True) 120 | 121 | print("\n✓ Files copied successfully") 122 | 123 | def vf_create_initial_config(vf_target_dir): 124 | """Create initial configuration""" 125 | print("\nCreating initial configuration...") 126 | 127 | vf_config_path = os.path.join(vf_target_dir, 'py_manager', 'config.json') 128 | 129 | vg_initial_config = { 130 | "manager_settings": { 131 | "port": 8080, 132 | "log_retention_days": 7, 133 | "check_interval_seconds": 5, 134 | "auto_restart_max_attempts": 3, 135 | "auto_restart_delay_seconds": 10 136 | }, 137 | "scripts": [] 138 | } 139 | 140 | # Ask if user wants to add scripts now 141 | vf_add_scripts = input("\nWould you like to add Python scripts now? (y/n): ").lower() 142 | 143 | if vf_add_scripts == 'y': 144 | while True: 145 | print("\nAdd a Python script (leave path empty to finish):") 146 | vf_path = input("Script path: ").strip() 147 | 148 | if not vf_path: 149 | break 150 | 151 | if not os.path.exists(vf_path): 152 | print(f"Warning: Script not found at {vf_path}") 153 | vf_continue = input("Add anyway? (y/n): ").lower() 154 | if vf_continue != 'y': 155 | continue 156 | 157 | vf_name = input("Display name (optional): ").strip() 158 | if not vf_name: 159 | vf_name = os.path.basename(vf_path).replace('.py', '').replace('_', ' ').title() 160 | 161 | vf_script_id = f"script_{len(vg_initial_config['scripts']) + 1}" 162 | 163 | vg_initial_config['scripts'].append({ 164 | "id": vf_script_id, 165 | "name": vf_name, 166 | "path": os.path.abspath(vf_path), 167 | "args": [], 168 | "auto_restart": True, 169 | "enabled": True, 170 | "max_memory_mb": 512, 171 | "log_file": f"{vf_script_id}.log" 172 | }) 173 | 174 | print(f"✓ Added: {vf_name}") 175 | 176 | # Save config 177 | with open(vf_config_path, 'w') as f: 178 | json.dump(vg_initial_config, f, indent=2) 179 | 180 | print("\n✓ Configuration created") 181 | 182 | def vf_create_shortcuts(vf_target_dir): 183 | """Create convenient shortcuts""" 184 | print("\nCreating shortcuts...") 185 | 186 | # Create a simple batch file for Windows 187 | if sys.platform == 'win32': 188 | vf_batch_content = f"""@echo off 189 | cd /d "{vf_target_dir}" 190 | python start_manager.py 191 | pause 192 | """ 193 | vf_batch_path = os.path.join(vf_target_dir, 'Start_Python_Manager.bat') 194 | with open(vf_batch_path, 'w') as f: 195 | f.write(vf_batch_content) 196 | print(f" ✓ Created: Start_Python_Manager.bat") 197 | 198 | # Create a shell script for Unix-like systems 199 | else: 200 | vf_shell_content = f"""#!/bin/bash 201 | cd "{vf_target_dir}" 202 | python3 start_manager.py 203 | """ 204 | vf_shell_path = os.path.join(vf_target_dir, 'start_python_manager.sh') 205 | with open(vf_shell_path, 'w') as f: 206 | f.write(vf_shell_content) 207 | os.chmod(vf_shell_path, 0o755) 208 | print(f" ✓ Created: start_python_manager.sh") 209 | 210 | def vf_print_instructions(vf_target_dir): 211 | """Print usage instructions""" 212 | print(f""" 213 | ╔══════════════════════════════════════════════╗ 214 | ║ Setup Complete! ✓ ║ 215 | ╚══════════════════════════════════════════════╝ 216 | 217 | Python Manager has been installed to: 218 | {vf_target_dir} 219 | 220 | To start Python Manager: 221 | """) 222 | 223 | if sys.platform == 'win32': 224 | print(f" 1. Double-click: Start_Python_Manager.bat") 225 | print(f" 2. Or run: python {os.path.join(vf_target_dir, 'start_manager.py')}") 226 | else: 227 | print(f" 1. Run: ./start_python_manager.sh") 228 | print(f" 2. Or run: python3 {os.path.join(vf_target_dir, 'start_manager.py')}") 229 | 230 | print(""" 231 | Once started, access the web interface at: 232 | http://localhost:5000 233 | 234 | Features: 235 | ✓ Add scripts from anywhere on your system 236 | ✓ Monitor CPU and memory usage 237 | ✓ View centralized logs 238 | ✓ Auto-restart on failure 239 | ✓ REST API for automation 240 | 241 | Managing Scripts: 242 | 1. Click "Manage Scripts" in the web interface 243 | 2. Browse or enter paths to Python scripts 244 | 3. Scripts can be from ANY folder on your system 245 | 4. Configure auto-restart and arguments as needed 246 | 247 | Happy managing! 248 | """) 249 | 250 | def vf_main(): 251 | """Main setup function""" 252 | vf_print_header() 253 | 254 | # Get source directory (where this script is) 255 | vg_source_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 256 | 257 | # Check requirements 258 | vf_check_requirements() 259 | 260 | # Get deployment path 261 | vg_target_dir = vf_get_deployment_path() 262 | 263 | # Copy files 264 | vf_copy_files(vg_source_dir, vg_target_dir) 265 | 266 | # Create initial config 267 | vf_create_initial_config(vg_target_dir) 268 | 269 | # Create shortcuts 270 | vf_create_shortcuts(vg_target_dir) 271 | 272 | # Print instructions 273 | vf_print_instructions(vg_target_dir) 274 | 275 | # Ask if user wants to start now 276 | vf_start_now = input("\nStart Python Manager now? (y/n): ").lower() 277 | if vf_start_now == 'y': 278 | os.chdir(vg_target_dir) 279 | subprocess.run([sys.executable, 'start_manager.py']) 280 | 281 | if __name__ == '__main__': 282 | vf_main() 283 | -------------------------------------------------------------------------------- /py_manager/py_api.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, jsonify, request 2 | from flask_socketio import SocketIO, emit 3 | from flask_cors import CORS 4 | import json 5 | import os 6 | import threading 7 | import time 8 | from datetime import datetime 9 | 10 | # Import our modules 11 | import py_process 12 | import py_logger 13 | import py_script_manager 14 | 15 | # Global variables 16 | vg_app = Flask(__name__) 17 | vg_socketio = None 18 | vg_config = None 19 | vg_update_thread = None 20 | vg_running = True 21 | 22 | def vf_load_api_config(): 23 | """Load API configuration""" 24 | global vg_config 25 | vf_config_path = os.path.join(os.path.dirname(__file__), 'api_config.json') 26 | 27 | try: 28 | with open(vf_config_path, 'r') as vf_file: 29 | vg_config = json.load(vf_file) 30 | return True 31 | except Exception as vf_error: 32 | print(f"Error loading API config: {vf_error}") 33 | return False 34 | 35 | @vg_app.route('/api/scripts/start-all', methods=['POST']) 36 | def route_start_all_scripts(): 37 | """Start all enabled scripts""" 38 | if not vf_check_auth(): 39 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 40 | 41 | ag_results = [] 42 | for vf_script in py_process.vg_config['scripts']: 43 | if vf_script['enabled'] and vf_script['id'] not in py_process.vg_processes: 44 | vf_result = py_process.vf_start_script(vf_script['id']) 45 | ag_results.append({ 46 | 'script_id': vf_script['id'], 47 | 'result': vf_result 48 | }) 49 | 50 | py_logger.vf_write_manager_log('API', f'Start all scripts via API: {len(ag_results)} scripts') 51 | 52 | return vf_api_response(True, {'results': ag_results}) 53 | 54 | @vg_app.route('/api/scripts/stop-all', methods=['POST']) 55 | def route_stop_all_scripts(): 56 | """Stop all running scripts""" 57 | if not vf_check_auth(): 58 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 59 | 60 | ag_results = [] 61 | for vf_script_id in list(py_process.vg_processes.keys()): 62 | vf_result = py_process.vf_stop_script(vf_script_id) 63 | ag_results.append({ 64 | 'script_id': vf_script_id, 65 | 'result': vf_result 66 | }) 67 | 68 | py_logger.vf_write_manager_log('API', f'Stop all scripts via API: {len(ag_results)} scripts') 69 | 70 | return vf_api_response(True, {'results': ag_results}) 71 | 72 | # Load configurations 73 | vf_load_api_config() 74 | py_process.vf_load_config() 75 | 76 | # Configure Flask app 77 | vg_app.config['SECRET_KEY'] = vg_config['api_settings']['secret_key'] 78 | 79 | # Initialize CORS 80 | CORS(vg_app, origins=vg_config['api_settings']['cors_origins']) 81 | 82 | # Initialize SocketIO 83 | """ 84 | vg_socketio = SocketIO( 85 | vg_app, 86 | cors_allowed_origins=vg_config['api_settings']['cors_origins'], 87 | ping_timeout=vg_config['websocket_settings']['ping_timeout'], 88 | ping_interval=vg_config['websocket_settings']['ping_interval'] 89 | ) 90 | """ 91 | # Initialize SocketIO with explicit CORS settings 92 | vg_socketio = SocketIO( 93 | vg_app, 94 | cors_allowed_origins="*", # Allow all origins for now 95 | async_mode='threading' 96 | ) 97 | 98 | # Middleware for optional authentication 99 | def vf_check_auth(): 100 | """Check authentication if enabled""" 101 | if not vg_config['api_settings']['auth_enabled']: 102 | return True 103 | 104 | vf_token = request.headers.get('Authorization') 105 | if vf_token and vf_token.replace('Bearer ', '') == vg_config['api_settings']['auth_token']: 106 | return True 107 | 108 | return False 109 | 110 | def vf_api_response(vf_success, vf_data=None, vf_error=None, vf_status_code=200): 111 | """Standardized API response format""" 112 | vf_response = { 113 | 'success': vf_success, 114 | 'timestamp': datetime.now().isoformat() 115 | } 116 | 117 | if vf_data is not None: 118 | vf_response['data'] = vf_data 119 | 120 | if vf_error is not None: 121 | vf_response['error'] = vf_error 122 | 123 | return jsonify(vf_response), vf_status_code 124 | 125 | # REST API Routes 126 | 127 | @vg_app.route('/api/health', methods=['GET']) 128 | def route_health(): 129 | """Health check endpoint""" 130 | return vf_api_response(True, {'status': 'healthy', 'version': '1.0.0'}) 131 | 132 | @vg_app.route('/api/scripts', methods=['GET']) 133 | def route_get_scripts(): 134 | """Get all configured scripts""" 135 | if not vf_check_auth(): 136 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 137 | 138 | ag_scripts = py_process.vg_config['scripts'] 139 | return vf_api_response(True, {'scripts': ag_scripts}) 140 | 141 | @vg_app.route('/api/scripts/status', methods=['GET']) 142 | def route_get_all_status(): 143 | """Get status of all scripts""" 144 | if not vf_check_auth(): 145 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 146 | 147 | ag_status = py_process.vf_get_all_status() 148 | return vf_api_response(True, {'status': ag_status}) 149 | 150 | @vg_app.route('/api/scripts//status', methods=['GET']) 151 | def route_get_script_status(script_id): 152 | """Get status of specific script""" 153 | if not vf_check_auth(): 154 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 155 | 156 | ag_all_status = py_process.vf_get_all_status() 157 | vf_script_status = None 158 | 159 | for vf_status in ag_all_status: 160 | if vf_status['id'] == script_id: 161 | vf_script_status = vf_status 162 | break 163 | 164 | if vf_script_status: 165 | return vf_api_response(True, {'status': vf_script_status}) 166 | else: 167 | return vf_api_response(False, error='Script not found', vf_status_code=404) 168 | 169 | @vg_app.route('/api/scripts//start', methods=['POST']) 170 | def route_start_script(script_id): 171 | """Start a script""" 172 | if not vf_check_auth(): 173 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 174 | 175 | vf_result = py_process.vf_start_script(script_id) 176 | 177 | # Log the action 178 | py_logger.vf_write_manager_log('API', f'Start script via API: {vf_result}', script_id) 179 | 180 | # Emit update via WebSocket (safely) 181 | vf_safe_emit_update() 182 | 183 | if vf_result['success']: 184 | return vf_api_response(True, vf_result) 185 | else: 186 | return vf_api_response(False, error=vf_result.get('error'), vf_status_code=400) 187 | 188 | @vg_app.route('/api/scripts//stop', methods=['POST']) 189 | def route_stop_script(script_id): 190 | """Stop a script""" 191 | if not vf_check_auth(): 192 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 193 | 194 | vf_result = py_process.vf_stop_script(script_id) 195 | 196 | # Log the action 197 | py_logger.vf_write_manager_log('API', f'Stop script via API: {vf_result}', script_id) 198 | 199 | # Emit update via WebSocket 200 | vf_safe_emit_update() 201 | 202 | if vf_result['success']: 203 | return vf_api_response(True, vf_result) 204 | else: 205 | return vf_api_response(False, error=vf_result.get('error'), vf_status_code=400) 206 | 207 | @vg_app.route('/api/scripts//restart', methods=['POST']) 208 | def route_restart_script(script_id): 209 | """Restart a script""" 210 | if not vf_check_auth(): 211 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 212 | 213 | vf_result = py_process.vf_restart_script(script_id) 214 | 215 | # Log the action 216 | py_logger.vf_write_manager_log('API', f'Restart script via API: {vf_result}', script_id) 217 | 218 | # Emit update via WebSocket 219 | vf_safe_emit_update() 220 | 221 | if vf_result['success']: 222 | return vf_api_response(True, vf_result) 223 | else: 224 | return vf_api_response(False, error=vf_result.get('error'), vf_status_code=400) 225 | 226 | @vg_app.route('/api/scripts//logs', methods=['GET']) 227 | def route_get_script_logs(script_id): 228 | """Get logs for a script""" 229 | if not vf_check_auth(): 230 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 231 | 232 | vf_lines = request.args.get('lines', 100, type=int) 233 | ag_logs = py_logger.vf_read_recent_logs(script_id, vf_lines) 234 | 235 | return vf_api_response(True, {'logs': ag_logs, 'script_id': script_id}) 236 | 237 | @vg_app.route('/api/manager/logs', methods=['GET']) 238 | def route_get_manager_logs(): 239 | """Get manager logs""" 240 | if not vf_check_auth(): 241 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 242 | 243 | vf_lines = request.args.get('lines', 100, type=int) 244 | ag_logs = py_logger.vf_read_recent_logs(None, vf_lines) 245 | 246 | return vf_api_response(True, {'logs': ag_logs}) 247 | 248 | @vg_app.route('/api/config/reload', methods=['POST']) 249 | def route_reload_config(): 250 | """Reload configuration""" 251 | if not vf_check_auth(): 252 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 253 | 254 | vf_success = py_process.vf_load_config() 255 | 256 | if vf_success: 257 | py_logger.vf_write_manager_log('API', 'Configuration reloaded via API') 258 | vf_safe_emit_update() 259 | return vf_api_response(True, {'message': 'Configuration reloaded'}) 260 | else: 261 | return vf_api_response(False, error='Failed to reload configuration', vf_status_code=500) 262 | 263 | # Script Management Routes 264 | 265 | @vg_app.route('/api/scripts/add', methods=['POST']) 266 | def route_add_script(): 267 | """Add a new script""" 268 | if not vf_check_auth(): 269 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 270 | 271 | vf_data = request.get_json() 272 | if not vf_data or 'path' not in vf_data: 273 | return vf_api_response(False, error='Script path is required', vf_status_code=400) 274 | 275 | # Validate script path 276 | vf_validation = py_script_manager.vf_validate_script_path(vf_data['path']) 277 | if not vf_validation['valid']: 278 | return vf_api_response(False, error=vf_validation['error'], vf_status_code=400) 279 | 280 | # Add script 281 | vf_result = py_script_manager.vf_add_script( 282 | vf_script_path=vf_data['path'], 283 | vf_name=vf_data.get('name'), 284 | vf_args=vf_data.get('args', []), 285 | vf_auto_restart=vf_data.get('auto_restart', True) 286 | ) 287 | 288 | if vf_result['success']: 289 | # Reload config in process manager 290 | py_process.vf_load_config() 291 | vf_safe_emit_update() 292 | py_logger.vf_write_manager_log('API', f'Added new script: {vf_result["script"]["name"]}') 293 | return vf_api_response(True, vf_result) 294 | else: 295 | return vf_api_response(False, error=vf_result['error'], vf_status_code=400) 296 | 297 | @vg_app.route('/api/scripts//remove', methods=['DELETE']) 298 | def route_remove_script(script_id): 299 | """Remove a script""" 300 | if not vf_check_auth(): 301 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 302 | 303 | # Stop script if running 304 | if script_id in py_process.vg_processes: 305 | py_process.vf_stop_script(script_id) 306 | 307 | # Remove from config 308 | vf_result = py_script_manager.vf_remove_script(script_id) 309 | 310 | if vf_result['success']: 311 | # Reload config 312 | py_process.vf_load_config() 313 | vf_safe_emit_update() 314 | py_logger.vf_write_manager_log('API', f'Removed script: {script_id}') 315 | return vf_api_response(True, {'message': 'Script removed'}) 316 | else: 317 | return vf_api_response(False, error=vf_result['error'], vf_status_code=400) 318 | 319 | @vg_app.route('/api/scripts//update', methods=['PUT']) 320 | def route_update_script(script_id): 321 | """Update script configuration""" 322 | if not vf_check_auth(): 323 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 324 | 325 | vf_data = request.get_json() 326 | if not vf_data: 327 | return vf_api_response(False, error='No update data provided', vf_status_code=400) 328 | 329 | vf_result = py_script_manager.vf_update_script(script_id, vf_data) 330 | 331 | if vf_result['success']: 332 | # Reload config 333 | py_process.vf_load_config() 334 | vf_safe_emit_update() 335 | py_logger.vf_write_manager_log('API', f'Updated script: {script_id}') 336 | return vf_api_response(True, {'message': 'Script updated'}) 337 | else: 338 | return vf_api_response(False, error=vf_result['error'], vf_status_code=400) 339 | 340 | @vg_app.route('/api/browse/directory', methods=['POST']) 341 | def route_browse_directory(): 342 | """Browse directory for Python files""" 343 | if not vf_check_auth(): 344 | return vf_api_response(False, error='Unauthorized', vf_status_code=401) 345 | 346 | vf_data = request.get_json() 347 | if not vf_data or 'directory' not in vf_data: 348 | return vf_api_response(False, error='Directory path is required', vf_status_code=400) 349 | 350 | vf_result = py_script_manager.vf_list_python_files(vf_data['directory']) 351 | 352 | if vf_result['success']: 353 | # Return the files directly in the data field for cleaner API response 354 | return vf_api_response(True, {'files': vf_result['files']}) 355 | else: 356 | return vf_api_response(False, error=vf_result['error'], vf_status_code=400) 357 | 358 | # WebSocket Events 359 | 360 | @vg_socketio.on('connect') 361 | def handle_connect(): 362 | """Handle client connection""" 363 | print(f"Client connected: {request.sid}") 364 | emit('connected', {'message': 'Connected to Python Manager'}) 365 | 366 | # Send initial status 367 | ag_status = py_process.vf_get_all_status() 368 | emit('status_update', {'status': ag_status}) 369 | 370 | @vg_socketio.on('disconnect') 371 | def handle_disconnect(): 372 | """Handle client disconnection""" 373 | print(f"Client disconnected: {request.sid}") 374 | 375 | @vg_socketio.on('request_status') 376 | def handle_status_request(): 377 | """Handle status request from client""" 378 | ag_status = py_process.vf_get_all_status() 379 | emit('status_update', {'status': ag_status}) 380 | 381 | @vg_socketio.on('subscribe_logs') 382 | def handle_subscribe_logs(data): 383 | """Subscribe to log updates for a specific script""" 384 | vf_script_id = data.get('script_id') 385 | # Implementation for log streaming would go here 386 | emit('log_subscription', {'subscribed': vf_script_id}) 387 | 388 | def vf_emit_status_update(): 389 | """Emit status update to all connected clients""" 390 | try: 391 | ag_status = py_process.vf_get_all_status() 392 | vg_socketio.emit('status_update', {'status': ag_status}, broadcast=True) 393 | except Exception as vf_error: 394 | # Silently fail if not in request context 395 | pass 396 | 397 | def vf_safe_emit_update(): 398 | """Safely emit update, handling context issues""" 399 | try: 400 | # Try to emit directly 401 | vf_emit_status_update() 402 | except RuntimeError: 403 | # If we're outside request context, schedule it 404 | vg_socketio.start_background_task(vf_emit_status_update) 405 | 406 | def vf_status_update_loop(): 407 | """Background thread to emit periodic status updates""" 408 | global vg_running 409 | 410 | while vg_running: 411 | try: 412 | vf_emit_status_update() 413 | time.sleep(vg_config['websocket_settings']['update_interval']) 414 | except Exception as vf_error: 415 | print(f"Error in status update loop: {vf_error}") 416 | time.sleep(5) 417 | 418 | def vf_start_api(): 419 | """Start the API server""" 420 | global vg_update_thread 421 | 422 | # Start the monitor thread 423 | vf_monitor_thread = threading.Thread(target=py_process.vf_monitor_processes) 424 | vf_monitor_thread.daemon = True 425 | vf_monitor_thread.start() 426 | 427 | # Start status update thread 428 | vg_update_thread = threading.Thread(target=vf_status_update_loop) 429 | vg_update_thread.daemon = True 430 | vg_update_thread.start() 431 | 432 | # Log API startup 433 | py_logger.vf_write_manager_log('API', 'API server started') 434 | 435 | # Run the Flask app with SocketIO 436 | vg_socketio.run( 437 | vg_app, 438 | host=vg_config['api_settings']['host'], 439 | port=vg_config['api_settings']['port'], 440 | debug=vg_config['api_settings']['debug'] 441 | ) 442 | 443 | if __name__ == '__main__': 444 | vf_start_api() -------------------------------------------------------------------------------- /py_manager/py_manager.css: -------------------------------------------------------------------------------- 1 | /* Reset and base styles */ 2 | * { 3 | margin: 0; 4 | padding: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | body { 9 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; 10 | background-color: #f5f5f5; 11 | color: #333; 12 | line-height: 1.6; 13 | } 14 | 15 | /* Container */ 16 | .container { 17 | min-height: 100vh; 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | /* Header */ 23 | .header { 24 | background-color: #2c3e50; 25 | color: white; 26 | padding: 1rem 2rem; 27 | display: flex; 28 | justify-content: space-between; 29 | align-items: center; 30 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 31 | } 32 | 33 | .header h1 { 34 | font-size: 1.5rem; 35 | font-weight: 500; 36 | } 37 | 38 | /* Connection Status */ 39 | .connection-status { 40 | display: flex; 41 | align-items: center; 42 | gap: 0.5rem; 43 | } 44 | 45 | .status-indicator { 46 | width: 10px; 47 | height: 10px; 48 | border-radius: 50%; 49 | background-color: #e74c3c; 50 | } 51 | 52 | .status-indicator.connected { 53 | background-color: #27ae60; 54 | } 55 | 56 | /* Main Content */ 57 | .main-content { 58 | flex: 1; 59 | padding: 2rem; 60 | display: grid; 61 | grid-template-columns: 1fr; 62 | gap: 2rem; 63 | } 64 | 65 | @media (min-width: 1200px) { 66 | .main-content { 67 | grid-template-columns: 2fr 1fr; 68 | } 69 | } 70 | 71 | /* Section Headers */ 72 | .section-header { 73 | display: flex; 74 | justify-content: space-between; 75 | align-items: center; 76 | margin-bottom: 1rem; 77 | } 78 | 79 | .section-header h2 { 80 | font-size: 1.25rem; 81 | color: #2c3e50; 82 | } 83 | 84 | /* Scripts Grid */ 85 | .scripts-grid { 86 | display: grid; 87 | grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); 88 | gap: 1rem; 89 | } 90 | 91 | /* Script Card */ 92 | .script-card { 93 | background: white; 94 | border-radius: 8px; 95 | padding: 1.5rem; 96 | box-shadow: 0 2px 4px rgba(0,0,0,0.1); 97 | transition: transform 0.2s, box-shadow 0.2s; 98 | } 99 | 100 | .script-card:hover { 101 | transform: translateY(-2px); 102 | box-shadow: 0 4px 8px rgba(0,0,0,0.15); 103 | } 104 | 105 | .script-header { 106 | display: flex; 107 | justify-content: space-between; 108 | align-items: start; 109 | margin-bottom: 1rem; 110 | } 111 | 112 | .script-name { 113 | font-size: 1.1rem; 114 | font-weight: 600; 115 | color: #2c3e50; 116 | } 117 | 118 | .script-status { 119 | display: inline-flex; 120 | align-items: center; 121 | gap: 0.3rem; 122 | padding: 0.2rem 0.5rem; 123 | border-radius: 4px; 124 | font-size: 0.85rem; 125 | font-weight: 500; 126 | } 127 | 128 | .script-status.running { 129 | background-color: #d4edda; 130 | color: #155724; 131 | } 132 | 133 | .script-status.stopped { 134 | background-color: #f8d7da; 135 | color: #721c24; 136 | } 137 | 138 | .status-dot { 139 | width: 8px; 140 | height: 8px; 141 | border-radius: 50%; 142 | background-color: currentColor; 143 | } 144 | 145 | /* Script Info */ 146 | .script-info { 147 | margin-bottom: 1rem; 148 | } 149 | 150 | .info-row { 151 | display: flex; 152 | justify-content: space-between; 153 | padding: 0.25rem 0; 154 | font-size: 0.9rem; 155 | } 156 | 157 | .info-label { 158 | color: #666; 159 | } 160 | 161 | .info-value { 162 | font-weight: 500; 163 | } 164 | 165 | /* Script Actions */ 166 | .script-actions { 167 | display: flex; 168 | gap: 0.5rem; 169 | flex-wrap: wrap; 170 | } 171 | 172 | /* Buttons */ 173 | .btn { 174 | padding: 0.5rem 1rem; 175 | border: none; 176 | border-radius: 4px; 177 | font-size: 0.9rem; 178 | cursor: pointer; 179 | transition: background-color 0.2s; 180 | font-weight: 500; 181 | } 182 | 183 | .btn:disabled { 184 | opacity: 0.5; 185 | cursor: not-allowed; 186 | } 187 | 188 | .btn-primary { 189 | background-color: #3498db; 190 | color: white; 191 | } 192 | 193 | .btn-primary:hover:not(:disabled) { 194 | background-color: #2980b9; 195 | } 196 | 197 | .btn-success { 198 | background-color: #27ae60; 199 | color: white; 200 | } 201 | 202 | .btn-success:hover:not(:disabled) { 203 | background-color: #229954; 204 | } 205 | 206 | .btn-danger { 207 | background-color: #e74c3c; 208 | color: white; 209 | } 210 | 211 | .btn-danger:hover:not(:disabled) { 212 | background-color: #c0392b; 213 | } 214 | 215 | .btn-warning { 216 | background-color: #f39c12; 217 | color: white; 218 | } 219 | 220 | .btn-warning:hover:not(:disabled) { 221 | background-color: #d68910; 222 | } 223 | 224 | .btn-secondary { 225 | background-color: #95a5a6; 226 | color: white; 227 | } 228 | 229 | .btn-secondary:hover:not(:disabled) { 230 | background-color: #7f8c8d; 231 | } 232 | 233 | /* Log Controls */ 234 | .log-controls { 235 | display: flex; 236 | gap: 0.5rem; 237 | align-items: center; 238 | } 239 | 240 | #log-selector { 241 | padding: 0.5rem; 242 | border: 1px solid #ddd; 243 | border-radius: 4px; 244 | background: white; 245 | } 246 | 247 | /* Log Viewer */ 248 | .log-viewer { 249 | background: #1e1e1e; 250 | color: #d4d4d4; 251 | border-radius: 8px; 252 | padding: 1rem; 253 | font-family: 'Consolas', 'Monaco', monospace; 254 | font-size: 0.85rem; 255 | height: 400px; 256 | overflow-y: auto; 257 | white-space: pre-wrap; 258 | word-break: break-all; 259 | } 260 | 261 | .log-entry { 262 | padding: 0.25rem 0; 263 | border-bottom: 1px solid #333; 264 | } 265 | 266 | .log-entry:last-child { 267 | border-bottom: none; 268 | } 269 | 270 | .log-empty { 271 | color: #666; 272 | text-align: center; 273 | padding: 2rem; 274 | } 275 | 276 | /* Footer */ 277 | .footer { 278 | background-color: #34495e; 279 | color: #ecf0f1; 280 | padding: 1rem 2rem; 281 | display: flex; 282 | justify-content: space-between; 283 | align-items: center; 284 | font-size: 0.9rem; 285 | } 286 | 287 | /* Modal */ 288 | .modal { 289 | display: none; 290 | position: fixed; 291 | z-index: 1000; 292 | left: 0; 293 | top: 0; 294 | width: 100%; 295 | height: 100%; 296 | background-color: rgba(0,0,0,0.5); 297 | overflow-y: auto; 298 | padding: 2rem 0; 299 | } 300 | 301 | /* Ensure confirmation modal appears above script manager modal */ 302 | #modal { 303 | z-index: 2000; 304 | } 305 | 306 | .modal-content { 307 | background-color: white; 308 | margin: 2rem auto; 309 | padding: 0; 310 | border-radius: 8px; 311 | width: 80%; 312 | max-width: 500px; 313 | max-height: 90vh; 314 | display: flex; 315 | flex-direction: column; 316 | box-shadow: 0 4px 16px rgba(0,0,0,0.2); 317 | } 318 | 319 | .modal-header { 320 | padding: 1rem 1.5rem; 321 | border-bottom: 1px solid #eee; 322 | display: flex; 323 | justify-content: space-between; 324 | align-items: center; 325 | flex-shrink: 0; 326 | } 327 | 328 | .modal-close { 329 | font-size: 1.5rem; 330 | cursor: pointer; 331 | color: #999; 332 | } 333 | 334 | .modal-close:hover { 335 | color: #333; 336 | } 337 | 338 | .modal-body { 339 | padding: 1.5rem; 340 | overflow-y: auto; 341 | flex: 1; 342 | } 343 | 344 | .modal-footer { 345 | padding: 1rem 1.5rem; 346 | border-top: 1px solid #eee; 347 | display: flex; 348 | justify-content: flex-end; 349 | gap: 0.5rem; 350 | flex-shrink: 0; 351 | } 352 | 353 | /* Loading spinner */ 354 | .spinner { 355 | display: inline-block; 356 | width: 16px; 357 | height: 16px; 358 | border: 2px solid #f3f3f3; 359 | border-top: 2px solid #3498db; 360 | border-radius: 50%; 361 | animation: spin 1s linear infinite; 362 | } 363 | 364 | @keyframes spin { 365 | 0% { transform: rotate(0deg); } 366 | 100% { transform: rotate(360deg); } 367 | } 368 | 369 | /* Toast notifications */ 370 | .toast-container { 371 | position: fixed; 372 | top: 20px; 373 | right: 20px; 374 | z-index: 3000; 375 | } 376 | 377 | .toast { 378 | background: white; 379 | border-radius: 4px; 380 | padding: 1rem 1.5rem; 381 | margin-bottom: 0.5rem; 382 | box-shadow: 0 2px 8px rgba(0,0,0,0.15); 383 | display: flex; 384 | align-items: center; 385 | gap: 0.5rem; 386 | min-width: 250px; 387 | animation: slideIn 0.3s ease-out; 388 | } 389 | 390 | .toast.success { 391 | border-left: 4px solid #27ae60; 392 | } 393 | 394 | .toast.error { 395 | border-left: 4px solid #e74c3c; 396 | } 397 | 398 | @keyframes slideIn { 399 | from { 400 | transform: translateX(100%); 401 | opacity: 0; 402 | } 403 | to { 404 | transform: translateX(0); 405 | opacity: 1; 406 | } 407 | } 408 | 409 | /* Bulk Actions */ 410 | .bulk-actions { 411 | display: flex; 412 | gap: 0.5rem; 413 | } 414 | 415 | /* Statistics Bar */ 416 | .stats-bar { 417 | background: white; 418 | padding: 1rem 2rem; 419 | display: flex; 420 | gap: 2rem; 421 | align-items: center; 422 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 423 | flex-wrap: wrap; 424 | } 425 | 426 | .stat-item { 427 | display: flex; 428 | align-items: center; 429 | gap: 0.5rem; 430 | } 431 | 432 | .stat-label { 433 | color: #666; 434 | font-size: 0.9rem; 435 | } 436 | 437 | .stat-value { 438 | font-weight: 600; 439 | font-size: 1.1rem; 440 | } 441 | 442 | .stat-running { 443 | color: #27ae60; 444 | } 445 | 446 | .stat-stopped { 447 | color: #e74c3c; 448 | } 449 | 450 | /* Improve script card hover */ 451 | .script-card { 452 | position: relative; 453 | overflow: hidden; 454 | } 455 | 456 | .script-card::before { 457 | content: ''; 458 | position: absolute; 459 | top: 0; 460 | left: 0; 461 | width: 4px; 462 | height: 100%; 463 | background: transparent; 464 | transition: background 0.3s; 465 | } 466 | 467 | .script-card:has(.script-status.running)::before { 468 | background: #27ae60; 469 | } 470 | 471 | .script-card:has(.script-status.stopped)::before { 472 | background: #e74c3c; 473 | } 474 | 475 | /* Add icons to buttons */ 476 | .btn-success::before { 477 | content: '▶ '; 478 | } 479 | 480 | .btn-danger::before { 481 | content: '■ '; 482 | } 483 | 484 | .btn-warning::before { 485 | content: '↻ '; 486 | } 487 | 488 | .btn-secondary::before { 489 | content: '📄 '; 490 | } 491 | 492 | .btn-primary::before { 493 | content: '↻ '; 494 | } 495 | 496 | /* Pulse animation for running status */ 497 | @keyframes pulse { 498 | 0% { 499 | box-shadow: 0 0 0 0 rgba(39, 174, 96, 0.4); 500 | } 501 | 70% { 502 | box-shadow: 0 0 0 10px rgba(39, 174, 96, 0); 503 | } 504 | 100% { 505 | box-shadow: 0 0 0 0 rgba(39, 174, 96, 0); 506 | } 507 | } 508 | 509 | .script-status.running .status-dot { 510 | animation: pulse 2s infinite; 511 | } 512 | 513 | /* Smooth transitions */ 514 | .script-card, 515 | .btn, 516 | .stat-value { 517 | transition: all 0.3s ease; 518 | } 519 | 520 | /* Loading overlay */ 521 | .loading-overlay { 522 | position: fixed; 523 | top: 0; 524 | left: 0; 525 | right: 0; 526 | bottom: 0; 527 | background: rgba(0, 0, 0, 0.5); 528 | display: flex; 529 | align-items: center; 530 | justify-content: center; 531 | z-index: 9999; 532 | } 533 | 534 | .loading-spinner { 535 | width: 50px; 536 | height: 50px; 537 | border: 3px solid #f3f3f3; 538 | border-top: 3px solid #3498db; 539 | border-radius: 50%; 540 | animation: spin 1s linear infinite; 541 | } 542 | 543 | /* Confirmation dialog improvements */ 544 | .modal-content { 545 | animation: modalFadeIn 0.3s ease; 546 | } 547 | 548 | /* Standard modal should center properly */ 549 | .modal-content { 550 | margin: 2rem auto; 551 | position: relative; 552 | max-height: 90vh; 553 | } 554 | 555 | @keyframes modalFadeIn { 556 | from { 557 | opacity: 0; 558 | transform: translateY(-20px); 559 | } 560 | to { 561 | opacity: 1; 562 | transform: translateY(0); 563 | } 564 | } 565 | 566 | /* Better log viewer */ 567 | .log-viewer { 568 | position: relative; 569 | } 570 | 571 | .log-viewer::before { 572 | content: 'LOGS'; 573 | position: absolute; 574 | top: 0.5rem; 575 | right: 0.5rem; 576 | font-size: 0.7rem; 577 | color: #666; 578 | letter-spacing: 1px; 579 | } 580 | 581 | /* Auto-scroll indicator */ 582 | .auto-scroll-indicator { 583 | position: absolute; 584 | bottom: 1rem; 585 | right: 1rem; 586 | background: #3498db; 587 | color: white; 588 | padding: 0.3rem 0.6rem; 589 | border-radius: 3px; 590 | font-size: 0.8rem; 591 | cursor: pointer; 592 | } 593 | 594 | /* Script Manager Modal Styles */ 595 | .modal-large { 596 | max-width: 800px; 597 | max-height: 85vh; 598 | } 599 | 600 | .modal-medium { 601 | max-width: 600px; 602 | max-height: 80vh; 603 | } 604 | 605 | /* Fix for Script Manager Modal scrolling */ 606 | #script-manager-modal .modal-content { 607 | height: 85vh; 608 | } 609 | 610 | #script-manager-modal .modal-body { 611 | display: flex; 612 | flex-direction: column; 613 | gap: 1rem; 614 | } 615 | 616 | /* Ensure sections don't overflow */ 617 | .script-manager-section { 618 | margin-bottom: 1rem; 619 | padding: 1rem; 620 | background-color: #f5f5f5; 621 | border-radius: 4px; 622 | flex-shrink: 0; 623 | } 624 | 625 | .script-manager-section h4 { 626 | margin-top: 0; 627 | margin-bottom: 1rem; 628 | color: #2c3e50; 629 | } 630 | 631 | .form-group { 632 | margin-bottom: 1rem; 633 | } 634 | 635 | .form-group label { 636 | display: block; 637 | margin-bottom: 0.5rem; 638 | font-weight: 600; 639 | color: #333; 640 | } 641 | 642 | .form-group input[type="text"] { 643 | width: calc(100% - 100px); 644 | padding: 0.5rem; 645 | border: 1px solid #ddd; 646 | border-radius: 4px; 647 | background-color: white; 648 | color: #333; 649 | margin-right: 0.5rem; 650 | } 651 | 652 | .form-group input[type="checkbox"] { 653 | margin-right: 0.5rem; 654 | } 655 | 656 | .form-group small { 657 | font-size: 0.85rem; 658 | color: #666; 659 | display: block; 660 | margin-top: 0.25rem; 661 | } 662 | 663 | .script-list { 664 | max-height: 200px; 665 | overflow-y: auto; 666 | border: 1px solid #ddd; 667 | border-radius: 4px; 668 | padding: 0.5rem; 669 | } 670 | 671 | .script-item { 672 | display: flex; 673 | justify-content: space-between; 674 | align-items: center; 675 | padding: 0.75rem; 676 | margin-bottom: 0.5rem; 677 | background-color: white; 678 | border: 1px solid #ddd; 679 | border-radius: 4px; 680 | } 681 | 682 | .script-item-info { 683 | flex: 1; 684 | } 685 | 686 | .script-item-name { 687 | font-weight: 600; 688 | margin-bottom: 0.25rem; 689 | } 690 | 691 | .script-item-path { 692 | font-size: 0.875rem; 693 | color: #666; 694 | font-family: monospace; 695 | } 696 | 697 | .script-item-actions { 698 | display: flex; 699 | gap: 0.5rem; 700 | } 701 | 702 | .browse-results { 703 | max-height: 250px; 704 | overflow-y: auto; 705 | margin-top: 1rem; 706 | border: 1px solid #ddd; 707 | border-radius: 4px; 708 | padding: 0.5rem; 709 | } 710 | 711 | .browse-item { 712 | padding: 0.5rem; 713 | margin-bottom: 0.25rem; 714 | background-color: white; 715 | border: 1px solid #ddd; 716 | border-radius: 4px; 717 | cursor: pointer; 718 | transition: background-color 0.2s; 719 | } 720 | 721 | .browse-item:hover { 722 | background-color: #f5f5f5; 723 | } 724 | 725 | .directory-browser { 726 | border-top: 1px solid #ddd; 727 | margin-top: 2rem; 728 | padding-top: 2rem; 729 | flex-shrink: 0; 730 | background-color: #f9f9f9; 731 | border-radius: 4px; 732 | padding: 1rem; 733 | } 734 | 735 | .directory-browser h4 { 736 | margin-top: 0; 737 | color: #2c3e50; 738 | } 739 | 740 | .btn-info { 741 | background-color: #17a2b8; 742 | color: white; 743 | } 744 | 745 | .btn-info:hover:not(:disabled) { 746 | background-color: #138496; 747 | } 748 | 749 | /* Small button variant */ 750 | .btn-sm { 751 | padding: 0.25rem 0.5rem; 752 | font-size: 0.85rem; 753 | } 754 | 755 | /* Dark mode styles */ 756 | body.dark-mode { 757 | background-color: #1a1a1a; 758 | color: #e0e0e0; 759 | } 760 | 761 | body.dark-mode .header { 762 | background-color: #1e1e1e; 763 | } 764 | 765 | body.dark-mode .stats-bar, 766 | body.dark-mode .script-card, 767 | body.dark-mode .modal-content { 768 | background-color: #2a2a2a; 769 | color: #e0e0e0; 770 | } 771 | 772 | body.dark-mode .script-manager-section, 773 | body.dark-mode .directory-browser { 774 | background-color: #333; 775 | } 776 | 777 | body.dark-mode .browse-item, 778 | body.dark-mode .script-item { 779 | background-color: #2a2a2a; 780 | border-color: #444; 781 | } 782 | 783 | body.dark-mode .browse-item:hover { 784 | background-color: #3a3a3a; 785 | } 786 | 787 | body.dark-mode input[type="text"], 788 | body.dark-mode select { 789 | background-color: #333; 790 | color: #e0e0e0; 791 | border-color: #444; 792 | } 793 | 794 | /* Directory Browser Styles */ 795 | .directory-browser { 796 | display: flex; 797 | flex-direction: column; 798 | height: 100%; 799 | } 800 | 801 | .browser-controls { 802 | display: flex; 803 | gap: 0.5rem; 804 | margin-bottom: 1rem; 805 | } 806 | 807 | .browser-controls input { 808 | flex: 1; 809 | padding: 0.5rem; 810 | border: 1px solid #ddd; 811 | border-radius: 4px; 812 | font-family: monospace; 813 | } 814 | 815 | .directory-contents { 816 | flex: 1; 817 | border: 1px solid #ddd; 818 | border-radius: 4px; 819 | background: white; 820 | overflow-y: auto; 821 | min-height: 300px; 822 | max-height: 400px; 823 | } 824 | 825 | .browser-item { 826 | display: flex; 827 | align-items: center; 828 | padding: 0.75rem 1rem; 829 | border-bottom: 1px solid #f0f0f0; 830 | cursor: pointer; 831 | transition: background-color 0.2s; 832 | } 833 | 834 | .browser-item:hover { 835 | background-color: #f5f5f5; 836 | } 837 | 838 | .browser-item.selected { 839 | background-color: #e3f2fd; 840 | } 841 | 842 | .browser-item.selected:hover { 843 | background-color: #bbdefb; 844 | } 845 | 846 | .browser-item-icon { 847 | margin-right: 0.75rem; 848 | font-size: 1.2rem; 849 | } 850 | 851 | .browser-item-name { 852 | flex: 1; 853 | font-family: monospace; 854 | font-size: 0.9rem; 855 | } 856 | 857 | .browser-item-type { 858 | color: #666; 859 | font-size: 0.85rem; 860 | } 861 | 862 | .browser-status { 863 | margin-top: 0.5rem; 864 | padding: 0.5rem; 865 | background: #f5f5f5; 866 | border-radius: 4px; 867 | font-size: 0.85rem; 868 | color: #666; 869 | } 870 | 871 | /* Directory browser dark mode */ 872 | body.dark-mode .directory-contents { 873 | background-color: #2a2a2a; 874 | border-color: #444; 875 | } 876 | 877 | body.dark-mode .browser-item { 878 | border-bottom-color: #333; 879 | } 880 | 881 | body.dark-mode .browser-item:hover { 882 | background-color: #333; 883 | } 884 | 885 | body.dark-mode .browser-item.selected { 886 | background-color: #1e3a5f; 887 | } 888 | 889 | body.dark-mode .browser-status { 890 | background-color: #333; 891 | color: #999; 892 | } 893 | 894 | body.dark-mode .form-group small { 895 | color: #999; 896 | } 897 | 898 | /* Responsive modal adjustments */ 899 | @media (max-height: 768px) { 900 | .modal { 901 | padding: 1rem 0; 902 | } 903 | 904 | .modal-content { 905 | margin: 1rem auto; 906 | max-height: 95vh; 907 | } 908 | 909 | #script-manager-modal .modal-content { 910 | height: 90vh; 911 | } 912 | } 913 | 914 | /* Mobile responsiveness */ 915 | @media (max-width: 768px) { 916 | .modal-content { 917 | width: 95%; 918 | margin: 1rem auto; 919 | } 920 | 921 | .modal-large { 922 | max-width: 95%; 923 | } 924 | 925 | .form-group input[type="text"] { 926 | width: 100%; 927 | margin-bottom: 0.5rem; 928 | } 929 | 930 | .form-group button { 931 | width: 100%; 932 | } 933 | } 934 | 935 | /* Ensure empty state messages are visible */ 936 | .browse-item:only-child { 937 | text-align: center; 938 | color: #666; 939 | font-style: italic; 940 | cursor: default; 941 | } 942 | 943 | .browse-item:only-child:hover { 944 | background-color: white; 945 | } 946 | 947 | /* Scrollbar styling for better visibility */ 948 | .script-list::-webkit-scrollbar, 949 | .browse-results::-webkit-scrollbar, 950 | .modal-body::-webkit-scrollbar { 951 | width: 8px; 952 | } 953 | 954 | .script-list::-webkit-scrollbar-track, 955 | .browse-results::-webkit-scrollbar-track, 956 | .modal-body::-webkit-scrollbar-track { 957 | background: #f1f1f1; 958 | border-radius: 4px; 959 | } 960 | 961 | .script-list::-webkit-scrollbar-thumb, 962 | .browse-results::-webkit-scrollbar-thumb, 963 | .modal-body::-webkit-scrollbar-thumb { 964 | background: #888; 965 | border-radius: 4px; 966 | } 967 | 968 | .script-list::-webkit-scrollbar-thumb:hover, 969 | .browse-results::-webkit-scrollbar-thumb:hover, 970 | .modal-body::-webkit-scrollbar-thumb:hover { 971 | background: #555; 972 | } -------------------------------------------------------------------------------- /py_manager/py_interface.js: -------------------------------------------------------------------------------- 1 | // Global variables - auto-detect based on current URL 2 | var vg_base_url = window.location.origin; 3 | var vg_api_base = vg_base_url + '/api'; 4 | var vg_socket = null; 5 | var vg_connected = false; 6 | var vg_scripts_status = {}; 7 | var vg_current_log_script = 'manager'; 8 | var vg_update_interval = null; 9 | var vg_websocket_available = false; 10 | 11 | // Global arrays 12 | var ag_log_buffer = []; 13 | var ag_scripts = []; 14 | 15 | // Initialize on page load 16 | window.addEventListener('DOMContentLoaded', function() { 17 | console.log('Initializing Python Manager Interface...'); 18 | console.log('API Base:', vg_api_base); 19 | 20 | // Check if Socket.IO is available 21 | if (typeof io !== 'undefined') { 22 | vg_websocket_available = true; 23 | vf_init_websocket(); 24 | } else { 25 | console.warn('Socket.IO not available, using polling mode only'); 26 | vf_update_connection_status(false); 27 | } 28 | 29 | vf_fetch_initial_data(); 30 | vf_start_polling(); 31 | }); 32 | 33 | // WebSocket functions 34 | function vf_init_websocket() { 35 | if (!vg_websocket_available) return; 36 | 37 | try { 38 | // Connect to same origin 39 | vg_socket = io(vg_base_url, { 40 | path: '/socket.io/', 41 | transports: ['polling', 'websocket'], 42 | reconnection: true, 43 | reconnectionDelay: 1000, 44 | reconnectionAttempts: 5 45 | }); 46 | 47 | vg_socket.on('connect', function() { 48 | vg_connected = true; 49 | vf_update_connection_status(true); 50 | console.log('Connected to WebSocket'); 51 | vf_show_notification('Connected to server', 'success'); 52 | }); 53 | 54 | vg_socket.on('disconnect', function() { 55 | vg_connected = false; 56 | vf_update_connection_status(false); 57 | console.log('Disconnected from WebSocket'); 58 | vf_show_notification('Disconnected from server', 'error'); 59 | }); 60 | 61 | vg_socket.on('status_update', function(data) { 62 | vf_handle_status_update(data.status); 63 | }); 64 | 65 | vg_socket.on('connect_error', function(error) { 66 | console.warn('WebSocket connection error:', error.message); 67 | }); 68 | } catch (error) { 69 | console.error('Failed to initialize WebSocket:', error); 70 | vg_websocket_available = false; 71 | } 72 | } 73 | 74 | // Connection status 75 | function vf_update_connection_status(vf_connected) { 76 | var vf_indicator = document.getElementById('connection-status'); 77 | var vf_text = document.getElementById('connection-text'); 78 | 79 | if (vf_connected) { 80 | vf_indicator.classList.add('connected'); 81 | vf_text.textContent = 'Connected'; 82 | } else { 83 | vf_indicator.classList.remove('connected'); 84 | vf_text.textContent = 'Disconnected'; 85 | } 86 | } 87 | 88 | // Fetch initial data 89 | function vf_fetch_initial_data() { 90 | // Fetch scripts configuration 91 | return fetch(vg_api_base + '/scripts') 92 | .then(vf_response => vf_response.json()) 93 | .then(vf_data => { 94 | if (vf_data.success) { 95 | ag_scripts = vf_data.data.scripts; 96 | vf_update_log_selector(); 97 | return true; 98 | } 99 | return false; 100 | }) 101 | .catch(vf_error => { 102 | console.error('Error fetching scripts:', vf_error); 103 | return false; 104 | }) 105 | .then(() => { 106 | // Fetch initial status after scripts are loaded 107 | vf_fetch_status(); 108 | }); 109 | } 110 | 111 | // Fetch status from API 112 | function vf_fetch_status() { 113 | fetch(vg_api_base + '/scripts/status') 114 | .then(vf_response => vf_response.json()) 115 | .then(vf_data => { 116 | if (vf_data.success) { 117 | vf_handle_status_update(vf_data.data.status); 118 | } 119 | }) 120 | .catch(vf_error => console.error('Error fetching status:', vf_error)); 121 | } 122 | 123 | // Handle status updates 124 | function vf_handle_status_update(af_status) { 125 | vg_scripts_status = {}; 126 | af_status.forEach(vf_script => { 127 | vg_scripts_status[vf_script.id] = vf_script; 128 | }); 129 | 130 | vf_render_scripts_grid(); 131 | vf_update_last_update_time(); 132 | } 133 | 134 | // Render scripts grid 135 | function vf_render_scripts_grid() { 136 | var vf_grid = document.getElementById('scripts-grid'); 137 | vf_grid.innerHTML = ''; 138 | 139 | for (var vf_script_id in vg_scripts_status) { 140 | var vf_script = vg_scripts_status[vf_script_id]; 141 | var vf_card = vf_create_script_card(vf_script); 142 | vf_grid.appendChild(vf_card); 143 | } 144 | } 145 | 146 | // Create script card 147 | function vf_create_script_card(vf_script) { 148 | var vf_card = document.createElement('div'); 149 | vf_card.className = 'script-card'; 150 | vf_card.id = 'script-' + vf_script.id; 151 | 152 | var vf_is_running = vf_script.status === 'running'; 153 | 154 | vf_card.innerHTML = ` 155 | 156 | ${vf_script.name} 157 | 158 | 159 | ${vf_script.status.toUpperCase()} 160 | 161 | 162 | 163 | 164 | 165 | ID: 166 | ${vf_script.id} 167 | 168 | ${vf_is_running ? ` 169 | 170 | PID: 171 | ${vf_script.pid} 172 | 173 | 174 | CPU: 175 | ${vf_script.cpu_percent.toFixed(1)}% 176 | 177 | 178 | Memory: 179 | ${vf_script.memory_mb.toFixed(1)} MB 180 | 181 | 182 | Uptime: 183 | ${vf_calculate_uptime(vf_script.start_time)} 184 | 185 | ` : ` 186 | 187 | Status: 188 | Not running 189 | 190 | ${vf_script.restart_attempts > 0 ? ` 191 | 192 | Restart attempts: 193 | ${vf_script.restart_attempts} 194 | 195 | ` : ''} 196 | `} 197 | 198 | 199 | 200 | ${vf_is_running ? ` 201 | Stop 202 | Restart 203 | ` : ` 204 | Start 205 | `} 206 | View Logs 207 | 208 | `; 209 | 210 | return vf_card; 211 | } 212 | 213 | // Update the fetch error handling to show user-friendly messages 214 | function vf_start_script(vf_script_id) { 215 | vf_set_button_loading(vf_script_id, true); 216 | 217 | fetch(vg_api_base + '/scripts/' + vf_script_id + '/start', { 218 | method: 'POST' 219 | }) 220 | .then(vf_response => { 221 | if (!vf_response.ok) { 222 | throw new Error('Network response was not ok'); 223 | } 224 | return vf_response.json(); 225 | }) 226 | .then(vf_data => { 227 | if (vf_data.success) { 228 | vf_show_notification('Script started successfully', 'success'); 229 | vf_fetch_status(); 230 | } else { 231 | vf_show_notification('Failed to start script: ' + (vf_data.error || 'Unknown error'), 'error'); 232 | } 233 | }) 234 | .catch(vf_error => { 235 | vf_show_notification('Error starting script', 'error'); 236 | console.error('Error:', vf_error); 237 | }) 238 | .finally(() => { 239 | setTimeout(() => { 240 | vf_set_button_loading(vf_script_id, false); 241 | }, 500); 242 | }); 243 | } 244 | 245 | function vf_stop_script(vf_script_id) { 246 | vf_confirm_action('Stop Script', 'Are you sure you want to stop this script?', function() { 247 | vf_set_button_loading(vf_script_id, true); 248 | 249 | fetch(vg_api_base + '/scripts/' + vf_script_id + '/stop', { 250 | method: 'POST' 251 | }) 252 | .then(vf_response => vf_response.json()) 253 | .then(vf_data => { 254 | if (vf_data.success) { 255 | vf_show_notification('Script stopped successfully', 'success'); 256 | vf_fetch_status(); 257 | } else { 258 | vf_show_notification('Failed to stop script: ' + vf_data.error, 'error'); 259 | } 260 | }) 261 | .catch(vf_error => { 262 | vf_show_notification('Error stopping script', 'error'); 263 | console.error('Error:', vf_error); 264 | }) 265 | .finally(() => { 266 | vf_set_button_loading(vf_script_id, false); 267 | }); 268 | }); 269 | } 270 | 271 | function vf_restart_script(vf_script_id) { 272 | vf_confirm_action('Restart Script', 'Are you sure you want to restart this script?', function() { 273 | vf_set_button_loading(vf_script_id, true); 274 | 275 | fetch(vg_api_base + '/scripts/' + vf_script_id + '/restart', { 276 | method: 'POST' 277 | }) 278 | .then(vf_response => vf_response.json()) 279 | .then(vf_data => { 280 | if (vf_data.success) { 281 | vf_show_notification('Script restarted successfully', 'success'); 282 | vf_fetch_status(); 283 | } else { 284 | vf_show_notification('Failed to restart script: ' + vf_data.error, 'error'); 285 | } 286 | }) 287 | .catch(vf_error => { 288 | vf_show_notification('Error restarting script', 'error'); 289 | console.error('Error:', vf_error); 290 | }) 291 | .finally(() => { 292 | vf_set_button_loading(vf_script_id, false); 293 | }); 294 | }); 295 | } 296 | 297 | // Log functions 298 | function vf_view_script_logs(vf_script_id) { 299 | vg_current_log_script = vf_script_id; 300 | document.getElementById('log-selector').value = vf_script_id; 301 | vf_fetch_logs(); 302 | } 303 | 304 | function vf_change_log_view() { 305 | vg_current_log_script = document.getElementById('log-selector').value; 306 | vf_fetch_logs(); 307 | } 308 | 309 | function vf_fetch_logs() { 310 | var vf_endpoint = vg_current_log_script === 'manager' 311 | ? '/manager/logs' 312 | : '/scripts/' + vg_current_log_script + '/logs'; 313 | 314 | fetch(vg_api_base + vf_endpoint + '?lines=100') 315 | .then(vf_response => vf_response.json()) 316 | .then(vf_data => { 317 | if (vf_data.success) { 318 | vf_display_logs(vf_data.data.logs); 319 | } 320 | }) 321 | .catch(vf_error => console.error('Error fetching logs:', vf_error)); 322 | } 323 | 324 | function vf_display_logs(af_logs) { 325 | var vf_viewer = document.getElementById('log-viewer'); 326 | 327 | if (af_logs.length === 0) { 328 | vf_viewer.innerHTML = 'No logs available'; 329 | return; 330 | } 331 | 332 | vf_viewer.innerHTML = af_logs.map(vf_log => { 333 | return '' + vf_escape_html(vf_log) + ''; 334 | }).join(''); 335 | 336 | // Scroll to bottom 337 | vf_viewer.scrollTop = vf_viewer.scrollHeight; 338 | } 339 | 340 | function vf_refresh_logs() { 341 | vf_fetch_logs(); 342 | vf_show_notification('Logs refreshed', 'success'); 343 | } 344 | 345 | function vf_clear_log_view() { 346 | document.getElementById('log-viewer').innerHTML = 'Log view cleared'; 347 | } 348 | 349 | // Configuration 350 | function vf_reload_config() { 351 | fetch(vg_api_base + '/config/reload', { 352 | method: 'POST' 353 | }) 354 | .then(vf_response => vf_response.json()) 355 | .then(vf_data => { 356 | if (vf_data.success) { 357 | vf_show_notification('Configuration reloaded', 'success'); 358 | vf_fetch_initial_data(); 359 | } else { 360 | vf_show_notification('Failed to reload configuration', 'error'); 361 | } 362 | }) 363 | .catch(vf_error => { 364 | vf_show_notification('Error reloading configuration', 'error'); 365 | console.error('Error:', vf_error); 366 | }); 367 | } 368 | 369 | // Utility functions 370 | function vf_calculate_uptime(vf_start_time) { 371 | var vf_start = new Date(vf_start_time); 372 | var vf_now = new Date(); 373 | var vf_diff = vf_now - vf_start; 374 | 375 | var vf_seconds = Math.floor(vf_diff / 1000); 376 | var vf_minutes = Math.floor(vf_seconds / 60); 377 | var vf_hours = Math.floor(vf_minutes / 60); 378 | var vf_days = Math.floor(vf_hours / 24); 379 | 380 | if (vf_days > 0) { 381 | return vf_days + 'd ' + (vf_hours % 24) + 'h'; 382 | } else if (vf_hours > 0) { 383 | return vf_hours + 'h ' + (vf_minutes % 60) + 'm'; 384 | } else if (vf_minutes > 0) { 385 | return vf_minutes + 'm ' + (vf_seconds % 60) + 's'; 386 | } else { 387 | return vf_seconds + 's'; 388 | } 389 | } 390 | 391 | function vf_update_last_update_time() { 392 | var vf_element = document.getElementById('last-update'); 393 | var vf_time = new Date().toLocaleTimeString(); 394 | vf_element.textContent = 'Last update: ' + vf_time; 395 | } 396 | 397 | function vf_update_log_selector() { 398 | var vf_selector = document.getElementById('log-selector'); 399 | 400 | // Keep manager option 401 | var vf_html = 'Manager Logs'; 402 | 403 | // Add script options 404 | ag_scripts.forEach(vf_script => { 405 | vf_html += `${vf_script.name} Logs`; 406 | }); 407 | 408 | vf_selector.innerHTML = vf_html; 409 | } 410 | 411 | function vf_escape_html(vf_text) { 412 | var vf_div = document.createElement('div'); 413 | vf_div.textContent = vf_text; 414 | return vf_div.innerHTML; 415 | } 416 | 417 | function vf_set_button_loading(vf_script_id, vf_loading) { 418 | var vf_card = document.getElementById('script-' + vf_script_id); 419 | if (!vf_card) return; 420 | 421 | var af_buttons = vf_card.querySelectorAll('.btn'); 422 | af_buttons.forEach(vf_button => { 423 | vf_button.disabled = vf_loading; 424 | }); 425 | } 426 | 427 | // Modal functions 428 | function vf_confirm_action(vf_title, vf_message, vf_callback) { 429 | var vf_modal = document.getElementById('modal'); 430 | var vf_title_elem = document.getElementById('modal-title'); 431 | var vf_message_elem = document.getElementById('modal-message'); 432 | var vf_confirm_btn = document.getElementById('modal-confirm'); 433 | 434 | vf_title_elem.textContent = vf_title; 435 | vf_message_elem.textContent = vf_message; 436 | 437 | // Remove old listener 438 | var vf_new_btn = vf_confirm_btn.cloneNode(true); 439 | vf_confirm_btn.parentNode.replaceChild(vf_new_btn, vf_confirm_btn); 440 | 441 | vf_new_btn.addEventListener('click', function() { 442 | vf_callback(); 443 | vf_close_modal(); 444 | }); 445 | 446 | vf_modal.style.display = 'block'; 447 | } 448 | 449 | function vf_close_modal() { 450 | document.getElementById('modal').style.display = 'none'; 451 | } 452 | 453 | // Enhanced notification function 454 | function vf_show_notification(vf_message, vf_type) { 455 | var vf_container = document.getElementById('toast-container'); 456 | var vf_toast = document.createElement('div'); 457 | vf_toast.className = 'toast ' + vf_type; 458 | 459 | // Choose icon based on type 460 | var vf_icon = ''; 461 | switch(vf_type) { 462 | case 'success': 463 | vf_icon = '✓'; 464 | break; 465 | case 'error': 466 | vf_icon = '✗'; 467 | break; 468 | case 'info': 469 | default: 470 | vf_icon = 'ℹ'; 471 | break; 472 | } 473 | 474 | vf_toast.innerHTML = ` 475 | ${vf_icon} 476 | ${vf_message} 477 | `; 478 | 479 | vf_container.appendChild(vf_toast); 480 | 481 | // Auto remove after 3 seconds 482 | setTimeout(function() { 483 | vf_toast.classList.add('removing'); 484 | setTimeout(function() { 485 | vf_toast.remove(); 486 | }, 300); 487 | }, 3000); 488 | } 489 | 490 | // Update polling to be more responsive when WebSocket is not available 491 | function vf_start_polling() { 492 | // Poll every 3 seconds if WebSocket is not available or disconnected 493 | vg_update_interval = setInterval(function() { 494 | if (!vg_connected) { 495 | vf_fetch_status(); 496 | } 497 | }, 3000); 498 | } 499 | 500 | // Close modal on outside click 501 | window.onclick = function(event) { 502 | var vf_modal = document.getElementById('modal'); 503 | var vf_script_modal = document.getElementById('script-manager-modal'); 504 | 505 | if (event.target == vf_modal) { 506 | vf_close_modal(); 507 | } else if (event.target == vf_script_modal) { 508 | vf_close_script_manager(); 509 | } 510 | } 511 | 512 | // Keyboard shortcuts 513 | document.addEventListener('keydown', function(e) { 514 | // Alt+R: Reload configuration 515 | if (e.altKey && e.key === 'r') { 516 | e.preventDefault(); 517 | vf_reload_config(); 518 | } 519 | 520 | // Alt+S: Refresh status 521 | if (e.altKey && e.key === 's') { 522 | e.preventDefault(); 523 | vf_fetch_status(); 524 | vf_show_notification('Status refreshed', 'info'); 525 | } 526 | 527 | // Alt+L: Focus log selector 528 | if (e.altKey && e.key === 'l') { 529 | e.preventDefault(); 530 | document.getElementById('log-selector').focus(); 531 | } 532 | }); 533 | 534 | // Add tooltips 535 | window.addEventListener('load', function() { 536 | // Add title attributes for keyboard shortcuts 537 | var vf_reload_btn = document.querySelector('button[onclick="vf_reload_config()"]'); 538 | if (vf_reload_btn) { 539 | vf_reload_btn.title = 'Reload Configuration (Alt+R)'; 540 | } 541 | }); 542 | 543 | // Start all scripts 544 | function vf_start_all_scripts() { 545 | vf_confirm_action( 546 | 'Start All Scripts', 547 | 'Are you sure you want to start all enabled scripts?', 548 | function() { 549 | vf_show_loading(true); 550 | var ag_promises = []; 551 | 552 | for (var vf_script_id in vg_scripts_status) { 553 | var vf_script = vg_scripts_status[vf_script_id]; 554 | if (vf_script.status === 'stopped' && vf_script.enabled) { 555 | ag_promises.push( 556 | fetch(vg_api_base + '/scripts/' + vf_script_id + '/start', { 557 | method: 'POST' 558 | }) 559 | ); 560 | } 561 | } 562 | 563 | Promise.all(ag_promises) 564 | .then(function() { 565 | vf_show_notification('All scripts started', 'success'); 566 | vf_fetch_status(); 567 | }) 568 | .catch(function(error) { 569 | vf_show_notification('Error starting some scripts', 'error'); 570 | console.error(error); 571 | }) 572 | .finally(function() { 573 | vf_show_loading(false); 574 | }); 575 | } 576 | ); 577 | } 578 | 579 | // Stop all scripts 580 | function vf_stop_all_scripts() { 581 | vf_confirm_action( 582 | 'Stop All Scripts', 583 | 'Are you sure you want to stop all running scripts?', 584 | function() { 585 | vf_show_loading(true); 586 | var ag_promises = []; 587 | 588 | for (var vf_script_id in vg_scripts_status) { 589 | var vf_script = vg_scripts_status[vf_script_id]; 590 | if (vf_script.status === 'running') { 591 | ag_promises.push( 592 | fetch(vg_api_base + '/scripts/' + vf_script_id + '/stop', { 593 | method: 'POST' 594 | }) 595 | ); 596 | } 597 | } 598 | 599 | Promise.all(ag_promises) 600 | .then(function() { 601 | vf_show_notification('All scripts stopped', 'success'); 602 | vf_fetch_status(); 603 | }) 604 | .catch(function(error) { 605 | vf_show_notification('Error stopping some scripts', 'error'); 606 | console.error(error); 607 | }) 608 | .finally(function() { 609 | vf_show_loading(false); 610 | }); 611 | } 612 | ); 613 | } 614 | 615 | // Update statistics 616 | function vf_update_statistics() { 617 | var vf_total = 0; 618 | var vf_running = 0; 619 | var vf_stopped = 0; 620 | var vf_total_cpu = 0; 621 | var vf_total_memory = 0; 622 | 623 | for (var vf_script_id in vg_scripts_status) { 624 | var vf_script = vg_scripts_status[vf_script_id]; 625 | vf_total++; 626 | 627 | if (vf_script.status === 'running') { 628 | vf_running++; 629 | vf_total_cpu += vf_script.cpu_percent || 0; 630 | vf_total_memory += vf_script.memory_mb || 0; 631 | } else { 632 | vf_stopped++; 633 | } 634 | } 635 | 636 | document.getElementById('stat-total').textContent = vf_total; 637 | document.getElementById('stat-running').textContent = vf_running; 638 | document.getElementById('stat-stopped').textContent = vf_stopped; 639 | document.getElementById('stat-cpu').textContent = vf_total_cpu.toFixed(1) + '%'; 640 | document.getElementById('stat-memory').textContent = vf_total_memory.toFixed(1) + ' MB'; 641 | } 642 | 643 | // Show loading overlay 644 | function vf_show_loading(vf_show) { 645 | var vf_overlay = document.getElementById('loading-overlay'); 646 | if (!vf_overlay) { 647 | vf_overlay = document.createElement('div'); 648 | vf_overlay.id = 'loading-overlay'; 649 | vf_overlay.className = 'loading-overlay'; 650 | vf_overlay.innerHTML = ''; 651 | document.body.appendChild(vf_overlay); 652 | } 653 | vf_overlay.style.display = vf_show ? 'flex' : 'none'; 654 | } 655 | 656 | // Auto-refresh logs if viewing a running script 657 | var vg_auto_refresh_logs = null; 658 | function vf_start_auto_refresh_logs() { 659 | vf_stop_auto_refresh_logs(); 660 | 661 | if (vg_current_log_script !== 'manager') { 662 | var vf_script = vg_scripts_status[vg_current_log_script]; 663 | if (vf_script && vf_script.status === 'running') { 664 | vg_auto_refresh_logs = setInterval(function() { 665 | vf_fetch_logs(); 666 | }, 2000); 667 | 668 | // Show indicator 669 | var vf_indicator = document.createElement('div'); 670 | vf_indicator.className = 'auto-scroll-indicator'; 671 | vf_indicator.textContent = 'Auto-scrolling'; 672 | vf_indicator.onclick = vf_stop_auto_refresh_logs; 673 | document.querySelector('.log-viewer').appendChild(vf_indicator); 674 | } 675 | } 676 | } 677 | 678 | function vf_stop_auto_refresh_logs() { 679 | if (vg_auto_refresh_logs) { 680 | clearInterval(vg_auto_refresh_logs); 681 | vg_auto_refresh_logs = null; 682 | 683 | var vf_indicator = document.querySelector('.auto-scroll-indicator'); 684 | if (vf_indicator) { 685 | vf_indicator.remove(); 686 | } 687 | } 688 | } 689 | 690 | // Update the handle status update function 691 | var original_vf_handle_status_update = vf_handle_status_update; 692 | vf_handle_status_update = function(af_status) { 693 | original_vf_handle_status_update(af_status); 694 | vf_update_statistics(); 695 | }; 696 | 697 | // Update log viewer to start auto-refresh 698 | var original_vf_change_log_view = vf_change_log_view; 699 | vf_change_log_view = function() { 700 | original_vf_change_log_view(); 701 | vf_start_auto_refresh_logs(); 702 | }; 703 | 704 | // Add smooth scroll to log viewer 705 | var original_vf_display_logs = vf_display_logs; 706 | vf_display_logs = function(af_logs) { 707 | original_vf_display_logs(af_logs); 708 | 709 | // Smooth scroll to bottom if auto-refreshing 710 | if (vg_auto_refresh_logs) { 711 | var vf_viewer = document.getElementById('log-viewer'); 712 | vf_viewer.scrollTo({ 713 | top: vf_viewer.scrollHeight, 714 | behavior: 'smooth' 715 | }); 716 | } 717 | }; 718 | 719 | // Add page visibility handling 720 | document.addEventListener('visibilitychange', function() { 721 | if (document.hidden) { 722 | // Stop auto-refresh when page is hidden 723 | vf_stop_auto_refresh_logs(); 724 | } else { 725 | // Resume when page is visible 726 | vf_fetch_status(); 727 | if (vg_current_log_script !== 'manager') { 728 | vf_start_auto_refresh_logs(); 729 | } 730 | } 731 | }); 732 | 733 | // Add double-click to toggle script 734 | document.addEventListener('dblclick', function(e) { 735 | var vf_card = e.target.closest('.script-card'); 736 | if (vf_card) { 737 | var vf_script_id = vf_card.id.replace('script-', ''); 738 | var vf_script = vg_scripts_status[vf_script_id]; 739 | 740 | if (vf_script) { 741 | if (vf_script.status === 'running') { 742 | vf_stop_script(vf_script_id); 743 | } else { 744 | vf_start_script(vf_script_id); 745 | } 746 | } 747 | } 748 | }); 749 | 750 | // Check if first time user 751 | if (!localStorage.getItem('welcomeShown')) { 752 | window.addEventListener('load', function() { 753 | setTimeout(function() { 754 | vf_show_notification('Welcome to Python Manager! Double-click any script card to toggle it.', 'info'); 755 | localStorage.setItem('welcomeShown', 'true'); 756 | }, 1000); 757 | }); 758 | } 759 | 760 | // Script Manager Functions 761 | function vf_show_script_manager() { 762 | // Populate current scripts 763 | vf_populate_script_list(); 764 | 765 | // Show modal 766 | document.getElementById('script-manager-modal').style.display = 'block'; 767 | } 768 | 769 | function vf_close_script_manager() { 770 | document.getElementById('script-manager-modal').style.display = 'none'; 771 | } 772 | 773 | function vf_populate_script_list() { 774 | var vf_list = document.getElementById('script-list'); 775 | vf_list.innerHTML = ''; 776 | 777 | ag_scripts.forEach(function(vf_script) { 778 | var vf_item = document.createElement('div'); 779 | vf_item.className = 'script-item'; 780 | vf_item.innerHTML = ` 781 | 782 | ${vf_script.name} 783 | ${vf_script.path} 784 | 785 | 786 | 787 | Remove 788 | 789 | 790 | `; 791 | vf_list.appendChild(vf_item); 792 | }); 793 | } 794 | 795 | function vf_add_script() { 796 | var vf_path = document.getElementById('new-script-path').value.trim(); 797 | var vf_name = document.getElementById('new-script-name').value.trim(); 798 | var vf_args = document.getElementById('new-script-args').value.trim(); 799 | var vf_auto_restart = document.getElementById('new-script-auto-restart').checked; 800 | 801 | if (!vf_path) { 802 | vf_show_notification('Please enter a script path', 'error'); 803 | return; 804 | } 805 | 806 | // Check if it's a full path (contains directory separators) 807 | if (!vf_path.includes('\\') && !vf_path.includes('/')) { 808 | vf_show_notification('Please enter the full path including directory (e.g., D:\\path\\to\\' + vf_path + ')', 'error'); 809 | document.getElementById('new-script-path').focus(); 810 | return; 811 | } 812 | 813 | // Check if it ends with .py 814 | if (!vf_path.endsWith('.py')) { 815 | vf_show_notification('Script path must end with .py', 'error'); 816 | return; 817 | } 818 | 819 | var vf_data = { 820 | path: vf_path, 821 | name: vf_name || null, 822 | args: vf_args ? vf_args.split(' ') : [], 823 | auto_restart: vf_auto_restart 824 | }; 825 | 826 | fetch(vg_api_base + '/scripts/add', { 827 | method: 'POST', 828 | headers: { 829 | 'Content-Type': 'application/json' 830 | }, 831 | body: JSON.stringify(vf_data) 832 | }) 833 | .then(vf_response => vf_response.json()) 834 | .then(vf_data => { 835 | if (vf_data.success) { 836 | vf_show_notification('Script added successfully', 'success'); 837 | // Clear form 838 | document.getElementById('new-script-path').value = ''; 839 | document.getElementById('new-script-name').value = ''; 840 | document.getElementById('new-script-args').value = ''; 841 | document.getElementById('new-script-auto-restart').checked = true; 842 | // Refresh data 843 | vf_fetch_initial_data().then(function() { 844 | // After data is refreshed, update the script list 845 | vf_populate_script_list(); 846 | }); 847 | } else { 848 | vf_show_notification('Failed to add script: ' + vf_data.error, 'error'); 849 | } 850 | }) 851 | .catch(vf_error => { 852 | vf_show_notification('Error adding script', 'error'); 853 | console.error('Error:', vf_error); 854 | }); 855 | } 856 | 857 | function vf_remove_script(vf_script_id) { 858 | vf_confirm_action( 859 | 'Remove Script', 860 | 'Are you sure you want to remove this script? This action cannot be undone.', 861 | function() { 862 | fetch(vg_api_base + '/scripts/' + vf_script_id + '/remove', { 863 | method: 'DELETE' 864 | }) 865 | .then(vf_response => vf_response.json()) 866 | .then(vf_data => { 867 | if (vf_data.success) { 868 | vf_show_notification('Script removed successfully', 'success'); 869 | vf_fetch_initial_data().then(function() { 870 | // After data is refreshed, update the script list 871 | vf_populate_script_list(); 872 | }); 873 | } else { 874 | vf_show_notification('Failed to remove script: ' + vf_data.error, 'error'); 875 | } 876 | }) 877 | .catch(vf_error => { 878 | vf_show_notification('Error removing script', 'error'); 879 | console.error('Error:', vf_error); 880 | }); 881 | } 882 | ); 883 | } 884 | 885 | // Handle native file browser selection 886 | function vf_handle_file_select(event) { 887 | var vf_file = event.target.files[0]; 888 | if (!vf_file) return; 889 | 890 | // Check if it's a Python file 891 | if (!vf_file.name.endsWith('.py')) { 892 | vf_show_notification('Please select a Python (.py) file', 'error'); 893 | event.target.value = ''; 894 | return; 895 | } 896 | 897 | // Get filename 898 | var vf_filename = vf_file.name; 899 | 900 | // Auto-fill the display name if empty 901 | if (!document.getElementById('new-script-name').value) { 902 | var vf_name = vf_filename.replace('.py', '').replace(/_/g, ' '); 903 | vf_name = vf_name.charAt(0).toUpperCase() + vf_name.slice(1); 904 | document.getElementById('new-script-name').value = vf_name; 905 | } 906 | 907 | // Automatically add the common scripts directory path 908 | document.getElementById('new-script-path').value = 'D:\\xampp\\htdocs\\mpy0\\scripts\\' + vf_filename; 909 | 910 | // Show success notification 911 | vf_show_notification('File path auto-completed: D:\\xampp\\htdocs\\mpy0\\scripts\\' + vf_filename, 'success'); 912 | 913 | // Reset file input for future use 914 | event.target.value = ''; 915 | } 916 | 917 | 918 | 919 | function vf_escape_quotes(vf_str) { 920 | return vf_str.replace(/'/g, "\\'").replace(/"/g, '\\"'); 921 | } 922 | 923 | // Dark mode toggle 924 | function vf_toggle_dark_mode() { 925 | document.body.classList.toggle('dark-mode'); 926 | localStorage.setItem('darkMode', document.body.classList.contains('dark-mode')); 927 | } 928 | 929 | // Load dark mode preference 930 | if (localStorage.getItem('darkMode') === 'true') { 931 | document.body.classList.add('dark-mode'); 932 | } -------------------------------------------------------------------------------- /py_manager/socket.io.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Socket.IO v4.8.1 3 | * (c) 2014-2024 Guillermo Rauch 4 | * Released under the MIT License. 5 | */ 6 | !function(t,n){"object"==typeof exports&&"undefined"!=typeof module?module.exports=n():"function"==typeof define&&define.amd?define(n):(t="undefined"!=typeof globalThis?globalThis:t||self).io=n()}(this,(function(){"use strict";function t(t,n){(null==n||n>t.length)&&(n=t.length);for(var i=0,r=Array(n);i=n.length?{done:!0}:{done:!1,value:n[e++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,u=!0,h=!1;return{s:function(){r=r.call(n)},n:function(){var t=r.next();return u=t.done,t},e:function(t){h=!0,s=t},f:function(){try{u||null==r.return||r.return()}finally{if(h)throw s}}}}function e(){return e=Object.assign?Object.assign.bind():function(t){for(var n=1;n1?{type:l[i],data:t.substring(1)}:{type:l[i]}:d},N=function(t,n){if(B){var i=function(t){var n,i,r,e,o,s=.75*t.length,u=t.length,h=0;"="===t[t.length-1]&&(s--,"="===t[t.length-2]&&s--);var f=new ArrayBuffer(s),c=new Uint8Array(f);for(n=0;n>4,c[h++]=(15&r)<<4|e>>2,c[h++]=(3&e)<<6|63&o;return f}(t);return C(i,n)}return{base64:!0,data:t}},C=function(t,n){return"blob"===n?t instanceof Blob?t:new Blob([t]):t instanceof ArrayBuffer?t:t.buffer},T=String.fromCharCode(30);function U(){return new TransformStream({transform:function(t,n){!function(t,n){y&&t.data instanceof Blob?t.data.arrayBuffer().then(k).then(n):b&&(t.data instanceof ArrayBuffer||w(t.data))?n(k(t.data)):g(t,!1,(function(t){p||(p=new TextEncoder),n(p.encode(t))}))}(t,(function(i){var r,e=i.length;if(e<126)r=new Uint8Array(1),new DataView(r.buffer).setUint8(0,e);else if(e<65536){r=new Uint8Array(3);var o=new DataView(r.buffer);o.setUint8(0,126),o.setUint16(1,e)}else{r=new Uint8Array(9);var s=new DataView(r.buffer);s.setUint8(0,127),s.setBigUint64(1,BigInt(e))}t.data&&"string"!=typeof t.data&&(r[0]|=128),n.enqueue(r),n.enqueue(i)}))}})}function M(t){return t.reduce((function(t,n){return t+n.length}),0)}function x(t,n){if(t[0].length===n)return t.shift();for(var i=new Uint8Array(n),r=0,e=0;e1?n-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:{};return t+"://"+this.i()+this.o()+this.opts.path+this.u(n)},i.i=function(){var t=this.opts.hostname;return-1===t.indexOf(":")?t:"["+t+"]"},i.o=function(){return this.opts.port&&(this.opts.secure&&Number(443!==this.opts.port)||!this.opts.secure&&80!==Number(this.opts.port))?":"+this.opts.port:""},i.u=function(t){var n=function(t){var n="";for(var i in t)t.hasOwnProperty(i)&&(n.length&&(n+="&"),n+=encodeURIComponent(i)+"="+encodeURIComponent(t[i]));return n}(t);return n.length?"?"+n:""},n}(I),X=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).h=!1,n}s(n,t);var r=n.prototype;return r.doOpen=function(){this.v()},r.pause=function(t){var n=this;this.readyState="pausing";var i=function(){n.readyState="paused",t()};if(this.h||!this.writable){var r=0;this.h&&(r++,this.once("pollComplete",(function(){--r||i()}))),this.writable||(r++,this.once("drain",(function(){--r||i()})))}else i()},r.v=function(){this.h=!0,this.doPoll(),this.emitReserved("poll")},r.onData=function(t){var n=this;(function(t,n){for(var i=t.split(T),r=[],e=0;e0&&void 0!==arguments[0]?arguments[0]:{};return e(t,{xd:this.xd},this.opts),new Y(tt,this.uri(),t)},n}(K);function tt(t){var n=t.xdomain;try{if("undefined"!=typeof XMLHttpRequest&&(!n||z))return new XMLHttpRequest}catch(t){}if(!n)try{return new(L[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}var nt="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),it=function(t){function n(){return t.apply(this,arguments)||this}s(n,t);var r=n.prototype;return r.doOpen=function(){var t=this.uri(),n=this.opts.protocols,i=nt?{}:_(this.opts,"agent","perMessageDeflate","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","localAddress","protocolVersion","origin","maxPayload","family","checkServerIdentity");this.opts.extraHeaders&&(i.headers=this.opts.extraHeaders);try{this.ws=this.createSocket(t,n,i)}catch(t){return this.emitReserved("error",t)}this.ws.binaryType=this.socket.binaryType,this.addEventListeners()},r.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.opts.autoUnref&&t.ws.C.unref(),t.onOpen()},this.ws.onclose=function(n){return t.onClose({description:"websocket connection closed",context:n})},this.ws.onmessage=function(n){return t.onData(n.data)},this.ws.onerror=function(n){return t.onError("websocket error",n)}},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;g(i,n.supportsBinary,(function(t){try{n.doWrite(i,t)}catch(t){}e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;rMath.pow(2,21)-1){u.enqueue(d);break}e=v*Math.pow(2,32)+a.getUint32(4),r=3}else{if(M(i)t){u.enqueue(d);break}}}})}(Number.MAX_SAFE_INTEGER,t.socket.binaryType),r=n.readable.pipeThrough(i).getReader(),e=U();e.readable.pipeTo(n.writable),t.U=e.writable.getWriter();!function n(){r.read().then((function(i){var r=i.done,e=i.value;r||(t.onPacket(e),n())})).catch((function(t){}))}();var o={type:"open"};t.query.sid&&(o.data='{"sid":"'.concat(t.query.sid,'"}')),t.U.write(o).then((function(){return t.onOpen()}))}))}))},r.write=function(t){var n=this;this.writable=!1;for(var i=function(){var i=t[r],e=r===t.length-1;n.U.write(i).then((function(){e&&R((function(){n.writable=!0,n.emitReserved("drain")}),n.setTimeoutFn)}))},r=0;r8e3)throw"URI too long";var n=t,i=t.indexOf("["),r=t.indexOf("]");-1!=i&&-1!=r&&(t=t.substring(0,i)+t.substring(i,r).replace(/:/g,";")+t.substring(r,t.length));for(var e,o,s=ut.exec(t||""),u={},h=14;h--;)u[ht[h]]=s[h]||"";return-1!=i&&-1!=r&&(u.source=n,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,n){var i=/\/{2,9}/g,r=n.replace(i,"/").split("/");"/"!=n.slice(0,1)&&0!==n.length||r.splice(0,1);"/"==n.slice(-1)&&r.splice(r.length-1,1);return r}(0,u.path),u.queryKey=(e=u.query,o={},e.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,n,i){n&&(o[n]=i)})),o),u}var ct="function"==typeof addEventListener&&"function"==typeof removeEventListener,at=[];ct&&addEventListener("offline",(function(){at.forEach((function(t){return t()}))}),!1);var vt=function(t){function n(n,i){var r;if((r=t.call(this)||this).binaryType="arraybuffer",r.writeBuffer=[],r.M=0,r.I=-1,r.R=-1,r.L=-1,r._=1/0,n&&"object"===c(n)&&(i=n,n=null),n){var o=ft(n);i.hostname=o.host,i.secure="https"===o.protocol||"wss"===o.protocol,i.port=o.port,o.query&&(i.query=o.query)}else i.host&&(i.hostname=ft(i.host).host);return $(r,i),r.secure=null!=i.secure?i.secure:"undefined"!=typeof location&&"https:"===location.protocol,i.hostname&&!i.port&&(i.port=r.secure?"443":"80"),r.hostname=i.hostname||("undefined"!=typeof location?location.hostname:"localhost"),r.port=i.port||("undefined"!=typeof location&&location.port?location.port:r.secure?"443":"80"),r.transports=[],r.D={},i.transports.forEach((function(t){var n=t.prototype.name;r.transports.push(n),r.D[n]=t})),r.opts=e({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,timestampParam:"t",rememberUpgrade:!1,addTrailingSlash:!0,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{},closeOnBeforeunload:!1},i),r.opts.path=r.opts.path.replace(/\/$/,"")+(r.opts.addTrailingSlash?"/":""),"string"==typeof r.opts.query&&(r.opts.query=function(t){for(var n={},i=t.split("&"),r=0,e=i.length;r1))return this.writeBuffer;for(var t,n=1,i=0;i=57344?i+=3:(r++,i+=4);return i}(t):Math.ceil(1.33*(t.byteLength||t.size))),i>0&&n>this.L)return this.writeBuffer.slice(0,i);n+=2}return this.writeBuffer},i.W=function(){var t=this;if(!this._)return!0;var n=Date.now()>this._;return n&&(this._=0,R((function(){t.F("ping timeout")}),this.setTimeoutFn)),n},i.write=function(t,n,i){return this.J("message",t,n,i),this},i.send=function(t,n,i){return this.J("message",t,n,i),this},i.J=function(t,n,i,r){if("function"==typeof n&&(r=n,n=void 0),"function"==typeof i&&(r=i,i=null),"closing"!==this.readyState&&"closed"!==this.readyState){(i=i||{}).compress=!1!==i.compress;var e={type:t,data:n,options:i};this.emitReserved("packetCreate",e),this.writeBuffer.push(e),r&&this.once("flush",r),this.flush()}},i.close=function(){var t=this,n=function(){t.F("forced close"),t.transport.close()},i=function i(){t.off("upgrade",i),t.off("upgradeError",i),n()},r=function(){t.once("upgrade",i),t.once("upgradeError",i)};return"opening"!==this.readyState&&"open"!==this.readyState||(this.readyState="closing",this.writeBuffer.length?this.once("drain",(function(){t.upgrading?r():n()})):this.upgrading?r():n()),this},i.B=function(t){if(n.priorWebsocketSuccess=!1,this.opts.tryAllTransports&&this.transports.length>1&&"opening"===this.readyState)return this.transports.shift(),this.q();this.emitReserved("error",t),this.F("transport error",t)},i.F=function(t,n){if("opening"===this.readyState||"open"===this.readyState||"closing"===this.readyState){if(this.clearTimeoutFn(this.Y),this.transport.removeAllListeners("close"),this.transport.close(),this.transport.removeAllListeners(),ct&&(this.P&&removeEventListener("beforeunload",this.P,!1),this.$)){var i=at.indexOf(this.$);-1!==i&&at.splice(i,1)}this.readyState="closed",this.id=null,this.emitReserved("close",t,n),this.writeBuffer=[],this.M=0}},n}(I);vt.protocol=4;var lt=function(t){function n(){var n;return(n=t.apply(this,arguments)||this).Z=[],n}s(n,t);var i=n.prototype;return i.onOpen=function(){if(t.prototype.onOpen.call(this),"open"===this.readyState&&this.opts.upgrade)for(var n=0;n1&&void 0!==arguments[1]?arguments[1]:{},r="object"===c(n)?n:i;return(!r.transports||r.transports&&"string"==typeof r.transports[0])&&(r.transports=(r.transports||["polling","websocket","webtransport"]).map((function(t){return st[t]})).filter((function(t){return!!t}))),t.call(this,n,r)||this}return s(n,t),n}(lt);pt.protocol;var dt="function"==typeof ArrayBuffer,yt=function(t){return"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(t):t.buffer instanceof ArrayBuffer},bt=Object.prototype.toString,wt="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===bt.call(Blob),gt="function"==typeof File||"undefined"!=typeof File&&"[object FileConstructor]"===bt.call(File);function mt(t){return dt&&(t instanceof ArrayBuffer||yt(t))||wt&&t instanceof Blob||gt&&t instanceof File}function kt(t,n){if(!t||"object"!==c(t))return!1;if(Array.isArray(t)){for(var i=0,r=t.length;i=0&&t.num1?e-1:0),s=1;s1?i-1:0),e=1;ei.l.retries&&(i.it.shift(),n&&n(t));else if(i.it.shift(),n){for(var e=arguments.length,o=new Array(e>1?e-1:0),s=1;s0&&void 0!==arguments[0]&&arguments[0];if(this.connected&&0!==this.it.length){var n=this.it[0];n.pending&&!t||(n.pending=!0,n.tryCount++,this.flags=n.flags,this.emit.apply(this,n.args))}},o.packet=function(t){t.nsp=this.nsp,this.io.ct(t)},o.onopen=function(){var t=this;"function"==typeof this.auth?this.auth((function(n){t.vt(n)})):this.vt(this.auth)},o.vt=function(t){this.packet({type:Bt.CONNECT,data:this.lt?e({pid:this.lt,offset:this.dt},t):t})},o.onerror=function(t){this.connected||this.emitReserved("connect_error",t)},o.onclose=function(t,n){this.connected=!1,delete this.id,this.emitReserved("disconnect",t,n),this.yt()},o.yt=function(){var t=this;Object.keys(this.acks).forEach((function(n){if(!t.sendBuffer.some((function(t){return String(t.id)===n}))){var i=t.acks[n];delete t.acks[n],i.withError&&i.call(t,new Error("socket has been disconnected"))}}))},o.onpacket=function(t){if(t.nsp===this.nsp)switch(t.type){case Bt.CONNECT:t.data&&t.data.sid?this.onconnect(t.data.sid,t.data.pid):this.emitReserved("connect_error",new Error("It seems you are trying to reach a Socket.IO server in v2.x with a v3.x client, but they are not compatible (more information here: https://socket.io/docs/v3/migrating-from-2-x-to-3-0/)"));break;case Bt.EVENT:case Bt.BINARY_EVENT:this.onevent(t);break;case Bt.ACK:case Bt.BINARY_ACK:this.onack(t);break;case Bt.DISCONNECT:this.ondisconnect();break;case Bt.CONNECT_ERROR:this.destroy();var n=new Error(t.data.message);n.data=t.data.data,this.emitReserved("connect_error",n)}},o.onevent=function(t){var n=t.data||[];null!=t.id&&n.push(this.ack(t.id)),this.connected?this.emitEvent(n):this.receiveBuffer.push(Object.freeze(n))},o.emitEvent=function(n){if(this.bt&&this.bt.length){var i,e=r(this.bt.slice());try{for(e.s();!(i=e.n()).done;){i.value.apply(this,n)}}catch(t){e.e(t)}finally{e.f()}}t.prototype.emit.apply(this,n),this.lt&&n.length&&"string"==typeof n[n.length-1]&&(this.dt=n[n.length-1])},o.ack=function(t){var n=this,i=!1;return function(){if(!i){i=!0;for(var r=arguments.length,e=new Array(r),o=0;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}_t.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var n=Math.random(),i=Math.floor(n*this.jitter*t);t=1&Math.floor(10*n)?t+i:t-i}return 0|Math.min(t,this.max)},_t.prototype.reset=function(){this.attempts=0},_t.prototype.setMin=function(t){this.ms=t},_t.prototype.setMax=function(t){this.max=t},_t.prototype.setJitter=function(t){this.jitter=t};var Dt=function(t){function n(n,i){var r,e;(r=t.call(this)||this).nsps={},r.subs=[],n&&"object"===c(n)&&(i=n,n=void 0),(i=i||{}).path=i.path||"/socket.io",r.opts=i,$(r,i),r.reconnection(!1!==i.reconnection),r.reconnectionAttempts(i.reconnectionAttempts||1/0),r.reconnectionDelay(i.reconnectionDelay||1e3),r.reconnectionDelayMax(i.reconnectionDelayMax||5e3),r.randomizationFactor(null!==(e=i.randomizationFactor)&&void 0!==e?e:.5),r.backoff=new _t({min:r.reconnectionDelay(),max:r.reconnectionDelayMax(),jitter:r.randomizationFactor()}),r.timeout(null==i.timeout?2e4:i.timeout),r.st="closed",r.uri=n;var o=i.parser||xt;return r.encoder=new o.Encoder,r.decoder=new o.Decoder,r.et=!1!==i.autoConnect,r.et&&r.open(),r}s(n,t);var i=n.prototype;return i.reconnection=function(t){return arguments.length?(this.kt=!!t,t||(this.skipReconnect=!0),this):this.kt},i.reconnectionAttempts=function(t){return void 0===t?this.At:(this.At=t,this)},i.reconnectionDelay=function(t){var n;return void 0===t?this.jt:(this.jt=t,null===(n=this.backoff)||void 0===n||n.setMin(t),this)},i.randomizationFactor=function(t){var n;return void 0===t?this.Et:(this.Et=t,null===(n=this.backoff)||void 0===n||n.setJitter(t),this)},i.reconnectionDelayMax=function(t){var n;return void 0===t?this.Ot:(this.Ot=t,null===(n=this.backoff)||void 0===n||n.setMax(t),this)},i.timeout=function(t){return arguments.length?(this.Bt=t,this):this.Bt},i.maybeReconnectOnOpen=function(){!this.ot&&this.kt&&0===this.backoff.attempts&&this.reconnect()},i.open=function(t){var n=this;if(~this.st.indexOf("open"))return this;this.engine=new pt(this.uri,this.opts);var i=this.engine,r=this;this.st="opening",this.skipReconnect=!1;var e=It(i,"open",(function(){r.onopen(),t&&t()})),o=function(i){n.cleanup(),n.st="closed",n.emitReserved("error",i),t?t(i):n.maybeReconnectOnOpen()},s=It(i,"error",o);if(!1!==this.Bt){var u=this.Bt,h=this.setTimeoutFn((function(){e(),o(new Error("timeout")),i.close()}),u);this.opts.autoUnref&&h.unref(),this.subs.push((function(){n.clearTimeoutFn(h)}))}return this.subs.push(e),this.subs.push(s),this},i.connect=function(t){return this.open(t)},i.onopen=function(){this.cleanup(),this.st="open",this.emitReserved("open");var t=this.engine;this.subs.push(It(t,"ping",this.onping.bind(this)),It(t,"data",this.ondata.bind(this)),It(t,"error",this.onerror.bind(this)),It(t,"close",this.onclose.bind(this)),It(this.decoder,"decoded",this.ondecoded.bind(this)))},i.onping=function(){this.emitReserved("ping")},i.ondata=function(t){try{this.decoder.add(t)}catch(t){this.onclose("parse error",t)}},i.ondecoded=function(t){var n=this;R((function(){n.emitReserved("packet",t)}),this.setTimeoutFn)},i.onerror=function(t){this.emitReserved("error",t)},i.socket=function(t,n){var i=this.nsps[t];return i?this.et&&!i.active&&i.connect():(i=new Lt(this,t,n),this.nsps[t]=i),i},i.wt=function(t){for(var n=0,i=Object.keys(this.nsps);n=this.At)this.backoff.reset(),this.emitReserved("reconnect_failed"),this.ot=!1;else{var i=this.backoff.duration();this.ot=!0;var r=this.setTimeoutFn((function(){n.skipReconnect||(t.emitReserved("reconnect_attempt",n.backoff.attempts),n.skipReconnect||n.open((function(i){i?(n.ot=!1,n.reconnect(),t.emitReserved("reconnect_error",i)):n.onreconnect()})))}),i);this.opts.autoUnref&&r.unref(),this.subs.push((function(){t.clearTimeoutFn(r)}))}},i.onreconnect=function(){var t=this.backoff.attempts;this.ot=!1,this.backoff.reset(),this.emitReserved("reconnect",t)},n}(I),Pt={};function $t(t,n){"object"===c(t)&&(n=t,t=void 0);var i,r=function(t){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",i=arguments.length>2?arguments[2]:void 0,r=t;i=i||"undefined"!=typeof location&&location,null==t&&(t=i.protocol+"//"+i.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?i.protocol+t:i.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==i?i.protocol+"//"+t:"https://"+t),r=ft(t)),r.port||(/^(http|ws)$/.test(r.protocol)?r.port="80":/^(http|ws)s$/.test(r.protocol)&&(r.port="443")),r.path=r.path||"/";var e=-1!==r.host.indexOf(":")?"["+r.host+"]":r.host;return r.id=r.protocol+"://"+e+":"+r.port+n,r.href=r.protocol+"://"+e+(i&&i.port===r.port?"":":"+r.port),r}(t,(n=n||{}).path||"/socket.io"),e=r.source,o=r.id,s=r.path,u=Pt[o]&&s in Pt[o].nsps;return n.forceNew||n["force new connection"]||!1===n.multiplex||u?i=new Dt(e,n):(Pt[o]||(Pt[o]=new Dt(e,n)),i=Pt[o]),r.query&&!n.query&&(n.query=r.queryKey),i.socket(r.path,n)}return e($t,{Manager:Dt,Socket:Lt,io:$t,connect:$t}),$t})); 7 | //# sourceMappingURL=socket.io.min.js.map 8 | --------------------------------------------------------------------------------