schedule_info_view.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. # -*- coding: utf-8 -*-
  2. """
  3. Schedule SubView: Info (F1).
  4. Interaktive Ansicht zum Bearbeiten der allgemeinen Job-Informationen:
  5. Name, Beschreibung, Aktiviert-Status und erweiterte Konfiguration.
  6. Bei bestehenden Jobs werden zusaetzlich Laufzeitstatistiken angezeigt.
  7. """
  8. from __future__ import annotations
  9. from textual.app import ComposeResult
  10. from textual.containers import Horizontal, Vertical
  11. from textual.widgets import Button, Input, Static, Switch
  12. from trixy_core.tui.views.base import MainView
  13. from trixy_core.tui.widgets.status_table import StatusTable
  14. class ScheduleInfoView(MainView):
  15. """F1 im Schedule-Kontext: Allgemeine Job-Informationen (editierbar)."""
  16. TITLE = "Info"
  17. VIEW_ID = "sched_info"
  18. DEFAULT_CSS = """
  19. ScheduleInfoView {
  20. padding: 1 2;
  21. }
  22. ScheduleInfoView .section-box {
  23. margin-bottom: 1;
  24. }
  25. ScheduleInfoView .section-header {
  26. margin-bottom: 1;
  27. }
  28. ScheduleInfoView .field-label {
  29. margin-top: 1;
  30. height: 1;
  31. }
  32. ScheduleInfoView .field-row {
  33. height: 3;
  34. align: left middle;
  35. }
  36. ScheduleInfoView .field-row .field-label {
  37. width: auto;
  38. margin-right: 2;
  39. margin-top: 0;
  40. }
  41. ScheduleInfoView #sched-save-btn {
  42. margin-top: 1;
  43. }
  44. ScheduleInfoView #sched-save-status {
  45. margin-top: 1;
  46. height: 1;
  47. }
  48. """
  49. def __init__(self, **kwargs) -> None:
  50. super().__init__(**kwargs)
  51. self._job_data: dict = {}
  52. self._detail_data: object | None = None
  53. def set_job(self, job_data: dict) -> None:
  54. """Setzt die Job-Basisdaten und verwirft gecachte Details."""
  55. self._job_data = job_data
  56. self._detail_data = None
  57. def compose(self) -> ComposeResult:
  58. # --- Allgemein ---
  59. with Vertical(classes="section-box"):
  60. yield Static("[bold]Allgemein[/bold]", classes="section-header")
  61. yield Static("Name", classes="field-label")
  62. yield Input(id="sched-info-name", placeholder="Job-Name")
  63. yield Static("Beschreibung", classes="field-label")
  64. yield Input(id="sched-info-desc", placeholder="Beschreibung")
  65. with Horizontal(classes="field-row"):
  66. yield Static("Aktiviert", classes="field-label")
  67. yield Switch(id="sched-info-enabled", value=True)
  68. # --- Konfiguration ---
  69. with Vertical(classes="section-box"):
  70. yield Static("[bold]Konfiguration[/bold]", classes="section-header")
  71. yield Static("Max. Ausfuehrungen (0 = unbegrenzt)", classes="field-label")
  72. yield Input(
  73. id="sched-info-max-runs",
  74. placeholder="0",
  75. type="integer",
  76. )
  77. yield Static("Max. Fehler", classes="field-label")
  78. yield Input(
  79. id="sched-info-max-failures",
  80. placeholder="3",
  81. type="integer",
  82. )
  83. yield Static("Timeout (Sek.)", classes="field-label")
  84. yield Input(
  85. id="sched-info-timeout",
  86. placeholder="60.0",
  87. type="number",
  88. )
  89. yield Static("Prioritaet", classes="field-label")
  90. yield Input(
  91. id="sched-info-priority",
  92. placeholder="0",
  93. type="integer",
  94. )
  95. with Horizontal(classes="field-row"):
  96. yield Static("Bei Start ausfuehren", classes="field-label")
  97. yield Switch(id="sched-info-run-on-startup", value=False)
  98. yield Static("Tags (kommagetrennt)", classes="field-label")
  99. yield Input(id="sched-info-tags", placeholder="tag1, tag2, ...")
  100. # --- Statistik (nur bei bestehendem Job sichtbar) ---
  101. with Vertical(id="sched-info-stats-section", classes="section-box"):
  102. yield Static("[bold]Statistik[/bold]", classes="section-header")
  103. yield StatusTable(id="sched-info-stats")
  104. # --- Speichern ---
  105. yield Button("Speichern", id="sched-save-btn", variant="success")
  106. yield Static("", id="sched-save-status")
  107. async def on_activate(self) -> None:
  108. """Laedt Detail-Daten und befuellt die Eingabefelder."""
  109. job_id = self._job_data.get("job_id", "")
  110. # Statistik-Bereich nur anzeigen wenn bestehender Job
  111. stats_section = self.query_one("#sched-info-stats-section", Vertical)
  112. stats_section.display = bool(job_id)
  113. if job_id:
  114. await self._load_detail()
  115. else:
  116. # Neuer Job: Felder mit Standardwerten befuellen
  117. self._populate_defaults()
  118. async def refresh_data(self) -> None:
  119. """Periodisches Update – nur Statistik aktualisieren bei bestehendem Job."""
  120. job_id = self._job_data.get("job_id", "")
  121. if job_id and self._detail_data is not None:
  122. await self._load_detail_stats_only()
  123. async def _load_detail(self) -> None:
  124. """Laedt detaillierte Job-Daten vom Server und befuellt alle Felder."""
  125. if not self.connection or not self._job_data:
  126. return
  127. job_id = self._job_data.get("job_id", "")
  128. if not job_id:
  129. return
  130. try:
  131. detail = await self.connection.request_schedule_detail(job_id)
  132. except Exception:
  133. return
  134. if detail is None or detail.error:
  135. return
  136. self._detail_data = detail
  137. self._populate_from_detail(detail)
  138. def _populate_defaults(self) -> None:
  139. """Befuellt Felder mit Standardwerten fuer einen neuen Job."""
  140. name = self._job_data.get("name", "")
  141. self.query_one("#sched-info-name", Input).value = name
  142. self.query_one("#sched-info-desc", Input).value = ""
  143. self.query_one("#sched-info-enabled", Switch).value = True
  144. self.query_one("#sched-info-max-runs", Input).value = "0"
  145. self.query_one("#sched-info-max-failures", Input).value = "3"
  146. self.query_one("#sched-info-timeout", Input).value = "60.0"
  147. self.query_one("#sched-info-priority", Input).value = "0"
  148. self.query_one("#sched-info-run-on-startup", Switch).value = False
  149. self.query_one("#sched-info-tags", Input).value = ""
  150. def _populate_from_detail(self, detail: object) -> None:
  151. """Befuellt alle Felder aus den geladenen Detail-Daten."""
  152. # Allgemein
  153. self.query_one("#sched-info-name", Input).value = detail.name or ""
  154. self.query_one("#sched-info-desc", Input).value = detail.description or ""
  155. self.query_one("#sched-info-enabled", Switch).value = detail.enabled
  156. # Konfiguration
  157. cfg = detail.config or {}
  158. self.query_one("#sched-info-max-runs", Input).value = str(
  159. cfg.get("max_runs", 0)
  160. )
  161. self.query_one("#sched-info-max-failures", Input).value = str(
  162. cfg.get("max_failures", 3)
  163. )
  164. self.query_one("#sched-info-timeout", Input).value = str(
  165. cfg.get("timeout_seconds", 60.0)
  166. )
  167. self.query_one("#sched-info-priority", Input).value = str(
  168. cfg.get("priority", 0)
  169. )
  170. self.query_one("#sched-info-run-on-startup", Switch).value = bool(
  171. cfg.get("run_on_startup", False)
  172. )
  173. tags = cfg.get("tags", [])
  174. self.query_one("#sched-info-tags", Input).value = ", ".join(tags)
  175. # Statistik
  176. self._update_stats(detail)
  177. def _update_stats(self, detail: object) -> None:
  178. """Aktualisiert die Statistik-Tabelle (nur-lesend)."""
  179. last_run = (
  180. detail.last_run.replace("T", " ")[:19] if detail.last_run else "—"
  181. )
  182. next_run = (
  183. detail.next_run.replace("T", " ")[:19] if detail.next_run else "—"
  184. )
  185. stats = self.query_one("#sched-info-stats", StatusTable)
  186. stats.update_data([
  187. ("Ausfuehrungen", str(detail.run_count)),
  188. ("Fehler", str(detail.failure_count)),
  189. ("Letzte Ausfuehrung", last_run),
  190. ("Naechste Ausfuehrung", next_run),
  191. ])
  192. async def _load_detail_stats_only(self) -> None:
  193. """Laedt nur die Statistik-Daten neu (ohne Formularfelder zu ueberschreiben)."""
  194. if not self.connection or not self._job_data:
  195. return
  196. job_id = self._job_data.get("job_id", "")
  197. if not job_id:
  198. return
  199. try:
  200. detail = await self.connection.request_schedule_detail(job_id)
  201. except Exception:
  202. return
  203. if detail is None or detail.error:
  204. return
  205. self._detail_data = detail
  206. self._update_stats(detail)
  207. def collect_data(self) -> dict:
  208. """
  209. Sammelt die eingegebenen Daten aus allen Formularfeldern.
  210. Returns:
  211. dict mit Schluesseln: name, description, enabled, config.
  212. config enthaelt: max_runs, max_failures, timeout_seconds,
  213. priority, run_on_startup, tags.
  214. """
  215. name = self.query_one("#sched-info-name", Input).value.strip()
  216. description = self.query_one("#sched-info-desc", Input).value.strip()
  217. enabled = self.query_one("#sched-info-enabled", Switch).value
  218. # Konfigurations-Felder auslesen und konvertieren
  219. try:
  220. max_runs = int(self.query_one("#sched-info-max-runs", Input).value)
  221. except (ValueError, TypeError):
  222. max_runs = 0
  223. try:
  224. max_failures = int(
  225. self.query_one("#sched-info-max-failures", Input).value
  226. )
  227. except (ValueError, TypeError):
  228. max_failures = 3
  229. try:
  230. timeout_seconds = float(
  231. self.query_one("#sched-info-timeout", Input).value
  232. )
  233. except (ValueError, TypeError):
  234. timeout_seconds = 60.0
  235. try:
  236. priority = int(self.query_one("#sched-info-priority", Input).value)
  237. except (ValueError, TypeError):
  238. priority = 0
  239. run_on_startup = self.query_one("#sched-info-run-on-startup", Switch).value
  240. tags_raw = self.query_one("#sched-info-tags", Input).value.strip()
  241. tags = [t.strip() for t in tags_raw.split(",") if t.strip()] if tags_raw else []
  242. return {
  243. "name": name,
  244. "description": description,
  245. "enabled": enabled,
  246. "config": {
  247. "max_runs": max_runs,
  248. "max_failures": max_failures,
  249. "timeout_seconds": timeout_seconds,
  250. "priority": priority,
  251. "run_on_startup": run_on_startup,
  252. "tags": tags,
  253. },
  254. }
  255. async def on_button_pressed(self, event: Button.Pressed) -> None:
  256. """Handler fuer Button-Klicks."""
  257. if event.button.id == "sched-save-btn":
  258. await self._save()
  259. async def _save(self) -> None:
  260. """Aggregiert Daten aller Schedule-SubViews und speichert den Job."""
  261. status = self.query_one("#sched-save-status", Static)
  262. if not self.connection:
  263. status.update("[red]Keine Verbindung zum Server.[/red]")
  264. return
  265. # Daten aus diesem View sammeln
  266. info_data = self.collect_data()
  267. if not info_data["name"]:
  268. status.update("[red]Name darf nicht leer sein.[/red]")
  269. return
  270. # Daten aus Geschwister-Views sammeln (Trigger, Conditions, Actions)
  271. trigger: dict = {}
  272. conditions: list[dict] = []
  273. actions: list[dict] = []
  274. view_map = self.app._view_map # type: ignore[attr-defined]
  275. trigger_view = view_map.get("sched_trigger")
  276. if trigger_view and hasattr(trigger_view, "collect_data"):
  277. trigger = trigger_view.collect_data()
  278. conditions_view = view_map.get("sched_conditions")
  279. if conditions_view and hasattr(conditions_view, "collect_data"):
  280. conditions = conditions_view.collect_data()
  281. actions_view = view_map.get("sched_actions")
  282. if actions_view and hasattr(actions_view, "collect_data"):
  283. actions = actions_view.collect_data()
  284. job_id = self._job_data.get("job_id", "")
  285. try:
  286. status.update("[yellow]Speichern...[/yellow]")
  287. result = await self.connection.save_schedule_job(
  288. job_id=job_id,
  289. name=info_data["name"],
  290. description=info_data["description"],
  291. trigger=trigger,
  292. conditions=conditions,
  293. actions=actions,
  294. config=info_data["config"],
  295. )
  296. except Exception as exc:
  297. status.update(f"[red]Fehler: {exc}[/red]")
  298. return
  299. if result is None or (hasattr(result, "error") and result.error):
  300. error_msg = getattr(result, "error", "Unbekannter Fehler")
  301. status.update(f"[red]Fehler: {error_msg}[/red]")
  302. return
  303. # Bei neuem Job die zurueckgegebene ID uebernehmen
  304. if not job_id and hasattr(result, "job_id") and result.job_id:
  305. self._job_data["job_id"] = result.job_id
  306. # Statistik-Bereich sichtbar machen
  307. stats_section = self.query_one("#sched-info-stats-section", Vertical)
  308. stats_section.display = True
  309. status.update("[green]Gespeichert.[/green]")