P42 Dashboard: erster Stand
FastAPI-Backend und React-Frontend mit Konten, Sendungsverfolgung, Projekt-Artikeln, Vorrat & Einkauf mit Rezeptabgleich, Reise-Weltkarte, Kanban-Projekten mit Priorität, Server-Monitoring und den Designs Matrix und Blackwall. Betrieb per Docker hinter nginx (siehe DEPLOY.md). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
dd58b3d1f8
75 files changed
+11418
No files matched your search
@@ -0,0 +1,131 @@
|
||||
"""Server-Monitoring pro Konto: eigene Serverliste + Erreichbarkeitsprüfung (HTTP oder TCP-Port)."""
|
||||
import asyncio
|
||||
import re
|
||||
import time
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException
|
||||
|
||||
from .auth import current_user
|
||||
from .db import now_iso, transaction
|
||||
from .links import ValidationError, normalize_url
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
TYPES = {"http", "tcp"}
|
||||
MAX_MONITORS = 50
|
||||
TIMEOUT = 5
|
||||
HOST_PORT = re.compile(r"(?P<host>[A-Za-z0-9.-]+|\[[0-9A-Fa-f:.]+\]):(?P<port>\d{1,5})")
|
||||
|
||||
|
||||
def _monitor(row) -> dict:
|
||||
return {"id": row["id"], "name": row["name"], "type": row["type"], "target": row["target"], "createdAt": row["created_at"]}
|
||||
|
||||
|
||||
def _validate(data: dict) -> dict:
|
||||
name = data.get("name").strip() if isinstance(data.get("name"), str) else ""
|
||||
if not name or len(name) > 60:
|
||||
raise ValidationError("Name fehlt oder ist zu lang (max. 60 Zeichen).")
|
||||
kind = data.get("type")
|
||||
if kind not in TYPES:
|
||||
raise ValidationError("Ungültiger Prüf-Typ.")
|
||||
target = data.get("target").strip() if isinstance(data.get("target"), str) else ""
|
||||
if kind == "http":
|
||||
target = normalize_url(target, "die Adresse")
|
||||
if not target:
|
||||
raise ValidationError("Adresse fehlt.")
|
||||
else:
|
||||
m = HOST_PORT.fullmatch(target)
|
||||
if not m or not 0 < int(m["port"]) < 65536:
|
||||
raise ValidationError("Bitte Host und Port angeben, z. B. 192.168.0.10:22")
|
||||
return {"name": name, "type": kind, "target": target}
|
||||
|
||||
|
||||
# ---------- Verwaltung ----------
|
||||
|
||||
@router.get("/api/monitors")
|
||||
def list_monitors(user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
rows = conn.execute("SELECT * FROM monitors WHERE user_id = ? ORDER BY id", (user["id"],)).fetchall()
|
||||
return [_monitor(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/api/monitors", status_code=201)
|
||||
def create_monitor(data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
values = _validate(data)
|
||||
with transaction() as conn:
|
||||
count = conn.execute("SELECT COUNT(*) FROM monitors WHERE user_id = ?", (user["id"],)).fetchone()[0]
|
||||
if count >= MAX_MONITORS:
|
||||
raise ValidationError(f"Maximal {MAX_MONITORS} Server pro Konto.")
|
||||
cur = conn.execute(
|
||||
"INSERT INTO monitors (user_id, name, type, target, created_at) VALUES (?, ?, ?, ?, ?)",
|
||||
(user["id"], values["name"], values["type"], values["target"], now_iso()),
|
||||
)
|
||||
return _monitor(conn.execute("SELECT * FROM monitors WHERE id = ?", (cur.lastrowid,)).fetchone())
|
||||
|
||||
|
||||
@router.put("/api/monitors/{monitor_id}")
|
||||
def update_monitor(monitor_id: int, data: dict = Body(...), user: dict = Depends(current_user)):
|
||||
values = _validate(data)
|
||||
with transaction() as conn:
|
||||
cur = conn.execute(
|
||||
"UPDATE monitors SET name = ?, type = ?, target = ? WHERE id = ? AND user_id = ?",
|
||||
(values["name"], values["type"], values["target"], monitor_id, user["id"]),
|
||||
)
|
||||
if not cur.rowcount:
|
||||
raise HTTPException(404, "Nicht gefunden.")
|
||||
return _monitor(conn.execute("SELECT * FROM monitors WHERE id = ?", (monitor_id,)).fetchone())
|
||||
|
||||
|
||||
@router.delete("/api/monitors/{monitor_id}")
|
||||
def delete_monitor(monitor_id: int, user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
cur = conn.execute("DELETE FROM monitors WHERE id = ? AND user_id = ?", (monitor_id, user["id"]))
|
||||
if not cur.rowcount:
|
||||
raise HTTPException(404, "Nicht gefunden.")
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ---------- Prüfung ----------
|
||||
|
||||
async def _check_http(client: httpx.AsyncClient, url: str) -> dict:
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
res = await client.get(url)
|
||||
except httpx.TimeoutException:
|
||||
return {"ok": False, "detail": "Zeitüberschreitung"}
|
||||
except httpx.ConnectError:
|
||||
return {"ok": False, "detail": "Nicht erreichbar"}
|
||||
except httpx.HTTPError as e:
|
||||
return {"ok": False, "detail": type(e).__name__}
|
||||
ms = round((time.perf_counter() - start) * 1000)
|
||||
# 4xx heißt: Server antwortet (z. B. Anmeldung nötig) – also erreichbar
|
||||
return {"ok": res.status_code < 500, "ms": ms, "detail": f"HTTP {res.status_code}"}
|
||||
|
||||
|
||||
async def _check_tcp(target: str) -> dict:
|
||||
m = HOST_PORT.fullmatch(target)
|
||||
host = m["host"].strip("[]")
|
||||
start = time.perf_counter()
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(asyncio.open_connection(host, int(m["port"])), TIMEOUT)
|
||||
except asyncio.TimeoutError:
|
||||
return {"ok": False, "detail": "Zeitüberschreitung"}
|
||||
except OSError as e:
|
||||
return {"ok": False, "detail": "Verbindung abgelehnt" if isinstance(e, ConnectionRefusedError) else "Nicht erreichbar"}
|
||||
ms = round((time.perf_counter() - start) * 1000)
|
||||
writer.close()
|
||||
return {"ok": True, "ms": ms, "detail": f"Port {m['port']} offen"}
|
||||
|
||||
|
||||
@router.get("/api/monitors/status")
|
||||
async def monitor_status(user: dict = Depends(current_user)):
|
||||
with transaction() as conn:
|
||||
rows = conn.execute("SELECT id, type, target FROM monitors WHERE user_id = ?", (user["id"],)).fetchall()
|
||||
# Zertifikate werden nicht geprüft: im Heimnetz sind selbstsignierte Zertifikate üblich
|
||||
async with httpx.AsyncClient(timeout=TIMEOUT, verify=False, follow_redirects=True) as client:
|
||||
results = await asyncio.gather(*(
|
||||
_check_http(client, r["target"]) if r["type"] == "http" else _check_tcp(r["target"]) for r in rows
|
||||
))
|
||||
checked = now_iso()
|
||||
return [{"id": r["id"], "checkedAt": checked, **res} for r, res in zip(rows, results)]
|
||||
Reference in new issue
Block a user