10.6. בניית לוח הבקרה

ה-API של JSON של המצלמה בסדר עבור curl, אך הבעלים רוצה להפנות טלפון אל URL ו-לראות משהו. משמעות הדבר היא הגשת HTML, JS ו-CSS מהמצלמה לצד ה-API.

10.6.1. קבצים סטטיים ממערכת הקבצים

microdot.Response.send_file() קורא קובץ ממערכת הקבצים של המצלמה וכותב אותו בחזרה עם ה-Content-Type הנכון המוסק מהסיומת. מסלול תופס-הכל אחד בתחתית טבלת הניתוב שולח כל דבר שלא תאם מסלול 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)

הממיר <path:filename> מהדף הקודם תואם מקטעים המכילים לוכסנים – app.js ו-css/site.css שניהם עוברים. בדיקת השפיות בת שתי השורות דוחה כל נתיב שמנסה לחמוק מ-/sdcard/static/ עם .. או נתיב מוחלט – בלעדיה לקוח סקרן יכול לקרוא את ../../boot.py או /flash/secrets.txt. send_file() עצמה אינה מבצעת חיטוי: היא פותחת כל נתיב שתמסור לה.

10.6.2. קבצי לוח הבקרה

שלושה קבצים על המצלמה תחת /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 טוען את זרם ה-MJPEG ישירות אל <img> – הדפדפן מטפל בתגובת ה-multipart באופן שקוף. המחוון קורא את הסף מ-/config בעת טעינת הדף ושולח את הערך החדש בחזרה בכל שינוי.

10.6.3. סוגי MIME ודחיסה

send_file() קורא את סיומת הקובץ ובוחר Content-Type מ-microdot.Response.types_map. .html הופך ל-text/html, .js הופך ל-text/javascript, .css הופך ל-text/css, .jpg הופך ל-image/jpeg. סיומות שהוספת ללוח הבקרה שלך (.svg, .ico) כבר רשומות. כל דבר לא ידוע מקבל כברירת מחדל application/octet-stream – כדי להוסיף מיפוי חדש, הרחב את Response.types_map פעם אחת בעת ההפעלה.

עבור נכסים שדחסת מראש (style.css.gz, app.js.gz) העבר compressed=True ו-microdot מגיש את הקובץ .gz עם Content-Encoding: gzip. הדפדפן מפרק את הדחיסה באופן שקוף ואתה חוסך כמה KB בכל טעינת דף.

הבעלים פותח כעת את http://yard-cam.local/index.html ורואה את הזרם החי, הסף הנוכחי, ויומן אירועים ריק.