10.6. Construirea tabloului de bord¶
API-ul JSON al camerei este în regulă pentru curl, dar proprietarul dorește să îndrepte un telefon către un URL și să vadă ceva. Asta înseamnă servirea de HTML, JS și CSS de pe cameră, alături de API.
10.6.1. Fișiere statice din sistemul de fișiere¶
microdot.Response.send_file() citește un fișier din sistemul de fișiere al camerei și îl scrie înapoi cu tipul Content-Type corect dedus din extensie. O rută universală în partea de jos a tabelului de rutare trimite tot ceea ce nu s-a potrivit cu o rută API:
@app.get('/<path:filename>')
async def static(request, filename):
if '..' in filename or filename.startswith('/'):
abort(403)
return Response.send_file('/sdcard/static/' + filename)
Convertorul <path:filename> de la pagina anterioară potrivește segmente care conțin bare oblice – atât app.js, cât și css/site.css trec prin el. Verificarea de bun-simț pe două linii respinge orice cale care încearcă să iasă din /sdcard/static/ cu .. sau cu o cale absolută – fără ea, un client curios ar putea citi ../../boot.py sau /flash/secrets.txt. send_file() în sine nu face nicio igienizare: deschide orice cale pe care i-o transmiți.
10.6.2. Fișierele tabloului de bord¶
Trei fișiere pe cameră sub /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-ul încarcă fluxul MJPEG direct într-un <img> – browserul gestionează răspunsul multipart în mod transparent. Glisorul citește pragul din /config la încărcarea paginii și trimite înapoi noua valoare la fiecare modificare.
10.6.3. Tipuri MIME și compresie¶
send_file() citește extensia fișierului și alege un Content-Type din microdot.Response.types_map. .html devine text/html, .js devine text/javascript, .css devine text/css, .jpg devine image/jpeg. Extensiile pe care le-ai adăugat la tabloul tău de bord (.svg, .ico) sunt deja înregistrate. Orice este necunoscut are valoarea implicită application/octet-stream – pentru a adăuga o nouă mapare, extinde Response.types_map o singură dată la pornire.
Pentru resursele pe care le-ai precomprimat (style.css.gz, app.js.gz), transmite compressed=True, iar microdot servește fișierul .gz cu Content-Encoding: gzip. Browserul îl decomprimă transparent și economisești câțiva KB la fiecare încărcare a paginii.
Proprietarul deschide acum http://yard-cam.local/index.html și vede fluxul în direct, pragul curent și un jurnal de evenimente gol.