10.6. การสร้างแดชบอร์ด

JSON API ของกล้องเหมาะสำหรับ curl แต่เจ้าของต้องการเปิด URL บนโทรศัพท์แล้ว เห็น บางอย่าง ซึ่งหมายความว่าต้องเสิร์ฟ HTML, JS และ CSS จากกล้องควบคู่กับ API

10.6.1. ไฟล์ static จาก filesystem

microdot.Response.send_file() อ่านไฟล์จาก filesystem ของกล้องและส่งกลับพร้อม Content-Type ที่ระบุจากนามสกุลไฟล์โดยอัตโนมัติ มี route แบบ catch-all อยู่ด้านล่างของตาราง routing ที่ส่งทุกอย่างที่ไม่ตรงกับ API route:

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

converter <path:filename> จากหน้าก่อนหน้ารับ segment ที่มี slash ได้ — ทั้ง app.js และ css/site.css ผ่านได้ การตรวจสอบความปลอดภัยสองบรรทัดปฏิเสธ path ที่พยายามหลุดออกจาก /sdcard/static/ ด้วย .. หรือ absolute path — หากไม่มีสิ่งนี้ client ที่อยากรู้อยากเห็นอาจอ่าน ../../boot.py หรือ /flash/secrets.txt ได้ send_file() เองไม่ทำการ sanitize เลย: มันเปิด path ใดก็ตามที่คุณส่งให้

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 stream ลงใน <img> โดยตรง — เบราว์เซอร์จัดการ multipart response อย่างโปร่งใส slider อ่านค่า threshold จาก /config เมื่อโหลดหน้า และส่งค่าใหม่กลับทุกครั้งที่เปลี่ยน

10.6.3. MIME type และการบีบอัด

send_file() อ่านนามสกุลของไฟล์และเลือก Content-Type จาก microdot.Response.types_map .html กลายเป็น text/html, .js กลายเป็น text/javascript, .css กลายเป็น text/css, .jpg กลายเป็น image/jpeg นามสกุลที่คุณเพิ่มในแดชบอร์ด (.svg, .ico) ถูกลงทะเบียนไว้แล้ว ส่วนที่ไม่รู้จักจะ default เป็น application/octet-stream — หากต้องการเพิ่ม mapping ใหม่ ให้ขยาย Response.types_map ครั้งเดียวตอนเริ่มต้น

สำหรับ asset ที่บีบอัดไว้ล่วงหน้า (style.css.gz, app.js.gz) ให้ส่ง compressed=True และ microdot จะเสิร์ฟไฟล์ .gz พร้อม Content-Encoding: gzip เบราว์เซอร์จะแตกไฟล์อย่างโปร่งใสและประหยัดพื้นที่ไม่กี่ KB ในแต่ละการโหลดหน้า

เจ้าของตอนนี้เปิด http://yard-cam.local/index.html แล้วเห็นภาพสดแบบเรียลไทม์ ค่า threshold ปัจจุบัน และ event log ที่ว่างเปล่า