Publié le 15 juin 2026
SENTINEL : une page de statut sans aucun JavaScript
SENTINEL est la page de statut de mon homelab, et elle n'embarque aucun JavaScript. L'état est collecté hors bande, figé en HTML statique chaque minute, et servi depuis K3s. Une page de statut devrait être la seule page qui se charge encore quand tout le reste est cassé.
La plupart des pages de statut sont des applications monopages qui démarrent un bundle JavaScript puis interrogent une API depuis votre navigateur pour découvrir ce qui tourne. Ça m'a toujours paru à l'envers. La page de statut est celle qu'on charge justement quand les choses sont cassées : elle devrait donc dépendre du moins possible. SENTINEL, la page de statut de mon homelab, prend la position inverse : elle n'embarque aucun JavaScript. Le navigateur reçoit du HTML fini, figé il y a une minute, et rien d'autre. Elle ressemble aussi à un CRT ambré, parce que si je dois la fixer pendant une panne, autant que ce soit agréable.
Une page de statut est une photographie, pas une vidéo
L'astuce est de découpler la collecte de l'affichage. Le navigateur ne devrait pas être ce qui découvre l'état, car dès l'instant où il l'est, votre page de statut dépend d'une API qui marche, d'un CORS qui se tient, d'un runtime JavaScript et du réseau de l'utilisateur, tous coopérant exactement au moment où quelque chose ne va déjà pas. À la place, un collecteur tourne sur une planification, sonde chaque service, et écrit les résultats dans un petit fichier JSON, puis une étape de gabarit fige ce JSON dans index.html. La page que reçoit le navigateur est une photographie du système au moment de la dernière exécution, pas un flux en direct qu'il doit assembler. Le coût, c'est la fraîcheur, bornée par la fréquence du collecteur, et la page affiche son propre âge pour que cette latence ne soit jamais cachée.
Rafraîchir sans JavaScript
Garder la photographie à jour ne demande qu'une ligne, et c'est la plus vieille astuce du web :
<meta http-equiv="refresh" content="60">
Le navigateur recharge la page toutes les soixante secondes et reçoit ce que le collecteur a figé en dernier. Aucun code d'interrogation, aucun websocket, aucun bundle à télécharger avant que la page puisse vous dire quoi que ce soit. Pour cette tâche précise, c'est exactement le bon outil, et il a la propriété agréable de fonctionner dans un navigateur texte, sur un téléphone avec une barre de réseau.
Le phosphore ambré en CSS pur
Le rendu CRT est tout en CSS, sans images ni canvas. La lueur est une pile de text-shadow dans l'ambre. Les lignes de balayage sont un repeating-linear-gradient posé sur l'ensemble à faible opacité. Un statut est un span coloré et une barre de disponibilité est un dégradé de fond. Comme il n'y a pas de JavaScript, la page entière fait quelques kilo-octets de HTML et une feuille de style.
:root { --amber:#f0a93a; }
.glow {
color: var(--amber);
text-shadow: 0 0 2px var(--amber), 0 0 8px rgba(240,169,58,.45);
}
/* lignes de balayage, dessinées sur toute la page */
body::after {
content: ""; position: fixed; inset: 0; pointer-events: none;
background: repeating-linear-gradient(
0deg, rgba(0,0,0,.16) 0 1px, transparent 1px 3px);
}
Où elle vit, et pourquoi elle échoue en douceur
Il y a deux pièces sur le cluster. Un CronJob exécute l'étape de sonde-et-gabarit chaque minute et écrit index.html sur un petit volume persistant. Un Deployment nginx monte ce même volume en lecture seule et le sert via un Ingress. Cette séparation est tout l'enjeu. Un pod nginx qui sert un fichier statique est à peu près ce qu'un service web fait de plus indestructible, et il ne dépend pas du collecteur pour vivre. Si le collecteur meurt, la page ne tombe pas, elle vieillit, et l'horodatage sur la page le rend évident d'un coup d'œil. Le mode de défaillance, c'est une photographie un peu ancienne, pas un écran blanc ni un spinner qui ne se résout jamais.
apiVersion: batch/v1
kind: CronJob
metadata: { name: sentinel-collect }
spec:
schedule: "* * * * *" # chaque minute
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: collect
image: registry.kerboul.me/sentinel:latest
volumeMounts:
- { name: site, mountPath: /out } # écrit index.html ici
volumes:
- name: site
persistentVolumeClaim: { claimName: sentinel-site }
# nginx monte le même PVC en lecture seule et sert /out. Il n'appelle jamais le collecteur.
La prochaine itération écrit un petit fichier d'historique pour que les barres de disponibilité couvrent les quatre-vingt-dix derniers jours plutôt que l'instant présent, toujours figé à la génération, toujours zéro JavaScript côté client. Si vous avez construit une page de statut qui survit à la chute de son propre backend, j'aimerais voir où vous avez placé la limite.