sat_config_view.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. # -*- coding: utf-8 -*-
  2. """
  3. Satellite SubView: Configuration (F4).
  4. Zeigt und bearbeitet die Client-Konfiguration des Satellites.
  5. Nutzt den Server als Proxy, um die Config des Satellites
  6. zu lesen und zu schreiben.
  7. """
  8. from __future__ import annotations
  9. from textual.app import ComposeResult
  10. from textual.containers import Vertical
  11. from textual.widgets import Static, Tree
  12. from textual.widgets.tree import TreeNode
  13. from trixy_core.tui.views.base import MainView
  14. from trixy_core.tui.config.field_registry import (
  15. FieldMeta,
  16. get_field_registry,
  17. infer_field_meta,
  18. )
  19. from trixy_core.tui.widgets.config_editors import ConfigEditorPanel
  20. class SatConfigView(MainView):
  21. """F4 im Satellite-Kontext: Client-Konfiguration."""
  22. TITLE = "Konfiguration"
  23. VIEW_ID = "sat_config"
  24. def __init__(self, **kwargs) -> None:
  25. super().__init__(**kwargs)
  26. self._sat_data: dict = {}
  27. self._config_data: dict = {}
  28. self._modified: dict[str, object] = {}
  29. self._selected_path: str = ""
  30. self._field_registry: dict[str, FieldMeta] = {}
  31. self._dynamic_options: dict[str, list[tuple[str, str]]] = {}
  32. def set_satellite(self, sat_data: dict) -> None:
  33. """Setzt die Satellite-Basisdaten."""
  34. self._sat_data = sat_data
  35. # Registry neu laden wenn Satellite wechselt
  36. self._config_data = {}
  37. self._modified = {}
  38. def compose(self) -> ComposeResult:
  39. yield Tree("Client-Konfiguration", id="sat-config-tree")
  40. yield ConfigEditorPanel(id="sat-config-editor")
  41. yield Static("", id="sat-config-status")
  42. async def on_activate(self) -> None:
  43. """Laedt die Satellite-Konfiguration."""
  44. if not self.connection or not self._sat_data:
  45. return
  46. # Client-Registry verwenden
  47. self._field_registry = get_field_registry("client")
  48. # Dynamische Optionen abfragen
  49. await self._fetch_dynamic_options()
  50. # Konfiguration laden
  51. await self._load_config()
  52. async def _fetch_dynamic_options(self) -> None:
  53. """Fragt dynamische Feld-Optionen vom Server ab."""
  54. if not self.connection:
  55. return
  56. sources: set[str] = set()
  57. for meta in self._field_registry.values():
  58. if meta.dynamic_source:
  59. sources.add(meta.dynamic_source)
  60. if not sources:
  61. return
  62. try:
  63. options = await self.connection.request_field_options(list(sources))
  64. if options:
  65. self._dynamic_options = options
  66. except Exception:
  67. pass
  68. async def _load_config(self) -> None:
  69. """Laedt die Remote-Konfiguration des Satellites."""
  70. if not self.connection or not self._sat_data:
  71. return
  72. sat_id = self._sat_data.get("satellite_id", "")
  73. if not sat_id:
  74. self._set_status("Keine Satellite-ID", error=True)
  75. return
  76. try:
  77. data = await self.connection.request_satellite_config(sat_id)
  78. except Exception:
  79. self._set_status("Fehler beim Laden der Konfiguration", error=True)
  80. return
  81. if data is None:
  82. self._set_status("Satellite nicht erreichbar oder nicht verbunden", error=True)
  83. return
  84. self._config_data = data
  85. self._build_tree()
  86. self._set_status("")
  87. def _build_tree(self) -> None:
  88. """Baut den Konfigurationsbaum auf."""
  89. tree = self.query_one("#sat-config-tree", Tree)
  90. tree.clear()
  91. alias = self._sat_data.get("alias", "Satellite")
  92. tree.root.set_label(f"[bold]{alias}[/bold] — Client-Konfiguration")
  93. self._add_tree_nodes(tree.root, self._config_data, "")
  94. tree.root.expand()
  95. def _add_tree_nodes(self, parent: TreeNode, data: dict, prefix: str) -> None:
  96. """Fuegt Knoten rekursiv zum Baum hinzu."""
  97. for key, value in sorted(data.items()):
  98. path = f"{prefix}.{key}" if prefix else key
  99. if isinstance(value, dict):
  100. node = parent.add(f"[bold]{key}[/bold]", data={"path": path, "is_section": True})
  101. self._add_tree_nodes(node, value, path)
  102. node.expand()
  103. else:
  104. if path in self._modified:
  105. display = f"[yellow]{key}[/yellow] = [yellow]{value}[/yellow] *"
  106. else:
  107. display = f"{key} = {value}"
  108. parent.add_leaf(display, data={"path": path, "value": value, "is_section": False})
  109. async def on_tree_node_selected(self, event: Tree.NodeSelected) -> None:
  110. """Wird aufgerufen wenn ein Baum-Knoten selektiert wird."""
  111. node_data = event.node.data
  112. if not node_data or node_data.get("is_section"):
  113. return
  114. path = node_data.get("path", "")
  115. value = node_data.get("value", "")
  116. self._selected_path = path
  117. # FieldMeta ermitteln (Registry oder Inferenz)
  118. meta = self._field_registry.get(path)
  119. if meta is None:
  120. meta = infer_field_meta(value)
  121. # Dynamische Optionen injizieren
  122. if meta.dynamic_source and meta.dynamic_source in self._dynamic_options:
  123. meta = FieldMeta(
  124. editor=meta.editor,
  125. choices=self._dynamic_options[meta.dynamic_source],
  126. min_value=meta.min_value,
  127. max_value=meta.max_value,
  128. step=meta.step,
  129. placeholder=meta.placeholder,
  130. requires_restart=meta.requires_restart,
  131. dynamic_source=meta.dynamic_source,
  132. hint=meta.hint,
  133. )
  134. # Editor anzeigen
  135. editor = self.query_one("#sat-config-editor", ConfigEditorPanel)
  136. await editor.show_editor(path, value, meta)
  137. self._set_status(f"Bearbeite: {path}")
  138. async def on_config_editor_panel_submitted(
  139. self, event: ConfigEditorPanel.Submitted
  140. ) -> None:
  141. """Verarbeitet validierte Werte vom Editor-Panel."""
  142. if event.error:
  143. self._set_status(f"Fehler: {event.error}", error=True)
  144. return
  145. if not self.connection or not self._sat_data:
  146. return
  147. sat_id = self._sat_data.get("satellite_id", "")
  148. if not sat_id:
  149. return
  150. try:
  151. result = await self.connection.write_satellite_config(
  152. satellite_id=sat_id,
  153. key_path=event.path,
  154. value=event.validated_value,
  155. )
  156. if result and result.success:
  157. self._modified[event.path] = event.validated_value
  158. msg = "Gespeichert"
  159. if event.requires_restart or (result and result.requires_restart):
  160. msg += " (Neustart erforderlich)"
  161. self._set_status(msg)
  162. await self._load_config()
  163. else:
  164. error = result.error if result else "Unbekannter Fehler"
  165. self._set_status(f"Fehler: {error}", error=True)
  166. except Exception as e:
  167. self._set_status(f"Fehler: {e}", error=True)
  168. def _set_status(self, text: str, error: bool = False) -> None:
  169. """Setzt die Statuszeile."""
  170. status = self.query_one("#sat-config-status", Static)
  171. if error:
  172. status.update(f"[red]{text}[/red]")
  173. elif text:
  174. status.update(f"[green]{text}[/green]")
  175. else:
  176. status.update("")