diff --git a/.gitignore b/.gitignore index 40a59965..6dec295d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,50 @@ docs/_build .tox pyrightconfig.json simplemonitor/html/node_modules + +# Web interface database files (but allow the directory structure) +simplemonitor.db +*.db +!webapp/simplemonitor.db + +# Generated web interface files (but keep templates and static for the Flask app) +# templates/ - Keep this for Flask templates +# static/ - Keep this for Flask static files + +# Docker volumes and exports +_monitor-export/ +html/ + +# Status page files (generated by SimpleMonitor) +status_page.html +html/index.html + +# Docker Compose override files +docker-compose.override.yml +docker-compose.local.yml + +# Web interface specific +webapp/simplemonitor.db + +# Temporary files +cookies.txt + +# Python cache +__pycache__/ +*.py[cod] +*$py.class + +# Environment files +.env +.env.local +.env.*.local + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db diff --git a/WEBAPP_README.md b/WEBAPP_README.md new file mode 100644 index 00000000..41e5587a --- /dev/null +++ b/WEBAPP_README.md @@ -0,0 +1,85 @@ +# SimpleMonitor Web Interface + +A Flask-based web interface for managing SimpleMonitor configurations with authentication, monitor management, and SMS alerting. + +## Features + +- **Authentication System**: Secure login with admin account setup +- **Monitor Management**: Add, edit, and delete monitors through a web UI +- **Dynamic Forms**: Automatically generated forms for different monitor types +- **SMS Alerts**: Configure Twilio SMS alerts for monitor failures +- **Real-time Status**: View monitor status and configuration +- **Settings Management**: Configure API keys and system settings + +## Monitor Types Supported + +- **Host Ping**: Check if a host is pingable +- **HTTP Check**: Verify web services are responding +- **DNS Check**: Test DNS resolution +- **Disk Space**: Monitor available disk space +- **Memory Check**: Monitor system memory usage +- **Process Check**: Verify processes are running +- **Command Check**: Execute custom commands + +## Quick Start + +1. **Start the services**: + ```bash + docker-compose up --build -d + ``` + +2. **Access the web interface**: + - Web Interface: http://localhost:5001 + - Status Page: http://localhost:8000 + +3. **Setup admin account**: + - Go to http://localhost:5001/setup + - Create your admin account + +4. **Configure Twilio (optional)**: + - Go to Settings page + - Enter your Twilio credentials + - Enable SMS alerts for monitors + +## URLs + +- `/` - Landing page (shows SimpleMonitor status) +- `/setup` - Setup admin account (first time only) +- `/login` - Login page +- `/dashboard` - Main dashboard +- `/monitors` - Monitor management +- `/monitors/add` - Add new monitor +- `/settings` - System settings + +## Configuration + +The web interface automatically generates SimpleMonitor configuration files: +- `monitor.ini` - Main configuration +- `monitors.ini` - Monitor definitions + +Configuration is automatically reloaded when changes are made through the web interface. + +## SMS Alerts + +To enable SMS alerts: + +1. Sign up for a Twilio account +2. Get your Account SID and Auth Token +3. Purchase a phone number for sending SMS +4. Configure these in the Settings page +5. Enable SMS alerts when adding/editing monitors + +## Development + +The web interface is built with: +- Flask (Python web framework) +- Bootstrap 5 (UI framework) +- SQLite (database) +- Twilio (SMS service) + +## Security Notes + +- Change the SECRET_KEY in docker-compose.yml for production +- Use strong passwords for admin accounts +- Consider using HTTPS in production +- Regularly update dependencies diff --git a/config_watcher.py b/config_watcher.py new file mode 100644 index 00000000..29e155e8 --- /dev/null +++ b/config_watcher.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +""" +Configuration Watcher for SimpleMonitor +Monitors configuration files for changes and reloads SimpleMonitor when needed +""" + +import os +import time +import hashlib +import requests +import subprocess +import signal +import sys +from pathlib import Path + +class ConfigWatcher: + def __init__(self, config_files=['monitor.ini', 'monitors.ini'], check_interval=30): + self.config_files = config_files + self.check_interval = check_interval + self.last_hashes = {} + self.webapp_url = os.environ.get('WEBAPP_URL', 'http://webapp:5000') + self.running = True + + # Set up signal handlers + signal.signal(signal.SIGINT, self.signal_handler) + signal.signal(signal.SIGTERM, self.signal_handler) + + # Initialize hashes + self.update_hashes() + + def signal_handler(self, signum, frame): + """Handle shutdown signals""" + print(f"Received signal {signum}, shutting down...") + self.running = False + sys.exit(0) + + def get_file_hash(self, filepath): + """Get MD5 hash of a file""" + try: + with open(filepath, 'rb') as f: + return hashlib.md5(f.read()).hexdigest() + except FileNotFoundError: + return None + + def update_hashes(self): + """Update stored hashes for all config files""" + for filepath in self.config_files: + self.last_hashes[filepath] = self.get_file_hash(filepath) + + def check_webapp_config(self): + """Check if webapp has updated configuration""" + try: + response = requests.get(f"{self.webapp_url}/api/config-hash", timeout=5) + if response.status_code == 200: + data = response.json() + webapp_hash = data.get('hash', '') + + # Compare with local hash + local_hash = ':'.join([ + self.last_hashes.get('monitor.ini', ''), + self.last_hashes.get('monitors.ini', '') + ]) + + if webapp_hash != local_hash and webapp_hash != 'no-config': + print(f"Configuration change detected: {webapp_hash} != {local_hash}") + return True + except Exception as e: + print(f"Error checking webapp config: {e}") + + return False + + def reload_simplemonitor(self): + """Reload SimpleMonitor configuration""" + try: + print("Reloading SimpleMonitor configuration...") + + # Method 1: Try to send SIGHUP to SimpleMonitor process + result = subprocess.run(['pkill', '-HUP', 'simplemonitor'], + capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + print("SimpleMonitor configuration reloaded via SIGHUP") + return True + else: + # Method 2: Try to restart via webapp API + try: + response = requests.post(f"{self.webapp_url}/api/reload-config", + timeout=10) + if response.status_code == 200: + print("SimpleMonitor configuration reloaded via API") + return True + except Exception as e: + print(f"API reload failed: {e}") + + print("Failed to reload SimpleMonitor configuration") + return False + + except Exception as e: + print(f"Error reloading SimpleMonitor: {e}") + return False + + def check_local_changes(self): + """Check for local file changes""" + changes_detected = False + + for filepath in self.config_files: + current_hash = self.get_file_hash(filepath) + last_hash = self.last_hashes.get(filepath) + + if current_hash != last_hash and current_hash is not None: + print(f"Local change detected in {filepath}") + changes_detected = True + self.last_hashes[filepath] = current_hash + + return changes_detected + + def run(self): + """Main monitoring loop""" + print("Configuration watcher started...") + print(f"Monitoring files: {self.config_files}") + print(f"Check interval: {self.check_interval} seconds") + print(f"Webapp URL: {self.webapp_url}") + + while self.running: + try: + # Check for local file changes + if self.check_local_changes(): + if self.reload_simplemonitor(): + print("Configuration reloaded successfully") + else: + print("Failed to reload configuration") + + # Check for webapp changes + if self.check_webapp_config(): + if self.reload_simplemonitor(): + print("Configuration reloaded from webapp changes") + # Update local hashes to match webapp + self.update_hashes() + else: + print("Failed to reload configuration from webapp") + + time.sleep(self.check_interval) + + except KeyboardInterrupt: + print("Received keyboard interrupt, shutting down...") + break + except Exception as e: + print(f"Error in monitoring loop: {e}") + time.sleep(self.check_interval) + + print("Configuration watcher stopped") + +if __name__ == '__main__': + watcher = ConfigWatcher() + watcher.run() diff --git a/debug_form_error.py b/debug_form_error.py new file mode 100644 index 00000000..9895318a --- /dev/null +++ b/debug_form_error.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +import requests +import re +from bs4 import BeautifulSoup + +# Create a session to maintain cookies +session = requests.Session() + +# Login +login_data = {'username': 'admin', 'password': 'admin123'} +session.post('http://localhost:5001/login', data=login_data) + +# Test form submission +monitor_data = { + 'name': 'Debug Monitor', + 'monitor_type': 'host', + 'config_host': '8.8.8.8', + 'config_tolerance': '2' +} + +response = session.post('http://localhost:5001/monitors/add', data=monitor_data) +print(f"Status Code: {response.status_code}") + +# Parse the HTML to find error messages +soup = BeautifulSoup(response.text, 'html.parser') + +# Look for alert messages +alerts = soup.find_all('div', class_='alert') +print(f"\nFound {len(alerts)} alert messages:") +for i, alert in enumerate(alerts, 1): + print(f"Alert {i}: {alert.get_text(strip=True)}") + +# Look for invalid feedback messages +invalid_feedbacks = soup.find_all('div', class_='invalid-feedback') +print(f"\nFound {len(invalid_feedbacks)} invalid feedback messages:") +for i, feedback in enumerate(invalid_feedbacks, 1): + if feedback.get_text(strip=True): + print(f"Invalid feedback {i}: {feedback.get_text(strip=True)}") + +# Look for any text containing "error" or "required" +error_text = soup.find_all(text=re.compile(r'error|required|invalid', re.I)) +print(f"\nFound {len(error_text)} error-related text elements:") +for i, text in enumerate(error_text[:10], 1): # Limit to first 10 + if text.strip(): + print(f"Error text {i}: {text.strip()}") + +# Check if the form has validation classes +form = soup.find('form', id='monitorForm') +if form: + print(f"\nForm validation classes: {form.get('class', [])}") + + # Check input fields for validation classes + inputs = form.find_all('input') + for input_field in inputs: + name = input_field.get('name', 'unnamed') + classes = input_field.get('class', []) + if 'is-invalid' in classes or 'is-valid' in classes: + print(f"Input '{name}' classes: {classes}") diff --git a/debug_login.py b/debug_login.py new file mode 100644 index 00000000..5deb8c96 --- /dev/null +++ b/debug_login.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +import requests +import json + +# Create a session to maintain cookies +session = requests.Session() + +# Test login +login_data = { + 'username': 'admin', + 'password': 'admin123' +} + +print("=== Testing Login ===") +try: + # First, get the login page to see if there are any issues + login_page_response = session.get('http://localhost:5001/login') + print(f"Login page status: {login_page_response.status_code}") + + # Now try to login + login_response = session.post('http://localhost:5001/login', data=login_data) + print(f"Login POST status: {login_response.status_code}") + print(f"Response headers: {dict(login_response.headers)}") + + # Check if we got redirected + if login_response.status_code == 302: + print("✅ Login successful! Got redirect.") + print(f"Redirect location: {login_response.headers.get('Location', 'None')}") + elif login_response.status_code == 200: + print("❌ Login failed - returned login page") + # Check if there are any error messages + if 'Invalid username or password' in login_response.text: + print("Error: Invalid username or password") + elif 'Logged in successfully' in login_response.text: + print("Success message found but no redirect") + else: + print("No clear error or success message") + else: + print(f"Unexpected status: {login_response.status_code}") + + # Try to access a protected page + print("\n=== Testing Protected Page Access ===") + dashboard_response = session.get('http://localhost:5001/dashboard') + print(f"Dashboard status: {dashboard_response.status_code}") + + if dashboard_response.status_code == 200: + print("✅ Successfully accessed dashboard") + elif dashboard_response.status_code == 302: + print("❌ Redirected to login (session not maintained)") + print(f"Redirect location: {dashboard_response.headers.get('Location', 'None')}") + else: + print(f"Unexpected dashboard status: {dashboard_response.status_code}") + +except Exception as e: + print(f"Error: {e}") diff --git a/docker-compose.yml b/docker-compose.yml index 0303970a..bf17c654 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,6 +8,8 @@ services: volumes: - http-content:/code/html - ./_monitor-export:/code/monitor-export + environment: + - WEBAPP_URL=http://webapp:5000 restart: always depends_on: - webserver @@ -22,5 +24,21 @@ services: - http-content:/usr/share/nginx/html restart: always + webapp: + build: + context: ./ + dockerfile: ./docker/webapp.Dockerfile + ports: + - "5001:5000" + volumes: + - ./monitor.ini:/code/monitor.ini + - ./monitors.ini:/code/monitors.ini + - /var/run/docker.sock:/var/run/docker.sock + environment: + - SECRET_KEY=your-secret-key-change-in-production + restart: always + depends_on: + - monitor + volumes: http-content: diff --git a/docker/monitor.Dockerfile b/docker/monitor.Dockerfile index 2ec3aea6..4a6edf07 100644 --- a/docker/monitor.Dockerfile +++ b/docker/monitor.Dockerfile @@ -7,7 +7,7 @@ FROM python:3.12-alpine LABEL version_dockerfile="21-01-2024:prod" \ version_image="python:3.12-alpine" -# >> package :: install +# >> package :: install (Layer 1: System packages - rarely change) RUN apk --no-cache add --update \ # __ install :: basics build-base \ @@ -23,7 +23,7 @@ RUN apk --no-cache add --update \ rust \ cargo -# >> env :: web/docker paths +# >> env :: web/docker paths (Layer 2: Environment variables) ENV DOCKER_ROOT=/code \ DOCKER_HTML_ROOT=/code/html \ DOCKER_HTML_BACKUP=/code/html-backup \ @@ -43,15 +43,24 @@ ENV MAIN_USER=simplemonitor \ MAIN_GROUP=simplemonitor \ MAIN_GROUP_ID=1500 -# >> setup :: root-directory +# >> setup :: root-directory (Layer 3: Directory structure) RUN mkdir -p $DOCKER_ROOT -COPY $SOURCE_ROOT $DOCKER_ROOT WORKDIR $DOCKER_ROOT +# >> upgrade pip (Layer 4: Python package manager) RUN pip install --upgrade pip -# >> install :: py-requirements -RUN pip install --no-cache-dir "$DOCKER_ROOT" +# >> copy requirements first for better caching (Layer 5: Dependencies) +COPY pyproject.toml poetry.lock README.md ./ + +# >> copy source code (Layer 6: Application code - changes frequently) +COPY . . + +# >> install package (Layer 7: Install SimpleMonitor) +RUN pip install --no-cache-dir . + +# >> install config watcher dependencies (Layer 8: Additional Python packages) +RUN pip install --no-cache-dir requests # >> prepare :: volumes RUN mkdir -p $VOLUME_MONITOR_EXPORT $DOCKER_HTML_ROOT diff --git a/docker/monitor.entrypoint.sh b/docker/monitor.entrypoint.sh index f4ea9a7d..d887106e 100644 --- a/docker/monitor.entrypoint.sh +++ b/docker/monitor.entrypoint.sh @@ -53,7 +53,7 @@ echo "VOLUME_MONITOR_EXPORT "$VOLUME_MONITOR_EXPORT # exec entrypoint.py # == == == == == == == == == == == == == == == cd /code -simplemonitor +python start_monitor_with_watcher.py # exec some other commands # == == == == == == == == == == == == == == == diff --git a/docker/webapp.Dockerfile b/docker/webapp.Dockerfile new file mode 100644 index 00000000..f0666b5d --- /dev/null +++ b/docker/webapp.Dockerfile @@ -0,0 +1,73 @@ +# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +# >> python @ alpine +# -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +FROM python:3.12-alpine + +# >> meta :: labels +LABEL version_dockerfile="21-01-2024:webapp" \ + version_image="python:3.12-alpine" + +# >> package :: install (Layer 1: System packages - rarely change) +RUN apk --no-cache add --update \ + # __ install :: basics + build-base \ + openssl \ + # __ install :: tools + bash \ + sudo \ + openrc \ + su-exec \ + bind-tools \ + openssl-dev \ + libffi-dev \ + rust \ + cargo \ + # __ install :: docker + docker-cli + +# >> env :: web/docker paths (Layer 2: Environment variables) +ENV DOCKER_ROOT=/code \ + DOCKER_WEBAPP_ROOT=/code/webapp \ + DOCKER_ENTRYPOINT_BINARY=/bin/webapp.entrypoint.sh \ + DOCKER_ENTRYPOINT_ORIGIN=/code/docker/webapp.entrypoint.sh + +# >> env :: source/host paths +ENV SOURCE_ROOT=./ + +# >> env :: user/groups +ENV MAIN_USER=simplemonitor \ + MAIN_USER_ID=1500 \ + MAIN_GROUP=simplemonitor \ + MAIN_GROUP_ID=1500 + +# >> setup :: root-directory (Layer 3: Directory structure) +RUN mkdir -p $DOCKER_ROOT +WORKDIR $DOCKER_ROOT + +# >> upgrade pip (Layer 4: Python package manager) +RUN pip install --upgrade pip + +# >> copy requirements first for better caching (Layer 5: Dependencies) +COPY requirements-webapp.txt ./ +RUN pip install --no-cache-dir -r requirements-webapp.txt + +# >> copy source code (Layer 6: Application code - changes frequently) +COPY . . + +# >> prepare :: webapp directory +RUN mkdir -p $DOCKER_WEBAPP_ROOT + +# >> add :: user, group, project-directory-rights +RUN addgroup -g $MAIN_GROUP_ID $MAIN_GROUP \ + && adduser -D -G $MAIN_GROUP -u $MAIN_USER_ID $MAIN_USER \ + && chown -R $MAIN_USER:$MAIN_GROUP $DOCKER_ROOT + +# >> entrypoint :: prepare +RUN cp $DOCKER_ENTRYPOINT_ORIGIN $DOCKER_ENTRYPOINT_BINARY \ + && chmod +x $DOCKER_ENTRYPOINT_BINARY + +# >> expose :: port +EXPOSE 5000 + +# Start the webapp +CMD ["/bin/webapp.entrypoint.sh"] diff --git a/docker/webapp.entrypoint.sh b/docker/webapp.entrypoint.sh new file mode 100644 index 00000000..18b056e4 --- /dev/null +++ b/docker/webapp.entrypoint.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +# ENVs +# == == == == == == == == == == == == == == == +# >> env :: web/docker paths +# ENV DOCKER_ROOT=/code \ +# DOCKER_WEBAPP_ROOT=/code/webapp \ +# DOCKER_ENTRYPOINT_BINARY=/bin/webapp.entrypoint.sh \ +# DOCKER_ENTRYPOINT_ORIGIN=/code/docker/webapp.entrypoint.sh + +# >> env :: source/host paths +# ENV SOURCE_ROOT=./ + +# >> env :: user/groups +# ENV MAIN_USER=simplemonitor \ +# MAIN_USER_ID=1500 \ +# MAIN_GROUP=simplemonitor \ +# MAIN_GROUP_ID=1500 + +echo "environment vars == == == == == == == == ==" +echo "== == == == == == == == == == == == == == ==" +echo "DOCKER_ROOT "$DOCKER_ROOT +echo "DOCKER_WEBAPP_ROOT "$DOCKER_WEBAPP_ROOT +echo "SOURCE_ROOT "$SOURCE_ROOT +echo "DOCKER_ENTRYPOINT_BINARY "$DOCKER_ENTRYPOINT_BINARY +echo "DOCKER_ENTRYPOINT_ORIGIN "$DOCKER_ENTRYPOINT_ORIGIN +echo "MAIN_USER "$MAIN_USER +echo "MAIN_USER_ID "$MAIN_USER_ID +echo "MAIN_GROUP "$MAIN_GROUP +echo "MAIN_GROUP_ID "$MAIN_GROUP_ID + +# exec webapp +# == == == == == == == == == == == == == == == +cd /code +python start_services.py + +# exec some other commands +# == == == == == == == == == == == == == == == +exec "$@" diff --git a/docker/webserver.Dockerfile b/docker/webserver.Dockerfile index 1a819721..9c88d07d 100644 --- a/docker/webserver.Dockerfile +++ b/docker/webserver.Dockerfile @@ -7,7 +7,7 @@ FROM nginx:1.15.1-alpine LABEL version_dockerfile="10-07-2018:prod" \ version_image="nginx:1.15.1-alpine" -# >> package :: install +# >> package :: install (Layer 1: System packages - rarely change) RUN apk --no-cache add --update \ # __ install :: basics build-base \ diff --git a/docs/docker-deployment.rst b/docs/docker-deployment.rst new file mode 100644 index 00000000..5a1dc7c1 --- /dev/null +++ b/docs/docker-deployment.rst @@ -0,0 +1,319 @@ +Docker Deployment +================= + +SimpleMonitor now includes a comprehensive Docker-based deployment strategy with a web interface for easy management and monitoring. + +Overview +-------- + +The Docker deployment consists of three main components: + +1. **SimpleMonitor Service** - The core monitoring engine +2. **Web Interface** - Flask-based web application for management +3. **Web Server** - Nginx server for serving status pages + +Architecture +------------ + +.. image:: docker-architecture.png + :alt: Docker Architecture Diagram + :align: center + +The deployment uses Docker Compose to orchestrate multiple containers: + +- **monitor**: Runs the SimpleMonitor service with configuration watching +- **webapp**: Flask web application for management interface +- **webserver**: Nginx server for serving status pages + +Quick Start +----------- + +1. **Clone the repository**: + + .. code-block:: bash + + git clone https://github.com/jamesoff/simplemonitor.git + cd simplemonitor + +2. **Start the services**: + + .. code-block:: bash + + docker-compose up -d + +3. **Access the interfaces**: + + - Web Management Interface: http://localhost:5001 + - Status Dashboard: http://localhost:8000 + +4. **Set up admin account**: + + - Navigate to http://localhost:5001/setup + - Create your admin account + - Start adding monitors through the web interface + +Features +-------- + +Web Management Interface +~~~~~~~~~~~~~~~~~~~~~~~~ + +The web interface provides: + +- **Dashboard**: Overview of all monitors with real-time status +- **Real-time Dashboard**: Live monitoring with auto-refresh capabilities +- **Monitor Management**: Add, edit, delete, and toggle monitors +- **Settings**: Configure Twilio SMS alerts and other settings +- **User Management**: Admin account creation and management + +Real-time Monitoring +~~~~~~~~~~~~~~~~~~~~ + +The real-time dashboard replicates the original SimpleMonitor status page with: + +- Auto-refresh capabilities (10s, 30s, 1min, 5min intervals) +- Live status updates +- Summary statistics +- Detailed monitor information +- Visual status indicators + +Configuration Management +~~~~~~~~~~~~~~~~~~~~~~~~ + +- **Dynamic Configuration**: Monitors added through the web interface are automatically added to SimpleMonitor +- **Configuration Watching**: The monitor service watches for configuration changes and reloads automatically +- **Persistent Storage**: All configurations are stored in SQLite database and configuration files + +Docker Services +--------------- + +Monitor Service +~~~~~~~~~~~~~~~ + +The monitor service runs SimpleMonitor with enhanced features: + +- **Configuration Watcher**: Automatically detects configuration changes +- **Auto-reload**: Reloads SimpleMonitor when configurations change +- **Optimized Build**: Layered Docker build for efficient caching + +Web Application +~~~~~~~~~~~~~~~ + +The Flask web application provides: + +- **RESTful API**: For real-time status data +- **Database Management**: SQLite database for storing monitor configurations +- **Authentication**: Secure login system with admin privileges +- **Twilio Integration**: SMS alerting capabilities + +Web Server +~~~~~~~~~~ + +The Nginx web server: + +- **Status Pages**: Serves the SimpleMonitor status page +- **Static Files**: Serves static assets for the web interface +- **Load Balancing**: Can be configured for high availability + +Configuration Files +------------------- + +The deployment uses several configuration files: + +- **docker-compose.yml**: Main orchestration file +- **monitor.ini**: SimpleMonitor main configuration +- **monitors.ini**: Monitor definitions +- **requirements-webapp.txt**: Python dependencies for web app + +Environment Variables +--------------------- + +The following environment variables can be configured: + +- **SECRET_KEY**: Flask secret key for sessions (required) +- **WEBAPP_URL**: URL for webapp service (for monitor communication) + +Docker Compose Configuration +---------------------------- + +The docker-compose.yml file defines three services: + +.. code-block:: yaml + + version: '2' + + services: + monitor: + build: + context: ./ + dockerfile: ./docker/monitor.Dockerfile + volumes: + - http-content:/code/html + - ./_monitor-export:/code/monitor-export + environment: + - WEBAPP_URL=http://webapp:5000 + restart: always + depends_on: + - webserver + + webserver: + build: + context: ./ + dockerfile: ./docker/webserver.Dockerfile + ports: + - "8000:80" + volumes: + - http-content:/usr/share/nginx/html + restart: always + + webapp: + build: + context: ./ + dockerfile: ./docker/webapp.Dockerfile + ports: + - "5001:5000" + volumes: + - ./simplemonitor.db:/code/simplemonitor.db + - ./monitor.ini:/code/monitor.ini + - ./monitors.ini:/code/monitors.ini + - /var/run/docker.sock:/var/run/docker.sock + environment: + - SECRET_KEY=your-secret-key-change-in-production + restart: always + depends_on: + - monitor + +Production Deployment +--------------------- + +For production deployment, consider the following: + +1. **Change Default Secrets**: + - Update SECRET_KEY in docker-compose.yml + - Use strong, unique passwords + +2. **SSL/TLS Configuration**: + - Configure reverse proxy with SSL certificates + - Update port mappings as needed + +3. **Database Backup**: + - Regular backups of simplemonitor.db + - Backup configuration files + +4. **Monitoring**: + - Monitor container health + - Set up log aggregation + - Configure alerting for container failures + +5. **Security**: + - Restrict Docker socket access + - Use secrets management + - Regular security updates + +Troubleshooting +--------------- + +Common Issues +~~~~~~~~~~~~~ + +**Containers not starting**: + - Check Docker and Docker Compose versions + - Verify port availability + - Check logs: ``docker-compose logs`` + +**Web interface not accessible**: + - Verify port 5001 is available + - Check webapp container logs + - Ensure database is initialized + +**Monitor service not updating**: + - Check configuration file permissions + - Verify volume mounts + - Check monitor container logs + +**Status page not updating**: + - Verify webserver container is running + - Check volume mounts for html content + - Ensure monitor service is generating status page + +Logs and Debugging +~~~~~~~~~~~~~~~~~~ + +View logs for specific services: + +.. code-block:: bash + + # All services + docker-compose logs + + # Specific service + docker-compose logs webapp + docker-compose logs monitor + docker-compose logs webserver + + # Follow logs + docker-compose logs -f webapp + +Advanced Configuration +---------------------- + +Custom Monitor Types +~~~~~~~~~~~~~~~~~~~~ + +To add custom monitor types: + +1. Create monitor class in simplemonitor/Monitors/ +2. Update webapp.py to include new monitor type +3. Add UI components in templates/ +4. Rebuild containers + +Custom Alerters +~~~~~~~~~~~~~~~ + +To add custom alerters: + +1. Create alerter class in simplemonitor/Alerters/ +2. Update webapp.py to include new alerter +3. Add configuration options in settings +4. Rebuild containers + +Scaling +------- + +The deployment can be scaled for high availability: + +- **Multiple Monitor Instances**: Run multiple monitor containers +- **Load Balancing**: Use external load balancer +- **Database Clustering**: Use external database +- **Container Orchestration**: Deploy on Kubernetes + +Migration from Standalone +------------------------- + +To migrate from standalone SimpleMonitor: + +1. **Backup Configuration**: + - Copy monitor.ini and monitors.ini + - Export any custom monitors/alerters + +2. **Deploy Docker Version**: + - Follow quick start instructions + - Copy configuration files to project root + +3. **Import Configuration**: + - Use web interface to add monitors + - Configure alerters through settings + +4. **Verify Setup**: + - Check all monitors are working + - Test alerting functionality + - Verify status page updates + +Support +------- + +For issues and questions: + +- **GitHub Issues**: https://github.com/jamesoff/simplemonitor/issues +- **Documentation**: https://simplemonitor.readthedocs.io/ +- **Community**: GitHub Discussions diff --git a/docs/index.rst b/docs/index.rst index 266295ce..7445de76 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,8 @@ Welcome to SimpleMonitor installation configuration + docker-deployment + web-interface .. toctree:: :maxdepth: 2 @@ -43,7 +45,9 @@ send their results back to a central location. SimpleMonitor supports Python 3.6.2 and higher on Windows, Linux and FreeBSD. -To get started, see :ref:`Installation`. +**New in this version**: Docker deployment with a comprehensive web interface for easy management and real-time monitoring. + +To get started, see :ref:`Installation` or jump straight to :ref:`Docker Deployment` for the quickest setup. Features ======== @@ -87,10 +91,21 @@ Some of the options include (for the complete list, see :ref:`Loggers` +- **Learn about the web interface**: See :ref:`Web Interface` +- **Configure advanced features**: See :ref:`Configuration` +- **Add custom monitors**: See :ref:`Creating Monitors` + +Need Help? +---------- + +- **GitHub Issues**: https://github.com/jamesoff/simplemonitor/issues +- **Documentation**: https://simplemonitor.readthedocs.io/ +- **Community**: GitHub Discussions diff --git a/docs/web-interface.rst b/docs/web-interface.rst new file mode 100644 index 00000000..fcd63902 --- /dev/null +++ b/docs/web-interface.rst @@ -0,0 +1,393 @@ +Web Interface +============= + +SimpleMonitor includes a comprehensive web interface built with Flask and Bootstrap for easy management and monitoring. + +Overview +-------- + +The web interface provides a modern, responsive interface for managing SimpleMonitor configurations and viewing real-time monitoring data. It includes user authentication, monitor management, and real-time dashboard capabilities. + +Features +-------- + +User Authentication +~~~~~~~~~~~~~~~~~~~ + +- **Admin Account Creation**: Secure setup process for initial admin account +- **Session Management**: Persistent login sessions +- **Role-based Access**: Admin privileges for configuration management +- **Secure Authentication**: Password hashing and session security + +Dashboard +~~~~~~~~~ + +The main dashboard provides: + +- **Overview Statistics**: Total monitors, active monitors, SMS alerts, HTTP checks +- **Recent Monitors**: Quick view of recently added monitors +- **Status Summary**: Visual indicators for monitor health +- **Quick Actions**: Direct links to add monitors and view real-time status + +Real-time Dashboard +~~~~~~~~~~~~~~~~~~~ + +The real-time dashboard replicates the original SimpleMonitor status page with enhanced features: + +- **Live Updates**: Auto-refresh capabilities with configurable intervals +- **Status Indicators**: Visual status badges for each monitor +- **Summary Bar**: Real-time summary of monitor status +- **Detailed Information**: Complete monitor details including uptime, failures, and age +- **Auto-refresh Controls**: Start/stop auto-refresh with configurable intervals + +Monitor Management +~~~~~~~~~~~~~~~~~~ + +Complete monitor lifecycle management: + +- **Add Monitors**: Dynamic form for adding different monitor types +- **Edit Monitors**: Modify existing monitor configurations +- **Delete Monitors**: Remove monitors with confirmation +- **Toggle Status**: Enable/disable monitors without deletion +- **View Details**: Detailed view of monitor configuration and status + +Monitor Types Supported +~~~~~~~~~~~~~~~~~~~~~~~ + +The web interface supports all SimpleMonitor monitor types: + +- **HTTP**: URL monitoring with content validation +- **Ping**: Host connectivity testing +- **TCP**: Port availability checking +- **DNS**: DNS record validation +- **Disk Space**: Storage monitoring +- **Memory**: System memory monitoring +- **Process**: Process existence checking +- **Command**: Custom command execution +- **Service**: System service monitoring + +Settings Management +~~~~~~~~~~~~~~~~~~~ + +Centralized configuration management: + +- **Twilio Integration**: SMS alerting configuration +- **API Keys**: Secure storage of external service credentials +- **System Settings**: Global configuration options +- **Alert Configuration**: SMS phone numbers and alert preferences + +API Endpoints +------------- + +The web interface provides RESTful API endpoints: + +Status API +~~~~~~~~~~ + +.. code-block:: http + + GET /api/status + +Returns real-time monitoring status including: +- Summary statistics (OK/failed counts) +- Monitor details +- Last update timestamp +- Refresh status + +Configuration API +~~~~~~~~~~~~~~~~~ + +.. code-block:: http + + GET /api/config-hash + POST /api/reload-config + +Configuration management endpoints: +- Get configuration hash for change detection +- Trigger configuration reload + +User Interface +-------------- + +Navigation +~~~~~~~~~~ + +The interface includes a responsive navigation bar with: + +- **Dashboard**: Main overview page +- **Monitors**: Monitor management interface +- **Real-time**: Live monitoring dashboard +- **Add Monitor**: Quick access to add new monitors (admin only) +- **Settings**: Configuration management (admin only) +- **User Menu**: Account management and logout + +Responsive Design +~~~~~~~~~~~~~~~~~ + +The interface is built with Bootstrap for responsive design: + +- **Mobile Friendly**: Optimized for mobile devices +- **Tablet Support**: Responsive layout for tablets +- **Desktop Optimized**: Full-featured desktop interface +- **Cross-browser**: Compatible with modern browsers + +Status Indicators +~~~~~~~~~~~~~~~~~ + +Visual status indicators throughout the interface: + +- **Success**: Green badges and icons for healthy monitors +- **Warning**: Yellow indicators for disabled or warning states +- **Error**: Red indicators for failed monitors +- **Info**: Blue indicators for informational content + +Getting Started +--------------- + +Initial Setup +~~~~~~~~~~~~~ + +1. **Access Setup Page**: + - Navigate to http://localhost:5001/setup + - Create your admin account + - Complete the setup process + +2. **Login**: + - Use your admin credentials to log in + - Access the main dashboard + +3. **Add Monitors**: + - Click "Add Monitor" in the navigation + - Select monitor type + - Configure monitor settings + - Save and activate + +Adding Monitors +~~~~~~~~~~~~~~~ + +1. **Select Monitor Type**: + - Choose from available monitor types + - Each type has specific configuration options + +2. **Configure Settings**: + - Fill in required fields (marked with *) + - Set optional parameters + - Configure alerting options + +3. **SMS Alerts** (Optional): + - Enable SMS alerts + - Enter phone number + - Configure Twilio settings in Settings page + +4. **Save and Activate**: + - Save the monitor configuration + - Monitor will be automatically added to SimpleMonitor + - Configuration will be reloaded automatically + +Managing Monitors +~~~~~~~~~~~~~~~~~ + +**View Monitors**: +- Access the Monitors page +- View all configured monitors +- See real-time status + +**Edit Monitors**: +- Click the edit button for any monitor +- Modify configuration +- Save changes + +**Toggle Status**: +- Enable/disable monitors without deletion +- Use the toggle button in the actions column + +**Delete Monitors**: +- Click the delete button +- Confirm deletion +- Monitor will be removed from configuration + +Real-time Monitoring +~~~~~~~~~~~~~~~~~~~~ + +**Access Real-time Dashboard**: +- Click "Real-time" in navigation +- View live monitoring data +- See status updates in real-time + +**Configure Auto-refresh**: +- Use the refresh interval dropdown +- Start/stop auto-refresh +- Monitor refresh status + +**View Details**: +- Click on monitor names for detailed view +- See complete configuration +- View historical information + +Settings Configuration +~~~~~~~~~~~~~~~~~~~~~~ + +**Twilio SMS Setup**: +1. Navigate to Settings page +2. Enter Twilio Account SID +3. Enter Twilio Auth Token +4. Save settings + +**SMS Alert Configuration**: +1. When adding/editing monitors +2. Enable SMS alerts checkbox +3. Enter phone number +4. Save monitor configuration + +Troubleshooting +--------------- + +Common Issues +~~~~~~~~~~~~~ + +**Login Issues**: +- Verify admin account exists +- Check password is correct +- Clear browser cache and cookies + +**Monitor Not Updating**: +- Check monitor configuration +- Verify SimpleMonitor service is running +- Check container logs + +**SMS Not Working**: +- Verify Twilio credentials +- Check phone number format +- Ensure Twilio account has credits + +**Real-time Dashboard Not Loading**: +- Check webapp container status +- Verify API endpoints are accessible +- Check browser console for errors + +Browser Compatibility +~~~~~~~~~~~~~~~~~~~~~ + +Supported browsers: +- Chrome 80+ +- Firefox 75+ +- Safari 13+ +- Edge 80+ + +Required features: +- JavaScript enabled +- Local storage support +- Fetch API support + +Security Considerations +----------------------- + +Authentication +~~~~~~~~~~~~~~ + +- **Password Hashing**: Passwords are hashed using Werkzeug +- **Session Security**: Secure session management +- **CSRF Protection**: Built-in CSRF protection +- **Input Validation**: All inputs are validated and sanitized + +Data Protection +~~~~~~~~~~~~~~~ + +- **Database Security**: SQLite database with proper permissions +- **Configuration Files**: Secure file permissions +- **API Security**: Protected API endpoints +- **HTTPS**: Use HTTPS in production + +Best Practices +~~~~~~~~~~~~~~ + +- **Regular Backups**: Backup database and configuration files +- **Strong Passwords**: Use strong, unique passwords +- **Regular Updates**: Keep containers updated +- **Monitor Access**: Limit access to management interface +- **Log Monitoring**: Monitor application logs + +Advanced Usage +-------------- + +Custom Monitor Types +~~~~~~~~~~~~~~~~~~~~ + +To add custom monitor types: + +1. **Create Monitor Class**: + - Extend base monitor class + - Implement required methods + - Add to simplemonitor/Monitors/ + +2. **Update Web Interface**: + - Add monitor type to webapp.py + - Create form template + - Add validation logic + +3. **Deploy Changes**: + - Rebuild containers + - Test new monitor type + +API Integration +~~~~~~~~~~~~~~~ + +The web interface provides APIs for integration: + +- **Status Data**: Real-time monitoring data +- **Configuration Management**: Add/remove monitors +- **Alert Management**: Configure alerting + +Example API usage: + +.. code-block:: python + + import requests + + # Get status + response = requests.get('http://localhost:5001/api/status') + status = response.json() + + # Get configuration hash + response = requests.get('http://localhost:5001/api/config-hash') + config_hash = response.json()['hash'] + +Performance Optimization +~~~~~~~~~~~~~~~~~~~~~~~~ + +- **Database Indexing**: Optimize database queries +- **Caching**: Implement caching for frequently accessed data +- **Load Balancing**: Use multiple webapp instances +- **CDN**: Use CDN for static assets + +Monitoring and Alerting +~~~~~~~~~~~~~~~~~~~~~~~ + +- **Health Checks**: Monitor web interface health +- **Error Tracking**: Track and alert on errors +- **Performance Monitoring**: Monitor response times +- **Uptime Monitoring**: Monitor service availability + +Support and Resources +--------------------- + +Documentation +~~~~~~~~~~~~~ + +- **API Documentation**: Available in web interface +- **Configuration Guide**: See configuration.rst +- **Docker Deployment**: See docker-deployment.rst + +Community +~~~~~~~~~ + +- **GitHub Issues**: Report bugs and request features +- **GitHub Discussions**: Ask questions and share ideas +- **Contributing**: See CONTRIBUTING.md + +Professional Support +~~~~~~~~~~~~~~~~~~~~ + +For enterprise support and custom development: +- Contact: james at jamesoff dot net +- GitHub: https://github.com/jamesoff/simplemonitor diff --git a/requirements-webapp.txt b/requirements-webapp.txt new file mode 100644 index 00000000..adc7c27a --- /dev/null +++ b/requirements-webapp.txt @@ -0,0 +1,6 @@ +Flask==2.3.3 +Flask-SQLAlchemy==3.0.5 +Werkzeug==2.3.7 +requests==2.31.0 +twilio==8.10.0 +beautifulsoup4==4.12.2 diff --git a/start_monitor_with_watcher.py b/start_monitor_with_watcher.py new file mode 100644 index 00000000..d7ec8f38 --- /dev/null +++ b/start_monitor_with_watcher.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +""" +Start SimpleMonitor with configuration watcher +""" + +import os +import sys +import subprocess +import time +import signal +import threading +from pathlib import Path + +def run_simplemonitor(): + """Run SimpleMonitor in the background""" + try: + print("Starting SimpleMonitor...") + process = subprocess.Popen(['simplemonitor'], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + print(f"SimpleMonitor started with PID: {process.pid}") + return process + except Exception as e: + print(f"Error starting SimpleMonitor: {e}") + return None + +def run_config_watcher(): + """Run configuration watcher""" + try: + print("Starting configuration watcher...") + from config_watcher import ConfigWatcher + watcher = ConfigWatcher() + watcher.run() + except Exception as e: + print(f"Error starting config watcher: {e}") + +def signal_handler(signum, frame): + """Handle shutdown signals""" + print(f"\nReceived signal {signum}, shutting down...") + sys.exit(0) + +def main(): + """Main startup function""" + print("Starting SimpleMonitor with configuration watcher...") + + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + # Start SimpleMonitor + monitor_process = run_simplemonitor() + if not monitor_process: + print("Failed to start SimpleMonitor") + sys.exit(1) + + # Wait a moment for SimpleMonitor to start + time.sleep(2) + + try: + # Start config watcher in a separate thread + watcher_thread = threading.Thread(target=run_config_watcher, daemon=True) + watcher_thread.start() + + # Wait for SimpleMonitor process + monitor_process.wait() + + except KeyboardInterrupt: + print("\nShutting down...") + finally: + # Clean up SimpleMonitor process + if monitor_process: + monitor_process.terminate() + monitor_process.wait() + +if __name__ == '__main__': + main() diff --git a/start_services.py b/start_services.py new file mode 100644 index 00000000..c3feb54f --- /dev/null +++ b/start_services.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +""" +Startup script for Flask web interface +""" + +import os +import sys +import time +import signal + +def run_webapp(): + """Run Flask webapp""" + try: + # Import and run the Flask app + from webapp import app, db + print("Initializing database...") + + # Ensure we're in the right directory + import os + os.chdir('/code') + + with app.app_context(): + db.create_all() + print("Database tables created successfully") + + # Sync monitors from service + from webapp import sync_monitors_from_service + print("Syncing monitors from service...") + sync_monitors_from_service() + print("Starting Flask webapp on port 5000...") + app.run(host='0.0.0.0', port=5000, debug=False, use_reloader=False) + except Exception as e: + print(f"Error starting Flask app: {e}") + import traceback + traceback.print_exc() + +def signal_handler(signum, frame): + """Handle shutdown signals""" + print("\nShutting down webapp...") + sys.exit(0) + +def main(): + """Main startup function""" + print("Starting SimpleMonitor Flask web interface...") + + # Set up signal handlers + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + + try: + # Start Flask app (this will block) + run_webapp() + except KeyboardInterrupt: + print("\nShutting down...") + +if __name__ == '__main__': + main() diff --git a/templates/add_monitor.html b/templates/add_monitor.html new file mode 100644 index 00000000..dbf36f33 --- /dev/null +++ b/templates/add_monitor.html @@ -0,0 +1,615 @@ +{% extends "base.html" %} + +{% block title %}Add Monitor - SimpleMonitor{% endblock %} + +{% block content %} + +
+
+
+
+

