10.6. Das Dashboard erstellen¶
Die JSON-API der Kamera ist für curl in Ordnung, aber der Besitzer möchte ein Telefon auf eine URL richten und etwas sehen. Das bedeutet, HTML, JS und CSS neben der API von der Kamera aus auszuliefern.
10.6.1. Statische Dateien aus dem Dateisystem¶
microdot.Response.send_file() liest eine Datei aus dem Dateisystem der Kamera und schreibt sie mit dem richtigen, aus der Erweiterung abgeleiteten Content-Type zurück. Eine Catch-all-Route am Ende der Routing-Tabelle sendet alles, was auf keine API-Route gepasst hat:
@app.get('/<path:filename>')
async def static(request, filename):
if '..' in filename or filename.startswith('/'):
abort(403)
return Response.send_file('/sdcard/static/' + filename)
Der <path:filename>-Konverter von der letzten Seite gleicht Segmente ab, die Schrägstriche enthalten – app.js und css/site.css kommen beide durch. Die zweizeilige Plausibilitätsprüfung lehnt jeden Pfad ab, der mit .. oder einem absoluten Pfad versucht, /sdcard/static/ zu entkommen – ohne sie könnte ein neugieriger Client ../../boot.py oder /flash/secrets.txt lesen. send_file() selbst führt keine Bereinigung durch: Es öffnet jeden Pfad, den Sie ihm übergeben.
10.6.2. Die Dashboard-Dateien¶
Drei Dateien auf der Kamera unter /sdcard/static/:
<!-- /sdcard/static/index.html -->
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>Backyard cam</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Backyard cam</h1>
<img id="stream" src="/stream.jpg">
<label>Threshold:
<input id="threshold" type="range" min="0" max="100">
<span id="threshold-value"></span>
</label>
<h2>Recent events</h2>
<ul id="events"></ul>
<script src="app.js"></script>
</body>
</html>
// /sdcard/static/app.js
const slider = document.getElementById('threshold');
const sliderValue = document.getElementById('threshold-value');
async function loadConfig() {
const r = await fetch('/config');
const cfg = await r.json();
slider.value = cfg.threshold;
sliderValue.textContent = cfg.threshold;
}
loadConfig();
slider.addEventListener('change', async () => {
sliderValue.textContent = slider.value;
await fetch('/config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({threshold: parseInt(slider.value)}),
});
});
/* /sdcard/static/style.css */
body { font-family: sans-serif; max-width: 720px; margin: 1em auto; }
img#stream { width: 100%; }
Das HTML lädt den MJPEG-Stream direkt in ein <img> – der Browser verarbeitet die Multipart-Antwort transparent. Der Schieberegler liest den Schwellenwert beim Laden der Seite aus /config und sendet bei jeder Änderung den neuen Wert zurück.
10.6.3. MIME-Typen und Komprimierung¶
send_file() liest die Dateierweiterung und wählt einen Content-Type aus microdot.Response.types_map. .html wird zu text/html, .js wird zu text/javascript, .css wird zu text/css, .jpg wird zu image/jpeg. Erweiterungen, die Sie Ihrem Dashboard hinzugefügt haben (.svg, .ico), sind bereits registriert. Alles Unbekannte fällt standardmäßig auf application/octet-stream zurück – um eine neue Zuordnung hinzuzufügen, erweitern Sie Response.types_map einmalig beim Start.
Für Assets, die Sie vorkomprimiert haben (style.css.gz, app.js.gz), übergeben Sie compressed=True, und microdot liefert die .gz-Datei mit Content-Encoding: gzip aus. Der Browser dekomprimiert transparent, und Sie sparen bei jedem Seitenaufruf ein paar KB.
Der Besitzer öffnet nun http://yard-cam.local/index.html und sieht den Live-Feed, den aktuellen Schwellenwert und ein leeres Ereignisprotokoll.