schedule_view.py 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. # -*- coding: utf-8 -*-
  2. """
  3. Schedule-View: Tabellarische Liste aller Schedule-Jobs.
  4. Zeigt Jobs in einer DataTable mit Name, Trigger, Status, Ausfuehrungszeiten.
  5. Enter/Klick auf eine Zeile oeffnet die Job-Detailansicht.
  6. Buttons zum Aktivieren/Deaktivieren, Erstellen und Loeschen.
  7. """
  8. from __future__ import annotations
  9. from textual.app import ComposeResult
  10. from textual.containers import Horizontal
  11. from textual.widgets import Static, Button, DataTable
  12. from trixy_core.tui.views.base import MainView
  13. class ScheduleView(MainView):
  14. """
  15. F6: Schedule-Ansicht.
  16. Zeigt alle Schedule-Jobs tabellarisch.
  17. Zeilen-Auswahl oeffnet Details, Buttons fuer Enable/Disable/Delete/New.
  18. """
  19. TITLE = "Schedule"
  20. VIEW_ID = "schedule"
  21. def __init__(self, **kwargs) -> None:
  22. super().__init__(**kwargs)
  23. self._jobs: list[dict] = []
  24. self._refresh_counter: int = 0
  25. def compose(self) -> ComposeResult:
  26. yield Static("", id="schedule-header")
  27. yield DataTable(id="schedule-table", cursor_type="row")
  28. with Horizontal(id="schedule-actions"):
  29. yield Button("Neu", id="schedule-btn-new", variant="success")
  30. yield Button("Aktivieren", id="schedule-btn-enable", variant="success")
  31. yield Button("Deaktivieren", id="schedule-btn-disable", variant="warning")
  32. yield Button("Loeschen", id="schedule-btn-delete", variant="error")
  33. yield Static("", id="schedule-status")
  34. async def on_activate(self) -> None:
  35. """Laedt die Job-Liste."""
  36. table = self.query_one("#schedule-table", DataTable)
  37. if not table.columns:
  38. table.add_columns("Name", "Trigger", "Status", "Letzte Ausfuehrung", "Naechste")
  39. await self._load_jobs()
  40. async def refresh_data(self) -> None:
  41. """Periodisches Update (alle 10 Sekunden)."""
  42. self._refresh_counter += 1
  43. if self._refresh_counter % 5 == 0:
  44. await self._load_jobs()
  45. async def _load_jobs(self) -> None:
  46. """Laedt die Job-Liste vom Server."""
  47. if not self.connection:
  48. return
  49. try:
  50. response = await self.connection.request_schedule_jobs()
  51. except Exception:
  52. self._set_status("Fehler beim Laden der Jobs", error=True)
  53. return
  54. if response is None:
  55. return
  56. self._jobs = response.jobs
  57. self._update_header()
  58. self._update_table()
  59. def _update_header(self) -> None:
  60. """Aktualisiert die Kopfzeile."""
  61. header = self.query_one("#schedule-header", Static)
  62. total = len(self._jobs)
  63. enabled = sum(1 for j in self._jobs if j.get("enabled", False))
  64. header.update(f"[bold]Schedule:[/bold] {enabled}/{total} aktiv")
  65. def _update_table(self) -> None:
  66. """Aktualisiert die Job-Tabelle."""
  67. table = self.query_one("#schedule-table", DataTable)
  68. table.clear()
  69. sorted_jobs = sorted(self._jobs, key=lambda j: j.get("name", ""))
  70. for job in sorted_jobs:
  71. name = job.get("name", job.get("job_id", ""))
  72. trigger_type = job.get("trigger_type", "—")
  73. enabled = job.get("enabled", False)
  74. state = job.get("state", "")
  75. if enabled:
  76. status = f"[green]Aktiv[/green] ({state})"
  77. else:
  78. status = f"[dim]Deaktiviert[/dim] ({state})"
  79. last_run = job.get("last_run", "—") or "—"
  80. next_run = job.get("next_run", "—") or "—"
  81. # Datum-Strings kuerzen
  82. if last_run != "—" and "T" in last_run:
  83. last_run = last_run.replace("T", " ")[:19]
  84. if next_run != "—" and "T" in next_run:
  85. next_run = next_run.replace("T", " ")[:19]
  86. table.add_row(
  87. name, trigger_type, status, last_run, next_run,
  88. key=job.get("job_id", name),
  89. )
  90. def _get_selected_job(self) -> dict | None:
  91. """Gibt den aktuell markierten Job zurueck."""
  92. try:
  93. table = self.query_one("#schedule-table", DataTable)
  94. row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)
  95. job_id = str(row_key.value) if row_key else ""
  96. if job_id:
  97. for j in self._jobs:
  98. if j.get("job_id") == job_id:
  99. return j
  100. except Exception:
  101. pass
  102. return None
  103. async def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
  104. """Enter auf einer Zeile: Oeffnet die Detail-Ansicht."""
  105. row_key = str(event.row_key.value) if event.row_key else ""
  106. job_data = None
  107. for j in self._jobs:
  108. if j.get("job_id") == row_key:
  109. job_data = j
  110. break
  111. if not job_data:
  112. return
  113. try:
  114. app = self.app
  115. if hasattr(app, "open_schedule_detail"):
  116. await app.open_schedule_detail(job_data)
  117. except Exception:
  118. pass
  119. async def on_button_pressed(self, event: Button.Pressed) -> None:
  120. """Verarbeitet Button-Klicks."""
  121. btn_id = event.button.id
  122. if not self.connection:
  123. return
  124. # "Neu" benoetigt kein ausgewaehltes Plugin
  125. if btn_id == "schedule-btn-new":
  126. try:
  127. app = self.app
  128. if hasattr(app, "open_schedule_detail"):
  129. await app.open_schedule_detail({"job_id": "", "name": "Neuer Job"})
  130. except Exception:
  131. pass
  132. return
  133. job = self._get_selected_job()
  134. if not job:
  135. self._set_status("Kein Job ausgewaehlt", error=True)
  136. return
  137. job_id = job.get("job_id", "")
  138. action = ""
  139. if btn_id == "schedule-btn-enable":
  140. action = "enable"
  141. elif btn_id == "schedule-btn-disable":
  142. action = "disable"
  143. elif btn_id == "schedule-btn-delete":
  144. action = "delete"
  145. if action:
  146. await self._schedule_action(action, job_id)
  147. async def _schedule_action(self, action: str, job_id: str) -> None:
  148. """Fuehrt eine Schedule-Aktion aus und aktualisiert die Liste."""
  149. try:
  150. result = await self.connection.schedule_action(action, job_id)
  151. if result and result.success:
  152. self._set_status(result.message)
  153. await self._load_jobs()
  154. else:
  155. error = result.error if result else "Unbekannter Fehler"
  156. self._set_status(f"Fehler: {error}", error=True)
  157. except Exception as e:
  158. self._set_status(f"Fehler: {e}", error=True)
  159. def _set_status(self, text: str, error: bool = False) -> None:
  160. """Setzt die Statuszeile."""
  161. status = self.query_one("#schedule-status", Static)
  162. if error:
  163. status.update(f"[red]{text}[/red]")
  164. else:
  165. status.update(f"[green]{text}[/green]")