+ Add New Monitor +

+

Configure a new monitoring target for your network infrastructure

+
+ + Back to Monitors + +
+
+
+ + +
+
+
+
+
+ Select Monitor Type +
+
+
+
+ {% for type_key, type_info in monitor_types.items() %} +
+
+
+
+
+ {% if type_key == 'http' %} + + {% elif type_key == 'host' %} + + {% elif type_key == 'dns' %} + + {% elif type_key == 'diskspace' %} + + {% elif type_key == 'memory' %} + + {% elif type_key == 'process' %} + + {% elif type_key == 'command' %} + + {% else %} + + {% endif %} +
+
{{ type_info.name }}
+

{{ type_info.description }}

+
+ + +
+
+
+
+
+ {% endfor %} +
+
+
+
+
+ + +
+
+
+
+
+ Monitor Configuration +
+
+
+
+ +
+
+
+ Basic Information +
+
+
+ + +
+ Please provide a valid monitor name. +
+
Choose a name that clearly identifies what this monitor checks.
+
+
+ +
+ Select a monitor type above +
+
+
+ + + + + +
+
+
+ SMS Alert Configuration +
+
+
+
+ + +
+
+ +
+ + +
+ + + Cancel + +
+
+
+
+
+ + +
+
+
+
+ Help & Tips +
+
+
+
+
+ +

