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 = '
' + JSON.stringify(value, null, 2) + ''; + } + content += `