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("/", 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("/", 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/", 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/", 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) @groups_bp.route("/order", methods=["GET"]) def get_group_order(): """Retrieve the saved group order from system settings.""" from models.models import SystemSetting session = Session() try: setting = session.query(SystemSetting).filter_by(key='group_order').first() if setting and setting.value: import json order = json.loads(setting.value) return jsonify({"order": order}) return jsonify({"order": None}) except Exception as e: print(f"Error loading group order: {e}") return jsonify({"order": None}) finally: session.close() @groups_bp.route("/order", methods=["POST"]) @require_role('admin') def save_group_order(): """Save the custom group order to system settings.""" from models.models import SystemSetting session = Session() try: data = request.get_json() order = data.get('order') if not order or not isinstance(order, list): return jsonify({"success": False, "error": "Invalid order data"}), 400 import json order_json = json.dumps(order) setting = session.query(SystemSetting).filter_by(key='group_order').first() if setting: setting.value = order_json else: setting = SystemSetting(key='group_order', value=order_json) session.add(setting) session.commit() return jsonify({"success": True}) except Exception as e: session.rollback() print(f"Error saving group order: {e}") return jsonify({"success": False, "error": str(e)}), 500 finally: session.close()