# -*- coding: utf-8 -*- """ Satellite SubView: Configuration (F4). Zeigt und bearbeitet die Client-Konfiguration des Satellites. Nutzt den Server als Proxy, um die Config des Satellites zu lesen und zu schreiben. """ from __future__ import annotations from textual.app import ComposeResult from textual.containers import Vertical from textual.widgets import Static, Tree from textual.widgets.tree import TreeNode from trixy_core.tui.views.base import MainView from trixy_core.tui.config.field_registry import ( FieldMeta, get_field_registry, infer_field_meta, ) from trixy_core.tui.widgets.config_editors import ConfigEditorPanel class SatConfigView(MainView): """F4 im Satellite-Kontext: Client-Konfiguration.""" TITLE = "Konfiguration" VIEW_ID = "sat_config" def __init__(self, **kwargs) -> None: super().__init__(**kwargs) self._sat_data: dict = {} self._config_data: dict = {} self._modified: dict[str, object] = {} self._selected_path: str = "" self._field_registry: dict[str, FieldMeta] = {} self._dynamic_options: dict[str, list[tuple[str, str]]] = {} def set_satellite(self, sat_data: dict) -> None: """Setzt die Satellite-Basisdaten.""" self._sat_data = sat_data # Registry neu laden wenn Satellite wechselt self._config_data = {} self._modified = {} def compose(self) -> ComposeResult: yield Tree("Client-Konfiguration", id="sat-config-tree") yield ConfigEditorPanel(id="sat-config-editor") yield Static("", id="sat-config-status") async def on_activate(self) -> None: """Laedt die Satellite-Konfiguration.""" if not self.connection or not self._sat_data: return # Client-Registry verwenden self._field_registry = get_field_registry("client") # Dynamische Optionen abfragen await self._fetch_dynamic_options() # Konfiguration laden await self._load_config() async def _fetch_dynamic_options(self) -> None: """Fragt dynamische Feld-Optionen vom Server ab.""" if not self.connection: return sources: set[str] = set() for meta in self._field_registry.values(): if meta.dynamic_source: sources.add(meta.dynamic_source) if not sources: return try: options = await self.connection.request_field_options(list(sources)) if options: self._dynamic_options = options except Exception: pass async def _load_config(self) -> None: """Laedt die Remote-Konfiguration des Satellites.""" if not self.connection or not self._sat_data: return sat_id = self._sat_data.get("satellite_id", "") if not sat_id: self._set_status("Keine Satellite-ID", error=True) return try: data = await self.connection.request_satellite_config(sat_id) except Exception: self._set_status("Fehler beim Laden der Konfiguration", error=True) return if data is None: self._set_status("Satellite nicht erreichbar oder nicht verbunden", error=True) return self._config_data = data self._build_tree() self._set_status("") def _build_tree(self) -> None: """Baut den Konfigurationsbaum auf.""" tree = self.query_one("#sat-config-tree", Tree) tree.clear() alias = self._sat_data.get("alias", "Satellite") tree.root.set_label(f"[bold]{alias}[/bold] — Client-Konfiguration") self._add_tree_nodes(tree.root, self._config_data, "") tree.root.expand() def _add_tree_nodes(self, parent: TreeNode, data: dict, prefix: str) -> None: """Fuegt Knoten rekursiv zum Baum hinzu.""" for key, value in sorted(data.items()): path = f"{prefix}.{key}" if prefix else key if isinstance(value, dict): node = parent.add(f"[bold]{key}[/bold]", data={"path": path, "is_section": True}) self._add_tree_nodes(node, value, path) node.expand() else: if path in self._modified: display = f"[yellow]{key}[/yellow] = [yellow]{value}[/yellow] *" else: display = f"{key} = {value}" parent.add_leaf(display, data={"path": path, "value": value, "is_section": False}) async def on_tree_node_selected(self, event: Tree.NodeSelected) -> None: """Wird aufgerufen wenn ein Baum-Knoten selektiert wird.""" node_data = event.node.data if not node_data or node_data.get("is_section"): return path = node_data.get("path", "") value = node_data.get("value", "") self._selected_path = path # FieldMeta ermitteln (Registry oder Inferenz) meta = self._field_registry.get(path) if meta is None: meta = infer_field_meta(value) # Dynamische Optionen injizieren if meta.dynamic_source and meta.dynamic_source in self._dynamic_options: meta = FieldMeta( editor=meta.editor, choices=self._dynamic_options[meta.dynamic_source], min_value=meta.min_value, max_value=meta.max_value, step=meta.step, placeholder=meta.placeholder, requires_restart=meta.requires_restart, dynamic_source=meta.dynamic_source, hint=meta.hint, ) # Editor anzeigen editor = self.query_one("#sat-config-editor", ConfigEditorPanel) await editor.show_editor(path, value, meta) self._set_status(f"Bearbeite: {path}") async def on_config_editor_panel_submitted( self, event: ConfigEditorPanel.Submitted ) -> None: """Verarbeitet validierte Werte vom Editor-Panel.""" if event.error: self._set_status(f"Fehler: {event.error}", error=True) return if not self.connection or not self._sat_data: return sat_id = self._sat_data.get("satellite_id", "") if not sat_id: return try: result = await self.connection.write_satellite_config( satellite_id=sat_id, key_path=event.path, value=event.validated_value, ) if result and result.success: self._modified[event.path] = event.validated_value msg = "Gespeichert" if event.requires_restart or (result and result.requires_restart): msg += " (Neustart erforderlich)" self._set_status(msg) await self._load_config() else: error = result.error if result else "Unbekannter Fehler" self._set_status(f"Fehler: {error}", error=True) except Exception as e: self._set_status(f"Fehler: {e}", error=True) def _set_status(self, text: str, error: bool = False) -> None: """Setzt die Statuszeile.""" status = self.query_one("#sat-config-status", Static) if error: status.update(f"[red]{text}[/red]") elif text: status.update(f"[green]{text}[/green]") else: status.update("")