Select a monitor type to see configuration help and examples.

+
+
+
+
+ + +
+
+
+ Monitor Types +
+
+
+
+
+ +
+
HTTP/HTTPS
+
Web services and APIs
+
+
+
+ +
+
Host Ping
+
Network connectivity
+
+
+
+ +
+
DNS
+
Domain resolution
+
+
+
+ +
+
Disk Space
+
Storage monitoring
+
+
+
+ +
+
Memory
+
RAM usage
+
+
+
+ +
+
Process
+
Running services
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 00000000..98288c7c --- /dev/null +++ b/templates/base.html @@ -0,0 +1,735 @@ + + + + + + {% block title %}SimpleMonitor{% endblock %} + + + + + + + + + + + + + +
+ + +
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + + {% block content %}{% endblock %} +
+
+ + + + {% block scripts %}{% endblock %} + + \ No newline at end of file diff --git a/templates/dashboard.html b/templates/dashboard.html new file mode 100644 index 00000000..3b73da58 --- /dev/null +++ b/templates/dashboard.html @@ -0,0 +1,438 @@ +{% extends "base.html" %} + +{% block title %}Dashboard - SimpleMonitor{% endblock %} + +{% block content %} + +
+
+
+
+

+ Dashboard +

+

Monitor your network infrastructure at a glance

