#!/usr/bin/env python3 """Graficzna aplikacja do monitorowania dostępności hostów.""" from __future__ import annotations import concurrent.futures import json import queue import threading import tkinter as tk from dataclasses import dataclass from datetime import datetime from pathlib import Path from tkinter import messagebox, ttk from host_checker import CheckResult, check __version__ = "0.0.1" CONFIG_FILE = Path(__file__).with_name("hosts.json") @dataclass class Host: target: str method: str = "http" class HostMonitorApp(tk.Tk): def __init__(self) -> None: super().__init__() self.title(f"Monitor dostępności hostów v{__version__}") self.geometry("1120x620") self.minsize(850, 450) self.hosts: list[Host] = [] self.stats: dict[str, dict[str, float | int | bool | str]] = {} self.results: queue.Queue[tuple[str, CheckResult]] = queue.Queue() self.executor = concurrent.futures.ThreadPoolExecutor(max_workers=10) self.running = False self.after_id: str | None = None self.selected_target: str | None = None self.worker_lock = threading.Lock() self._build_ui() self._load_config() self.protocol("WM_DELETE_WINDOW", self._close) self.after(200, self._consume_results) def _build_ui(self) -> None: controls = ttk.Frame(self, padding=10) controls.pack(fill="x") ttk.Label(controls, text="Host / URL:").grid(row=0, column=0, sticky="w") self.target_var = tk.StringVar() ttk.Entry(controls, textvariable=self.target_var, width=38).grid(row=0, column=1, padx=5) ttk.Label(controls, text="Metoda:").grid(row=0, column=2, sticky="e") self.method_var = tk.StringVar(value="http") ttk.Combobox(controls, textvariable=self.method_var, values=("http", "icmp"), state="readonly", width=8).grid(row=0, column=3, padx=5) ttk.Button(controls, text="Dodaj / aktualizuj", command=self._add_host).grid(row=0, column=4, padx=5) ttk.Button(controls, text="Usuń zaznaczone", command=self._remove_selected).grid(row=0, column=5, padx=5) ttk.Label(controls, text="Interwał [s]:").grid(row=1, column=0, sticky="w", pady=(8, 0)) self.interval_var = tk.StringVar(value="10") ttk.Spinbox(controls, from_=1, to=86400, textvariable=self.interval_var, width=8).grid(row=1, column=1, sticky="w", padx=5, pady=(8, 0)) ttk.Label(controls, text="Timeout [s]:").grid(row=1, column=2, sticky="e", pady=(8, 0)) self.timeout_var = tk.StringVar(value="3") ttk.Spinbox(controls, from_=0.1, to=300, increment=0.5, textvariable=self.timeout_var, width=8).grid(row=1, column=3, sticky="w", padx=5, pady=(8, 0)) self.start_button = ttk.Button(controls, text="Uruchom monitoring", command=self._toggle_monitoring) self.start_button.grid(row=1, column=4, padx=5, pady=(8, 0)) ttk.Button(controls, text="Zapisz konfigurację", command=self._save_config).grid(row=1, column=5, padx=5, pady=(8, 0)) 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") 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} for column in columns: self.table.heading(column, text=headings[column]) self.table.column(column, width=widths[column], anchor="center") self.table.column("target", anchor="w") scrollbar = ttk.Scrollbar(table_frame, orient="vertical", command=self.table.yview) self.table.configure(yscrollcommand=scrollbar.set) self.table.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") self.table.bind("<>", self._select_host) self.table.tag_configure("up", foreground="#16803c") self.table.tag_configure("down", foreground="#b42318") self.table.tag_configure("unknown", foreground="#666666") self.status_var = tk.StringVar(value="Gotowy. Dodaj hosty i uruchom monitoring.") ttk.Label(self, textvariable=self.status_var, relief="sunken", anchor="w", padding=5).pack(fill="x", side="bottom") def _select_host(self, _event: tk.Event[tk.Misc] | None = None) -> None: selected = self.table.selection() if len(selected) != 1: self.selected_target = None return target = selected[0] host = next((item for item in self.hosts if item.target == target), None) if host is None: self.selected_target = None return self.selected_target = target self.target_var.set(host.target) self.method_var.set(host.method) def _add_host(self) -> None: target = self.target_var.get().strip() method = self.method_var.get() if not target: messagebox.showwarning("Brak hosta", "Wpisz adres hosta lub URL.") 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) if duplicate: messagebox.showwarning("Host już istnieje", "Host o tym adresie jest już na liście.") return was_update = selected_host is not None if selected_host: old_target = selected_host.target selected_host.target = target selected_host.method = method if old_target != target: if old_target in self.stats: self.stats[target] = self.stats.pop(old_target) self.selected_target = target else: existing = next((host for host in self.hosts if host.target == target), None) if existing: existing.method = method self.selected_target = target else: self.hosts.append(Host(target, method)) self.selected_target = target if not was_update: self.target_var.set("") self._refresh_table() self._save_config(silent=True) def _remove_selected(self) -> None: selected = set(self.table.selection()) if not selected: return self.hosts = [host for host in self.hosts if host.target not in selected] for target in selected: self.stats.pop(target, None) if self.selected_target in selected: self.selected_target = None self._refresh_table() self._save_config(silent=True) def _toggle_monitoring(self) -> None: if self.running: self.running = False if self.after_id: self.after_cancel(self.after_id) self.start_button.configure(text="Uruchom monitoring") self.status_var.set("Monitoring zatrzymany.") return if not self.hosts: messagebox.showwarning("Brak hostów", "Dodaj co najmniej jeden host.") return try: float(self.interval_var.get()) float(self.timeout_var.get()) except ValueError: messagebox.showerror("Błędne ustawienia", "Interwał i timeout muszą być liczbami.") return self.running = True self.start_button.configure(text="Zatrzymaj monitoring") self._schedule_check(0) def _schedule_check(self, 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) def _start_check(self) -> None: if not self.running: return timeout = float(self.timeout_var.get()) 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() def _put_result(self, target: str, future: concurrent.futures.Future[CheckResult]) -> None: try: self.results.put((target, future.result())) except Exception as error: # nie przerywaj monitorowania przez pojedynczy host self.results.put((target, CheckResult(target, "?", False, str(error), 0))) def _consume_results(self) -> None: changed = False while True: try: target, result = self.results.get_nowait() except queue.Empty: break stats = self.stats.setdefault(target, {"tests": 0, "ok": 0, "fail": 0, "total_ms": 0.0, "available": False, "detail": "", "last": ""}) stats["tests"] = int(stats["tests"]) + 1 stats["ok"] = int(stats["ok"]) + int(result.available) stats["fail"] = int(stats["fail"]) + int(not result.available) stats["total_ms"] = float(stats["total_ms"]) + result.elapsed_ms stats["available"] = result.available stats["detail"] = result.detail stats["last"] = datetime.now().strftime("%H:%M:%S") changed = True if changed: self._refresh_table() self.after(200, self._consume_results) def _refresh_table(self) -> None: existing = set(self.table.get_children()) current = {host.target for host in self.hosts} for target in existing - current: self.table.delete(target) for host in self.hosts: stats = self.stats.get(host.target, {}) tests = int(stats.get("tests", 0)) ok = int(stats.get("ok", 0)) fail = int(stats.get("fail", 0)) 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) 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,)) else: self.table.insert("", "end", iid=host.target, values=values, tags=(tag,)) if self.selected_target in current: self.table.selection_set(self.selected_target) self.table.focus(self.selected_target) def _load_config(self) -> None: 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))) self.timeout_var.set(str(data.get("timeout", 3))) self._refresh_table() except (OSError, ValueError, KeyError, TypeError) as error: messagebox.showwarning("Błąd konfiguracji", f"Nie udało się odczytać hosts.json: {error}") 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]} 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}") except (OSError, ValueError) as error: messagebox.showerror("Błąd zapisu", str(error)) def _close(self) -> None: self.running = False if self.after_id: self.after_cancel(self.after_id) self._save_config(silent=True) self.executor.shutdown(wait=False, cancel_futures=True) self.destroy() if __name__ == "__main__": HostMonitorApp().mainloop()