diff --git a/.gitignore b/.gitignore index 2eea525..b85d767 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,7 @@ -.env \ No newline at end of file +.env +__pycache__ + +static/mqttui-logo-svg.svg +static/mqttui-logo.png + +.DS_Store \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b7ba28..1fce653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.1.0] - 2024-08-24 +### Added +- Debug Bar feature for enhanced developer insights + - Real-time websocket connection/disconnect status + - MQTT connection status and last message details + - Request duration tracking + - Toggle functionality to show/hide the Debug Bar + ## [1.0.0] - 2024-08-19 ### Added - Initial release of MQTT Web Interface diff --git a/README.md b/README.md index 667b532..119b485 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ MQTT Web Interface is an open-source web application that provides a real-time v ![Application Screenshot](static/screenshot.png) +![Debug Screenshot](static/screenshot_1.png) + ## Features - Real-time visualization of MQTT topic hierarchy and message flow @@ -15,6 +17,7 @@ MQTT Web Interface is an open-source web application that provides a real-time v - Display of message statistics (connection count, topic count, message count) - Interactive network graph showing topic relationships - Dockerized for easy deployment +- Debug Bar ## Installation diff --git a/app.py b/app.py index 7979f6c..8d37343 100644 --- a/app.py +++ b/app.py @@ -1,13 +1,25 @@ __version__ = "1.0.0" from flask import Flask, render_template, request, jsonify, send_from_directory -from flask_socketio import SocketIO +from flask_socketio import SocketIO, emit import paho.mqtt.client as mqtt from datetime import datetime import os +from debug_bar import debug_bar, debug_bar_middleware +import logging app = Flask(__name__, static_url_path='/static') -socketio = SocketIO(app) +socketio = SocketIO(app, async_mode='threading') + +logging.basicConfig(level=logging.DEBUG) + +app.before_request(debug_bar_middleware) + +@app.after_request +def after_request(response): + debug_bar.record('request', 'status_code', response.status_code) + debug_bar.end_request() + return response # MQTT setup mqtt_client = mqtt.Client() @@ -19,17 +31,47 @@ messages = [] topics = set() connection_count = 0 +active_websockets = 0 error_log = [] +@socketio.on('connect') +def handle_connect(): + global active_websockets + active_websockets += 1 + debug_bar.record('performance', 'active_websockets', active_websockets) + logging.info(f"WebSocket connected. Total active: {active_websockets}") + +@socketio.on('disconnect') +def handle_disconnect(): + global active_websockets + active_websockets -= 1 + debug_bar.record('performance', 'active_websockets', active_websockets) + logging.info(f"WebSocket disconnected. Total active: {active_websockets}") + def on_connect(client, userdata, flags, rc): global connection_count + connection_status = 'Connected' if rc == 0 else f'Failed (rc: {rc})' + debug_bar.record('mqtt', 'connection_status', connection_status) + debug_bar.record('mqtt', 'broker', mqtt_broker) + debug_bar.record('mqtt', 'port', mqtt_port) + debug_bar.record('mqtt', 'username', mqtt_username if mqtt_username else 'Not set') + debug_bar.record('mqtt', 'password', 'Set' if mqtt_password else 'Not set') + if rc == 0: - print("Connected to MQTT broker") connection_count += 1 client.subscribe("#") # Subscribe to all topics + logging.info(f"Connected to MQTT broker. Total connections: {connection_count}") else: - print(f"Failed to connect to MQTT broker with code {rc}") - error_log.append(f"Failed to connect to MQTT broker with code {rc}") + error_message = f"Connection failed with code {rc}" + error_log.append(error_message) + debug_bar.record('mqtt', 'last_error', error_message) + logging.error(error_message) + +def on_disconnect(client, userdata, rc): + disconnect_reason = 'Clean disconnect' if rc == 0 else f'Unexpected disconnect (rc: {rc})' + debug_bar.record('mqtt', 'last_disconnect', disconnect_reason) + error_log.append(f"Disconnected: {disconnect_reason}") + logging.warning(f"Disconnected from MQTT broker: {disconnect_reason}") def on_message(client, userdata, msg): message = { @@ -42,9 +84,12 @@ def on_message(client, userdata, msg): if len(messages) > 100: messages.pop(0) socketio.emit('mqtt_message', message) + debug_bar.record('mqtt', 'last_message', message) + logging.debug(f"MQTT message received: {message}") mqtt_client.on_connect = on_connect mqtt_client.on_message = on_message +mqtt_client.on_disconnect = on_disconnect @app.route('/') def index(): @@ -55,6 +100,7 @@ def publish_message(): topic = request.form['topic'] message = request.form['message'] mqtt_client.publish(topic, message) + debug_bar.record('mqtt', 'last_publish', {'topic': topic, 'message': message}) return jsonify(success=True) @app.route('/stats') @@ -70,6 +116,30 @@ def get_stats(): def send_static(path): return send_from_directory('static', path) +@app.route('/debug-bar') +def get_debug_bar_data(): + try: + data = debug_bar.get_data() + return jsonify(data) + except Exception as e: + logging.error(f"Error fetching debug bar data: {e}") + return jsonify({"error": "Failed to fetch debug bar data"}), 500 + +@app.route('/toggle-debug-bar', methods=['POST']) +def toggle_debug_bar(): + if debug_bar.enabled: + debug_bar.disable() + else: + debug_bar.enable() + return jsonify(enabled=debug_bar.enabled) + +@app.route('/record-client-performance', methods=['POST']) +def record_client_performance(): + data = request.json + debug_bar.record('performance', 'page_load_time', f"{data['pageLoadTime']}ms") + debug_bar.record('performance', 'dom_ready_time', f"{data['domReadyTime']}ms") + return jsonify(success=True) + @app.route('/version') def get_version(): return jsonify({'version': __version__}) @@ -81,8 +151,9 @@ def get_version(): mqtt_client.connect(mqtt_broker, mqtt_port, 60) mqtt_client.loop_start() except Exception as e: - print(f"Failed to connect to MQTT broker: {str(e)}") - error_log.append(f"Failed to connect to MQTT broker: {str(e)}") + error_message = f"Failed to connect to MQTT broker: {str(e)}" + debug_bar.record('mqtt', 'connection_error', error_message) + error_log.append(error_message) + logging.error(error_message) - print(f"Starting MQTT Web Interface v{__version__}") - socketio.run(app, host='0.0.0.0', port=5000, debug=True) \ No newline at end of file + socketio.run(app, host='0.0.0.0', port=5000, debug=True, use_reloader=False) \ No newline at end of file diff --git a/debug_bar.py b/debug_bar.py new file mode 100644 index 0000000..1ee8b77 --- /dev/null +++ b/debug_bar.py @@ -0,0 +1,70 @@ +import time +import psutil +from flask import request +from threading import Lock +import logging + +class DebugBarPanel: + def __init__(self, name): + self.name = name + self.data = {} + + def record(self, key, value): + self.data[key] = value + + def get_data(self): + return self.data + +class DebugBar: + def __init__(self): + self.panels = {} + self.enabled = False + self.start_time = None + self.lock = Lock() + try: + self.process = psutil.Process() + except Exception as e: + logging.error(f"Failed to initialize psutil Process: {e}") + self.process = None + + def add_panel(self, name): + with self.lock: + if name not in self.panels: + self.panels[name] = DebugBarPanel(name) + + def record(self, panel, key, value): + with self.lock: + if panel in self.panels: + self.panels[panel].record(key, value) + + def start_request(self): + self.start_time = time.time() + + def end_request(self): + if self.start_time: + duration = time.time() - self.start_time + self.record('request', 'duration', f"{duration:.2f}s") + self.start_time = None + + def get_data(self): + with self.lock: + #self.update_performance_metrics() + return {name: panel.get_data() for name, panel in self.panels.items()} + + def enable(self): + self.enabled = True + + def disable(self): + self.enabled = False + +debug_bar = DebugBar() + +# Initialize default panels +debug_bar.add_panel('mqtt') +debug_bar.add_panel('request') +debug_bar.add_panel('performance') + +def debug_bar_middleware(): + debug_bar.start_request() + debug_bar.record('request', 'path', request.path) + debug_bar.record('request', 'method', request.method) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index fdd6493..71a57c5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ Flask==2.0.1 Flask-SocketIO==5.1.1 paho-mqtt==1.5.1 -Werkzeug==2.0.1 \ No newline at end of file +Werkzeug==2.0.1 +psutil==5.9.0 \ No newline at end of file diff --git a/static/screenshot_1.png b/static/screenshot_1.png new file mode 100644 index 0000000..c52076b Binary files /dev/null and b/static/screenshot_1.png differ diff --git a/static/script.js b/static/script.js index d050e78..ddc142a 100644 --- a/static/script.js +++ b/static/script.js @@ -216,9 +216,91 @@ document.getElementById('topic-filter').addEventListener('change', function(e) { document.getElementById('message-list').innerHTML = ''; }); + + +let debugBar; +let debugBarToggle; + +function initDebugBar() { + debugBar = document.createElement('div'); + debugBar.id = 'debug-bar'; + debugBar.style.display = 'none'; + document.body.appendChild(debugBar); + + debugBarToggle = document.createElement('button'); + debugBarToggle.id = 'debug-bar-toggle'; + debugBarToggle.innerHTML = '🐞 Debug'; + debugBarToggle.onclick = toggleDebugBar; + document.body.appendChild(debugBarToggle); + + const closeButton = document.createElement('button'); + closeButton.id = 'debug-bar-close'; + closeButton.innerHTML = '×'; + closeButton.onclick = closeDebugBar; + debugBar.appendChild(closeButton); + + updateDebugBar(); + setInterval(updateDebugBar, 1000); // Update every second +} + +function toggleDebugBar() { + fetch('/toggle-debug-bar', { method: 'POST' }) + .then(response => response.json()) + .then(data => { + debugBar.style.display = data.enabled ? 'block' : 'none'; + debugBarToggle.classList.toggle('active', data.enabled); + }); +} + +function closeDebugBar() { + debugBar.style.display = 'none'; + fetch('/toggle-debug-bar', { method: 'POST' }); + debugBarToggle.classList.remove('active'); +} + +function trackClientPerformance() { + const perfData = window.performance.timing; + const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart; + const domReadyTime = perfData.domContentLoadedEventEnd - perfData.navigationStart; + + fetch('/record-client-performance', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + pageLoadTime, + domReadyTime, + }), + }); +} + +function updateDebugBar() { + fetch('/debug-bar') + .then(response => response.json()) + .then(data => { + let content = '
'; + for (const [panelName, panelData] of Object.entries(data)) { + content += `

