10.6. Stavba dashboardu¶
JSON API kamery je v pořádku pro curl, ale majitel chce namířit telefon na URL a vidět něco. To znamená servírovat z kamery HTML, JS a CSS vedle API.
10.6.1. Statické soubory ze souborového systému¶
microdot.Response.send_file() přečte soubor ze souborového systému kamery a zapíše jej zpět se správným Content-Type odvozeným z přípony. Jedna univerzální routa na konci směrovací tabulky odešle vše, co se neshodovalo s žádnou API routou:
@app.get('/<path:filename>')
async def static(request, filename):
if '..' in filename or filename.startswith('/'):
abort(403)
return Response.send_file('/sdcard/static/' + filename)
Převodník <path:filename> z předchozí stránky odpovídá segmentům, které obsahují lomítka – projdou jím jak app.js, tak css/site.css. Dvouřádková kontrola odmítne jakoukoli cestu, která se pokouší uniknout z /sdcard/static/ pomocí .. nebo absolutní cesty – bez ní by zvědavý klient mohl číst ../../boot.py nebo /flash/secrets.txt. send_file() sám o sobě neprovádí žádnou sanitizaci: otevře jakoukoli cestu, kterou mu předáte.
10.6.2. Soubory dashboardu¶
Tři soubory na kameře pod /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%; }
HTML načte MJPEG stream přímo do <img> – prohlížeč zpracuje multipart odpověď transparentně. Posuvník při načtení stránky přečte práh z /config a při každé změně odešle novou hodnotu zpět.
10.6.3. Typy MIME a komprese¶
send_file() přečte příponu souboru a vybere Content-Type z microdot.Response.types_map. .html se stane text/html, .js se stane text/javascript, .css se stane text/css, .jpg se stane image/jpeg. Přípony, které jste přidali do svého dashboardu (.svg, .ico), jsou již zaregistrované. Cokoli neznámého má výchozí application/octet-stream – pro přidání nového mapování rozšiřte Response.types_map jednou při startu.
Pro aktiva, která jste předem zkomprimovali (style.css.gz, app.js.gz), předejte compressed=True a microdot odešle soubor .gz s Content-Encoding: gzip. Prohlížeč jej transparentně dekomprimuje a vy ušetříte pár KB při každém načtení stránky.
Majitel nyní otevře http://yard-cam.local/index.html a uvidí živý přenos, aktuální práh a prázdný protokol událostí.