health_view.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. # -*- coding: utf-8 -*-
  2. """
  3. Health-View: Systemmetriken und Instanz-Status.
  4. Zeigt OS-Info, RAM, CPU, Disk, Netzwerk, Uptime
  5. und Trixy-spezifische Informationen.
  6. """
  7. from __future__ import annotations
  8. from textual.app import ComposeResult
  9. from textual.containers import Vertical
  10. from textual.widgets import Static
  11. from trixy_core.tui.views.base import MainView
  12. from trixy_core.tui.widgets.status_table import StatusTable
  13. def _make_bar(percent: float, width: int = 30) -> str:
  14. """Erstellt einen farbigen ASCII-Balken."""
  15. filled = int(width * percent / 100)
  16. empty = width - filled
  17. bar = "\u2593" * filled + "\u2591" * empty
  18. if percent > 80:
  19. color = "red"
  20. elif percent > 60:
  21. color = "yellow"
  22. else:
  23. color = "green"
  24. return f"[{color}]{bar}[/{color}] {percent:.1f}%"
  25. def _iface_icon(iface_type: str) -> str:
  26. """Gibt ein kurzes Label fuer den Interface-Typ zurueck."""
  27. mapping = {
  28. "wifi": "[cyan]W-LAN[/cyan]",
  29. "ethernet": "[green]LAN[/green]",
  30. "loopback": "Loopback",
  31. "other": "Andere",
  32. }
  33. return mapping.get(iface_type, "Andere")
  34. def _format_bytes(num_bytes: int) -> str:
  35. """Formatiert Bytes in lesbare Groesse."""
  36. for unit in ("B", "KB", "MB", "GB", "TB"):
  37. if abs(num_bytes) < 1024.0:
  38. return f"{num_bytes:.1f} {unit}"
  39. num_bytes /= 1024.0
  40. return f"{num_bytes:.1f} PB"
  41. def _format_uptime(seconds: float) -> str:
  42. """Formatiert Sekunden in lesbare Uptime."""
  43. days = int(seconds // 86400)
  44. hours = int((seconds % 86400) // 3600)
  45. minutes = int((seconds % 3600) // 60)
  46. secs = int(seconds % 60)
  47. parts = []
  48. if days > 0:
  49. parts.append(f"{days}d")
  50. if hours > 0:
  51. parts.append(f"{hours}h")
  52. if minutes > 0:
  53. parts.append(f"{minutes}m")
  54. parts.append(f"{secs}s")
  55. return " ".join(parts)
  56. class HealthView(MainView):
  57. """
  58. F1: System-Health Ansicht.
  59. Zeigt System-Metriken und Trixy-Status, periodisch aktualisiert.
  60. """
  61. TITLE = "Health"
  62. VIEW_ID = "health"
  63. def compose(self) -> ComposeResult:
  64. with Vertical(classes="metric-section"):
  65. yield Static("[bold]System[/bold]", classes="metric-title")
  66. yield StatusTable(id="health-system")
  67. with Vertical(classes="metric-section"):
  68. yield Static("[bold]RAM[/bold]", classes="metric-title")
  69. yield StatusTable(id="health-ram")
  70. with Vertical(classes="metric-section"):
  71. yield Static("[bold]CPU[/bold]", classes="metric-title")
  72. yield StatusTable(id="health-cpu")
  73. with Vertical(classes="metric-section"):
  74. yield Static("[bold]Disk[/bold]", classes="metric-title")
  75. yield StatusTable(id="health-disk")
  76. with Vertical(classes="metric-section"):
  77. yield Static("[bold]Netzwerk[/bold]", classes="metric-title")
  78. yield StatusTable(id="health-network")
  79. with Vertical(classes="metric-section"):
  80. yield Static("[bold]Trixy[/bold]", classes="metric-title")
  81. yield StatusTable(id="health-trixy")
  82. async def on_activate(self) -> None:
  83. """Laedt initiale Daten."""
  84. await self.refresh_data()
  85. async def refresh_data(self) -> None:
  86. """Aktualisiert alle Metriken."""
  87. if not self.connection:
  88. return
  89. try:
  90. status = await self.connection.request_status()
  91. except Exception:
  92. return
  93. if status is None:
  94. return
  95. # System-Tabelle
  96. system_table = self.query_one("#health-system", StatusTable)
  97. system_table.update_data([
  98. ("Betriebssystem", f"{status.os_name} {status.os_version}"),
  99. ("Hostname", status.hostname),
  100. ("Python", status.python_version),
  101. ("Uptime", _format_uptime(status.uptime_seconds)),
  102. ])
  103. # RAM-Tabelle
  104. ram_table = self.query_one("#health-ram", StatusTable)
  105. ram_table.update_data([
  106. ("Gesamt", f"{status.ram_total_mb:.0f} MB"),
  107. ("Belegt", f"{status.ram_used_mb:.0f} MB"),
  108. ("Auslastung", _make_bar(status.ram_percent)),
  109. ])
  110. # CPU-Tabelle
  111. cpu_table = self.query_one("#health-cpu", StatusTable)
  112. cpu_rows = [
  113. ("Modell", status.cpu_model or "Unbekannt"),
  114. ("Kerne", str(status.cpu_count)),
  115. ("Auslastung", _make_bar(status.cpu_percent)),
  116. ]
  117. if status.cpu_freq_mhz > 0:
  118. cpu_rows.append(("Frequenz", f"{status.cpu_freq_mhz:.0f} MHz"))
  119. cpu_table.update_data(cpu_rows)
  120. # Disk-Tabelle
  121. disk_table = self.query_one("#health-disk", StatusTable)
  122. disk_table.update_data([
  123. ("Gesamt", f"{status.disk_total_gb:.1f} GB"),
  124. ("Belegt", f"{status.disk_used_gb:.1f} GB"),
  125. ("Auslastung", _make_bar(status.disk_percent)),
  126. ])
  127. # Netzwerk-Tabelle
  128. net_table = self.query_one("#health-network", StatusTable)
  129. net_rows = [
  130. ("Gesendet gesamt", _format_bytes(status.net_bytes_sent)),
  131. ("Empfangen gesamt", _format_bytes(status.net_bytes_recv)),
  132. ("Verbindungen", str(status.net_connections)),
  133. ]
  134. # Pro-Interface Stats (wenn vom Server geliefert)
  135. interfaces = getattr(status, "net_interfaces", None) or []
  136. # Loopback und down-Interfaces rausfiltern
  137. relevant = [
  138. i for i in interfaces
  139. if i.get("type") != "loopback" and i.get("is_up", False)
  140. ]
  141. if relevant:
  142. # Trennzeile
  143. net_rows.append(("", ""))
  144. for iface in relevant:
  145. name = iface.get("name", "?")
  146. iface_type = iface.get("type", "other")
  147. bytes_sent = iface.get("bytes_sent", 0)
  148. bytes_recv = iface.get("bytes_recv", 0)
  149. sat_count = iface.get("satellites_count", 0)
  150. icon = _iface_icon(iface_type)
  151. label = f"{icon} {name}"
  152. # Header-Zeile fuer das Interface (fett)
  153. net_rows.append((f"[bold]{label}[/bold]", ""))
  154. net_rows.append((" Gesendet", _format_bytes(bytes_sent)))
  155. net_rows.append((" Empfangen", _format_bytes(bytes_recv)))
  156. if sat_count > 0:
  157. net_rows.append((" Satellites", str(sat_count)))
  158. net_table.update_data(net_rows)
  159. # Trixy-Tabelle
  160. trixy_table = self.query_one("#health-trixy", StatusTable)
  161. trixy_rows = [
  162. ("Instanz-Typ", status.instance_type),
  163. ("Version", status.trixy_version),
  164. ]
  165. if status.satellites_total > 0 or status.instance_type == "server":
  166. trixy_rows.append((
  167. "Satellites",
  168. f"{status.satellites_connected}/{status.satellites_total} verbunden"
  169. ))
  170. if status.plugins_loaded:
  171. trixy_rows.append(("Plugins", ", ".join(status.plugins_loaded)))
  172. else:
  173. trixy_rows.append(("Plugins", "Keine"))
  174. if status.services_running:
  175. trixy_rows.append(("Services", ", ".join(status.services_running)))
  176. trixy_table.update_data(trixy_rows)