diff --git a/README.md b/README.md index c842cee..7d0fc50 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Uruchom: python3 app.py ``` -Aplikacja pozwala dodawać, aktualizować i usuwać hosty, wybierać metodę `HTTP`/`ICMP`, ustawić interwał oraz timeout. Konfiguracja jest automatycznie zapisywana w pliku `hosts.json` obok aplikacji. Tabela pokazuje bieżący status, liczbę testów, liczbę odpowiedzi OK/FAIL, procent dostępności i średni czas odpowiedzi. +Aplikacja pozwala dodawać, aktualizować i usuwać hosty, wybierać metodę `HTTP`/`ICMP` oraz ustawić osobny interwał i timeout. Kliknięcie hosta na liście uzupełnia formularz jego ustawieniami. Konfiguracja jest automatycznie zapisywana w pliku `hosts.json` obok aplikacji. Tabela pokazuje bieżący status, interwał, liczbę testów, liczbę odpowiedzi OK/FAIL, procent dostępności i średni czas odpowiedzi. Przycisk „Uruchom monitoring” rozpoczyna cykliczne sprawdzanie wszystkich zapisanych hostów. Wyniki są zbierane równolegle, więc wolny host nie blokuje pozostałych. diff --git a/app.py b/app.py index 24fd358..ae233b5 100644 --- a/app.py +++ b/app.py @@ -24,6 +24,7 @@ CONFIG_FILE = Path(__file__).with_name("hosts.json") class Host: target: str method: str = "http" + interval: float = 10.0 class HostMonitorApp(tk.Tk): @@ -38,6 +39,7 @@ class HostMonitorApp(tk.Tk): self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10) self.running = False self.after_id: str | None = None + self.host_after_ids: dict[str, str] = {} self.selected_target: str | None = None self.worker_lock = threading.Lock() self._build_ui() @@ -70,10 +72,10 @@ class HostMonitorApp(tk.Tk): table_frame = ttk.Frame(self, padding=(10, 0, 10, 10)) table_frame.pack(fill="both", expand=True) - columns = ("target", "method", "status", "tests", "ok", "fail", "availability", "avg", "last") + columns = ("target", "method", "interval", "status", "tests", "ok", "fail", "availability", "avg", "last") self.table = ttk.Treeview(table_frame, columns=columns, show="headings", selectmode="extended") - headings = {"target": "Host / URL", "method": "Metoda", "status": "Status", "tests": "Testy", "ok": "OK", "fail": "FAIL", "availability": "Dostępność", "avg": "Śr. czas", "last": "Ostatni test"} - widths = {"target": 260, "method": 70, "status": 75, "tests": 55, "ok": 45, "fail": 50, "availability": 90, "avg": 80, "last": 180} + headings = {"target": "Host / URL", "method": "Metoda", "interval": "Interwał [s]", "status": "Status", "tests": "Testy", "ok": "OK", "fail": "FAIL", "availability": "Dostępność", "avg": "Śr. czas", "last": "Ostatni test"} + widths = {"target": 240, "method": 70, "interval": 85, "status": 75, "tests": 55, "ok": 45, "fail": 50, "availability": 90, "avg": 80, "last": 180} for column in columns: self.table.heading(column, text=headings[column]) self.table.column(column, width=widths[column], anchor="center") @@ -103,6 +105,7 @@ class HostMonitorApp(tk.Tk): self.selected_target = target self.target_var.set(host.target) self.method_var.set(host.method) + self.interval_var.set(str(host.interval).rstrip("0").rstrip(".")) def _add_host(self) -> None: target = self.target_var.get().strip() @@ -110,6 +113,13 @@ class HostMonitorApp(tk.Tk): if not target: messagebox.showwarning("Brak hosta", "Wpisz adres hosta lub URL.") return + try: + interval = float(self.interval_var.get()) + if interval <= 0: + raise ValueError + except ValueError: + messagebox.showerror("Błędny interwał", "Interwał musi być liczbą większą od zera.") + return selected_host = next((host for host in self.hosts if host.target == self.selected_target), None) duplicate = next((host for host in self.hosts if host.target == target and host is not selected_host), None) @@ -120,8 +130,13 @@ class HostMonitorApp(tk.Tk): was_update = selected_host is not None if selected_host: old_target = selected_host.target + if self.running: + old_after_id = self.host_after_ids.pop(old_target, None) + if old_after_id: + self.after_cancel(old_after_id) selected_host.target = target selected_host.method = method + selected_host.interval = interval if old_target != target: if old_target in self.stats: self.stats[target] = self.stats.pop(old_target) @@ -130,13 +145,16 @@ class HostMonitorApp(tk.Tk): existing = next((host for host in self.hosts if host.target == target), None) if existing: existing.method = method + existing.interval = interval self.selected_target = target else: - self.hosts.append(Host(target, method)) + self.hosts.append(Host(target, method, interval)) self.selected_target = target if not was_update: self.target_var.set("") self._refresh_table() + if was_update and self.running: + self._schedule_check(self.selected_target or target) self._save_config(silent=True) def _remove_selected(self) -> None: @@ -154,8 +172,9 @@ class HostMonitorApp(tk.Tk): def _toggle_monitoring(self) -> None: if self.running: self.running = False - if self.after_id: - self.after_cancel(self.after_id) + for after_id in self.host_after_ids.values(): + self.after_cancel(after_id) + self.host_after_ids.clear() self.start_button.configure(text="Uruchom monitoring") self.status_var.set("Monitoring zatrzymany.") return @@ -163,31 +182,39 @@ class HostMonitorApp(tk.Tk): messagebox.showwarning("Brak hostów", "Dodaj co najmniej jeden host.") return try: - float(self.interval_var.get()) float(self.timeout_var.get()) + if any(host.interval <= 0 for host in self.hosts): + raise ValueError except ValueError: - messagebox.showerror("Błędne ustawienia", "Interwał i timeout muszą być liczbami.") + messagebox.showerror("Błędne ustawienia", "Interwały hostów i timeout muszą być poprawnymi liczbami.") return self.running = True self.start_button.configure(text="Zatrzymaj monitoring") - self._schedule_check(0) + for host in self.hosts: + self._schedule_check(host.target, 0) - def _schedule_check(self, delay_ms: int | None = None) -> None: + def _schedule_check(self, target: str, delay_ms: int | None = None) -> None: if not self.running: return if delay_ms is None: - delay_ms = max(1, int(float(self.interval_var.get()) * 1000)) - self.after_id = self.after(delay_ms, self._start_check) + host = next((item for item in self.hosts if item.target == target), None) + if host is None: + return + delay_ms = max(1, int(host.interval * 1000)) + self.host_after_ids[target] = self.after(delay_ms, lambda: self._start_check(target)) - def _start_check(self) -> None: + def _start_check(self, target: str) -> None: if not self.running: return timeout = float(self.timeout_var.get()) + host = next((item for item in self.hosts if item.target == target), None) + if host is None: + self.host_after_ids.pop(target, None) + return with self.worker_lock: - for host in self.hosts: - future = self.executor.submit(check, host.target, host.method, timeout) - future.add_done_callback(lambda item, target=host.target: self._put_result(target, item)) - self._schedule_check() + future = self.executor.submit(check, host.target, host.method, timeout) + future.add_done_callback(lambda item, target=host.target: self._put_result(target, item)) + self._schedule_check(target) def _put_result(self, target: str, future: concurrent.futures.Future[CheckResult]) -> None: try: @@ -228,7 +255,7 @@ class HostMonitorApp(tk.Tk): availability = f"{ok / tests * 100:.1f}%" if tests else "-" avg = f"{float(stats.get('total_ms', 0)) / tests:.0f} ms" if tests else "-" last = f"{stats.get('last', '-')} ({stats.get('detail', '-')})" if tests else "-" - values = (host.target, host.method.upper(), "OK" if stats.get("available") else ("FAIL" if tests else "-"), tests, ok, fail, availability, avg, last) + values = (host.target, host.method.upper(), f"{host.interval:g}", "OK" if stats.get("available") else ("FAIL" if tests else "-"), tests, ok, fail, availability, avg, last) tag = "up" if stats.get("available") else ("down" if tests else "unknown") if host.target in existing: self.table.item(host.target, values=values, tags=(tag,)) @@ -242,8 +269,17 @@ class HostMonitorApp(tk.Tk): if CONFIG_FILE.exists(): try: data = json.loads(CONFIG_FILE.read_text(encoding="utf-8")) - self.hosts = [Host(str(item["target"]), str(item.get("method", "http"))) for item in data.get("hosts", []) if item.get("target")] - self.interval_var.set(str(data.get("interval", 10))) + default_interval = float(data.get("interval", 10)) + self.hosts = [ + Host( + str(item["target"]), + str(item.get("method", "http")), + float(item.get("interval", default_interval)), + ) + for item in data.get("hosts", []) + if item.get("target") + ] + self.interval_var.set(str(default_interval)) self.timeout_var.set(str(data.get("timeout", 3))) self._refresh_table() except (OSError, ValueError, KeyError, TypeError) as error: @@ -251,7 +287,11 @@ class HostMonitorApp(tk.Tk): def _save_config(self, silent: bool = False) -> None: try: - data = {"interval": float(self.interval_var.get()), "timeout": float(self.timeout_var.get()), "hosts": [{"target": host.target, "method": host.method} for host in self.hosts]} + data = { + "interval": float(self.interval_var.get()), + "timeout": float(self.timeout_var.get()), + "hosts": [{"target": host.target, "method": host.method, "interval": host.interval} for host in self.hosts], + } CONFIG_FILE.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") if not silent: self.status_var.set(f"Zapisano konfigurację: {CONFIG_FILE.name}") @@ -260,8 +300,9 @@ class HostMonitorApp(tk.Tk): def _close(self) -> None: self.running = False - if self.after_id: - self.after_cancel(self.after_id) + for after_id in self.host_after_ids.values(): + self.after_cancel(after_id) + self.host_after_ids.clear() self._save_config(silent=True) self.executor.shutdown(wait=False, cancel_futures=True) self.destroy()