${panelName}

'; + } + content += '
'; + debugBar.innerHTML = content; + debugBar.appendChild(document.getElementById('debug-bar-close')); + }); +} document.addEventListener('DOMContentLoaded', function() { initChart(); initNetwork(); setInterval(updateChart, 1000); setInterval(updateStats, 5000); + initDebugBar(); + trackClientPerformance(); }); \ No newline at end of file diff --git a/static/styles.css b/static/styles.css index 4189f09..d70a98b 100644 --- a/static/styles.css +++ b/static/styles.css @@ -47,4 +47,113 @@ input[type="text"]:focus, textarea:focus, select:focus { outline: none; border-color: #60A5FA; box-shadow: 0 0 0 2px rgba(96, 165, 250, 0.2); +} + +/* Debug bar styles */ +#debug-bar { + position: fixed; + bottom: 0; + left: 0; + right: 0; + background-color: rgba(0, 0, 0, 0.95); + color: #fff; + padding: 15px; + font-family: 'Courier New', monospace; + font-size: 13px; + max-height: 60%; + overflow-y: auto; + z-index: 1000; + border-top: 2px solid #3b82f6; + box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.5); +} + +#debug-bar h2 { + margin-top: 0; + font-size: 18px; + color: #3b82f6; +} + +#debug-bar h3 { + margin-top: 10px; + font-size: 16px; + color: #60a5fa; + border-bottom: 1px solid #3b82f6; + padding-bottom: 5px; +} + +#debug-bar ul { + list-style-type: none; + padding-left: 0; +} + +#debug-bar li { + margin-bottom: 8px; + word-break: break-all; +} + +#debug-bar pre { + background-color: rgba(255, 255, 255, 0.1); + padding: 5px; + border-radius: 3px; + overflow-x: auto; +} + +#debug-bar-toggle { + position: fixed; + bottom: 10px; + right: 10px; + background-color: #3b82f6; + color: white; + border: none; + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + z-index: 1001; + font-weight: bold; + transition: background-color 0.3s; +} + +#debug-bar-toggle:hover { + background-color: #2563eb; +} + +#debug-bar-toggle.active { + background-color: #1e40af; +} + +#debug-bar-close { + position: absolute; + top: 5px; + right: 5px; + background: none; + border: none; + color: #fff; + font-size: 20px; + cursor: pointer; +} + +.debug-content { + display: flex; + flex-wrap: wrap; + gap: 10px; +} + +.debug-panel { + background-color: rgba(59, 130, 246, 0.1); + border-radius: 4px; + padding: 10px; + flex: 1 1 calc(33% - 10px); + min-width: 250px; + margin-bottom: 10px; +} + +.debug-panel h3 { + margin-top: 0; +} + +/* Responsive design for smaller screens */ +@media (max-width: 768px) { + .debug-panel { + flex: 1 1 100%; + } } \ No newline at end of file