10.6. Construire le tableau de bord¶
L’API JSON de la caméra convient très bien à curl, mais le propriétaire veut pointer un téléphone vers une URL et voir quelque chose. Cela implique de servir du HTML, du JS et du CSS depuis la caméra, aux côtés de l’API.
10.6.1. Fichiers statiques depuis le système de fichiers¶
microdot.Response.send_file() lit un fichier depuis le système de fichiers de la caméra et le renvoie avec le Content-Type correct déduit de l’extension. Une route attrape-tout en bas de la table de routage envoie tout ce qui n’a pas correspondu à une route d’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)
Le convertisseur <path:filename> de la page précédente correspond aux segments contenant des barres obliques – app.js et css/site.css passent tous les deux. La vérification de cohérence sur deux lignes rejette tout chemin qui tenterait de sortir de /sdcard/static/ à l’aide de .. ou d’un chemin absolu – sans elle, un client curieux pourrait lire ../../boot.py ou /flash/secrets.txt. send_file() n’effectue elle-même aucun nettoyage : elle ouvre n’importe quel chemin que vous lui transmettez.
10.6.2. Les fichiers du tableau de bord¶
Trois fichiers sur la caméra, sous /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%; }
Le HTML charge le flux MJPEG directement dans une balise <img> – le navigateur gère la réponse multipart de manière transparente. Le curseur lit le seuil depuis /config au chargement de la page et renvoie la nouvelle valeur à chaque changement.
10.6.3. Types MIME et compression¶
send_file() lit l’extension du fichier et choisit un Content-Type depuis microdot.Response.types_map. .html devient text/html, .js devient text/javascript, .css devient text/css, .jpg devient image/jpeg. Les extensions que vous avez ajoutées à votre tableau de bord (.svg, .ico) sont déjà enregistrées. Tout type inconnu prend par défaut application/octet-stream – pour ajouter un nouveau mappage, étendez Response.types_map une fois au démarrage.
Pour les ressources que vous avez précompressées (style.css.gz, app.js.gz), passez compressed=True et microdot sert le fichier .gz avec Content-Encoding: gzip. Le navigateur le décompresse de manière transparente et vous économisez quelques Ko à chaque chargement de page.
Le propriétaire ouvre désormais http://yard-cam.local/index.html et voit le flux en direct, le seuil actuel et un journal d’événements vide.