Neues Web-Frontend mit Statusanzeige, Vorschau-Leiste und relativen Links

This commit is contained in:
Joachim Hummel committed 2026-10-17 20:52:49 +02:00
1 parent cb21f948ee
commit aded536568
3 files changed
+202 -23

No files matched your search

+22 -21
View File
@@ -3,8 +3,9 @@
per HTTP für beliebig viele Zuschauer bereit (hinter Zoraxy / Reverse Proxy).
Endpunkte:
/ Übersicht aller Kameras
/ Übersicht aller Kameras (Web-Frontend, frontend.html)
/<name>/ Webseite mit Livebild einer Kamera
/status.json Status aller Kameras (vom Frontend abgefragt)
/<name>/stream.mjpg Livestream (multipart/x-mixed-replace)
/<name>/snapshot.jpg aktuelles Einzelbild
/stream.mjpg Alias für die erste Kamera (Abwärtskompatibilität)
@@ -17,6 +18,7 @@ Konfiguration über die Datei .env (neben relay.py) oder Umgebungsvariablen:
"""
import base64
import hmac
import json
import os
import threading
import time
@@ -48,22 +50,15 @@ AUTH_USER = os.environ.get("AUTH_USER", "")
AUTH_PASS = os.environ.get("AUTH_PASS", "")
BOUNDARY = "frame"
HEAD = ("<!doctype html><html lang=de><head><meta charset=utf-8>"
"<meta name=viewport content='width=device-width,initial-scale=1'><title>Webcam</title>"
"<style>html,body{margin:0;background:#111;color:#eee;font-family:sans-serif}"
"img{display:block;width:100%}a{color:#8cf}"
".grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:8px;padding:8px}"
"h3{margin:4px 0}</style></head><body>")
FRONTEND = os.path.join(os.path.dirname(os.path.abspath(__file__)), "frontend.html")
def single_page(name):
return (HEAD + f"<img src=stream.mjpg alt='{name}'></body></html>").encode()
def overview_page():
cells = "".join(f"<div><h3><a href='{n}/'>{n}</a></h3><img src='{n}/stream.mjpg' alt='{n}'></div>"
for n in CAMERAS)
return (HEAD + f"<div class=grid>{cells}</div></body></html>").encode()
def page(root, current):
"""Frontend ausliefern; Links sind relativ, damit es auch unter einem Proxy-Prefix läuft."""
with open(FRONTEND, encoding="utf-8") as f:
html = f.read()
return (html.replace("__ROOT__", json.dumps(root))
.replace("__CURRENT__", json.dumps(current))).encode()
class Hub:
@@ -89,6 +84,10 @@ class Hub:
hubs = {name: Hub() for name in CAMERAS}
def is_online(hub):
return bool(hub.last) and time.time() - hub.last < 10
def pull_camera(name, url, hub):
"""Liest die Kamera dauerhaft; baut die Verbindung bei Fehlern neu auf."""
while True:
@@ -140,16 +139,18 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parts = [p for p in self.path.split("?")[0].split("/") if p]
if parts == ["health"]:
now = time.time()
lines = [f"{n}: {'ok' if h.last and now - h.last < 10 else 'keine Frames'}"
for n, h in hubs.items()]
lines = [f"{n}: {'ok' if is_online(h) else 'keine Frames'}" for n, h in hubs.items()]
ok = all(l.endswith(": ok") for l in lines)
return self.send_bytes(200 if ok else 503, "text/plain", ("\n".join(lines) + "\n").encode())
if not self.authorized():
return self.send_bytes(401, "text/plain", b"Login erforderlich",
{"WWW-Authenticate": 'Basic realm="Webcam"'})
if not parts:
return self.send_bytes(200, "text/html; charset=utf-8", overview_page())
return self.send_bytes(200, "text/html; charset=utf-8", page("", ""))
if parts == ["status.json"]:
body = json.dumps({"cameras": [{"name": n, "online": is_online(h)}
for n, h in hubs.items()]}).encode()
return self.send_bytes(200, "application/json", body)
if parts == ["stream.mjpg"] or parts == ["snapshot.jpg"]:
parts = [next(iter(CAMERAS))] + parts # Alias: erste Kamera
name = parts[0]
@@ -159,8 +160,8 @@ class Handler(BaseHTTPRequestHandler):
what = parts[1] if len(parts) == 2 else ""
if what == "":
if not self.path.split("?")[0].endswith("/"):
return self.send_bytes(301, "text/plain", b"", {"Location": f"/{name}/"})
return self.send_bytes(200, "text/html; charset=utf-8", single_page(name))
return self.send_bytes(301, "text/plain", b"", {"Location": f"{name}/"}) # relativ: Proxy-Prefix bleibt erhalten
return self.send_bytes(200, "text/html; charset=utf-8", page("../", name))
if what == "snapshot.jpg":
if hub.frame is None:
return self.send_bytes(503, "text/plain", b"Noch kein Bild")