+
+
+ + Real-time View + + {% if session.is_admin %} + + Add Monitor + + {% endif %} +
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
{{ monitors|length }}
+
Total Monitors
+
+
+
+
+
+
+
+
+
+ +
+
+
+
{{ monitors|selectattr('enabled')|list|length }}
+
Healthy
+
+
+
+
+
+
+
+
+
+ +
+
+
+
0
+
Failed
+
+
+
+
+
+
+
+
+
+ +
+
+
+
{{ monitors|rejectattr('enabled')|list|length }}
+
Disabled
+
+
+
+
+
+ + +
+
+
+
+
+ Monitor Status Overview +
+
+
+ +
+
+
+
+
+
+
+ Status Distribution +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Recent Activity +
+
+
+
+
+ +

No recent activity to display

+
+
+
+
+
+
+
+
+
+ Quick Actions +
+
+
+
+ {% if session.is_admin %} + + Add New Monitor + + + Settings + + {% endif %} + + Real-time View + + + All Monitors + +
+
+
+
+
+ + +
+
+
+
+
+ Monitors Overview +
+ + View All + +
+
+ {% if monitors %} +
+ {% for monitor in monitors[:6] %} +
+
+
+
+ {% if monitor.monitor_type == 'http' %} + + {% elif monitor.monitor_type == 'host' %} + + {% elif monitor.monitor_type == 'dns' %} + + {% elif monitor.monitor_type == 'diskspace' %} + + {% elif monitor.monitor_type == 'memory' %} + + {% elif monitor.monitor_type == 'process' %} + + {% else %} + + {% endif %} +
+
+
{{ monitor.name }}
+
+ {% if monitor.enabled %} + Active + {% else %} + Disabled + {% endif %} + {{ monitor.monitor_type|title }} +
+
+
+ {% if monitor.sms_enabled %} + + {% endif %} +
+
+
+ + + {{ monitor.created_at.strftime('%Y-%m-%d') }} + + + + +
+
+
+ {% endfor %} +
+ {% else %} +
+ +
No Monitors Configured
+

