| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628 |
- # -*- coding: utf-8 -*-
- """
- AdminCommands Plugin.
- Stellt 27 Admin-Only Intents bereit fuer System-Info, Satellite-Verwaltung,
- Plugin-Status, Training-Steuerung und History/Log-Abfragen.
- Alle Intents erfordern das System-Command Wakeword.
- Patterns und Examples sind in intents_de.yml definiert (multilingual).
- """
- from __future__ import annotations
- import asyncio
- import logging
- import os
- import socket
- import time
- from datetime import datetime, timezone
- from pathlib import Path
- from typing import TYPE_CHECKING, Any
- from trixy_core.plugins.trixy_plugin import TrixyPlugin
- from trixy_core.nlp.decorators import intent, admin_only
- from trixy_core.nlp.handler import IntentResult, IntentReceivedData
- from trixy_core.utils.debug import pinfo, pdebug, perror, pwarn
- if TYPE_CHECKING:
- from trixy_core.activity.tracker import ActivityTracker
- # Audit-Logger fuer Admin-Befehle
- _audit_logger: logging.Logger | None = None
- def _get_audit_logger() -> logging.Logger:
- """Liefert den Audit-Logger (lazy init)."""
- global _audit_logger
- if _audit_logger is None:
- _audit_logger = logging.getLogger("admin_audit")
- _audit_logger.setLevel(logging.INFO)
- if not _audit_logger.handlers:
- log_dir = Path("logs")
- log_dir.mkdir(exist_ok=True)
- handler = logging.FileHandler(log_dir / "admin_audit.log", encoding="utf-8")
- handler.setFormatter(logging.Formatter(
- "%(asctime)s | %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
- ))
- _audit_logger.addHandler(handler)
- return _audit_logger
- def _audit(intent_name: str, satellite_id: str, room_id: str, detail: str = "") -> None:
- """Schreibt einen Audit-Eintrag."""
- logger = _get_audit_logger()
- msg = f"ADMIN | intent={intent_name} | satellite={satellite_id} | room={room_id}"
- if detail:
- msg += f" | {detail}"
- logger.info(msg)
- class AdminCommandsPlugin(TrixyPlugin):
- """Admin-Befehle fuer System-Verwaltung per Sprache."""
- NAME = "admin_commands"
- VERSION = "1.0.0"
- DESCRIPTION = "Admin-Only Sprachbefehle fuer System, Satellites, Training und Logs"
- AUTHOR = "Trixy"
- def __init__(self, **kwargs) -> None:
- super().__init__(**kwargs)
- self._start_time: float = 0.0
- async def on_load(self) -> None:
- """Plugin geladen — Startzeit merken."""
- self._start_time = time.time()
- pinfo("[AdminCommands] Plugin geladen")
- async def on_unload(self) -> None:
- """Plugin entladen."""
- pdebug("[AdminCommands] Plugin entladen")
- def _get_tracker(self) -> "ActivityTracker | None":
- """Liefert den ActivityTracker Service."""
- try:
- return self.application.services.get_service("ActivityTracker")
- except Exception:
- return None
- # =========================================================================
- # System Info (1-6)
- # =========================================================================
- @intent("admin_hostname_query", description="Hostname abfragen")
- @admin_only
- async def handle_hostname_query(self, data: IntentReceivedData) -> IntentResult:
- """Gibt den Hostnamen zurueck."""
- _audit("admin_hostname_query", data.satellite_id, data.room_id)
- hostname = socket.gethostname()
- return IntentResult.success_with_response(f"Der Hostname ist {hostname}.")
- @intent("admin_domain_query", description="Domain abfragen")
- @admin_only
- async def handle_domain_query(self, data: IntentReceivedData) -> IntentResult:
- """Gibt den FQDN zurueck."""
- _audit("admin_domain_query", data.satellite_id, data.room_id)
- fqdn = socket.getfqdn()
- return IntentResult.success_with_response(f"Die Domain ist {fqdn}.")
- @intent("admin_ip_query", description="IP-Adresse abfragen")
- @admin_only
- async def handle_ip_query(self, data: IntentReceivedData) -> IntentResult:
- """Gibt die IP-Adresse zurueck."""
- _audit("admin_ip_query", data.satellite_id, data.room_id)
- ip = self._get_primary_ip()
- return IntentResult.success_with_response(f"Die IP-Adresse ist {ip}.")
- @intent("admin_uptime_query", description="Server-Laufzeit abfragen")
- @admin_only
- async def handle_uptime_query(self, data: IntentReceivedData) -> IntentResult:
- """Gibt die Server-Laufzeit zurueck."""
- _audit("admin_uptime_query", data.satellite_id, data.room_id)
- uptime_seconds = time.time() - self._start_time
- text = self._format_duration(uptime_seconds)
- return IntentResult.success_with_response(f"Der Server laeuft seit {text}.")
- @intent("admin_system_time_set", description="Systemuhrzeit setzen")
- @admin_only
- async def handle_system_time_set(self, data: IntentReceivedData, time_value: str = "") -> IntentResult:
- """Setzt die Systemuhrzeit."""
- _audit("admin_system_time_set", data.satellite_id, data.room_id, f"time={time_value}")
- if not time_value:
- return IntentResult.failure("Keine Uhrzeit angegeben.", "Bitte eine Uhrzeit angeben.")
- try:
- proc = await asyncio.create_subprocess_exec(
- "timedatectl", "set-time", time_value,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- _, stderr = await proc.communicate()
- if proc.returncode != 0:
- err = stderr.decode().strip()
- return IntentResult.failure(err, f"Fehler beim Setzen der Uhrzeit: {err}")
- return IntentResult.success_with_response(f"Uhrzeit auf {time_value} gesetzt.")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_system_date_set", description="Systemdatum setzen")
- @admin_only
- async def handle_system_date_set(self, data: IntentReceivedData, date_value: str = "") -> IntentResult:
- """Setzt das Systemdatum."""
- _audit("admin_system_date_set", data.satellite_id, data.room_id, f"date={date_value}")
- if not date_value:
- return IntentResult.failure("Kein Datum angegeben.", "Bitte ein Datum angeben.")
- try:
- proc = await asyncio.create_subprocess_exec(
- "timedatectl", "set-time", date_value,
- stdout=asyncio.subprocess.PIPE,
- stderr=asyncio.subprocess.PIPE,
- )
- _, stderr = await proc.communicate()
- if proc.returncode != 0:
- err = stderr.decode().strip()
- return IntentResult.failure(err, f"Fehler beim Setzen des Datums: {err}")
- return IntentResult.success_with_response(f"Datum auf {date_value} gesetzt.")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- # =========================================================================
- # Satellite Management (7-13)
- # =========================================================================
- @intent("admin_registration_enable", description="Registrierungsmodus aktivieren")
- @admin_only
- async def handle_registration_enable(self, data: IntentReceivedData) -> IntentResult:
- """Aktiviert den Registrierungsmodus."""
- _audit("admin_registration_enable", data.satellite_id, data.room_id)
- timeout = self.get_config_value("registration_timeout_seconds", 60)
- try:
- self.application.registration.enable_registration_mode(timeout)
- return IntentResult.success_with_response(
- f"Registrierungsmodus aktiviert fuer {timeout} Sekunden."
- )
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_registration_disable", description="Registrierungsmodus deaktivieren")
- @admin_only
- async def handle_registration_disable(self, data: IntentReceivedData) -> IntentResult:
- """Deaktiviert den Registrierungsmodus."""
- _audit("admin_registration_disable", data.satellite_id, data.room_id)
- try:
- self.application.registration.disable_registration_mode()
- return IntentResult.success_with_response("Registrierungsmodus deaktiviert.")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_registration_status", description="Registrierungsmodus Status")
- @admin_only
- async def handle_registration_status(self, data: IntentReceivedData) -> IntentResult:
- """Gibt den Status des Registrierungsmodus zurueck."""
- _audit("admin_registration_status", data.satellite_id, data.room_id)
- try:
- active = self.application.registration.registration_mode
- if active:
- return IntentResult.success_with_response("Ja, der Registrierungsmodus ist aktiv.")
- return IntentResult.success_with_response("Nein, der Registrierungsmodus ist nicht aktiv.")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_satellite_count_registered", description="Anzahl registrierter Satellites")
- @admin_only
- async def handle_satellite_count_registered(self, data: IntentReceivedData) -> IntentResult:
- """Gibt die Anzahl registrierter Satellites zurueck."""
- _audit("admin_satellite_count_registered", data.satellite_id, data.room_id)
- try:
- count = self.application.satellites.count
- return IntentResult.success_with_response(
- f"Es {'ist' if count == 1 else 'sind'} {count} Satellite{'s' if count != 1 else ''} registriert."
- )
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_satellite_count_connected", description="Anzahl verbundener Satellites")
- @admin_only
- async def handle_satellite_count_connected(self, data: IntentReceivedData) -> IntentResult:
- """Gibt die Anzahl verbundener Satellites zurueck."""
- _audit("admin_satellite_count_connected", data.satellite_id, data.room_id)
- try:
- count = self.application.satellites.connected_count
- return IntentResult.success_with_response(
- f"Es {'ist' if count == 1 else 'sind'} {count} Satellite{'s' if count != 1 else ''} verbunden."
- )
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_satellite_last_connected", description="Letzte Verbindung eines Satellites")
- @admin_only
- async def handle_satellite_last_connected(self, data: IntentReceivedData, alias: str = "") -> IntentResult:
- """Gibt die letzte Verbindungszeit eines Satellites zurueck."""
- _audit("admin_satellite_last_connected", data.satellite_id, data.room_id, f"alias={alias}")
- if not alias:
- return IntentResult.failure("Kein Alias angegeben.", "Bitte einen Satellite-Namen angeben.")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- history = tracker.get_satellite_connection_history(alias)
- if not history:
- return IntentResult.success_with_response(f"Keine Verbindungshistorie fuer {alias} gefunden.")
- last = history[-1]
- connected_at = last.get("connected_at", "unbekannt")
- try:
- dt = datetime.fromisoformat(connected_at)
- formatted = dt.strftime("%d.%m.%Y um %H:%M Uhr")
- except (ValueError, TypeError):
- formatted = connected_at
- return IntentResult.success_with_response(f"{alias} war zuletzt am {formatted} verbunden.")
- @intent("admin_satellite_unregister", description="Satellite-Registrierung loeschen")
- @admin_only
- async def handle_satellite_unregister(self, data: IntentReceivedData, alias: str = "") -> IntentResult:
- """Loescht die Registrierung eines Satellites."""
- _audit("admin_satellite_unregister", data.satellite_id, data.room_id, f"alias={alias}")
- if not alias:
- return IntentResult.failure("Kein Alias angegeben.", "Bitte einen Satellite-Namen angeben.")
- try:
- # Satellite nach Alias suchen
- satellite = None
- for s in self.application.satellites.get_all():
- if s.alias.lower() == alias.lower():
- satellite = s
- break
- if not satellite:
- return IntentResult.success_with_response(f"Kein Satellite mit dem Namen {alias} gefunden.")
- # Registrierung loeschen
- if self.application.registration.delete_registration(satellite.mac_address):
- self.application.satellites.remove(satellite.id)
- return IntentResult.success_with_response(f"Registrierung von {alias} wurde geloescht.")
- return IntentResult.failure("Loeschen fehlgeschlagen.", f"Registrierung von {alias} konnte nicht geloescht werden.")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- # =========================================================================
- # Plugin Management (14)
- # =========================================================================
- @intent("admin_plugin_count", description="Anzahl aktiver Plugins")
- @admin_only
- async def handle_plugin_count(self, data: IntentReceivedData) -> IntentResult:
- """Gibt die Anzahl geladener/aktiver Plugins zurueck."""
- _audit("admin_plugin_count", data.satellite_id, data.room_id)
- try:
- total = self.application.plugins.count
- enabled = self.application.plugins.enabled_count
- return IntentResult.success_with_response(
- f"Es sind {total} Plugins geladen, davon {enabled} aktiv."
- )
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- # =========================================================================
- # Training (15-19)
- # =========================================================================
- @intent("admin_training_status", description="Trainings-Status abfragen")
- @admin_only
- async def handle_training_status(self, data: IntentReceivedData) -> IntentResult:
- """Gibt den aktuellen Trainings-Status zurueck."""
- _audit("admin_training_status", data.satellite_id, data.room_id)
- try:
- trainer_service = self.application.services.get_service("TrainerService")
- active = trainer_service.active_trainer_id
- if active:
- progress = trainer_service.current_progress
- return IntentResult.success_with_response(
- f"Ja, Training '{active}' laeuft. Status: {progress.state.name}."
- )
- return IntentResult.success_with_response("Nein, es laeuft kein Training.")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_training_start", description="Training starten")
- @admin_only
- async def handle_training_start(self, data: IntentReceivedData, model_type: str = "") -> IntentResult:
- """Startet ein Training."""
- _audit("admin_training_start", data.satellite_id, data.room_id, f"model_type={model_type}")
- if not model_type:
- return IntentResult.failure("Kein Modelltyp angegeben.", "Bitte einen Modelltyp angeben.")
- try:
- trainer_service = self.application.services.get_service("TrainerService")
- success, error = await trainer_service.start_training(model_type)
- if success:
- return IntentResult.success_with_response(f"Training '{model_type}' wurde gestartet.")
- return IntentResult.failure(error, f"Training konnte nicht gestartet werden: {error}")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_training_stop", description="Training stoppen")
- @admin_only
- async def handle_training_stop(self, data: IntentReceivedData) -> IntentResult:
- """Stoppt das laufende Training."""
- _audit("admin_training_stop", data.satellite_id, data.room_id)
- try:
- trainer_service = self.application.services.get_service("TrainerService")
- success, error = await trainer_service.stop_training()
- if success:
- return IntentResult.success_with_response("Training wurde gestoppt.")
- return IntentResult.failure(error, f"Training konnte nicht gestoppt werden: {error}")
- except Exception as e:
- return IntentResult.failure(str(e), f"Fehler: {e}")
- @intent("admin_training_last", description="Letztes Training abfragen")
- @admin_only
- async def handle_training_last(self, data: IntentReceivedData) -> IntentResult:
- """Gibt Informationen zum letzten Training zurueck."""
- _audit("admin_training_last", data.satellite_id, data.room_id)
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- last = tracker.get_last_training()
- if not last:
- return IntentResult.success_with_response("Es wurde noch kein Training durchgefuehrt.")
- return IntentResult.success_with_response(self._format_training_entry(last))
- @intent("admin_training_last_specific", description="Letztes Training nach Typ abfragen")
- @admin_only
- async def handle_training_last_specific(self, data: IntentReceivedData, model_type: str = "") -> IntentResult:
- """Gibt Informationen zum letzten Training eines bestimmten Typs zurueck."""
- _audit("admin_training_last_specific", data.satellite_id, data.room_id, f"model_type={model_type}")
- if not model_type:
- return IntentResult.failure("Kein Modelltyp angegeben.", "Bitte einen Modelltyp angeben.")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- last = tracker.get_last_training(model_type)
- if not last:
- return IntentResult.success_with_response(f"Es wurde noch kein '{model_type}' Training durchgefuehrt.")
- return IntentResult.success_with_response(self._format_training_entry(last))
- # =========================================================================
- # History / Logs (20-27)
- # =========================================================================
- @intent("admin_last_requests", description="Letzte Anfragen anzeigen")
- @admin_only
- async def handle_last_requests(self, data: IntentReceivedData, count: int = 5) -> IntentResult:
- """Gibt die letzten N Anfragen zurueck."""
- _audit("admin_last_requests", data.satellite_id, data.room_id, f"count={count}")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- intents = tracker.get_last_intents(int(count))
- if not intents:
- return IntentResult.success_with_response("Keine Anfragen protokolliert.")
- lines = []
- for entry in intents:
- intent_name = entry.get("intent", "?")
- text = entry.get("original_text", "")[:50]
- lines.append(f"{intent_name}: {text}")
- return IntentResult.success_with_response(
- f"Die letzten {len(intents)} Anfragen: " + ". ".join(lines) + "."
- )
- @intent("admin_last_request_room", description="Letzte Anfrage im Raum")
- @admin_only
- async def handle_last_request_room(self, data: IntentReceivedData, room: str = "") -> IntentResult:
- """Gibt die letzte Anfrage in einem bestimmten Raum zurueck."""
- _audit("admin_last_request_room", data.satellite_id, data.room_id, f"room={room}")
- if not room:
- return IntentResult.failure("Kein Raum angegeben.", "Bitte einen Raum angeben.")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- intents = tracker.get_last_intents_by_room(room, 1)
- if not intents:
- return IntentResult.success_with_response(f"Keine Anfragen im Raum {room} protokolliert.")
- last = intents[-1]
- text = last.get("original_text", "unbekannt")
- return IntentResult.success_with_response(f"Die letzte Anfrage im {room} war: {text}.")
- @intent("admin_last_conversation", description="Letzte Conversation anzeigen")
- @admin_only
- async def handle_last_conversation(self, data: IntentReceivedData) -> IntentResult:
- """Gibt Informationen zur letzten Conversation zurueck."""
- _audit("admin_last_conversation", data.satellite_id, data.room_id)
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- convs = tracker.get_last_conversations(1)
- if not convs:
- return IntentResult.success_with_response("Keine Conversations protokolliert.")
- conv = convs[-1]
- started = conv.get("started_at", "unbekannt")
- try:
- dt = datetime.fromisoformat(started)
- formatted = dt.strftime("%d.%m.%Y um %H:%M Uhr")
- except (ValueError, TypeError):
- formatted = started
- satellite = conv.get("satellite_id", "unbekannt")
- return IntentResult.success_with_response(
- f"Die letzte Conversation war am {formatted} mit Satellite {satellite}."
- )
- @intent("admin_last_conversations", description="Letzte Conversations anzeigen")
- @admin_only
- async def handle_last_conversations(self, data: IntentReceivedData, count: int = 5) -> IntentResult:
- """Gibt die letzten N Conversations zurueck."""
- _audit("admin_last_conversations", data.satellite_id, data.room_id, f"count={count}")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- convs = tracker.get_last_conversations(int(count))
- if not convs:
- return IntentResult.success_with_response("Keine Conversations protokolliert.")
- lines = []
- for conv in convs:
- started = conv.get("started_at", "?")
- try:
- dt = datetime.fromisoformat(started)
- formatted = dt.strftime("%d.%m. %H:%M")
- except (ValueError, TypeError):
- formatted = started
- satellite = conv.get("satellite_id", "?")[:8]
- lines.append(f"{formatted} ({satellite})")
- return IntentResult.success_with_response(
- f"Die letzten {len(convs)} Conversations: " + ", ".join(lines) + "."
- )
- @intent("admin_last_errors", description="Letzte Fehler anzeigen")
- @admin_only
- async def handle_last_errors(self, data: IntentReceivedData, count: int = 5) -> IntentResult:
- """Gibt die letzten N Fehler zurueck."""
- _audit("admin_last_errors", data.satellite_id, data.room_id, f"count={count}")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- errors = tracker.get_last_errors(int(count))
- if not errors:
- return IntentResult.success_with_response("Keine Fehler protokolliert.")
- lines = []
- for err in errors:
- source = err.get("source", "?")
- message = err.get("message", "?")[:60]
- lines.append(f"{source}: {message}")
- return IntentResult.success_with_response(
- f"Die letzten {len(errors)} Fehler: " + ". ".join(lines) + "."
- )
- @intent("admin_error_count_hours", description="Fehleranzahl in Stunden")
- @admin_only
- async def handle_error_count_hours(self, data: IntentReceivedData, hours: int = 24) -> IntentResult:
- """Gibt die Fehleranzahl der letzten N Stunden zurueck."""
- _audit("admin_error_count_hours", data.satellite_id, data.room_id, f"hours={hours}")
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- count = tracker.get_error_count_since(int(hours))
- return IntentResult.success_with_response(
- f"Es gab {count} Fehler in den letzten {hours} Stunden."
- )
- @intent("admin_config_tool_last", description="Letzte Config-Tool Verbindung")
- @admin_only
- async def handle_config_tool_last(self, data: IntentReceivedData) -> IntentResult:
- """Gibt die letzte Config-Tool-Verbindung zurueck."""
- _audit("admin_config_tool_last", data.satellite_id, data.room_id)
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- last = tracker.get_last_config_connection()
- if not last:
- return IntentResult.success_with_response("Kein Config-Tool war bisher verbunden.")
- connected_at = last.get("connected_at", "unbekannt")
- peer = last.get("peer_address", "unbekannt")
- try:
- dt = datetime.fromisoformat(connected_at)
- formatted = dt.strftime("%d.%m.%Y um %H:%M Uhr")
- except (ValueError, TypeError):
- formatted = connected_at
- return IntentResult.success_with_response(
- f"Das letzte Config-Tool war am {formatted} von {peer} verbunden."
- )
- @intent("admin_cache_clear", description="Activity-Cache leeren")
- @admin_only
- async def handle_cache_clear(self, data: IntentReceivedData) -> IntentResult:
- """Loescht alle Activity-Daten."""
- _audit("admin_cache_clear", data.satellite_id, data.room_id)
- tracker = self._get_tracker()
- if not tracker:
- return IntentResult.failure("ActivityTracker nicht verfuegbar.")
- tracker.clear_all()
- return IntentResult.success_with_response("Cache wurde geleert.")
- # =========================================================================
- # Hilfsmethoden
- # =========================================================================
- @staticmethod
- def _get_primary_ip() -> str:
- """Ermittelt die primaere IP-Adresse."""
- try:
- # Verbindung zu externem Host (ohne tatsaechlich zu senden)
- s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
- s.settimeout(0.1)
- s.connect(("8.8.8.8", 80))
- ip = s.getsockname()[0]
- s.close()
- return ip
- except Exception:
- return socket.gethostbyname(socket.gethostname())
- @staticmethod
- def _format_duration(seconds: float) -> str:
- """Formatiert eine Dauer in lesbaren Text."""
- total = int(seconds)
- days = total // 86400
- hours = (total % 86400) // 3600
- minutes = (total % 3600) // 60
- parts = []
- if days > 0:
- parts.append(f"{days} {'Tag' if days == 1 else 'Tagen'}")
- if hours > 0:
- parts.append(f"{hours} {'Stunde' if hours == 1 else 'Stunden'}")
- if minutes > 0:
- parts.append(f"{minutes} {'Minute' if minutes == 1 else 'Minuten'}")
- if not parts:
- return "weniger als einer Minute"
- return ", ".join(parts)
- @staticmethod
- def _format_training_entry(entry: dict[str, Any]) -> str:
- """Formatiert einen Trainings-Eintrag als Text."""
- trainer_id = entry.get("trainer_id", "unbekannt")
- started_at = entry.get("started_at", "unbekannt")
- success = entry.get("success", False)
- duration = entry.get("duration_seconds", 0)
- try:
- dt = datetime.fromisoformat(started_at)
- formatted = dt.strftime("%d.%m.%Y um %H:%M Uhr")
- except (ValueError, TypeError):
- formatted = started_at
- status = "erfolgreich" if success else "fehlgeschlagen"
- duration_text = AdminCommandsPlugin._format_duration(duration) if duration else "unbekannt"
- return (
- f"Das letzte Training '{trainer_id}' lief am {formatted}, "
- f"Dauer: {duration_text}, Status: {status}."
- )
|