from pathlib import Path
import textwrap, zipfile, os, json
root = Path("/mnt/data/serverwatch")
root.mkdir(exist_ok=True)
serverwatch_py = r'''#!/usr/bin/env python3
import argparse
import os
import platform
import shutil
import socket
import subprocess
import time
from datetime import datetime
try:
import psutil
from rich.console import Console
from rich.table import Table
from rich.panel import Panel
from rich.layout import Layout
from rich.live import Live
from rich.progress_bar import ProgressBar
from rich.text import Text
from rich.align import Align
except ImportError as e:
print("ServerWatch: chybí závislosti. Spusť: sudo /opt/serverwatch/install.sh")
raise SystemExit(1)
console = Console()
VERSION = "1.0.0"
SERVICES = [
("nginx", "Nginx"),
("php8.3-fpm", "PHP 8.3 FPM"),
("mariadb", "MariaDB"),
("mysql", "MySQL"),
("docker", "Docker"),
("ssh", "SSH"),
("sshd", "SSHD"),
]
def run(cmd):
try:
return subprocess.check_output(cmd, stderr=subprocess.DEVNULL, text=True, timeout=2).strip()
except Exception:
return ""
def service_state(name):
try:
p = subprocess.run(["systemctl", "is-active", name], capture_output=True, text=True, timeout=2)
return p.stdout.strip() == "active"
except Exception:
return False
def existing_services():
found = []
seen_labels = set()
for service, label in SERVICES:
status = run(["systemctl", "show", service, "--property=LoadState", "--value"])
if status and status != "not-found" and label not in seen_labels:
found.append((service, label))
seen_labels.add(label)
return found
def human_bytes(n):
n = float(n)
for unit in ["B", "KB", "MB", "GB", "TB"]:
if n < 1024:
return f"{n:.1f} {unit}"
n /= 1024
return f"{n:.1f} PB"
def uptime():
sec = int(time.time() - psutil.boot_time())
days, sec = divmod(sec, 86400)
hours, sec = divmod(sec, 3600)
mins, _ = divmod(sec, 60)
if days:
return f"{days}d {hours:02}h {mins:02}m"
return f"{hours}h {mins:02}m"
def temperature():
try:
temps = psutil.sensors_temperatures()
vals = []
for entries in temps.values():
for e in entries:
if e.current and 0 < e.current < 150:
vals.append(e.current)
return f"{max(vals):.0f} °C" if vals else "N/A"
except Exception:
return "N/A"
def public_ip():
# No external network call: report primary local/server IP.
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("1.1.1.1", 80))
ip = s.getsockname()[0]
s.close()
return ip
except Exception:
return socket.gethostbyname(socket.gethostname())
def metric_row(label, percent, detail):
grid = Table.grid(expand=True)
grid.add_column(width=8)
grid.add_column(ratio=1)
grid.add_column(width=24, justify="right")
style = "green" if percent < 70 else ("yellow" if percent < 90 else "red")
grid.add_row(
f"[bold]{label}[/]",
ProgressBar(total=100, completed=max(0, min(100, percent)), width=None, style="grey37", complete_style=style),
f"[{style}]{percent:5.1f}%[/] {detail}"
)
return grid
def snapshot(prev_net=None, prev_time=None):
cpu = psutil.cpu_percent(interval=0.15)
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
swap = psutil.swap_memory()
net = psutil.net_io_counters()
now = time.time()
rx = tx = 0.0
if prev_net and prev_time:
dt = max(now - prev_time, .001)
rx = max(0, net.bytes_recv - prev_net.bytes_recv) / dt
tx = max(0, net.bytes_sent - prev_net.bytes_sent) / dt
return {
"cpu": cpu, "mem": mem, "disk": disk, "swap": swap,
"net": net, "rx": rx, "tx": tx, "time": now,
"load": os.getloadavg() if hasattr(os, "getloadavg") else (0,0,0),
}
def dashboard_render(data, services):
layout = Layout()
layout.split_column(
Layout(name="header", size=3),
Layout(name="metrics", size=10),
Layout(name="middle", size=7),
Layout(name="processes", ratio=1),
Layout(name="footer", size=3),
)
host = socket.gethostname()
header = Table.grid(expand=True)
header.add_column()
header.add_column(justify="right")
header.add_row(
f"[bold cyan]SERVERWATCH[/] [dim]v{VERSION}[/] [bold]{host}[/]",
f"[green]● ONLINE[/] Uptime [bold]{uptime()}[/]"
)
layout["header"].update(Panel(header, border_style="cyan"))
metrics = Table.grid(expand=True)
metrics.add_row(metric_row("CPU", data["cpu"], f"{psutil.cpu_count()} threads"))
metrics.add_row(metric_row("RAM", data["mem"].percent, f"{human_bytes(data['mem'].used)} / {human_bytes(data['mem'].total)}"))
metrics.add_row(metric_row("DISK", data["disk"].percent, f"{human_bytes(data['disk'].used)} / {human_bytes(data['disk'].total)}"))
metrics.add_row(metric_row("SWAP", data["swap"].percent, f"{human_bytes(data['swap'].used)} / {human_bytes(data['swap'].total)}"))
layout["metrics"].update(Panel(metrics, title="Resources", border_style="blue"))
mid = Table.grid(expand=True)
mid.add_column(ratio=1)
mid.add_column(ratio=1)
load = data["load"]
svc_text = []
for name, label in services:
ok = service_state(name)
svc_text.append(f"[green]●[/] {label}" if ok else f"[red]●[/] {label}")
if not svc_text:
svc_text = ["[dim]No watched services detected[/]"]
left = (
f"[bold]LOAD[/] {load[0]:.2f} {load[1]:.2f} {load[2]:.2f}\n"
f"[bold]TEMP[/] {temperature()}\n"
f"[bold]NET ↓[/] {human_bytes(data['rx'])}/s\n"
f"[bold]NET ↑[/] {human_bytes(data['tx'])}/s\n"
f"[bold]IP[/] {public_ip()}"
)
mid.add_row(left, "\n".join(svc_text[:5]))
layout["middle"].update(Panel(mid, title="System / Services", border_style="magenta"))
proc = Table(expand=True, box=None)
proc.add_column("PID", width=9)
proc.add_column("PROCESS", ratio=1)
proc.add_column("CPU", justify="right", width=9)
proc.add_column("RAM", justify="right", width=12)
rows = []
for p in psutil.process_iter(["pid", "name", "cpu_percent", "memory_info"]):
try:
info = p.info
rows.append((info["cpu_percent"] or 0, info["pid"], info["name"] or "?", info["memory_info"].rss))
except Exception:
pass
for cpu, pid, name, ram in sorted(rows, reverse=True)[:7]:
proc.add_row(str(pid), name[:40], f"{cpu:.1f}%", human_bytes(ram))
layout["processes"].update(Panel(proc, title="Top processes", border_style="green"))
footer = Align.center("[bold]Q[/] Quit [bold]D[/] Doctor [bold]S[/] Services [bold]P[/] Processes [dim]Refresh 1s[/]")
layout["footer"].update(Panel(footer, border_style="grey50"))
return layout
def dashboard():
services = existing_services()
prev_net = psutil.net_io_counters()
prev_time = time.time()
console.clear()
try:
with Live(console=console, screen=True, refresh_per_second=4) as live:
while True:
data = snapshot(prev_net, prev_time)
prev_net, prev_time = data["net"], data["time"]
live.update(dashboard_render(data, services), refresh=True)
time.sleep(1)
except KeyboardInterrupt:
pass
def show_info():
mem = psutil.virtual_memory()
disk = psutil.disk_usage("/")
t = Table(title="ServerWatch · System info")
t.add_column("Item", style="cyan")
t.add_column("Value")
rows = [
("Hostname", socket.gethostname()),
("OS", f"{platform.system()} {platform.release()}"),
("Architecture", platform.machine()),
("CPU", platform.processor() or run(["lscpu"])[:80] or "N/A"),
("CPU threads", str(psutil.cpu_count())),
("RAM", human_bytes(mem.total)),
("Disk /", human_bytes(disk.total)),
("IP", public_ip()),
("Uptime", uptime()),
("Python", platform.python_version()),
]
for a,b in rows: t.add_row(a,b)
console.print(t)
def show_services():
t = Table(title="ServerWatch · Services")
t.add_column("Service")
t.add_column("State")
svcs = existing_services()
if not svcs:
console.print("[yellow]Nebyly nalezeny sledované služby.[/]")
return
for name, label in svcs:
ok = service_state(name)
t.add_row(label, "[green]● running[/]" if ok else "[red]● stopped[/]")
console.print(t)
def show_processes():
t = Table(title="ServerWatch · Top processes")
t.add_column("PID")
t.add_column("Process")
t.add_column("CPU", justify="right")
t.add_column("RAM", justify="right")
# prime CPU counters
for p in psutil.process_iter():
try: p.cpu_percent(None)
except Exception: pass
time.sleep(.25)
rows = []
for p in psutil.process_iter(["pid","name","memory_info"]):
try:
rows.append((p.cpu_percent(None), p.info["pid"], p.info["name"], p.info["memory_info"].rss))
except Exception: pass
for cpu,pid,name,ram in sorted(rows, reverse=True)[:20]:
t.add_row(str(pid), (name or "?")[:50], f"{cpu:.1f}%", human_bytes(ram))
console.print(t)
def doctor():
checks = []
cpu = psutil.cpu_percent(interval=.4)
mem = psutil.virtual_memory().percent
disk = psutil.disk_usage("/").percent
checks.append(("CPU load", cpu < 90, f"{cpu:.1f}%"))
checks.append(("RAM usage", mem < 90, f"{mem:.1f}%"))
checks.append(("Disk usage", disk < 90, f"{disk:.1f}%"))
for name, label in existing_services():
checks.append((label, service_state(name), "running" if service_state(name) else "stopped"))
failed = sum(1 for _, ok, _ in checks if not ok)
score = max(0, round(100 * (len(checks)-failed) / max(1,len(checks))))
console.print(Panel.fit(f"[bold cyan]SERVERWATCH SYSTEM DIAGNOSTICS[/]\nHealth score: [bold]{score}/100[/]"))
for label, ok, detail in checks:
console.print(f"{'[green]✓[/]' if ok else '[red]✗[/]'} {label}: {detail}")
if shutil.which("apt"):
upgradable = run(["bash","-lc","apt list --upgradable 2>/dev/null | tail -n +2 | wc -l"])
if upgradable and upgradable != "0":
console.print(f"[yellow]⚠[/] {upgradable} balíčků lze aktualizovat")
def simple():
d = snapshot()
console.print(
f"[bold cyan]ServerWatch[/] {socket.gethostname()} "
f"CPU [bold]{d['cpu']:.1f}%[/] RAM [bold]{d['mem'].percent:.1f}%[/] "
f"DISK [bold]{d['disk'].percent:.1f}%[/] Uptime [bold]{uptime()}[/]"
)
def main():
parser = argparse.ArgumentParser(description="ServerWatch - lightweight Linux server monitoring CLI")
parser.add_argument("command", nargs="?", choices=["doctor","services","processes","info"], help="Command")
parser.add_argument("--simple", action="store_true", help="One-line summary")
parser.add_argument("--version", action="version", version=f"ServerWatch {VERSION}")
args = parser.parse_args()
if args.simple: return simple()
if args.command == "doctor": return doctor()
if args.command == "services": return show_services()
if args.command == "processes": return show_processes()
if args.command == "info": return show_info()
dashboard()
if __name__ == "__main__":
main()
'''
install_sh = r'''#!/usr/bin/env bash
set -euo pipefail
if [ "${EUID}" -ne 0 ]; then
echo "Spusť instalaci jako root: sudo ./install.sh"
exit 1
fi
APP_DIR="/opt/serverwatch"
BIN="/usr/local/bin/serverwatch"
echo "== ServerWatch installer =="
if ! command -v python3 >/dev/null 2>&1; then
apt-get update
apt-get install -y python3 python3-venv
fi
mkdir -p "$APP_DIR"
cp serverwatch.py "$APP_DIR/serverwatch.py"
chmod 755 "$APP_DIR/serverwatch.py"
python3 -m venv "$APP_DIR/venv"
"$APP_DIR/venv/bin/pip" install --upgrade pip >/dev/null
"$APP_DIR/venv/bin/pip" install -r requirements.txt
cat > "$BIN" <<'EOF'
#!/usr/bin/env bash
exec /opt/serverwatch/venv/bin/python /opt/serverwatch/serverwatch.py "$@"
EOF
chmod 755 "$BIN"
echo
echo "✓ ServerWatch nainstalován."
echo "Spusť: serverwatch"
echo "Další: serverwatch doctor | services | processes | info | --simple"
'''
uninstall_sh = r'''#!/usr/bin/env bash
set -e
if [ "${EUID}" -ne 0 ]; then
echo "Spusť jako root: sudo ./uninstall.sh"
exit 1
fi
rm -f /usr/local/bin/serverwatch
rm -rf /opt/serverwatch
echo "ServerWatch byl odinstalován."
'''
readme = r'''# ServerWatch v1.0
Lehký interaktivní monitoring Linux VPS přímo v terminálu.
## Funkce
- živý fullscreen dashboard
- CPU, RAM, disk, swap
- load average, teplota (pokud ji VPS poskytuje)
- aktuální RX/TX síťový provoz
- uptime a IP
- automatická detekce běžných služeb (Nginx, PHP-FPM, MariaDB/MySQL, Docker, SSH)
- TOP procesy
- diagnostika `serverwatch doctor`
- jednorázový přehled `serverwatch --simple`
## Instalace
```bash
unzip serverwatch.zip
cd serverwatch
chmod +x install.sh
sudo ./install.sh
```
Potom odkudkoliv:
```bash
serverwatch
```
Další příkazy:
```bash
serverwatch --simple
serverwatch doctor
serverwatch services
serverwatch processes
serverwatch info
serverwatch --version
```
Fullscreen dashboard ukončíš `Ctrl+C`.
## Odinstalace
```bash
sudo ./uninstall.sh
```
## Poznámky
ServerWatch neodesílá telemetrii na externí server. Primární IP zjišťuje lokálně přes routing socket; nevykonává HTTP dotaz na externí "what is my IP" službu.
V1 je zaměřena na Debian/Ubuntu VPS se systemd. Teplota na virtuálních serverech často není hypervizorem zpřístupněna, takže může zobrazit N/A.
'''
(root/"serverwatch.py").write_text(serverwatch_py, encoding="utf-8")
(root/"install.sh").write_text(install_sh, encoding="utf-8")
(root/"uninstall.sh").write_text(uninstall_sh, encoding="utf-8")
(root/"requirements.txt").write_text("psutil>=5.9,<8\nrich>=13,<15\n", encoding="utf-8")
(root/"README.md").write_text(readme, encoding="utf-8")
os.chmod(root/"install.sh", 0o755)
os.chmod(root/"uninstall.sh", 0o755)
os.chmod(root/"serverwatch.py", 0o755)
zip_path = Path("/mnt/data/serverwatch-v1.0.zip")
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as z:
for p in root.rglob("*"):
if p.is_file():
z.write(p, p.relative_to(root.parent))
print(zip_path)