Get started by adding your first monitor

+ {% if session.is_admin %} + + Add Monitor + + {% endif %} +
+ {% endif %} +
+
+
+
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/edit_monitor.html b/templates/edit_monitor.html new file mode 100644 index 00000000..29f6fa64 --- /dev/null +++ b/templates/edit_monitor.html @@ -0,0 +1,194 @@ +{% extends "base.html" %} + +{% block title %}Edit Monitor{% endblock %} + +{% block content %} +
+
+

Edit Monitor

+

Modify monitor configuration

+
+
+ +
+
+
+
+
Monitor Configuration
+
+
+
+
+ + +
A unique name for this monitor
+
+ +
+ + +
+ +
+
+ + +
+
+ +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+ + +
Include country code (e.g., +1 for US)
+
+
+ +
+ + Cancel + + +
+
+
+
+
+ +
+
+
+
Monitor Info
+
+
+
+
Created:
+
{{ monitor.created_at.strftime('%Y-%m-%d %H:%M') }}
+ +
Last Updated:
+
{{ monitor.updated_at.strftime('%Y-%m-%d %H:%M') }}
+ +
Status:
+
+ {% if monitor.enabled %} + Enabled + {% else %} + Disabled + {% endif %} +
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 00000000..07928ccd --- /dev/null +++ b/templates/index.html @@ -0,0 +1,356 @@ +{% extends "base.html" %} + +{% block title %}SimpleMonitor - Professional Network Monitoring{% endblock %} + +{% block content %} + +
+
+
+
+

+ Professional Network Monitoring + Made Simple +

+

+ Monitor your network infrastructure with ease. Get real-time alerts, + beautiful dashboards, and comprehensive reporting - all in one powerful platform. +

+
+ {% if not session.user_id %} + + Get Started + + + Login + + {% else %} + + Dashboard + + + Real-time View + + {% endif %} +
+
+
+
--
+
Uptime
+
+
+
--
+
Monitors
+
+
+
--
+
Alerts
+
+
+
+
+
+
+
+ +
Live Monitoring
+

Real-time status updates

+
+
+
+
+
+
+
+ + +
+
+
+
+ +
+

Real-time Monitoring

+

+ Monitor your network infrastructure in real-time with instant alerts + and comprehensive status reporting. +

+
+
+
+
+
+ +
+

Mobile Alerts

+

+ Get instant SMS notifications when issues occur. Never miss a critical + alert with our Twilio integration. +

+
+
+
+
+
+ +
+

Beautiful Dashboards

+

+ Visualize your monitoring data with stunning charts and graphs. + Get insights at a glance with our modern interface. +

+
+
+
+
+
+ +
+

Easy Configuration

+

+ Set up monitors in minutes with our intuitive web interface. + No complex configuration files needed. +

+
+
+
+
+
+ +
+

Secure & Reliable

+

+ Built with security in mind. User authentication, encrypted communications, + and enterprise-grade reliability. +

+
+
+
+
+
+ +
+

Docker Ready

+

+ Deploy with Docker Compose in minutes. Pre-configured containers + for easy setup and maintenance. +

+
+
+
+ + +
+
+
+
0
+
Active Monitors
+
+
+
+
+
0%
+
System Uptime
+
+
+
+
+
0
+
Alerts Sent
+
+
+
+
+
0ms
+
Avg Response
+
+
+
+ + +
+
+
+ Supported Monitor Types +
+
+
+
+
+
+ +
+
HTTP/HTTPS
+
Web services
+
+
+
+
+
+ +
+
Ping
+
Network hosts
+
+
+
+
+
+ +
+
DNS
+
Domain resolution
+
+
+
+
+
+ +
+
Disk Space
+
Storage monitoring
+
+
+
+
+
+ +
+
Memory
+
RAM usage
+
+
+
+
+
+ +
+
Process
+
Running services
+
+
+
+
+
+ +
+
Command
+
Custom scripts
+
+
+
+
+
+ +
+
More...
+
Extensible
+
+
+
+
+
+
+ + +{% if not session.user_id %} +
+
+

Ready to Get Started?

+

Set up your monitoring in minutes with our easy-to-use interface.

