diff --git a/backend/app.py b/backend/app.py index 5dcb0f7..94ea62e 100755 --- a/backend/app.py +++ b/backend/app.py @@ -90,7 +90,9 @@ def init_db(): name TEXT NOT NULL, description TEXT NOT NULL, status INTEGER DEFAULT 0, - ip_address TEXT + ip_address TEXT, + last_seen TIMESTAMP, + connection_status TEXT DEFAULT 'unknown' ); CREATE TABLE IF NOT EXISTS job ( @@ -106,6 +108,15 @@ def init_db(): FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE ); + + CREATE TABLE IF NOT EXISTS socket_uptime ( + id TEXT PRIMARY KEY, + socket_id TEXT NOT NULL, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + status TEXT NOT NULL, + duration_seconds INTEGER, + FOREIGN KEY (socket_id) REFERENCES socket (id) ON DELETE CASCADE + ); ''') db.commit() @@ -356,11 +367,44 @@ def socket_to_dict(socket): latest_job = get_latest_job_for_socket(socket['id']) waiting_jobs = get_waiting_jobs_for_socket(socket['id']) + # Verbindungsstatus-Informationen + connection_status = socket.get('connection_status', 'unknown') + last_seen = socket.get('last_seen') + uptime_info = None + + if last_seen and connection_status == 'offline': + # Berechne wie lange die Steckdose offline ist + try: + last_seen_dt = datetime.datetime.fromisoformat(last_seen) + now = datetime.datetime.utcnow() + offline_duration = int((now - last_seen_dt).total_seconds()) + + # Formatiere die Offline-Zeit benutzerfreundlich + hours, remainder = divmod(offline_duration, 3600) + minutes, seconds = divmod(remainder, 60) + + uptime_info = { + 'offline_since': last_seen, + 'offline_duration': offline_duration, + 'offline_duration_formatted': f"{hours}h {minutes}m {seconds}s" + } + except (ValueError, TypeError): + # Wenn das Datum nicht geparst werden kann + uptime_info = { + 'offline_since': last_seen, + 'offline_duration': None, + 'offline_duration_formatted': "Unbekannt" + } + return { 'id': socket['id'], 'name': socket['name'], 'description': socket['description'], 'status': socket['status'], + 'ipAddress': socket.get('ip_address'), + 'connectionStatus': connection_status, + 'lastSeen': last_seen, + 'uptimeInfo': uptime_info, 'latestJob': job_to_dict(latest_job) if latest_job else None, 'waitingJobs': [job_to_dict(job) for job in waiting_jobs] if waiting_jobs else [] } @@ -510,6 +554,87 @@ def job_to_dict(job): 'remainingMinutes': calculate_remaining_time(job) } +# Socket Uptime-Überwachung +def log_socket_connection_event(socket_id, status, duration_seconds=None): + """Speichert ein Ereignis zum Verbindungsstatus einer Steckdose""" + event_id = str(uuid.uuid4()) + timestamp = datetime.datetime.utcnow().isoformat() + + db = get_db() + db.execute( + 'INSERT INTO socket_uptime (id, socket_id, timestamp, status, duration_seconds) VALUES (?, ?, ?, ?, ?)', + (event_id, socket_id, timestamp, status, duration_seconds) + ) + db.commit() + app.logger.info(f"Verbindungsstatus für Steckdose {socket_id} geändert: {status}") + + # Aktualisiere auch den Verbindungsstatus in der socket-Tabelle + db.execute( + 'UPDATE socket SET connection_status = ?, last_seen = ? WHERE id = ?', + (status, timestamp if status == 'online' else None, socket_id) + ) + db.commit() + + return event_id + +def get_socket_uptime_events(socket_id=None, limit=100): + """Ruft Verbindungsereignisse für eine oder alle Steckdosen ab""" + db = get_db() + + if socket_id: + rows = db.execute(''' + SELECT su.*, s.name, s.ip_address FROM socket_uptime su + JOIN socket s ON su.socket_id = s.id + WHERE su.socket_id = ? + ORDER BY su.timestamp DESC + LIMIT ? + ''', (socket_id, limit)).fetchall() + else: + rows = db.execute(''' + SELECT su.*, s.name, s.ip_address FROM socket_uptime su + JOIN socket s ON su.socket_id = s.id + ORDER BY su.timestamp DESC + LIMIT ? + ''', (limit,)).fetchall() + + return [dict(row) for row in rows] + +def check_socket_connection(socket_id): + """Überprüft die Verbindung zu einer Steckdose und aktualisiert den Status""" + socket = get_socket_by_id(socket_id) + if not socket or not socket['ip_address']: + return False + + previous_status = socket.get('connection_status', 'unknown') + last_seen = socket.get('last_seen') + + try: + device = get_socket_device(socket['ip_address']) + if device: + # Verbindung erfolgreich + if previous_status != 'online': + # Status hat sich von offline/unknown auf online geändert + duration = None + if previous_status == 'offline' and last_seen: + # Berechne die Dauer des Ausfalls + offline_since = datetime.datetime.fromisoformat(last_seen) + now = datetime.datetime.utcnow() + duration = int((now - offline_since).total_seconds()) + + log_socket_connection_event(socket_id, 'online', duration) + return True + else: + # Keine Verbindung möglich + if previous_status != 'offline': + # Status hat sich von online/unknown auf offline geändert + log_socket_connection_event(socket_id, 'offline') + return False + except Exception as e: + app.logger.error(f"Fehler bei der Überprüfung der Steckdose {socket['ip_address']}: {e}") + if previous_status != 'offline': + log_socket_connection_event(socket_id, 'offline') + return False + # Steckdosen-Steuerung mit PyP100 def get_socket_device(ip_address): try: @@ -1072,6 +1197,11 @@ def stats(): total_sockets = db.execute('SELECT COUNT(*) as count FROM socket').fetchone()['count'] available_sockets = db.execute('SELECT COUNT(*) as count FROM socket WHERE status = 0').fetchone()['count'] + # Verbindungsstatistiken + online_sockets = db.execute("SELECT COUNT(*) as count FROM socket WHERE connection_status = 'online'").fetchone()['count'] + offline_sockets = db.execute("SELECT COUNT(*) as count FROM socket WHERE connection_status = 'offline'").fetchone()['count'] + unknown_sockets = db.execute("SELECT COUNT(*) as count FROM socket WHERE connection_status = 'unknown' OR connection_status IS NULL").fetchone()['count'] + # Job-Statistiken total_jobs = db.execute('SELECT COUNT(*) as count FROM job').fetchone()['count'] @@ -1095,11 +1225,30 @@ def stats(): avg_duration_result = db.execute('SELECT AVG(duration_in_minutes) as avg FROM job').fetchone() avg_duration = int(avg_duration_result['avg']) if avg_duration_result['avg'] else 0 + # Steckdosen-Fehlerstatistiken (letzten 7 Tage) + seven_days_ago = (datetime.datetime.utcnow() - timedelta(days=7)).isoformat() + outages = db.execute(''' + SELECT COUNT(*) as count FROM socket_uptime + WHERE status = 'offline' + AND timestamp > ? + ''', (seven_days_ago,)).fetchone()['count'] + + # Steckdosen mit aktuellen Problemen + problem_sockets = db.execute(''' + SELECT s.name, s.connection_status, s.last_seen + FROM socket s + WHERE s.connection_status = 'offline' + ''').fetchall() + return jsonify({ 'printers': { 'total': total_sockets, 'available': available_sockets, - 'utilization_rate': (total_sockets - available_sockets) / total_sockets if total_sockets > 0 else 0 + 'utilization_rate': (total_sockets - available_sockets) / total_sockets if total_sockets > 0 else 0, + 'online': online_sockets, + 'offline': offline_sockets, + 'unknown': unknown_sockets, + 'connectivity_rate': online_sockets / total_sockets if total_sockets > 0 else 0 }, 'jobs': { 'total': total_jobs, @@ -1109,9 +1258,62 @@ def stats(): }, 'users': { 'total': total_users + }, + 'uptime': { + 'outages_last_7_days': outages, + 'problem_printers': [{'name': row['name'], 'status': row['connection_status'], 'last_seen': row['last_seen']} for row in problem_sockets] } }) +@app.route('/api/uptime', methods=['GET']) +@login_required +@admin_required +def uptime_stats(): + """Liefert detaillierte Uptime-Statistiken für das Dashboard.""" + socket_id = request.args.get('socket_id') + limit = int(request.args.get('limit', 100)) + + # Rufe die letzten Uptime-Ereignisse ab + events = get_socket_uptime_events(socket_id, limit) + + # Gruppiere Ereignisse nach Steckdose + sockets = {} + for event in events: + socket_id = event['socket_id'] + if socket_id not in sockets: + sockets[socket_id] = { + 'id': socket_id, + 'name': event['name'], + 'ip_address': event['ip_address'], + 'events': [] + } + + # Füge Ereignis zur Steckdosenliste hinzu + sockets[socket_id]['events'].append({ + 'id': event['id'], + 'timestamp': event['timestamp'], + 'status': event['status'], + 'duration_seconds': event['duration_seconds'] + }) + + # Hole den aktuellen Status aller Steckdosen + all_sockets = get_all_sockets() + current_status = {} + for socket in all_sockets: + current_status[socket['id']] = { + 'connection_status': socket.get('connection_status', 'unknown'), + 'last_seen': socket.get('last_seen') + } + + # Füge den aktuellen Status zu den Socket-Informationen hinzu + for socket_id, socket_data in sockets.items(): + if socket_id in current_status: + socket_data['current_status'] = current_status[socket_id] + + return jsonify({ + 'sockets': list(sockets.values()) + }) + # Regelmäßige Überprüfung der Jobs und automatische Abschaltung der Steckdosen def check_jobs(): """Überprüft abgelaufene Jobs und schaltet Steckdosen automatisch aus.""" @@ -1147,24 +1349,63 @@ def check_jobs(): app.logger.info(f"{len(expired_jobs)} abgelaufene Jobs überprüft, {handled_jobs} Steckdosen aktualisiert.") -# Hintergrund-Thread für das Job-Polling +def check_socket_connections(): + """Überprüft periodisch die Verbindung zu allen Steckdosen.""" + with app.app_context(): + sockets = get_all_sockets() + app.logger.info(f"Überprüfe Verbindungsstatus von {len(sockets)} Steckdosen") + + online_count = 0 + offline_count = 0 + + for socket in sockets: + if not socket['ip_address']: + continue # Überspringe Steckdosen ohne IP-Adresse + + is_online = check_socket_connection(socket['id']) + if is_online: + online_count += 1 + else: + offline_count += 1 + + app.logger.info(f"Verbindungsüberprüfung abgeschlossen: {online_count} online, {offline_count} offline") + +# Hintergrund-Thread für das Job-Polling und Steckdosen-Monitoring def background_job_checker(): - """Hintergrund-Thread, der regelmäßig abgelaufene Jobs überprüft.""" - app.logger.info("Starte Hintergrund-Thread für Job-Überprüfung") + """Hintergrund-Thread, der regelmäßig abgelaufene Jobs und Steckdosenverbindungen überprüft.""" + app.logger.info("Starte Hintergrund-Thread für Job-Überprüfung und Steckdosen-Monitoring") + + # Standardintervall für Socket-Überprüfungen (5 Minuten) + socket_check_interval = int(os.environ.get('SOCKET_CHECK_INTERVAL', '300')) + last_socket_check = 0 + while True: try: + # Überprüfe Jobs bei jedem Durchlauf check_jobs() + + # Überprüfe Steckdosen nur in längeren Intervallen + current_time = time.time() + if current_time - last_socket_check >= socket_check_interval: + check_socket_connections() + last_socket_check = current_time + except Exception as e: - app.logger.error(f"Fehler im Hintergrund-Thread für Job-Überprüfung: {e}") + app.logger.error(f"Fehler im Hintergrund-Thread: {e}") # Pause zwischen den Überprüfungen time.sleep(app.config['JOB_CHECK_INTERVAL']) -# CLI-Befehl für manuelle Ausführung +# CLI-Befehle für manuelle Ausführung @app.cli.command("check-jobs") def cli_check_jobs(): """CLI-Befehl zur manuellen Überprüfung abgelaufener Jobs.""" check_jobs() + +@app.cli.command("check-sockets") +def cli_check_sockets(): + """CLI-Befehl zur manuellen Überprüfung aller Steckdosenverbindungen.""" + check_socket_connections() @app.route('/api/job//status', methods=['GET']) def job_status(job_id): diff --git a/backend/templates/stats.html b/backend/templates/stats.html index cb06838..c711a71 100644 --- a/backend/templates/stats.html +++ b/backend/templates/stats.html @@ -18,9 +18,47 @@ -
-
API-Antwort:
-

+                
+                
+
+
+
+
Drucker mit Verbindungsproblemen
+
+
+
Keine Verbindungsprobleme festgestellt.
+
+
+
+
+ + +
+
+
+
+
Steckdosen-Verfügbarkeit
+
+
+
+ +
+ +
+
+
+
+ + +
+
+
Stats API-Antwort:
+

+                    
+
+
Uptime API-Antwort:
+

+                    
@@ -29,23 +67,48 @@ {% endblock %} {% block scripts %} + + {% endblock %} \ No newline at end of file