- Python 55.7%
- JavaScript 19.1%
- CSS 14.8%
- HTML 10.3%
| docs | ||
| web | ||
| .env.example | ||
| .gitignore | ||
| bugs_job.py | ||
| changes_interp_job.py | ||
| docker-compose.shot.yml | ||
| docker-compose.yml | ||
| LICENSE | ||
| monitor.py | ||
| README.md | ||
| refresh_watcher.sh | ||
| RUNBOOK.md | ||
| shot.py | ||
| site_snapshot_git.py | ||
t4health — barre de santé communautaire de TR4KER
Live : https://t4health.ekaii.fr · statut & latence en temps réel de tr4ker.net et de tous ses services.
Projet communautaire indépendant — non affilié à TR4KER, ni géré par son équipe. Fait par des membres, pour les membres. Source d'info indicative : en cas de doute, référez-vous au site officiel.
- Backend : un seul fichier Python, stdlib pure (
monitor.py, aucune dépendance pip), SQLite, ~9 Mo de RAM. - Frontend : HTML/CSS/JS self-contained (Apache ECharts vendored, zéro CDN), thème sombre, responsive, PWA.
- Fallback sans JavaScript :
/nojs(page rendue serveur, courbes SVG, meta-refresh).
1. Pourquoi un simple ping ne suffit pas (l'insight fondateur)
TR4KER est derrière Cloudflare. Le front est une SPA : l'edge CF sert le HTML statique
(et répond 200 OK) même quand le backend d'origine est mort. Un monitoring naïf
(« GET / → 200 → tout va bien ») est donc structurellement aveugle : il verra un site
« opérationnel » pendant que l'API, le tracker et le chat sont hors service.
La conséquence architecturale : une sonde par service applicatif, chacune exerçant le
vrai chemin de code du service (une vraie annonce bencodée pour le tracker, une vraie
recherche pour l'index, un vrai handshake WebSocket pour le chat…). La sonde site
(GET / sur l'edge) est conservée exprès comme témoin : quand elle est verte et que tout
le reste est rouge, c'est précisément la signature « edge vivant / origine morte ».
Second niveau du même problème : les pages d'erreur Cloudflare (52x) reviennent parfois en
200 habillé ou en HTML — d'où is_cf_502() qui reconnaît à la fois les codes 502/503/504
et les corps d'erreur signés Cloudflare, pour classer « origine injoignable » et non « site down ».
2. Architecture
┌───────────────────────────── hôte ─────────────────────────────┐
internet ── Traefik ──► conteneur t4health (python:3.12-slim, stdlib pure) │
t4health.ekaii.fr │ monitor.py :9790 │
│ ├─ prober_loop : 8 sondes / 30 s ──► SQLite /data │
│ ├─ stats_loop : stats publiques TR4KER / 5 min │
│ ├─ tracker_loop : change-tracker (10 sources) / 10 min │
│ └─ HTTP server : / (SPA), /nojs, /api/*, /admin, RSS… │
│ │
│ conteneur t4shot (playwright) │
│ └─ screenshots tr4ker.net / 5 min ──► /data/shots (partagé) │
│ │
│ hôte : bugs_job.py (cron horaire) — mine le chat TR4KER, │
│ résume les bugs via LLM ──► data/bugs.json (servi par /bugs) │
└─────────────────────────────────────────────────────────────────┘
Tout le code est monté en volume (:ro) dans les conteneurs : mettre à jour = éditer le
fichier sur l'hôte + docker restart t4health. Pas d'image à rebuilder.
3. Les 8 sondes — ce que chacune teste vraiment
| # | Sonde | Cible | Ce qu'elle prouve réellement |
|---|---|---|---|
| 1 | site |
GET / (HTML) |
L'edge Cloudflare sert le site. Volontairement superficielle : c'est le témoin — verte + tout le reste rouge = origine morte derrière un edge vivant. |
| 2 | api |
GET /api/public/config |
L'application d'origine répond. Exige 200 ET un corps JSON : un 200 HTML est le fallback SPA de l'edge, pas l'API (→ warn). 502/503/504 ou page d'erreur CF = origine injoignable. |
| 3 | session |
GET /api/me (cookie) |
Le chemin authentifié complet (middleware session + DB utilisateur). Un 401/403 = l'origine va bien, c'est notre cookie qui a expiré → warn, jamais down : on ne fait pas passer une panne du monitoring pour une panne du service. |
| 4 | chat |
handshake WebSocket /api/ws |
Le serveur temps réel. La stdlib n'a pas de client WS : on fait l'Upgrade HTTP/1.1 à la main sur un socket TLS et on exige le 101 Switching Protocols. Un reverse-proxy qui répond 200 en HTML échouerait ici. |
| 5 | tracker |
vraie annonce BitTorrent sur tk.tr4ker.net/announce/<passkey> |
L'application tracker elle-même, pas sa reachability : info_hash synthétique + event=stopped (ne laisse aucun état peer), et on exige une réponse bencodée (d…e). Même un failure reason bencodé compte up : l'appli a parsé et répondu. Sans passkey, dégrade en simple reachability (annoncé tel quel dans detail). |
| 6 | torznab |
GET /api/torznab?t=caps (clé API) |
La surface d'intégration *arr (Prowlarr/Radarr/Sonarr) : exige le XML <caps>. |
| 7 | search |
torznab t=search&q=1984 |
Une vraie recherche qui traverse l'index/la DB (pas un simple caps statique) ; compte les <item>. 0 résultat = warn (l'index répond mais vide — suspect). |
| 8 | cert |
TLS notAfter de tr4ker.net + tk.tr4ker.net |
Expiration du certificat (min des deux hôtes). Sondée 1 round sur 20, et exclue du graphe de latence : son « ms » est un handshake TLS, pas comparable. <12 j = warn, expiré = down. |
Chaque échantillon stocke (state, http, latency_ms, detail) — le detail est toujours une
phrase honnête sur ce qui a été observé (« annonce ok », « origine injoignable (CF 502) »,
« cookie expiré (401) »…), jamais un code brut sans contexte.
Services critiques : api et tracker — eux seuls peuvent faire basculer l'état global en « Panne majeure ».
4. Réutilisation de session (le cookie)
Les sondes authentifiées (session, chat, stats, profils, avatars) réutilisent un cookie
de session déjà établi, monté en lecture seule dans le conteneur (/secrets/t4.cookie).
Pourquoi ne pas se loguer à chaque sonde ?
- le login TR4KER est rate-limité (et surveillé) : 2 880 logins/jour feraient bloquer le compte ;
- aucun mot de passe n'a besoin d'exister dans la config du monitor (le cookie suffit, et il est révocable) ;
- l'expiration du cookie est un état prévu et distinct :
sessionpassewarnavec « cookie expiré », le reste du monitoring continue.
Le cookie appartient au bot d'entraide communautaire (même compte membre) et est partagé
entre t4health, t4shot et bugs_job via des montages :ro.
5. État global glissant & anti-flap
compute_overall() ne regarde pas que le dernier échantillon — sinon on afficherait
« Opérationnel » entre deux coupures d'une panne qui flappe. Il combine :
- instantané : états courants des 8 sondes ;
- fenêtre glissante (
T4H_STATE_WINDOW, 300 s) : nombre dedownet médiane de latence par service ; - lenteur : médiane récente OU latence courante >
T4H_SLOW_MS(2 500 ms) → étatslow(« Ralentissements ») même en HTTP 200 ; - instabilité : ≥ 2 coupures dans la fenêtre alors que le service est up maintenant → « Instable » ;
- panne soutenue qui flappe : un service critique majoritairement down sur la fenêtre (> 50 %) est traité comme down même si l'échantillon courant est up.
Hiérarchie résultante : down (critique) > degraded (panne non critique / instable / warn) > slow > up.
Les alertes abonnés ont leur propre débounce, plus conservateur (evaluate_alerts()) :
une panne critique doit être soutenue ≥ 180 s (T4H_ALERT_DOWN_AFTER) avant d'alerter,
et le « rétabli » exige 120 s stables (T4H_ALERT_UP_AFTER). La phase (up/down) est
persistée en DB (meta_kv) : un restart du conteneur ne renvoie pas de fausses alertes.
Promesse aux abonnés : uniquement les vraies pannes, pas de spam — c'est ce mécanisme qui la tient.
Les incidents (table incidents) sont les transitions franchissant la frontière down
(up/warn ↔ down), notifiées sur Discord/ntfy et exposées en RSS (/incidents.xml).
6. Change-tracker (« Nouveautés »)
Toutes les 10 min (tracker_loop), 10 sources sont capturées, canonicalisées, puis
diffées contre leur dernier snapshot (table snapshots) :
| Source | Ce qu'on détecte |
|---|---|
config |
/api/public/config : ouverture/fermeture des inscriptions, mode maintenance, tout paramètre public |
frontend |
hash des assets (= déploiement front détecté), noms de chunks (= nouvelles features), endpoints /api/* référencés dans le JS, nouveaux hôtes tiers contactés |
api_shapes |
schéma (clés + types) de 3 endpoints JSON : champs ajoutés/retirés/retypés |
torznab |
catégories de recherche, capacités searching |
tracker |
interval/min interval d'annonce, apparition de /scrape |
robots / security |
diff ligne à ligne de robots.txt / security.txt (avec garde anti-fallback SPA) |
headers |
en-têtes de sécurité (CSP, HSTS…), noms des cookies posés |
dns |
A/AAAA/MX/TXT/NS via DNS-over-HTTPS (indépendant du résolveur local) |
cert |
émetteur, serial (= renouvellement), SANs (= nouveaux domaines) |
Deux règles de robustesse qui évitent tout le bruit :
- fetch échoué → cycle ignoré (jamais de faux « tout a été supprimé » pendant une panne) ;
- premier passage = baseline muette (aucun événement au bootstrap).
Les événements ont une sévérité (info/notable/major) ; notable+ part sur Discord.
UI : /changes, flux RSS /changes.xml, modération (masquage) via l'admin.
En bonus, track_stats_anomaly() alerte sur toute variation ≥ 15 % des compteurs
membres/torrents (± 35 % = major) — le canari des purges et des migrations.
7. Abonnements aux pannes
POST /api/subscribe accepte deux cibles : un topic ntfy (ou URL ntfy self-hostée) et un
webhook HTTPS (payload spécifique Discord détecté par hôte, sinon JSON générique).
Garde-fous :
- anti-SSRF (
safe_public_url) : HTTPS obligatoire, résolution DNS de l'hôte et rejet de toute IP privée / loopback / link-local / réservée — le monitor ne peut pas être utilisé pour frapper le LAN ; - confirmation par l'envoi : l'abonnement n'est stocké que si le message de bienvenue part vraiment (une cible invalide n'entre jamais en base) ;
- token de désinscription dans chaque message (
/api/unsub?t=…), plafond global (T4H_SUBS_MAX), rate-limit 6 tentatives / 10 min / IP ; - en-tête
Titlentfy translittéré en ASCII (exigence ntfy), corps UTF-8.
8. Couche communautaire
- Chat (
/api/messages) : pseudo optionnel mémorisé, réponses (fils), badge staff. Anti-spam : champ honeypot + 8 s entre messages par IP, 400 caractères max, rétention des 500 derniers. Rendu XSS-safe (échappement systématique, jamais d'innerHTMLsur données utilisateur). Modération (suppression, réponse « Staff ») via/admin. - « Je suis affecté » (
/api/affected) : compteur façon Downdetector, dédupliqué 10 min/IP, histogramme temporel — corrèle le ressenti utilisateur avec les sondes. - Profils (
/api/user?u=) : lookup d'un membre TR4KER (via la session), en respectant les drapeaux de vie privée (hide_uploads/hide_ratiofiltrés côté serveur), cache 5 min, rate-limit 30/min/IP. Avatars proxifiés (/api/avatar) car l'image d'origine exige le cookie. - Bugs & réponses staff (
/bugs) :bugs_job.py(cron hôte) mine les canaux de chat TR4KER, un LLM en extrait les bugs distincts + la position du staff →data/bugs.json. Un statut « corrigé/reconnu » exige un message staff ; les messages du bot lui-même sont exclus (anti-boucle d'écho). Bouton « rafraîchir » de l'admin → flagdata/bugs.refreshrelu par un watcher hôte. - Vantages (
/api/vantages) : points de vue multi-réseaux. Le monitor s'auto-déclare (« Hébergeur EU ») ; d'autres sondes peuvent poster viaPOST /api/vantage+ token. Règle de lecture affichée : un seul réseau rouge = problème local ; tous rouges = vraie panne.
9. t4shot — monitoring visuel
Conteneur séparé (image Playwright), car un navigateur n'a rien à faire dans un conteneur
stdlib. Toutes les 5 min : screenshot de tr4ker.net (avec le cookie de session), delta
visuel vs capture précédente (niveaux de gris 64×64, différence moyenne normalisée 0–1).
Attrape la classe de pannes invisible en HTTP : le site répond 200 mais a l'air cassé
(CSS mort, page blanche, page d'erreur habillée). Conserve les 10 dernières captures +
meta.json, servis par t4health (/api/shots, /shots/…) — le dossier data/shots est le
seul point de contact entre les deux conteneurs.
10. Stockage
SQLite (/data/t4health.db, WAL) — tables : checks (échantillons), incidents,
laststate, messages, stats, vantages, affected, subs, meta_kv, snapshots,
changes. Écritures sérialisées par un lock process-unique ; lectures concurrentes libres
(serveur HTTP threadé). history_json downsample côté serveur (~400 points max par série)
pour que le front reste léger sur 7 j.
11. Frontend
- Self-contained : ECharts vendored (
web/echarts.min.js, Apache-2.0), aucun CDN, aucune requête tierce — la page de statut doit marcher quand tout le reste brûle. - Assets versionnés par query-string (
style.css?v=N,app.js?v=N) — voir RUNBOOK. /nojs: version 100 % serveur (SVG inline, meta-refresh) pour Tor Browser / JS désactivé, chat inclus (formulaire POST classique)./badge.svg: badge de statut embarquable.- PWA (manifest + service-worker minimal),
robots.txt/sitemap.xml(la page est censée être trouvée par « tr4ker down »).
12. Sécurité & vie privée
- Aucun secret dans ce dépôt : tout vient d'
.env(voir.env.example) et d'un cookie monté hors repo. Le.gitignorerefuse.env,*.cookie,data/,*.db,*.log. - Admin : header
X-Admin-Tokencomparé en temps constant (hmac.compare_digest) ; pas de session, pas de cookie admin. - Anti-SSRF sur les cibles d'abonnement (voir §7). Path-traversal bloqué sur les fichiers statiques et les captures (normpath + préfixe).
- IPs des visiteurs : utilisées pour le rate-limiting et stockées brutes pour
affected/messages(modération) — jamais affichées publiquement. - Publier le code révèle les mécanismes anti-spam et de sonde : assumé, ils ne reposent pas sur le secret.
13. Déployer
Voir RUNBOOK.md (déploiement complet, mises à jour, gotchas) et
docker-compose.yml / .env.example. Résumé : cp .env.example .env (remplir), déposer le
cookie dans ./secrets/t4.cookie, docker compose up -d.
14. Licence
MIT (voir LICENSE). web/echarts.min.js est Apache ECharts,
licence Apache-2.0, vendored non modifié.