+ + Create Admin Account + +
+
+{% endif %} +{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 00000000..8e3d13b0 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,238 @@ +{% extends "base.html" %} + +{% block title %}Login - SimpleMonitor{% endblock %} + +{% block content %} +
+
+
+
+ +
+
+ +
+

Welcome Back

+

Sign in to your SimpleMonitor account

+
+ + +
+
+ + +
+ Please provide a valid username. +
+
+ +
+ +
+ + +
+
+ Please provide a valid password. +
+
+ +
+ +
+
+ + +
+

+ Don't have an account? + + Create Admin Account + +

+
+
+
+ + +
+
+
+
+ +
+
Real-time Monitoring
+
+
+
+
+
+ +
+
Instant Alerts
+
+
+
+
+
+ +
+
Analytics
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/monitors.html b/templates/monitors.html new file mode 100644 index 00000000..b484ed23 --- /dev/null +++ b/templates/monitors.html @@ -0,0 +1,626 @@ +{% extends "base.html" %} + +{% block title %}Monitors - SimpleMonitor{% endblock %} + +{% block content %} + +
+
+
+
+

+ Monitors +

+

Manage and monitor your network infrastructure

+
+ {% if session.is_admin %} + + Add Monitor + + {% endif %} +
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+
+
+ + +
+
+
+
+
+
+ +
+
+
+
{{ monitors|length }}
+
Total Monitors
+
+
+
+
+
+
+
+
+
+ +
+
+
+
{{ monitors|selectattr('enabled')|list|length }}
+
Healthy
+
+
+
+
+
+
+
+
+
+ +
+
+
+
0
+
Failed
+
+
+
+
+
+
+
+
+
+ +
+
+
+
{{ monitors|rejectattr('enabled')|list|length }}
+
Disabled
+
+
+
+
+
+ + +
+
+
+ Monitors List +
+
+ {{ monitors|length }} monitors +
+ + + + +
+
+
+
+ +
+
+ {% for monitor in monitors %} +
+
+
+
+ {% if monitor.monitor_type == 'http' %} + + {% elif monitor.monitor_type == 'host' %} + + {% elif monitor.monitor_type == 'dns' %} + + {% elif monitor.monitor_type == 'diskspace' %} + + {% elif monitor.monitor_type == 'memory' %} + + {% elif monitor.monitor_type == 'process' %} + + {% elif monitor.monitor_type == 'command' %} + + {% else %} + + {% endif %} +
+
+
{{ monitor.name }}
+
+ {% if monitor.enabled %} + Active + {% else %} + Disabled + {% endif %} + {{ monitor.monitor_type|title }} +
+ {% if monitor.sms_enabled %} +
+ + SMS Alerts Enabled +
+ {% endif %} +
+
+ +
+
+
+ + + {{ monitor.created_at.strftime('%Y-%m-%d') }} + + + + +
+
+
+ {% endfor %} +
+
+ + +
+
+ + + + + + + + + + + + + {% for monitor in monitors %} + + + + + + + + + {% endfor %} + +
NameTypeStatusCreatedSMSActions
+
+ {% if monitor.monitor_type == 'http' %} + + {% elif monitor.monitor_type == 'host' %} + + {% elif monitor.monitor_type == 'dns' %} + + {% elif monitor.monitor_type == 'diskspace' %} + + {% elif monitor.monitor_type == 'memory' %} + + {% elif monitor.monitor_type == 'process' %} + + {% elif monitor.monitor_type == 'command' %} + + {% else %} + + {% endif %} +
+
{{ monitor.name }}
+ {% if monitor.sms_enabled %} + + SMS Enabled + + {% endif %} +
+
+
+ {{ monitor.monitor_type|title }} + + {% if monitor.enabled %} + Active + {% else %} + Disabled + {% endif %} + + {{ monitor.created_at.strftime('%Y-%m-%d') }} + + {% if monitor.sms_enabled %} + + {% else %} + + {% endif %} + +
+ + + + {% if session.is_admin %} + + + +
+ +
+
+ +
+ {% endif %} +
+
+
+
+
+
+ + +
+ +
No Monitors Found
+

Try adjusting your search or filter criteria

+ +
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/realtime_dashboard.html b/templates/realtime_dashboard.html new file mode 100644 index 00000000..1c163dcb --- /dev/null +++ b/templates/realtime_dashboard.html @@ -0,0 +1,671 @@ +{% extends "base.html" %} + +{% block title %}Real-time Dashboard - SimpleMonitor{% endblock %} + +{% block content %} + +
+
+
+
+

+ Real-time Dashboard +

+

Live monitoring with instant updates and alerts

+
+
+ + + +
+
+
+
+ + +
+
+
+
+
+
+
0
+
Healthy
+
+
+
0
+
Failed
+
+
+
0
+
Total
+
+
+
+
+
+
100% Healthy
+
+
+
+
+
+ + Last updated: Never +
+
+ + Auto-refresh: Disabled +
+
+
+
+
+ + +
+
+
+
+
+ Monitor Status Over Time +
+
+
+ +
+
+
+
+
+
+
+ Status Distribution +
+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Average Response Time +
+
+
+ +
+
+
+
+
+
+
+ Alert History +
+
+
+ +
+
+
+
+ + +
+
+
+ Live Monitor Status +
+
+ + Live + + Never updated +
+
+
+ +
+
+ Loading... +
+

Loading monitoring data...

+
+ + + + + +
+
+ + + + + + + + + + + + + + + + +
MonitorStatusHostResponse TimeUptimeFailuresLast FailureAge
+
+
+
+
+ + +
+
+
+ Auto-refresh Settings +
+
+
+
+
+ + +
+
+ +
+ Disabled + +
+
+
+
+
+{% endblock %} + +{% block scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 00000000..64ed8a81 --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,122 @@ +{% extends "base.html" %} + +{% block title %}Settings{% endblock %} + +{% block content %} +
+
+

Settings

+

Configure system settings and API keys

+
+
+ +
+
+
+
+
+ API Configuration +
+
+
+
+
+
+ Twilio SMS Configuration +
+

Configure Twilio for SMS alerts when monitors fail

+ +
+
+
+ + +
Your Twilio Account SID
+
+
+
+
+ + +
Your Twilio Auth Token
+
+
+
+ +
+ + +
Your Twilio phone number (include country code)
+
+
+ +
+ +
+ + Back to Dashboard + + +
+
+
+
+
+ +
+
+
+
+ Setup Instructions +
+
+
+
Twilio Setup:
+
    +
  1. Sign up for a Twilio account
  2. +
  3. Get your Account SID and Auth Token from the console
  4. +
  5. Purchase a phone number for sending SMS
  6. +
  7. Enter the credentials above
  8. +
+ +
+ +
Monitor Configuration:
+
    +
  • Add monitors through the "Add Monitor" page
  • +
  • Enable SMS alerts for individual monitors
  • +
  • Configuration is automatically reloaded
  • +
+
+
+ +
+
+
+ System Status +
+
+
+
+
Flask App:
+
Running
+ +
SimpleMonitor:
+
Running
+ +
Web Server:
+
Running
+ +
Database:
+
Connected
+
+
+
+
+
+{% endblock %} diff --git a/templates/setup.html b/templates/setup.html new file mode 100644 index 00000000..8a90296b --- /dev/null +++ b/templates/setup.html @@ -0,0 +1,375 @@ +{% extends "base.html" %} + +{% block title %}Setup - SimpleMonitor{% endblock %} + +{% block content %} +
+
+
+
+ +
+
+ +
+

Setup SimpleMonitor

+

Create your admin account to get started

+
+ + +
+
+ + +
+ Please provide a valid username. +
+
+ + Choose a secure username for your admin account +
+
+ +
+ +
+ + +
+
+ Please provide a valid password. +
+
+ + Use a strong password with at least 8 characters +
+
+ +
+ +
+ + +
+
+ Passwords do not match. +
+
+ + +
+
+ Password Strength + Weak +
+
+
+
+
+ +
+ +
+
+ + +
+
+ +
+
Security Notice
+

+ This will create the first admin account for your SimpleMonitor instance. + Make sure to use a strong password and keep your credentials secure. +

+
+
+
+ + +
+
+
+
+ +
+
Real-time Monitoring
+
+
+
+
+
+ +
+
SMS Alerts
+
+
+
+
+
+ +
+
Analytics
+
+
+
+
+
+
+
+{% endblock %} + +{% block scripts %} + + + +{% endblock %} \ No newline at end of file diff --git a/templates/status_fallback.html b/templates/status_fallback.html new file mode 100644 index 00000000..a3e3fb96 --- /dev/null +++ b/templates/status_fallback.html @@ -0,0 +1,52 @@ +{% extends "base.html" %} + +{% block title %}SimpleMonitor Status{% endblock %} + +{% block content %} +
+
+
+
+
+ SimpleMonitor Status +
+
+
+
+ + SimpleMonitor is running. The web interface is available at + http://localhost:8000 +
+ +
+ +
+
+
+
System Information
+ + Flask Web Interface: Running
+ SimpleMonitor: Running
+ Web Server: Running on port 8000 +
+
+
+
+
+
+
+
+
+{% endblock %} diff --git a/templates/view_monitor.html b/templates/view_monitor.html new file mode 100644 index 00000000..6e28ebff --- /dev/null +++ b/templates/view_monitor.html @@ -0,0 +1,133 @@ +{% extends "base.html" %} + +{% block title %}{{ monitor.name }} - Monitor Details{% endblock %} + +{% block content %} +
+
+
+

{{ monitor.name }}

+
+ {% if session.is_admin %} + + Edit + + {% endif %} + + Back to Monitors + +
+
+
+
+ +
+
+
+
+
Monitor Configuration
+
+
+
+
Name:
+
+ {{ monitor.name }} + {% if not monitor.enabled %} + Disabled + {% endif %} +
+ +
Type:
+
+ {{ monitor.monitor_type }} +
+ +
Status:
+
+ {% if monitor.enabled %} + + Enabled + + {% else %} + + Disabled + + {% endif %} +
+ +
SMS Alerts:
+
+ {% if monitor.sms_enabled %} + + {{ monitor.sms_phone }} + + {% else %} + + Not configured + + {% endif %} +
+
+ + {% if config %} +
+
Configuration Details:
+
+ {% for key, value in config.items() %} +
{{ key|title|replace('_', ' ') }}:
+
+ {{ value }} +
+ {% endfor %} +
+ {% endif %} +
+
+
+ +
+
+
+
Monitor Information
+
+
+
+
Created:
+
{{ monitor.created_at.strftime('%Y-%m-%d %H:%M') }}
+ +
Last Updated:
+
{{ monitor.updated_at.strftime('%Y-%m-%d %H:%M') }}
+ +
Monitor ID:
+
{{ monitor.id }}
+
+
+
+ + {% if session.is_admin %} +
+
+
Quick Actions
+
+
+
+
+ +
+ +
+ +
+
+
+
+ {% endif %} +
+
+{% endblock %} diff --git a/test_direct_flask.py b/test_direct_flask.py new file mode 100644 index 00000000..0d30ded4 --- /dev/null +++ b/test_direct_flask.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +import requests +import json + +# Create a session to maintain cookies +session = requests.Session() + +# Test login +login_data = { + 'username': 'admin', + 'password': 'admin123' +} + +print("=== Testing Login ===") +try: + login_response = session.post('http://localhost:5001/login', data=login_data) + print(f"Login Status Code: {login_response.status_code}") + + if login_response.status_code == 302: + print("✅ Login successful! Redirected to dashboard.") + elif login_response.status_code == 200: + print("❌ Login failed - returned login page") + else: + print(f"Unexpected login response: {login_response.status_code}") + +except Exception as e: + print(f"Error during login: {e}") + +print("\n=== Testing Add Monitor ===") +# Test data for adding a monitor +monitor_data = { + 'name': 'Direct Flask Test Monitor', + 'monitor_type': 'host', + 'config_host': '8.8.8.8', + 'config_tolerance': '2' +} + +try: + add_response = session.post('http://localhost:5001/monitors/add', data=monitor_data) + print(f"Add Monitor Status Code: {add_response.status_code}") + + if add_response.status_code == 302: + print("✅ Monitor added successfully! Redirected to monitors page.") + elif add_response.status_code == 200: + print("❌ Form validation error - returned add monitor page") + # Check if there are any error messages in the response + if 'error' in add_response.text.lower() or 'invalid' in add_response.text.lower(): + print("Response contains error messages") + else: + print(f"Unexpected status: {add_response.status_code}") + +except Exception as e: + print(f"Error: {e}") + +# Check if monitor was added +print("\n=== Checking Monitors ===") +try: + monitors_response = session.get('http://localhost:5001/monitors') + if 'Direct Flask Test Monitor' in monitors_response.text: + print("✅ Test monitor found in monitors page") + else: + print("❌ No test monitor found") + +except Exception as e: + print(f"Error checking monitors: {e}") diff --git a/webapp.py b/webapp.py new file mode 100644 index 00000000..7298b054 --- /dev/null +++ b/webapp.py @@ -0,0 +1,810 @@ +#!/usr/bin/env python3 +""" +SimpleMonitor Web Interface +A Flask-based web interface for managing SimpleMonitor configurations +""" + +import os +import json +import configparser +import subprocess +import signal +import time +import hashlib +from datetime import datetime +from flask import Flask, render_template, request, redirect, url_for, flash, jsonify, session +from flask_sqlalchemy import SQLAlchemy +from werkzeug.security import generate_password_hash, check_password_hash +from functools import wraps +import requests +from twilio.rest import Client +from bs4 import BeautifulSoup + +app = Flask(__name__) +app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'dev-secret-key-change-in-production') +app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:////code/webapp/simplemonitor.db' +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + +db = SQLAlchemy(app) + +# Database Models +class User(db.Model): + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + password_hash = db.Column(db.String(120), nullable=False) + is_admin = db.Column(db.Boolean, default=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + +class Monitor(db.Model): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(120), unique=True, nullable=False) + monitor_type = db.Column(db.String(50), nullable=False) + config = db.Column(db.Text, nullable=False) # JSON config + enabled = db.Column(db.Boolean, default=True) + sms_enabled = db.Column(db.Boolean, default=False) + sms_phone = db.Column(db.String(20), nullable=True) + created_at = db.Column(db.DateTime, default=datetime.utcnow) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + + # Real-time status fields (not stored in DB, populated at runtime) + status = None + host = None + detail = None + uptime = None + failures = None + last_failure = None + age = None + +class Settings(db.Model): + id = db.Column(db.Integer, primary_key=True) + key = db.Column(db.String(100), unique=True, nullable=False) + value = db.Column(db.Text, nullable=False) + updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow) + +def sync_monitors_from_service(): + """Sync monitors from SimpleMonitor service to database""" + try: + # Get monitors from SimpleMonitor status page + response = requests.get('http://webserver:80', timeout=5) + if response.status_code == 200: + # Parse HTML to extract monitor data + soup = BeautifulSoup(response.text, 'html.parser') + table = soup.find('table', class_='table') + + if table: + # Get existing monitors from database + existing_monitors = {m.name: m for m in Monitor.query.all()} + + # Extract monitor data from table + rows = table.find_all('tr')[1:] # Skip header row + for row in rows: + cells = row.find_all('td') + if len(cells) >= 10: # Ensure we have enough columns + monitor_name = cells[0].get_text(strip=True) + status = cells[1].get_text(strip=True) + host = cells[2].get_text(strip=True) + failed_at = cells[3].get_text(strip=True) + vfc = cells[4].get_text(strip=True) + uptime = cells[5].get_text(strip=True) + detail = cells[6].get_text(strip=True) + failures = cells[7].get_text(strip=True) + last_failure = cells[8].get_text(strip=True) + age = cells[9].get_text(strip=True) + + # Check if monitor already exists in database + if monitor_name not in existing_monitors: + # Create new monitor in database + new_monitor = Monitor( + name=monitor_name, + monitor_type='unknown', # We don't have this info from HTML + enabled=True, + config=json.dumps({ + 'host': host, + 'detail': detail, + 'uptime': uptime, + 'failures': failures, + 'last_failure': last_failure, + 'age': age, + 'status': status, + 'failed_at': failed_at, + 'vfc': vfc + }), + sms_enabled=False, + sms_phone='', + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + db.session.add(new_monitor) + print(f"Added monitor '{monitor_name}' to database") + else: + # Update existing monitor with latest status + existing_monitor = existing_monitors[monitor_name] + existing_monitor.config = json.dumps({ + 'host': host, + 'detail': detail, + 'uptime': uptime, + 'failures': failures, + 'last_failure': last_failure, + 'age': age, + 'status': status, + 'failed_at': failed_at, + 'vfc': vfc + }) + existing_monitor.updated_at = datetime.utcnow() + print(f"Updated monitor '{monitor_name}' in database") + + # Commit changes + db.session.commit() + print("Monitor sync completed successfully") + return True + except Exception as e: + print(f"Error syncing monitors: {e}") + db.session.rollback() + return False + +# Monitor type definitions +MONITOR_TYPES = { + 'host': { + 'name': 'Host Ping', + 'description': 'Check if a host is pingable', + 'fields': [ + {'name': 'host', 'type': 'text', 'label': 'Host/IP', 'required': True, 'placeholder': '8.8.8.8'}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '2'}, + ] + }, + 'http': { + 'name': 'HTTP Check', + 'description': 'Check if a URL returns HTTP 200', + 'fields': [ + {'name': 'url', 'type': 'url', 'label': 'URL', 'required': True, 'placeholder': 'https://example.com'}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '1'}, + {'name': 'timeout', 'type': 'number', 'label': 'Timeout (seconds)', 'required': False, 'default': '5'}, + ] + }, + 'dns': { + 'name': 'DNS Check', + 'description': 'Check if a DNS record is resolvable', + 'fields': [ + {'name': 'record', 'type': 'text', 'label': 'DNS Record', 'required': True, 'placeholder': 'google.com'}, + {'name': 'record_type', 'type': 'select', 'label': 'Record Type', 'required': False, 'options': ['A', 'AAAA', 'MX', 'CNAME'], 'default': 'A'}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '1'}, + ] + }, + 'diskspace': { + 'name': 'Disk Space', + 'description': 'Check available disk space', + 'fields': [ + {'name': 'partition', 'type': 'text', 'label': 'Partition', 'required': True, 'placeholder': '/'}, + {'name': 'limit', 'type': 'text', 'label': 'Minimum Free Space', 'required': True, 'placeholder': '1G'}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '1'}, + ] + }, + 'memory': { + 'name': 'Memory Check', + 'description': 'Check available memory', + 'fields': [ + {'name': 'percent_free', 'type': 'number', 'label': 'Minimum Free %', 'required': True, 'placeholder': '10'}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '1'}, + ] + }, + 'process': { + 'name': 'Process Check', + 'description': 'Check if a process is running', + 'fields': [ + {'name': 'process_name', 'type': 'text', 'label': 'Process Name', 'required': True, 'placeholder': 'python3'}, + {'name': 'min_count', 'type': 'number', 'label': 'Minimum Count', 'required': False, 'default': '1'}, + {'name': 'max_count', 'type': 'number', 'label': 'Maximum Count', 'required': False}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '1'}, + ] + }, + 'command': { + 'name': 'Command Check', + 'description': 'Execute a command and check its output', + 'fields': [ + {'name': 'command', 'type': 'text', 'label': 'Command', 'required': True, 'placeholder': 'uptime'}, + {'name': 'result_regexp', 'type': 'text', 'label': 'Expected Regex', 'required': False}, + {'name': 'result_max', 'type': 'number', 'label': 'Max Result Value', 'required': False}, + {'name': 'tolerance', 'type': 'number', 'label': 'Tolerance', 'required': False, 'default': '1'}, + ] + } +} + +# Authentication decorator +def login_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + return redirect(url_for('login')) + return f(*args, **kwargs) + return decorated_function + +def admin_required(f): + @wraps(f) + def decorated_function(*args, **kwargs): + if 'user_id' not in session: + flash('Please log in to access this page', 'error') + return redirect(url_for('login')) + user = User.query.get(session['user_id']) + if not user: + flash('User not found', 'error') + session.clear() + return redirect(url_for('login')) + if not user.is_admin: + flash('Admin access required', 'error') + return redirect(url_for('dashboard')) + return f(*args, **kwargs) + return decorated_function + +# Routes +@app.route('/') +def index(): + """Landing page""" + return render_template('index.html') + +@app.route('/setup', methods=['GET', 'POST']) +def setup(): + """Setup admin account""" + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + if not username or not password: + flash('Username and password are required', 'error') + return render_template('setup.html') + + # Check if admin already exists + if User.query.filter_by(is_admin=True).first(): + flash('Admin account already exists', 'error') + return redirect(url_for('login')) + + # Create admin user + user = User( + username=username, + password_hash=generate_password_hash(password), + is_admin=True + ) + db.session.add(user) + db.session.commit() + + flash('Admin account created successfully', 'success') + return redirect(url_for('login')) + + # Check if admin already exists + if User.query.filter_by(is_admin=True).first(): + flash('Admin account already exists', 'error') + return redirect(url_for('login')) + + return render_template('setup.html') + +@app.route('/login', methods=['GET', 'POST']) +def login(): + """Login page""" + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + + user = User.query.filter_by(username=username).first() + + if user and check_password_hash(user.password_hash, password): + session['user_id'] = user.id + session['username'] = user.username + session['is_admin'] = user.is_admin + flash('Logged in successfully', 'success') + return redirect(url_for('dashboard')) + else: + flash('Invalid username or password', 'error') + + return render_template('login.html') + +@app.route('/logout') +def logout(): + """Logout""" + session.clear() + flash('Logged out successfully', 'success') + return redirect(url_for('index')) + +@app.route('/dashboard') +@login_required +def dashboard(): + """Main dashboard""" + monitors = Monitor.query.all() + return render_template('dashboard.html', monitors=monitors) + +@app.route('/realtime') +@login_required +def realtime_dashboard(): + """Real-time dashboard page""" + # Sync monitors from service first + sync_monitors_from_service() + return render_template('realtime_dashboard.html') + +@app.route('/monitors') +@login_required +def monitors(): + """Monitor management page""" + # Sync monitors from service first + sync_monitors_from_service() + + # Get monitors from database (now synced) + db_monitors = Monitor.query.all() + + # Get monitors from SimpleMonitor status for real-time data + try: + response = requests.get('http://localhost:5001/api/status', timeout=5) + if response.status_code == 200: + status_data = response.json() + if status_data['status'] == 'success': + # Create a mapping of monitor names to their real-time status + status_map = {m['name']: m for m in status_data['monitors']} + + # Update database monitors with real-time status + for monitor in db_monitors: + if monitor.name in status_map: + status_data = status_map[monitor.name] + monitor.status = status_data['status'] + monitor.host = status_data.get('host', '') + monitor.detail = status_data.get('detail', '') + monitor.uptime = status_data.get('uptime', '') + monitor.failures = status_data.get('failures', '') + monitor.last_failure = status_data.get('last_failure', '') + monitor.age = status_data.get('age', '') + + all_monitors = db_monitors + else: + all_monitors = db_monitors + else: + all_monitors = db_monitors + except: + all_monitors = db_monitors + + return render_template('monitors.html', monitors=all_monitors, monitor_types=MONITOR_TYPES) + +@app.route('/monitors/add', methods=['GET', 'POST']) +@admin_required +def add_monitor(): + """Add new monitor""" + if request.method == 'POST': + try: + # Debug: Print form data + print(f"Form data: {dict(request.form)}") + + name = request.form.get('name', '').strip() + monitor_type = request.form.get('monitor_type', '').strip() + + if not name: + flash('Monitor name is required', 'error') + return render_template('add_monitor.html', monitor_types=MONITOR_TYPES) + + if not monitor_type: + flash('Monitor type is required', 'error') + return render_template('add_monitor.html', monitor_types=MONITOR_TYPES) + + if monitor_type not in MONITOR_TYPES: + flash('Invalid monitor type', 'error') + return render_template('add_monitor.html', monitor_types=MONITOR_TYPES) + + sms_enabled = 'sms_enabled' in request.form + sms_phone = request.form.get('sms_phone', '').strip() + + # Build config from form data + config = {} + for key, value in request.form.items(): + if key.startswith('config_') and value: + config[key[7:]] = value # Remove 'config_' prefix + + # Validate required fields + required_fields = MONITOR_TYPES[monitor_type]['fields'] + for field in required_fields: + if field['required'] and field['name'] not in config: + flash(f"Field '{field['label']}' is required", 'error') + return render_template('add_monitor.html', monitor_types=MONITOR_TYPES, selected_type=monitor_type) + + # Create monitor + monitor = Monitor( + name=name, + monitor_type=monitor_type, + config=json.dumps(config), + sms_enabled=sms_enabled, + sms_phone=sms_phone + ) + + db.session.add(monitor) + db.session.commit() + flash('Monitor added successfully', 'success') + + # Reload SimpleMonitor configuration + reload_simplemonitor_config() + + return redirect(url_for('monitors')) + + except Exception as e: + db.session.rollback() + print(f"Error adding monitor: {str(e)}") + flash(f'Error adding monitor: {str(e)}', 'error') + return render_template('add_monitor.html', monitor_types=MONITOR_TYPES) + + return render_template('add_monitor.html', monitor_types=MONITOR_TYPES) + +@app.route('/monitors//edit', methods=['GET', 'POST']) +@admin_required +def edit_monitor(monitor_id): + """Edit monitor""" + monitor = Monitor.query.get_or_404(monitor_id) + + if request.method == 'POST': + monitor.name = request.form['name'] + monitor.monitor_type = request.form['monitor_type'] + monitor.sms_enabled = 'sms_enabled' in request.form + monitor.sms_phone = request.form.get('sms_phone', '') + + # Build config from form data + config = {} + for key, value in request.form.items(): + if key.startswith('config_') and value: + config[key[7:]] = value + + monitor.config = json.dumps(config) + monitor.updated_at = datetime.utcnow() + + try: + db.session.commit() + flash('Monitor updated successfully', 'success') + reload_simplemonitor_config() + return redirect(url_for('monitors')) + except Exception as e: + db.session.rollback() + flash(f'Error updating monitor: {str(e)}', 'error') + + config = json.loads(monitor.config) if monitor.config else {} + return render_template('edit_monitor.html', monitor=monitor, monitor_types=MONITOR_TYPES, config=config) + +@app.route('/monitors/') +@login_required +def view_monitor(monitor_id): + """View monitor details""" + monitor = Monitor.query.get_or_404(monitor_id) + config = json.loads(monitor.config) if monitor.config else {} + return render_template('view_monitor.html', monitor=monitor, config=config) + +@app.route('/monitors//toggle', methods=['POST']) +@admin_required +def toggle_monitor(monitor_id): + """Toggle monitor enabled/disabled status""" + monitor = Monitor.query.get_or_404(monitor_id) + + try: + monitor.enabled = not monitor.enabled + monitor.updated_at = datetime.utcnow() + db.session.commit() + + status = "enabled" if monitor.enabled else "disabled" + flash(f'Monitor {status} successfully', 'success') + reload_simplemonitor_config() + except Exception as e: + db.session.rollback() + flash(f'Error toggling monitor: {str(e)}', 'error') + + return redirect(url_for('monitors')) + +@app.route('/monitors//delete', methods=['POST']) +@admin_required +def delete_monitor(monitor_id): + """Delete monitor""" + monitor = Monitor.query.get_or_404(monitor_id) + + try: + db.session.delete(monitor) + db.session.commit() + flash('Monitor deleted successfully', 'success') + reload_simplemonitor_config() + except Exception as e: + db.session.rollback() + flash(f'Error deleting monitor: {str(e)}', 'error') + + return redirect(url_for('monitors')) + +@app.route('/settings', methods=['GET', 'POST']) +@admin_required +def settings(): + """Settings page""" + if request.method == 'POST': + # Update settings + twilio_sid = request.form.get('twilio_sid', '') + twilio_token = request.form.get('twilio_token', '') + twilio_from = request.form.get('twilio_from', '') + + settings_to_update = { + 'twilio_sid': twilio_sid, + 'twilio_token': twilio_token, + 'twilio_from': twilio_from + } + + for key, value in settings_to_update.items(): + setting = Settings.query.filter_by(key=key).first() + if setting: + setting.value = value + else: + setting = Settings(key=key, value=value) + db.session.add(setting) + + try: + db.session.commit() + flash('Settings updated successfully', 'success') + except Exception as e: + db.session.rollback() + flash(f'Error updating settings: {str(e)}', 'error') + + # Get current settings + settings = {} + for setting in Settings.query.all(): + settings[setting.key] = setting.value + + return render_template('settings.html', settings=settings) + +@app.route('/api/config-hash') +def api_config_hash(): + """API endpoint for SimpleMonitor to check configuration changes""" + return jsonify({'hash': get_config_hash()}) + +@app.route('/api/reload-config', methods=['POST']) +def api_reload_config(): + """API endpoint to trigger configuration reload""" + try: + reload_simplemonitor_config() + return jsonify({'status': 'success', 'message': 'Configuration reloaded'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + +@app.route('/api/status') +def api_status(): + """API endpoint to get current monitoring status""" + try: + # Try to get status from SimpleMonitor status page + status_url = 'http://webserver:80' + response = requests.get(status_url, timeout=5) + + if response.status_code == 200: + # Parse the HTML to extract status information + from bs4 import BeautifulSoup + soup = BeautifulSoup(response.text, 'html.parser') + + # Extract summary information + summary = soup.find('div', {'id': 'summary'}) + updated_info = soup.find('div', {'id': 'updated'}) + refresh_status = soup.find('div', {'id': 'refresh_status'}) + + # Extract monitor data from table + monitors_data = [] + table = soup.find('table', class_='table') + if table: + rows = table.find_all('tr')[1:] # Skip header row + for row in rows: + cells = row.find_all('td') + if len(cells) >= 10: # Ensure we have enough columns + detail_text = cells[6].get_text(strip=True) + + # Extract response time from detail field + response_time = None + response_time_ms = None + + # Look for patterns like "200 in 0.19s", "0.19s", "123ms", etc. + import re + time_patterns = [ + r'(\d+(?:\.\d+)?)\s*s(?:econds?)?', # "0.19s", "1.5s" + r'(\d+(?:\.\d+)?)\s*ms', # "123ms" + r'in\s+(\d+(?:\.\d+)?)\s*s', # "in 0.19s" + r'in\s+(\d+(?:\.\d+)?)\s*ms' # "in 123ms" + ] + + for pattern in time_patterns: + match = re.search(pattern, detail_text, re.IGNORECASE) + if match: + time_value = float(match.group(1)) + if 'ms' in pattern: + response_time_ms = int(time_value) + response_time = f"{response_time_ms}ms" + else: + response_time_ms = int(time_value * 1000) + response_time = f"{response_time_ms}ms" + break + + monitor_data = { + 'name': cells[0].get_text(strip=True), + 'status': cells[1].get_text(strip=True), + 'host': cells[2].get_text(strip=True), + 'failed_at': cells[3].get_text(strip=True), + 'vfc': cells[4].get_text(strip=True), + 'uptime': cells[5].get_text(strip=True), + 'detail': detail_text, + 'response_time': response_time or 'N/A', + 'response_time_ms': response_time_ms, + 'failures': cells[7].get_text(strip=True), + 'last_failure': cells[8].get_text(strip=True), + 'age': cells[9].get_text(strip=True) + } + monitors_data.append(monitor_data) + + # Extract summary stats + summary_text = summary.get_text() if summary else "" + ok_count = 0 + fail_count = 0 + + # Count OK and failed monitors + for monitor in monitors_data: + if 'OK' in monitor['status']: + ok_count += 1 + else: + fail_count += 1 + + return jsonify({ + 'status': 'success', + 'summary': { + 'ok': ok_count, + 'failed': fail_count, + 'total': len(monitors_data) + }, + 'updated': updated_info.get_text(strip=True) if updated_info else 'Unknown', + 'refresh_status': refresh_status.get_text(strip=True) if refresh_status else '', + 'monitors': monitors_data, + 'timestamp': int(time.time()) + }) + else: + return jsonify({'status': 'error', 'message': 'Failed to fetch status'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + +def get_config_hash(): + """Get hash of current configuration for change detection""" + try: + with open('monitor.ini', 'rb') as f: + monitor_hash = hashlib.md5(f.read()).hexdigest() + with open('monitors.ini', 'rb') as f: + monitors_hash = hashlib.md5(f.read()).hexdigest() + return f"{monitor_hash}:{monitors_hash}" + except FileNotFoundError: + return "no-config" + +def reload_simplemonitor_config(): + """Reload SimpleMonitor configuration""" + try: + # Generate new configuration files + generate_monitor_ini() + generate_monitors_ini() + + # Update configuration hash + config_hash = get_config_hash() + + # Try to reload SimpleMonitor using Docker API + try: + # Method 1: Try to send SIGHUP to the monitor container + result = subprocess.run([ + 'docker', 'exec', 'simplemonitor-monitor-1', + 'pkill', '-HUP', 'simplemonitor' + ], capture_output=True, text=True, timeout=10) + + if result.returncode == 0: + flash('SimpleMonitor configuration reloaded successfully', 'success') + else: + # Method 2: Restart the monitor container + subprocess.run([ + 'docker', 'restart', 'simplemonitor-monitor-1' + ], check=True, timeout=30) + flash('SimpleMonitor container restarted with new configuration', 'success') + + except subprocess.TimeoutExpired: + flash('Configuration reload timed out, but files were updated', 'warning') + except subprocess.CalledProcessError as e: + flash(f'Error reloading SimpleMonitor: {e}', 'error') + except FileNotFoundError: + # Docker not available, just update files + flash('Configuration files updated (Docker not available for reload)', 'info') + + except Exception as e: + flash(f'Error updating configuration: {str(e)}', 'error') + +def generate_monitor_ini(): + """Generate monitor.ini from database""" + config = configparser.ConfigParser() + + # Monitor section + config['monitor'] = { + 'interval': '60', + 'monitors': 'monitors.ini' + } + + # Reporting section + config['reporting'] = { + 'loggers': 'html,logfile' + } + + # HTML logger + config['html'] = { + 'type': 'html', + 'filename': 'index.html', + 'folder': 'html', + 'tz': 'UTC' + } + + # Log file logger + config['logfile'] = { + 'type': 'logfile', + 'filename': 'monitor.log' + } + + # Add SMS alerter if any monitors have SMS enabled + sms_monitors = Monitor.query.filter_by(sms_enabled=True).first() + if sms_monitors: + twilio_sid = Settings.query.filter_by(key='twilio_sid').first() + twilio_token = Settings.query.filter_by(key='twilio_token').first() + twilio_from = Settings.query.filter_by(key='twilio_from').first() + + if twilio_sid and twilio_token and twilio_from: + config['reporting']['alerters'] = 'sms' + config['sms'] = { + 'type': 'twilio_sms', + 'account_sid': twilio_sid.value, + 'auth_token': twilio_token.value, + 'target': '+1234567890', # This will be overridden per monitor + 'sender': twilio_from.value + } + + # Write to file + with open('monitor.ini', 'w') as f: + config.write(f) + +def generate_monitors_ini(): + """Generate monitors.ini from database""" + config = configparser.ConfigParser() + + # Defaults + config['defaults'] = { + 'tolerance': '2' + } + + # Add monitors from database + for monitor in Monitor.query.filter_by(enabled=True).all(): + monitor_config = json.loads(monitor.config) + monitor_config['type'] = monitor.monitor_type + + # Remove runtime data that shouldn't be in config file + runtime_fields = ['host', 'detail', 'uptime', 'failures', 'last_failure', 'age', 'status', 'failed_at', 'vfc'] + for field in runtime_fields: + monitor_config.pop(field, None) + + # Add SMS phone if enabled + if monitor.sms_enabled and monitor.sms_phone: + monitor_config['sms_phone'] = monitor.sms_phone + + config[monitor.name] = monitor_config + + # Write to file + with open('monitors.ini', 'w') as f: + config.write(f) + +def send_sms_alert(phone_number, message): + """Send SMS alert via Twilio""" + try: + twilio_sid = Settings.query.filter_by(key='twilio_sid').first() + twilio_token = Settings.query.filter_by(key='twilio_token').first() + twilio_from = Settings.query.filter_by(key='twilio_from').first() + + if not all([twilio_sid, twilio_token, twilio_from]): + return False + + client = Client(twilio_sid.value, twilio_token.value) + message = client.messages.create( + body=message, + from_=twilio_from.value, + to=phone_number + ) + return True + except Exception as e: + print(f"Error sending SMS: {e}") + return False + +if __name__ == '__main__': + with app.app_context(): + db.create_all() + print("Database tables created successfully") + app.run(host='0.0.0.0', port=5000, debug=False)