Complete Redesign of Backend Handling for Client Group Assignments

This commit is contained in:
2025-09-14 05:20:49 +00:00
parent c5a8571e97
commit e8d71b8349
10 changed files with 407 additions and 80 deletions

View File

@@ -1,21 +1,20 @@
{ {
"appName": "Infoscreen-Management", "appName": "Infoscreen-Management",
"version": "2025.1.0-alpha.4", "version": "2025.1.0-alpha.5",
"copyright": "© 2025 Third-Age-Applications", "copyright": "© 2025 Third-Age-Applications",
"supportContact": "support@third-age-applications.com", "supportContact": "support@third-age-applications.com",
"description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.", "description": "Eine zentrale Verwaltungsoberfläche für digitale Informationsbildschirme.",
"techStack": { "techStack": {
"frontend": "React, Vite, TypeScript, Tailwind CSS", "Frontend": "React, Vite, TypeScript",
"backend": "Python (Flask), SQLAlchemy", "Backend": "Python (Flask), SQLAlchemy",
"database": "MariaDB", "Database": "MariaDB",
"realtime": "Mosquitto (MQTT)", "Realtime": "Mosquitto (MQTT)",
"containerization": "Docker" "Containerization": "Docker"
}, },
"openSourceComponents": { "openSourceComponents": {
"frontend": [ "frontend": [
{ "name": "React", "license": "MIT" }, { "name": "React", "license": "MIT" },
{ "name": "Vite", "license": "MIT" }, { "name": "Vite", "license": "MIT" },
{ "name": "Tailwind CSS", "license": "MIT" },
{ "name": "Lucide Icons", "license": "ISC" }, { "name": "Lucide Icons", "license": "ISC" },
{ "name": "Syncfusion UI Components", "license": "Kommerziell / Community" } { "name": "Syncfusion UI Components", "license": "Kommerziell / Community" }
], ],
@@ -31,6 +30,13 @@
"commitId": "a1b2c3d4e5f6" "commitId": "a1b2c3d4e5f6"
}, },
"changelog": [ "changelog": [
{
"version": "2025.1.0-alpha.5",
"date": "2025-09-14",
"changes": [
"Komplettes Redesign des Backend-Handlings der Gruppenzuordnungen von neuen Clients und der Schritte bei Änderung der Gruppenzuordnung."
]
},
{ {
"version": "2025.1.0-alpha.4", "version": "2025.1.0-alpha.4",
"date": "2025-09-01", "date": "2025-09-01",

View File

@@ -59,11 +59,11 @@ export async function setClientDescription(uuid: string, description: string) {
return await res.json(); return await res.json();
} }
export async function updateClientGroup(clientIds: string[], groupName: string) { export async function updateClientGroup(clientIds: string[], groupId: number) {
const res = await fetch('/api/clients/group', { const res = await fetch('/api/clients/group', {
method: 'PUT', method: 'PUT',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ client_ids: clientIds, group_name: groupName }), body: JSON.stringify({ client_ids: clientIds, group_id: groupId }),
}); });
if (!res.ok) throw new Error('Fehler beim Aktualisieren der Clients'); if (!res.ok) throw new Error('Fehler beim Aktualisieren der Clients');
return await res.json(); return await res.json();

View File

@@ -72,7 +72,7 @@ L10n.load({
const Infoscreen_groups: React.FC = () => { const Infoscreen_groups: React.FC = () => {
const toast = useToast(); const toast = useToast();
const [clients, setClients] = useState<KanbanClient[]>([]); const [clients, setClients] = useState<KanbanClient[]>([]);
const [groups, setGroups] = useState<{ keyField: string; headerText: string }[]>([]); const [groups, setGroups] = useState<{ keyField: string; headerText: string; id?: number }[]>([]);
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const [newGroupName, setNewGroupName] = useState(''); const [newGroupName, setNewGroupName] = useState('');
const [draggedCard, setDraggedCard] = useState<{ id: string; fromColumn: string } | null>(null); const [draggedCard, setDraggedCard] = useState<{ id: string; fromColumn: string } | null>(null);
@@ -130,7 +130,10 @@ const Infoscreen_groups: React.FC = () => {
timeOut: 5000, timeOut: 5000,
showCloseButton: false, showCloseButton: false,
}); });
setGroups([...groups, { keyField: newGroup.name, headerText: newGroup.name }]); setGroups([
...groups,
{ keyField: newGroup.name, headerText: newGroup.name, id: newGroup.id },
]);
setNewGroupName(''); setNewGroupName('');
setShowDialog(false); setShowDialog(false);
} catch (err) { } catch (err) {
@@ -149,9 +152,12 @@ const Infoscreen_groups: React.FC = () => {
// Clients der Gruppe in "Nicht zugeordnet" verschieben // Clients der Gruppe in "Nicht zugeordnet" verschieben
const groupClients = clients.filter(c => c.Status === groupName); const groupClients = clients.filter(c => c.Status === groupName);
if (groupClients.length > 0) { if (groupClients.length > 0) {
// Ermittle die ID der Zielgruppe "Nicht zugeordnet"
const target = groups.find(g => g.headerText === 'Nicht zugeordnet');
if (!target || !target.id) throw new Error('Zielgruppe "Nicht zugeordnet" nicht gefunden');
await updateClientGroup( await updateClientGroup(
groupClients.map(c => c.Id), groupClients.map(c => c.Id),
'Nicht zugeordnet' target.id
); );
} }
await deleteGroup(groupName); await deleteGroup(groupName);
@@ -271,7 +277,10 @@ const Infoscreen_groups: React.FC = () => {
const clientIds = dropped.map((card: KanbanClient) => card.Id); const clientIds = dropped.map((card: KanbanClient) => card.Id);
try { try {
await updateClientGroup(clientIds, targetGroupName); // Ermittle Zielgruppen-ID anhand des Namens
const target = groups.find(g => g.headerText === targetGroupName);
if (!target || !target.id) throw new Error('Zielgruppe nicht gefunden');
await updateClientGroup(clientIds, target.id);
fetchGroups().then((groupData: Group[]) => { fetchGroups().then((groupData: Group[]) => {
const groupMap = Object.fromEntries(groupData.map(g => [g.id, g.name])); const groupMap = Object.fromEntries(groupData.map(g => [g.id, g.name]));
setGroups( setGroups(

View File

@@ -1,4 +1,4 @@
declare module "*.json" { declare module '*.json' {
const value: any; const value: unknown;
export default value; export default value;
} }

View File

@@ -6,6 +6,9 @@ WORKDIR /app
COPY listener/requirements.txt ./ COPY listener/requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt RUN pip install --no-cache-dir -r requirements.txt
# Mosquitto-Tools für MQTT-Tests installieren
RUN apt-get update && apt-get install -y --no-install-recommends mosquitto-clients && rm -rf /var/lib/apt/lists/*
COPY listener/ ./listener COPY listener/ ./listener
COPY models/ ./models COPY models/ ./models

View File

@@ -1,8 +1,6 @@
import os import os
import json import json
import logging import logging
import threading
import time
import datetime import datetime
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
from sqlalchemy import create_engine from sqlalchemy import create_engine
@@ -27,12 +25,11 @@ logging.basicConfig(level=logging.DEBUG,
engine = create_engine(DB_URL) engine = create_engine(DB_URL)
Session = sessionmaker(bind=engine) Session = sessionmaker(bind=engine)
# MQTT-Callback
def on_message(client, userdata, msg): def on_message(client, userdata, msg):
topic = msg.topic topic = msg.topic
logging.debug(f"Empfangene Nachricht auf Topic: {topic}") logging.debug(f"Empfangene Nachricht auf Topic: {topic}")
try: try:
# Heartbeat-Handling # Heartbeat-Handling
if topic.startswith("infoscreen/") and topic.endswith("/heartbeat"): if topic.startswith("infoscreen/") and topic.endswith("/heartbeat"):
@@ -41,57 +38,53 @@ def on_message(client, userdata, msg):
client_obj = session.query(Client).filter_by(uuid=uuid).first() client_obj = session.query(Client).filter_by(uuid=uuid).first()
if client_obj: if client_obj:
client_obj.last_alive = datetime.datetime.now(datetime.UTC) client_obj.last_alive = datetime.datetime.now(datetime.UTC)
session.commit() session.commit()
logging.info( logging.info(
f"Heartbeat von {uuid} empfangen, last_alive (UTC) aktualisiert.") f"Heartbeat von {uuid} empfangen, last_alive (UTC) aktualisiert.")
session.close() session.close()
return return
# Discovery-Handling # Discovery-Handling
payload = json.loads(msg.payload.decode()) if topic == "infoscreen/discovery":
logging.info(f"Discovery empfangen: {payload}") payload = json.loads(msg.payload.decode())
if "uuid" in payload: logging.info(f"Discovery empfangen: {payload}")
uuid = payload["uuid"]
session = Session() if "uuid" in payload:
existing = session.query(Client).filter_by(uuid=uuid).first() uuid = payload["uuid"]
if not existing: session = Session()
new_client = Client( existing = session.query(Client).filter_by(uuid=uuid).first()
uuid=uuid,
hardware_token=payload.get("hardware_token"), if not existing:
ip=payload.get("ip"), new_client = Client(
type=payload.get("type"), uuid=uuid,
hostname=payload.get("hostname"), hardware_token=payload.get("hardware_token"),
os_version=payload.get("os_version"), ip=payload.get("ip"),
software_version=payload.get("software_version"), type=payload.get("type"),
macs=",".join(payload.get("macs", [])), hostname=payload.get("hostname"),
model=payload.get("model"), os_version=payload.get("os_version"),
registration_time=datetime.datetime.now(datetime.UTC), software_version=payload.get("software_version"),
) macs=",".join(payload.get("macs", [])),
session.add(new_client) model=payload.get("model"),
session.commit() registration_time=datetime.datetime.now(datetime.UTC),
logging.info(f"Neuer Client registriert: {uuid}") )
session.add(new_client)
session.commit()
logging.info(f"Neuer Client registriert: {uuid}")
else:
logging.info(f"Client bereits bekannt: {uuid}")
session.close()
# Discovery-ACK senden
ack_topic = f"infoscreen/{uuid}/discovery_ack"
client.publish(ack_topic, json.dumps({"status": "ok"}))
logging.info(f"Discovery-ACK gesendet an {ack_topic}")
else: else:
logging.info(f"Client bereits bekannt: {uuid}") logging.warning("Discovery ohne UUID empfangen, ignoriert.")
session.close()
# Discovery-ACK senden
ack_topic = f"infoscreen/{uuid}/discovery_ack"
client.publish(ack_topic, json.dumps({"status": "ok"}))
logging.info(f"Discovery-ACK gesendet an {ack_topic}")
else:
logging.warning("Discovery ohne UUID empfangen, ignoriert.")
except Exception as e: except Exception as e:
logging.error(f"Fehler bei Verarbeitung: {e}") logging.error(f"Fehler bei Verarbeitung: {e}")
topic_parts = msg.topic.split('/')
if len(topic_parts) == 3 and topic_parts[0] == "infoscreen" and topic_parts[1] == "request_group_id":
client_id = topic_parts[2]
session = Session()
client_obj = session.query(Client).filter_by(uuid=client_id).first()
group_id = client_obj.group_id if client_obj else None
session.close()
response_topic = f"infoscreen/response_group_id/{client_id}"
client.publish(response_topic, json.dumps({"group_id": group_id}))
def main(): def main():
mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311, callback_api_version=2) mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311, callback_api_version=2)
@@ -99,9 +92,9 @@ def main():
mqtt_client.connect("mqtt", 1883) mqtt_client.connect("mqtt", 1883)
mqtt_client.subscribe("infoscreen/discovery") mqtt_client.subscribe("infoscreen/discovery")
mqtt_client.subscribe("infoscreen/+/heartbeat") mqtt_client.subscribe("infoscreen/+/heartbeat")
mqtt_client.subscribe("infoscreen/request_group_id/#")
logging.info( logging.info(
"Listener gestartet und abonniert auf infoscreen/discovery, infoscreen/+/heartbeat und infoscreen/request_group_id/#") "Listener gestartet und abonniert auf infoscreen/discovery und infoscreen/+/heartbeat")
mqtt_client.loop_forever() mqtt_client.loop_forever()

View File

@@ -32,15 +32,32 @@ logging.basicConfig(
def main(): def main():
# Fix für die veraltete API - explizit callback_api_version setzen # Fix für die veraltete API - explizit callback_api_version setzen
client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2) client = mqtt.Client(callback_api_version=mqtt.CallbackAPIVersion.VERSION2)
client.reconnect_delay_set(min_delay=1, max_delay=30)
POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen
# 0 = aus; z.B. 600 für alle 10 Min
REFRESH_SECONDS = int(os.getenv("REFRESH_SECONDS", "0"))
last_payloads = {} # group_id -> payload
last_published_at = {} # group_id -> epoch seconds
# Beim (Re-)Connect alle bekannten retained Payloads erneut senden
def on_connect(client, userdata, flags, reasonCode, properties=None):
logging.info(
f"MQTT connected (reasonCode={reasonCode}) - republishing {len(last_payloads)} groups")
for gid, payload in last_payloads.items():
topic = f"infoscreen/events/{gid}"
client.publish(topic, payload, retain=True)
client.on_connect = on_connect
client.connect("mqtt", 1883) client.connect("mqtt", 1883)
client.loop_start() client.loop_start()
POLL_INTERVAL = 30 # Sekunden, Empfehlung für seltene Änderungen
last_payloads = {} # group_id -> payload
while True: while True:
now = datetime.datetime.now(datetime.timezone.utc) now = datetime.datetime.now(datetime.timezone.utc)
# Hole alle aktiven Events (Vergleich mit UTC) # Hole alle aktiven Events (Vergleich mit UTC)
events = get_active_events(now, now) events = get_active_events(now, now)
# Gruppiere Events nach group_id # Gruppiere Events nach group_id
groups = {} groups = {}
for event in events: for event in events:
@@ -54,20 +71,32 @@ def main():
"end": str(getattr(event, "end", "")), "end": str(getattr(event, "end", "")),
"group_id": gid, "group_id": gid,
}) })
# Sende pro Gruppe die Eventliste als retained Message, nur bei Änderung # Sende pro Gruppe die Eventliste als retained Message, nur bei Änderung
for gid, event_list in groups.items(): for gid, event_list in groups.items():
# stabile Reihenfolge, um unnötige Publishes zu vermeiden
event_list.sort(key=lambda e: (e.get("start"), e.get("id")))
payload = json.dumps(
event_list, sort_keys=True, separators=(",", ":"))
topic = f"infoscreen/events/{gid}" topic = f"infoscreen/events/{gid}"
payload = json.dumps(event_list)
if last_payloads.get(gid) != payload: should_send = (last_payloads.get(gid) != payload)
if not should_send and REFRESH_SECONDS:
last_ts = last_published_at.get(gid, 0)
if time.time() - last_ts >= REFRESH_SECONDS:
should_send = True
if should_send:
result = client.publish(topic, payload, retain=True) result = client.publish(topic, payload, retain=True)
if result.rc != mqtt.MQTT_ERR_SUCCESS: if result.rc != mqtt.MQTT_ERR_SUCCESS:
logging.error( logging.error(
f"Fehler beim Publish für Gruppe {gid}: {mqtt.error_string(result.rc)}") f"Fehler beim Publish für Gruppe {gid}: {mqtt.error_string(result.rc)}")
else: else:
logging.info( logging.info(f"Events für Gruppe {gid} gesendet")
f"Events für Gruppe {gid} gesendet: {payload}") last_payloads[gid] = payload
last_payloads[gid] = payload last_published_at[gid] = time.time()
# Entferne Gruppen, die nicht mehr existieren (optional: retained Message löschen)
# Entferne Gruppen, die nicht mehr existieren (leere retained Message senden)
for gid in list(last_payloads.keys()): for gid in list(last_payloads.keys()):
if gid not in groups: if gid not in groups:
topic = f"infoscreen/events/{gid}" topic = f"infoscreen/events/{gid}"
@@ -78,7 +107,9 @@ def main():
else: else:
logging.info( logging.info(
f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)") f"Events für Gruppe {gid} entfernt (leere retained Message gesendet)")
del last_payloads[gid] del last_payloads[gid]
last_published_at.pop(gid, None)
time.sleep(POLL_INTERVAL) time.sleep(POLL_INTERVAL)

142
server/mqtt_helper.py Normal file
View File

@@ -0,0 +1,142 @@
"""
Einfache MQTT-Hilfsfunktion für Client-Gruppenzuordnungen
"""
import os
import json
import logging
import paho.mqtt.client as mqtt
logger = logging.getLogger(__name__)
def publish_client_group(client_uuid: str, group_id: int) -> bool:
"""
Publiziert die Gruppenzuordnung eines Clients als retained message
Args:
client_uuid: UUID des Clients
group_id: ID der Gruppe
Returns:
bool: True bei Erfolg, False bei Fehler
"""
try:
# MQTT-Konfiguration aus .env
broker_host = os.getenv("MQTT_BROKER_HOST", "mqtt")
broker_port = int(os.getenv("MQTT_BROKER_PORT", 1883))
username = os.getenv("MQTT_USER")
password = os.getenv("MQTT_PASSWORD")
# Topic und Payload
topic = f"infoscreen/{client_uuid}/group_id"
payload = json.dumps({
"group_id": group_id,
"client_uuid": client_uuid
})
# MQTT-Client erstellen und verbinden
client = mqtt.Client()
if username and password:
client.username_pw_set(username, password)
client.connect(broker_host, broker_port, 60)
# Retained message publizieren
result = client.publish(topic, payload, qos=1, retain=True)
result.wait_for_publish(timeout=5.0)
client.disconnect()
logger.info(
f"Group assignment published for client {client_uuid}: group_id={group_id}")
return True
except Exception as e:
logger.error(
f"Error publishing group assignment for client {client_uuid}: {e}")
return False
def publish_multiple_client_groups(client_group_mappings: dict) -> tuple[int, int]:
"""
Publiziert Gruppenzuordnungen für mehrere Clients in einer Verbindung
Args:
client_group_mappings: Dict mit {client_uuid: group_id}
Returns:
tuple: (success_count, failed_count)
"""
try:
broker_host = os.getenv("MQTT_BROKER_HOST", "mqtt")
broker_port = int(os.getenv("MQTT_BROKER_PORT", 1883))
username = os.getenv("MQTT_USER")
password = os.getenv("MQTT_PASSWORD")
client = mqtt.Client()
if username and password:
client.username_pw_set(username, password)
client.connect(broker_host, broker_port, 60)
success_count = 0
failed_count = 0
for client_uuid, group_id in client_group_mappings.items():
try:
topic = f"infoscreen/{client_uuid}/group_id"
payload = json.dumps({
"group_id": group_id,
"client_uuid": client_uuid
})
result = client.publish(topic, payload, qos=1, retain=True)
result.wait_for_publish(timeout=5.0)
success_count += 1
except Exception as e:
logger.error(f"Failed to publish for {client_uuid}: {e}")
failed_count += 1
client.disconnect()
logger.info(
f"Bulk publish completed: {success_count} success, {failed_count} failed")
return success_count, failed_count
except Exception as e:
logger.error(f"Error in bulk publish: {e}")
return 0, len(client_group_mappings)
def delete_client_group_message(client_uuid: str) -> bool:
"""
Löscht die retained message für einen Client (bei Client-Löschung)
"""
try:
broker_host = os.getenv("MQTT_BROKER_HOST", "mqtt")
broker_port = int(os.getenv("MQTT_BROKER_PORT", 1883))
username = os.getenv("MQTT_USER")
password = os.getenv("MQTT_PASSWORD")
topic = f"infoscreen/{client_uuid}/group_id"
client = mqtt.Client()
if username and password:
client.username_pw_set(username, password)
client.connect(broker_host, broker_port, 60)
# Leere retained message löscht die vorherige
result = client.publish(topic, "", qos=1, retain=True)
result.wait_for_publish(timeout=5.0)
client.disconnect()
logger.info(f"Deleted retained group message for client {client_uuid}")
return True
except Exception as e:
logger.error(
f"Error deleting group message for client {client_uuid}: {e}")
return False

View File

@@ -1,12 +1,49 @@
from server.database import Session from server.database import Session
from models.models import Client, ClientGroup from models.models import Client, ClientGroup
from flask import Blueprint, request, jsonify from flask import Blueprint, request, jsonify
from server.mqtt_helper import publish_client_group, delete_client_group_message, publish_multiple_client_groups
import sys import sys
sys.path.append('/workspace') sys.path.append('/workspace')
clients_bp = Blueprint("clients", __name__, url_prefix="/api/clients") clients_bp = Blueprint("clients", __name__, url_prefix="/api/clients")
@clients_bp.route("/sync-all-groups", methods=["POST"])
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"]) @clients_bp.route("/without_description", methods=["GET"])
def get_clients_without_description(): def get_clients_without_description():
session = Session() session = Session()
@@ -46,10 +83,20 @@ def set_client_description(uuid):
if not client: if not client:
session.close() session.close()
return jsonify({"error": "Client nicht gefunden"}), 404 return jsonify({"error": "Client nicht gefunden"}), 404
client.description = description client.description = description
session.commit() session.commit()
# MQTT: Gruppenzuordnung publizieren (wichtig für neue Clients aus SetupMode)
mqtt_success = publish_client_group(client.uuid, client.group_id)
session.close() session.close()
return jsonify({"success": True})
response = {"success": True}
if not mqtt_success:
response["warning"] = "Beschreibung gespeichert, aber MQTT-Publishing fehlgeschlagen"
return jsonify(response)
@clients_bp.route("", methods=["GET"]) @clients_bp.route("", methods=["GET"])
@@ -83,18 +130,51 @@ def get_clients():
def update_clients_group(): def update_clients_group():
data = request.get_json() data = request.get_json()
client_ids = data.get("client_ids", []) client_ids = data.get("client_ids", [])
group_id = data.get("group_id")
group_name = data.get("group_name") 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() session = Session()
group = session.query(ClientGroup).filter_by(name=group_name).first()
if not group: # 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() session.close()
return jsonify({"error": "Gruppe nicht gefunden"}), 404 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( session.query(Client).filter(Client.uuid.in_(client_ids)).update(
{Client.group_id: group.id}, synchronize_session=False {Client.group_id: target_group_id}, synchronize_session=False
) )
session.commit() session.commit()
session.close() session.close()
return jsonify({"success": True})
# 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"]) @clients_bp.route("/<uuid>", methods=["PATCH"])
@@ -194,7 +274,16 @@ def delete_client(uuid):
if not client: if not client:
session.close() session.close()
return jsonify({"error": "Client nicht gefunden"}), 404 return jsonify({"error": "Client nicht gefunden"}), 404
session.delete(client) session.delete(client)
session.commit() session.commit()
session.close() session.close()
return jsonify({"success": True})
# 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)

View File

@@ -0,0 +1,54 @@
#!/usr/bin/env python3
"""
Einmaliges Skript zur Synchronisation aller bestehenden Client-Gruppenzuordnungen
Verwendung: python sync_existing_clients.py
"""
from server.mqtt_helper import publish_multiple_client_groups
from models.models import Client
from server.database import Session
import sys
import os
sys.path.append('/workspace')
def main():
print("Synchronisiere bestehende Client-Gruppenzuordnungen mit MQTT...")
session = Session()
try:
# Alle aktiven Clients abrufen
clients = session.query(Client).filter(Client.is_active == True).all()
if not clients:
print("Keine aktiven Clients gefunden.")
return
print(f"Gefunden: {len(clients)} aktive Clients")
# Mapping erstellen
client_group_mappings = {
client.uuid: client.group_id for client in clients}
# Alle auf einmal publizieren
success_count, failed_count = publish_multiple_client_groups(
client_group_mappings)
print(f"Synchronisation abgeschlossen:")
print(f" Erfolgreich: {success_count}")
print(f" Fehlgeschlagen: {failed_count}")
print(f" Gesamt: {len(clients)}")
if failed_count == 0:
print("✅ Alle Clients erfolgreich synchronisiert!")
else:
print(
f"⚠️ {failed_count} Clients konnten nicht synchronisiert werden.")
except Exception as e:
print(f"Fehler: {e}")
finally:
session.close()
if __name__ == "__main__":
main()