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.
196 lines
6.1 KiB
Python
196 lines
6.1 KiB
Python
from models.models import Client
|
|
# Neue Route: Liefert alle Gruppen mit zugehörigen Clients und deren Alive-Status
|
|
|
|
from server.database import Session
|
|
from models.models import ClientGroup
|
|
from flask import Blueprint, request, jsonify
|
|
from server.permissions import admin_or_higher, require_role
|
|
from sqlalchemy import func
|
|
import sys
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
|
|
sys.path.append('/workspace')
|
|
|
|
groups_bp = Blueprint("groups", __name__, url_prefix="/api/groups")
|
|
|
|
|
|
def get_grace_period():
|
|
"""Wählt die Grace-Periode abhängig von ENV."""
|
|
env = os.environ.get("ENV", "production").lower()
|
|
if env == "development" or env == "dev":
|
|
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_DEV", "15"))
|
|
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_PROD", "180"))
|
|
|
|
|
|
def is_client_alive(last_alive, is_active):
|
|
"""Berechnet, ob ein Client als alive gilt."""
|
|
if not last_alive or not is_active:
|
|
return False
|
|
grace_period = get_grace_period()
|
|
# last_alive kann ein String oder datetime sein
|
|
if isinstance(last_alive, str):
|
|
last_alive_str = last_alive[:-
|
|
1] if last_alive.endswith('Z') else last_alive
|
|
try:
|
|
last_alive_dt = datetime.fromisoformat(last_alive_str)
|
|
except Exception:
|
|
return False
|
|
else:
|
|
last_alive_dt = last_alive
|
|
return datetime.utcnow() - last_alive_dt <= timedelta(seconds=grace_period)
|
|
|
|
|
|
@groups_bp.route("", methods=["POST"])
|
|
@admin_or_higher
|
|
def create_group():
|
|
data = request.get_json()
|
|
name = data.get("name")
|
|
if not name or not name.strip():
|
|
return jsonify({"error": "Gruppenname erforderlich"}), 400
|
|
|
|
session = Session()
|
|
if session.query(ClientGroup).filter_by(name=name).first():
|
|
session.close()
|
|
return jsonify({"error": "Gruppe existiert bereits"}), 409
|
|
|
|
group = ClientGroup(name=name, is_active=True)
|
|
session.add(group)
|
|
session.commit()
|
|
result = {
|
|
"id": group.id,
|
|
"name": group.name,
|
|
"created_at": group.created_at.isoformat() if group.created_at else None,
|
|
"is_active": group.is_active,
|
|
}
|
|
session.close()
|
|
return jsonify(result), 201
|
|
|
|
|
|
@groups_bp.route("", methods=["GET"])
|
|
def get_groups():
|
|
session = Session()
|
|
groups = session.query(ClientGroup).all()
|
|
result = [
|
|
{
|
|
"id": g.id,
|
|
"name": g.name,
|
|
"created_at": g.created_at.isoformat() if g.created_at else None,
|
|
"is_active": g.is_active,
|
|
}
|
|
for g in groups
|
|
]
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
@groups_bp.route("/<int:group_id>", methods=["PUT"])
|
|
@admin_or_higher
|
|
def update_group(group_id):
|
|
data = request.get_json()
|
|
session = Session()
|
|
group = session.query(ClientGroup).filter_by(id=group_id).first()
|
|
if not group:
|
|
session.close()
|
|
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
|
if "name" in data:
|
|
group.name = data["name"]
|
|
if "is_active" in data:
|
|
group.is_active = bool(data["is_active"])
|
|
session.commit()
|
|
result = {
|
|
"id": group.id,
|
|
"name": group.name,
|
|
"created_at": group.created_at.isoformat() if group.created_at else None,
|
|
"is_active": group.is_active,
|
|
}
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
@groups_bp.route("/<int:group_id>", methods=["DELETE"])
|
|
@admin_or_higher
|
|
def delete_group(group_id):
|
|
session = Session()
|
|
group = session.query(ClientGroup).filter_by(id=group_id).first()
|
|
if not group:
|
|
session.close()
|
|
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
|
session.delete(group)
|
|
session.commit()
|
|
session.close()
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@groups_bp.route("/byname/<string:group_name>", methods=["DELETE"])
|
|
@admin_or_higher
|
|
def delete_group_by_name(group_name):
|
|
session = Session()
|
|
group = session.query(ClientGroup).filter_by(name=group_name).first()
|
|
if not group:
|
|
session.close()
|
|
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
|
session.delete(group)
|
|
session.commit()
|
|
session.close()
|
|
return jsonify({"success": True})
|
|
|
|
|
|
@groups_bp.route("/byname/<string:old_name>", methods=["PUT"])
|
|
@admin_or_higher
|
|
def rename_group_by_name(old_name):
|
|
data = request.get_json()
|
|
new_name = data.get("newName")
|
|
if not new_name or not new_name.strip():
|
|
return jsonify({"error": "Neuer Name erforderlich"}), 400
|
|
|
|
session = Session()
|
|
group = session.query(ClientGroup).filter_by(name=old_name).first()
|
|
if not group:
|
|
session.close()
|
|
return jsonify({"error": "Gruppe nicht gefunden"}), 404
|
|
|
|
# Prüfe, ob der neue Name schon existiert
|
|
if session.query(ClientGroup).filter(func.binary(ClientGroup.name) == new_name).first():
|
|
session.close()
|
|
return jsonify({"error": f'Gruppe mit dem Namen "{new_name}" existiert bereits', "duplicate_name": new_name}), 409
|
|
|
|
group.name = new_name
|
|
session.commit()
|
|
result = {
|
|
"id": group.id,
|
|
"name": group.name,
|
|
"created_at": group.created_at.isoformat() if group.created_at else None,
|
|
"is_active": group.is_active,
|
|
}
|
|
session.close()
|
|
return jsonify(result)
|
|
|
|
|
|
@groups_bp.route("/with_clients", methods=["GET"])
|
|
def get_groups_with_clients():
|
|
session = Session()
|
|
groups = session.query(ClientGroup).all()
|
|
result = []
|
|
for g in groups:
|
|
clients = session.query(Client).filter_by(group_id=g.id).all()
|
|
client_list = []
|
|
for c in clients:
|
|
client_list.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": is_client_alive(c.last_alive, c.is_active),
|
|
})
|
|
result.append({
|
|
"id": g.id,
|
|
"name": g.name,
|
|
"created_at": g.created_at.isoformat() if g.created_at else None,
|
|
"is_active": g.is_active,
|
|
"clients": client_list,
|
|
})
|
|
session.close()
|
|
return jsonify(result)
|