Files
infoscreen/server/routes/groups.py
RobbStarkAustria df9f29bc6a docs/dev: sync backend rework, MQTT, and devcontainer hygiene
README: add Versioning (unified SemVer, pre-releases, build metadata); emphasize UTC handling and streaming endpoint; add Dev Container notes (UI-only Remote Containers, npm ci, idempotent aliases)
TECH-CHANGELOG: backend rework notes (serialization camelCase, UTC normalization, streaming metadata); add component build metadata template (image tags/SHAs)
Copilot instructions: integrate maintenance guardrails; reinforce UTC and camelCase conventions; document MQTT topics and scheduler retained payload behavior
Devcontainer: map Remote Containers to UI; remove in-container install; switch to npm ci; make aliases idempotent
2025-11-29 15:35:13 +00:00

211 lines
6.5 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, timezone
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.
Clients send heartbeats every ~65s. Grace period allows 2 missed
heartbeats plus safety margin before marking offline.
"""
env = os.environ.get("ENV", "production").lower()
if env == "development" or env == "dev":
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_DEV", "180"))
return int(os.environ.get("HEARTBEAT_GRACE_PERIOD_PROD", "170"))
def _to_utc(dt: datetime) -> datetime:
if dt is None:
return None
if dt.tzinfo is None:
return dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
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
# Vergleiche immer in UTC und mit tz-aware Datetimes
last_alive_utc = _to_utc(last_alive_dt)
now_utc = datetime.now(timezone.utc)
return (now_utc - last_alive_utc) <= 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)