Dashboard Add top-right user dropdown using Syncfusion DropDownButton: shows username + role; menu entries “Profil” and “Abmelden”. Replace custom dropdown logic with Syncfusion component; position at header’s right edge. Update /logout page to call backend logout and redirect to /login (reliable user switching). Build/Config Add @syncfusion/ej2-react-splitbuttons and @syncfusion/ej2-splitbuttons dependencies. Update Vite optimizeDeps.include to pre-bundle splitbuttons and avoid import-analysis errors. Docs README: Rework Architecture Overview with clearer data flow: Listener consumes MQTT (discovery/heartbeats) and updates API. Scheduler reads from API and publishes events via MQTT to clients. Clients send via MQTT and receive via MQTT. Worker receives commands directly from API and reports results back (no MQTT). Explicit note: MariaDB is accessed exclusively by the API Server; Dashboard never talks to DB directly. README: Add SplitButtons to “Syncfusion Components Used”; add troubleshooting steps for @syncfusion/ej2-react-splitbuttons import issues (optimizeDeps + volume reset). Copilot instructions: Document header user menu and splitbuttons technical notes (deps, optimizeDeps, dev-container node_modules volume). Program info Bump to 2025.1.0-alpha.10 with changelog: UI: Header user menu (DropDownButton with username/role; Profil/Abmelden). Frontend: Syncfusion SplitButtons integration + Vite pre-bundling config. Fix: Added README guidance for splitbuttons import errors. No breaking changes.
297 lines
9.6 KiB
Python
297 lines
9.6 KiB
Python
from server.database import Session
|
|
from models.models import Client, ClientGroup
|
|
from flask import Blueprint, request, jsonify
|
|
from server.permissions import admin_or_higher
|
|
from server.mqtt_helper import publish_client_group, delete_client_group_message, publish_multiple_client_groups
|
|
import sys
|
|
sys.path.append('/workspace')
|
|
|
|
clients_bp = Blueprint("clients", __name__, url_prefix="/api/clients")
|
|
|
|
|
|
@clients_bp.route("/sync-all-groups", methods=["POST"])
|
|
@admin_or_higher
|
|
def sync_all_client_groups():
|
|
"""
|
|
Administrative Route: Synchronisiert alle bestehenden Client-Gruppenzuordnungen mit MQTT
|
|
Nützlich für die einmalige Migration bestehender Clients
|
|
"""
|
|
session = Session()
|
|
try:
|
|
# Alle aktiven Clients abrufen
|
|
clients = session.query(Client).filter(Client.is_active == True).all()
|
|
|
|
if not clients:
|
|
session.close()
|
|
return jsonify({"message": "Keine aktiven Clients gefunden", "synced": 0})
|
|
|
|
# Alle Clients synchronisieren
|
|
client_group_mappings = {
|
|
client.uuid: client.group_id for client in clients}
|
|
success_count, failed_count = publish_multiple_client_groups(
|
|
client_group_mappings)
|
|
|
|
session.close()
|
|
|
|
return jsonify({
|
|
"success": True,
|
|
"message": f"Synchronisation abgeschlossen",
|
|
"synced": success_count,
|
|
"failed": failed_count,
|
|
"total": len(clients)
|
|
})
|
|
|
|
except Exception as e:
|
|
session.close()
|
|
return jsonify({"error": f"Fehler bei der Synchronisation: {str(e)}"}), 500
|
|
|
|
|
|
@clients_bp.route("/without_description", methods=["GET"])
|
|
def get_clients_without_description():
|
|
session = Session()
|
|
clients = session.query(Client).filter(
|
|
(Client.description == None) | (Client.description == "")
|
|
).all()
|
|
result = [
|
|
{
|
|
"uuid": c.uuid,
|
|
"hardware_token": c.hardware_token,
|
|
"ip": c.ip,
|
|
"type": c.type,
|
|
"hostname": c.hostname,
|
|
"os_version": c.os_version,
|
|
"software_version": c.software_version,
|
|
"macs": c.macs,
|
|
"model": c.model,
|
|
"registration_time": c.registration_time.isoformat() if c.registration_time else None,
|
|
"last_alive": c.last_alive.isoformat() if c.last_alive else None,
|
|
"is_active": c.is_active,
|
|
"group_id": c.group_id,
|
|
}
|
|
for c in clients
|
|
]
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
@clients_bp.route("/<uuid>/description", methods=["PUT"])
|
|
@admin_or_higher
|
|
def set_client_description(uuid):
|
|
data = request.get_json()
|
|
description = data.get("description", "").strip()
|
|
if not description:
|
|
return jsonify({"error": "Beschreibung darf nicht leer sein"}), 400
|
|
session = Session()
|
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
|
if not client:
|
|
session.close()
|
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
|
|
|
client.description = description
|
|
session.commit()
|
|
|
|
# MQTT: Gruppenzuordnung publizieren (wichtig für neue Clients aus SetupMode)
|
|
mqtt_success = publish_client_group(client.uuid, client.group_id)
|
|
|
|
session.close()
|
|
|
|
response = {"success": True}
|
|
if not mqtt_success:
|
|
response["warning"] = "Beschreibung gespeichert, aber MQTT-Publishing fehlgeschlagen"
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
@clients_bp.route("", methods=["GET"])
|
|
def get_clients():
|
|
session = Session()
|
|
clients = session.query(Client).all()
|
|
result = [
|
|
{
|
|
"uuid": c.uuid,
|
|
"hardware_token": c.hardware_token,
|
|
"ip": c.ip,
|
|
"type": c.type,
|
|
"hostname": c.hostname,
|
|
"os_version": c.os_version,
|
|
"software_version": c.software_version,
|
|
"macs": c.macs,
|
|
"model": c.model,
|
|
"description": c.description,
|
|
"registration_time": c.registration_time.isoformat() if c.registration_time else None,
|
|
"last_alive": c.last_alive.isoformat() if c.last_alive else None,
|
|
"is_active": c.is_active,
|
|
"group_id": c.group_id,
|
|
}
|
|
for c in clients
|
|
]
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
@clients_bp.route("/group", methods=["PUT"])
|
|
@admin_or_higher
|
|
def update_clients_group():
|
|
data = request.get_json()
|
|
client_ids = data.get("client_ids", [])
|
|
group_id = data.get("group_id")
|
|
group_name = data.get("group_name")
|
|
|
|
if not isinstance(client_ids, list) or len(client_ids) == 0:
|
|
return jsonify({"error": "client_ids muss eine nicht-leere Liste sein"}), 400
|
|
|
|
session = Session()
|
|
|
|
# Bestimme Ziel-Gruppe: Priorität hat group_id, ansonsten group_name
|
|
group = None
|
|
if group_id is not None:
|
|
group = session.query(ClientGroup).filter_by(id=group_id).first()
|
|
if not group:
|
|
session.close()
|
|
return jsonify({"error": f"Gruppe mit id={group_id} nicht gefunden"}), 404
|
|
elif group_name:
|
|
group = session.query(ClientGroup).filter_by(name=group_name).first()
|
|
if not group:
|
|
session.close()
|
|
return jsonify({"error": f"Gruppe '{group_name}' nicht gefunden"}), 404
|
|
else:
|
|
session.close()
|
|
return jsonify({"error": "Entweder group_id oder group_name ist erforderlich"}), 400
|
|
|
|
# WICHTIG: group.id vor dem Schließen puffern, um DetachedInstanceError zu vermeiden
|
|
target_group_id = group.id
|
|
|
|
session.query(Client).filter(Client.uuid.in_(client_ids)).update(
|
|
{Client.group_id: target_group_id}, synchronize_session=False
|
|
)
|
|
session.commit()
|
|
session.close()
|
|
|
|
# MQTT: Gruppenzuordnungen für alle betroffenen Clients publizieren (nutzt gecachten target_group_id)
|
|
client_group_mappings = {
|
|
client_id: target_group_id for client_id in client_ids}
|
|
success_count, failed_count = publish_multiple_client_groups(
|
|
client_group_mappings)
|
|
|
|
response = {"success": True}
|
|
if failed_count > 0:
|
|
response[
|
|
"warning"] = f"Gruppenzuordnung gespeichert, aber {failed_count} MQTT-Publishing(s) fehlgeschlagen"
|
|
|
|
return jsonify(response)
|
|
|
|
|
|
@clients_bp.route("/<uuid>", methods=["PATCH"])
|
|
@admin_or_higher
|
|
def update_client(uuid):
|
|
data = request.get_json()
|
|
session = Session()
|
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
|
if not client:
|
|
session.close()
|
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
|
allowed_fields = ["description", "model"]
|
|
updated = False
|
|
for field in allowed_fields:
|
|
if field in data:
|
|
setattr(client, field, data[field])
|
|
updated = True
|
|
if updated:
|
|
session.commit()
|
|
result = {"success": True}
|
|
else:
|
|
result = {"error": "Keine gültigen Felder zum Aktualisieren übergeben"}
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
# Neue Route: Liefert die aktuelle group_id für einen Client
|
|
@clients_bp.route("/<uuid>/group", methods=["GET"])
|
|
def get_client_group(uuid):
|
|
session = Session()
|
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
|
if not client:
|
|
session.close()
|
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
|
group_id = client.group_id
|
|
session.close()
|
|
return jsonify({"group_id": group_id})
|
|
|
|
# Neue Route: Liefert alle Clients mit Alive-Status
|
|
|
|
|
|
@clients_bp.route("/with_alive_status", methods=["GET"])
|
|
def get_clients_with_alive_status():
|
|
session = Session()
|
|
clients = session.query(Client).all()
|
|
result = []
|
|
for c in clients:
|
|
result.append({
|
|
"uuid": c.uuid,
|
|
"description": c.description,
|
|
"ip": c.ip,
|
|
"last_alive": c.last_alive.isoformat() if c.last_alive else None,
|
|
"is_active": c.is_active,
|
|
"is_alive": bool(c.last_alive and c.is_active),
|
|
})
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
@clients_bp.route("/<uuid>/restart", methods=["POST"])
|
|
@admin_or_higher
|
|
def restart_client(uuid):
|
|
"""
|
|
Route to restart a specific client by UUID.
|
|
Sends an MQTT message to the broker to trigger the restart.
|
|
"""
|
|
import paho.mqtt.client as mqtt
|
|
import json
|
|
|
|
# MQTT broker configuration
|
|
MQTT_BROKER = "mqtt"
|
|
MQTT_PORT = 1883
|
|
MQTT_TOPIC = f"clients/{uuid}/restart"
|
|
|
|
# Connect to the database to check if the client exists
|
|
session = Session()
|
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
|
if not client:
|
|
session.close()
|
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
|
session.close()
|
|
|
|
# Send MQTT message
|
|
try:
|
|
mqtt_client = mqtt.Client()
|
|
mqtt_client.connect(MQTT_BROKER, MQTT_PORT)
|
|
payload = {"action": "restart"}
|
|
mqtt_client.publish(MQTT_TOPIC, json.dumps(payload))
|
|
mqtt_client.disconnect()
|
|
return jsonify({"success": True, "message": f"Restart signal sent to client {uuid}"}), 200
|
|
except Exception as e:
|
|
return jsonify({"error": f"Failed to send MQTT message: {str(e)}"}), 500
|
|
|
|
|
|
@clients_bp.route("/<uuid>", methods=["DELETE"])
|
|
@admin_or_higher
|
|
def delete_client(uuid):
|
|
session = Session()
|
|
client = session.query(Client).filter_by(uuid=uuid).first()
|
|
if not client:
|
|
session.close()
|
|
return jsonify({"error": "Client nicht gefunden"}), 404
|
|
|
|
session.delete(client)
|
|
session.commit()
|
|
session.close()
|
|
|
|
# MQTT: Retained message für gelöschten Client entfernen
|
|
mqtt_success = delete_client_group_message(uuid)
|
|
|
|
response = {"success": True}
|
|
if not mqtt_success:
|
|
response["warning"] = "Client gelöscht, aber MQTT-Message-Löschung fehlgeschlagen"
|
|
|
|
return jsonify(response)
|