10.6. Izgradnja nadzorne ploče

JSON API kamere u redu je za curl, ali vlasnik želi usmjeriti telefon prema URL-u i vidjeti nešto. To znači posluživanje HTML-a, JS-a i CSS-a s kamere uz API.

10.6.1. Statične datoteke iz datotečnog sustava

microdot.Response.send_file() čita datoteku iz datotečnog sustava kamere i šalje je natrag s ispravnim Content-Type zaključenim iz nastavka. Jedna sveobuhvatna ruta na dnu tablice usmjeravanja šalje sve što se nije podudaralo s API rutom:

@app.get('/<path:filename>')
async def static(request, filename):
    if '..' in filename or filename.startswith('/'):
        abort(403)
    return Response.send_file('/sdcard/static/' + filename)

Pretvarač <path:filename> s prethodne stranice podudara se sa segmentima koji sadrže kose crte – i app.js i css/site.css prolaze. Dvolinijska provjera ispravnosti odbija svaku putanju koja pokušava pobjeći iz /sdcard/static/ pomoću .. ili apsolutne putanje – bez nje bi znatiželjni klijent mogao pročitati ../../boot.py ili /flash/secrets.txt. send_file() sam ne provodi nikakvo čišćenje: otvara koju god putanju mu predate.

10.6.2. Datoteke nadzorne ploče

Tri datoteke na kameri 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 učitava MJPEG stream izravno u <img> – preglednik transparentno obrađuje multipart odgovor. Klizač čita prag iz /config pri učitavanju stranice i šalje novu vrijednost natrag pri svakoj promjeni.

10.6.3. MIME tipovi i kompresija

send_file() čita nastavak datoteke i bira Content-Type iz microdot.Response.types_map. .html postaje text/html, .js postaje text/javascript, .css postaje text/css, .jpg postaje image/jpeg. Nastavci koje ste dodali svojoj nadzornoj ploči (.svg, .ico) već su registrirani. Sve nepoznato prema zadanom postaje application/octet-stream – za dodavanje novog preslikavanja, jednom proširite Response.types_map pri pokretanju.

Za resurse koje ste unaprijed komprimirali (style.css.gz, app.js.gz) proslijedite compressed=True i microdot poslužuje .gz datoteku s Content-Encoding: gzip. Preglednik je transparentno raspakira, a vi uštedite nekoliko KB pri svakom učitavanju stranice.

Vlasnik sada otvara http://yard-cam.local/index.html i vidi prijenos uživo, trenutni prag i prazan zapisnik događaja.