10.6. Побудова панелі моніторингу¶
JSON API камери чудово підходить для 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. Браузер прозоро декомпресує, і ви заощаджуєте кілька КБ при кожному завантаженні сторінки.
Власник тепер відкриває http://yard-cam.local/index.html і бачить живий потік, поточний поріг і порожній журнал подій.