Tous les articles

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.

SENTINEL SENTINEL // statut homelab SERVICE STATUT DISPO 90 JOURS proxmox-cluster ACTIF 99.98% nextcloud ACTIF 99.95% k3s-ingress ACTIF 100% jellyfin DÉGRADÉ 98.71% gitea ACTIF 99.90% restic-backups ACTIF 99.99% mis à jour il y a 41 s · figé à la génération · 0 Ko de JavaScript
La page elle-même : monospace, ambre, une lueur et des lignes de balayage entièrement en CSS. Le point vert est une vieille habitude, mais toute la palette vit du côté ambré.

Une page de statut est une photographie, pas une vidéo

La collecte et l'affichage doivent donc être découplés. 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'une politique de partage entre origines (CORS) correctement configurée, 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. Il n'y a ni code d'interrogation ni websocket, et rien à télécharger avant que la page puisse vous dire quoi que ce soit. Ça fonctionne dans un navigateur texte, et sur un téléphone avec une seule 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 l'expose via un Ingress, la ressource Kubernetes qui publie un service vers l'extérieur. Un pod nginx qui sert un fichier statique a très peu de raisons de tomber, et il ne dépend pas du collecteur pour vivre. Si le collecteur meurt, la page ne tombe pas, elle vieillit, et l'horodatage le rend évident d'un coup d'œil. On lit alors une photographie un peu ancienne au lieu d'un écran blanc.

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.
collecté hors bande · toutes les 60 s · peut mourir sans faire tomber la page chemin toujours actif · statique · 0 JS services proxmox · docker · k3s collecteur CronJob * * * * * PVC index.html (figé) nginx sert du statique navigateur refresh 60 s · 0 JS nginx dépend d'un fichier, pas du collecteur en vie.
La collecte et le service ne se touchent jamais directement. Ils se rencontrent autour d'un fichier sur un volume.

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. La limite connue : si le CronJob échoue plusieurs fois de suite, la page vieillit sans que rien ne le signale ailleurs que sur elle-même.