Track long-running tasks in real time: sessions, steps, speed, ETA, HTTP API and TUI dashboard.
Runs on your machine from its package; the steps are below.

At most: moves money, runs code or commands, deletes data, or sends messages for you.
Odel charges nothing per call. The publisher may charge for its own service.
Runs locally over stdio
This server isn't hosted — your MCP client launches it from a package registry. Use one of the commands below, or drop the config into your client (e.g. Claude Desktop).
Run
uvx mcp-tracking
MCP client config
{
"mcpServers": {
"task-tracking-sessions-eta-dashboard": {
"command": "uvx",
"args": [
"mcp-tracking"
]
}
}
}English summary. Local-first tracking of long-running tasks: an MCP server for Claude or Lyra, a small HTTP API on 127.0.0.1:8765 and a Textual terminal dashboard. Sessions have items, progress, logs and templates (download, machine, free, lyra_task, movie); pollers feed qBittorrent and Bazarr sessions automatically. Install: pip install . then mcp-tracking, mcp-tracking-api, mcp-tracking-ui. No cloud, no telemetry.
Serveur MCP de suivi en temps reel avec dashboard terminal. Permet a Claude/Lyra de tracker n'importe quelle operation longue ET alimente automatiquement les sessions depuis le media-server (qBittorrent, Bazarr, conversion DV).
Un agent LLM (grand modèle de langage) qui lance une tâche longue (conversion, clone de VM, téléchargement) ne peut pas rester bloqué à la regarder, et il ne voit rien de ce qui se passe entre deux de ses tours. Ce serveur lui sert de mémoire de travail partagée :
tracking_get.metrics.py) : l'agent reçoit des chiffres au lieu d'extrapoler lui-même.running sans mise à jour depuis 10 minutes
est marquée stale : l'agent distingue « encore en cours » de « processus mort ».pid de son processus ;
l'heure de démarrage relevée dans /proc évite de signaler un pid réutilisé par un
autre programme. tracking_stop envoie SIGTERM et garde la session, tracking_kill
envoie SIGKILL et la supprime.flock et réécrit de façon atomique (storage.py) : plusieurs agents et scripts peuvent
écrire en même temps.download, machine, movie, lyra_task, free)
pour que chaque agent décrive son travail de la même façon.Local-first et self-hosted : aucun cloud, aucune télémétrie, tout reste sur la machine (on-prem). Utilisé par Lyra, l'assistant vocal French-first, et par n'importe quel client MCP (Claude Code, Claude Desktop...).
![]()
Enregistree avec docs/demo/record.sh : server.py --test alimente quatre sessions simulees dans un repertoire d'etat temporaire (TRACKING_STATE_DIR), puis server.py --ui ouvre le dashboard dessus. Les sessions reelles ne sont pas touchees.
MCP/tracking/
server.py -- Serveur MCP (outils Claude/Lyra) + point d'entree --ui / --test
api.py -- API HTTP locale (127.0.0.1:8765) pour les scripts externes
mutations.py -- Mutations d'une session, partagees par api.py ET server.py
(horodatage items, historique, niveaux de log, auto-completion)
metrics.py -- Metriques derivees (vitesse, ETA, ecoule, stale) -- logique pure,
calculees a la lecture, jamais stockees
storage.py -- Persistence JSON atomique + verrou fichier + cache mtime + purge TTL
models.py -- Modeles pydantic (TrackingSession, TrackingItem, LogEntry, ProgressPoint)
templates.py -- Templates builtin + templates utilisateur (JSON)
ui.py -- Dashboard Textual (TUI temps reel) + modales stop/kill
sim.py -- Simulations de demo (server.py --test)
poller.py -- Daemon polling qBittorrent (10s) + Bazarr (60s)
tracking-api.service -- Unite systemd (systeme) pour api.py
tracking-poller.service -- Unite systemd (systeme) pour poller.py
install.sh / deploy.sh -- Installation initiale / redeploiement des services
Makefile -- make test | smoke | deploy | ui
tests/ -- unitaires (storage, metrics) + integration/ (API HTTP reelle)
| Fichier | Emplacement | Surcharge |
|---|---|---|
tracking_state.json | ~/.local/state/tracking/ | TRACKING_STATE_DIR |
poller_state.json | ~/.local/state/tracking/ | TRACKING_STATE_DIR |
templates.json (templates utilisateur, optionnel) | ~/.config/tracking/ | TRACKING_TEMPLATES_FILE |
credentials/*.cred (qBittorrent, Bazarr) | a cote du code, gitignore | -- |
Un ancien tracking_state.json a cote du code est migre automatiquement au premier
demarrage (copie, jamais supprime).
Variables d'environnement de retention :
| Variable | Defaut | Role |
|---|---|---|
TRACKING_TTL_DAYS | 7 | Purge des sessions done / error / paused |
TRACKING_TTL_RUNNING_H | 24 | Purge des sessions running orphelines (plus mises a jour) |
Claude/Lyra (outils MCP)
|
v
server.py ─────────────────────────────────────────┐
|
qBittorrent API (poll 10s) |
| |
Bazarr API (poll 60s) ──> poller.py ──> api.py ──> mutations.py ──> storage.py ──> ~/.local/state/tracking/tracking_state.json
| | |
dv_webhook_server.py | v
| | ui.py
v | (rafraichit chaque seconde)
dv_convert.py ──────────────────────────────────────>
(metriques temps reel ffmpeg/dovi_tool)
Le fichier d'etat est ecrit a chaque modification via ecriture atomique (os.replace) sous verrou
fichier (tracking_state.lock). Tous les processus (MCP, API, poller, dashboard) partagent cet
unique fichier ; chaque lecture verifie le mtime pour invalider son cache.
Toute mutation (HTTP ou MCP) passe par mutations.py, qui garantit le meme comportement sur les
deux chemins : started_at / finished_at poses sur les items et la session, historique de
progression (fenetre glissante de 40 points), niveaux de log info / warn / error,
auto-completion quand tous les items sont termines.
GET /sessions et tracking_get renvoient un bloc metrics calcule a la volee par metrics.py :
| Champ | Sens |
|---|---|
percent | progression (plafonnee a 100) |
rate, rate_str | vitesse sur les 120 dernieres secondes (2.0 MB/s, 30.0 u/min) |
eta_seconds, eta_str | temps restant estime (session running uniquement) |
elapsed_seconds, elapsed_str | depuis created_at jusqu'a finished_at ou maintenant |
idle_seconds, stale | stale = running sans mise a jour depuis 10 min (affiche dans le TUI) |
uvx mcp-tracking # serveur MCP (stdio) ; avant publication : uvx --from git+https://github.com/amineutron/mcp-tracking mcp-tracking
uvx --from mcp-tracking mcp-tracking-api # API HTTP 127.0.0.1:8765
uvx --from mcp-tracking mcp-tracking-ui # tableau de bord terminal
Configuration Claude Desktop / Claude Code (mcpServers) :
{ "tracking": { "command": "uvx", "args": ["mcp-tracking"] } }
cd <dossier du dépôt>
# Creer le venv et installer les dependances
uv venv .venv
uv pip install "mcp[cli]>=1.0.0" "pydantic>=2.0" "textual>=0.80.0" "fastapi"
Le MCP est enregistre dans Claude Code (scope user) :
claude mcp list # -> tracking: Connected
Pour reenregistrer :
claude mcp add tracking -s user -- \
<dossier du dépôt>/.venv/bin/python \
<dossier du dépôt>/server.py
Deux services tournent en permanence et se lancent au boot :
| Service | Role | Port |
|---|---|---|
tracking-api.service | API HTTP locale pour scripts externes | 127.0.0.1:8765 |
tracking-poller.service | Poll qBittorrent (10s) + Bazarr (60s) | -- |
cd <dossier du dépôt>
./install.sh # premiere fois : venv + services (demande sudo)
sudo ./deploy.sh # apres chaque mise a jour du code : stop, unites, restart, verif
make smoke # sante rapide
Les instances MCP server.py deja ouvertes par des sessions Claude Code ne sont pas
redemarrees par deploy.sh : reconnecter tracking via /mcp dans ces sessions.
# Etat
systemctl status tracking-api.service tracking-poller.service
# Logs en direct
journalctl -fu tracking-poller.service
journalctl -fu tracking-api.service
# Redemarrage
sudo systemctl restart tracking-api.service tracking-poller.service
# Test API
curl http://127.0.0.1:8765/health
curl http://127.0.0.1:8765/sessions
Cherche "MCP Tracking" dans wofi/launcher. Lance le dashboard dans Kitty.
# Toutes les sessions
<dossier du dépôt>/.venv/bin/python \
<dossier du dépôt>/server.py --ui
# Filtre direct au lancement
.venv/bin/python server.py --ui --filter download
.venv/bin/python server.py --ui --filter movie
.venv/bin/python server.py --ui --filter errors
open_tracking_ui() # toutes les sessions
open_tracking_ui(filter_template="lyra_task") # vue Lyra uniquement
open_tracking_ui(filter_template="errors") # erreurs uniquement
.venv/bin/python server.py --test
Simule 4 sessions en parallele : download, machine (12 noeuds), free, movie (pipeline DV complet).
[TEMPLATE] Nom de la session id:xxxxxxxx (status)
[=============> ] 54.2% 27100 MB / 50000 MB
champ_extra1: valeur | champ_extra2: valeur
[ok] item-1 100.0 GB -- termine
[>] item-2 frame: 94231 / 172800 (54.5%) speed: 3.2x
[ ] item-3 --
[!] item-4 erreur detail
Logs Erreurs
14:32:01 Message log 1 [!] item-4
14:32:04 Message log 2 14:32:08 ECHEC: details
14:32:07 Message log 3 --
-- --
-- --
| Icone | Statut | Couleur |
|---|---|---|
[ ] | pending | gris |
[>] | running | cyan |
[ok] | done | vert |
[!] | error | rouge |
| Couleur | Statut |
|---|---|
| cyan | running |
| vert | done |
| rouge | error |
| jaune | paused |
| Touche | Action |
|---|---|
f | Filtre suivant (cycle dynamique par template) |
e | Basculer filtre erreurs uniquement |
r | Refresh manuel |
s | Stop propre d'une session (saisir l'ID) -> status paused |
k | Kill force d'une session (saisir l'ID) -> suppression |
q | Quitter |
| Fleches / Molette | Scroll |
Appuyer sur s ou k ouvre un modal avec un champ de saisie pour l'ID de session.
s marque la session en paused et ajoute un logk supprime definitivement la session du dashboardEchap annuleLe cycle de filtres est construit automatiquement depuis les sessions presentes :
all -> download -> free -> movie -> lyra_task -> errors -> all -> ...
all toujours presenterrors n'apparait que si au moins une session a une erreurfiltre: movie | 2/5 session(s)allLe poller interroge http://localhost:8080/api/v2/torrents/info toutes les 10 secondes.
[DOWNLOAD] avec nom, taille, vitesse, ETAcredentials/qbt-password.cred (chiffre systemd-creds --user, genere par media-server/scripts/secrets/rotate-secrets.sh)Le poller interroge l'API Bazarr toutes les 60 secondes.
[SUBTITLES] unique liste tous les episodes/films sans sous-titres FRSous-titres manquants (151)credentials/bazarr-api-key.cred (meme mecanisme). Sans credential, le poll concerne est simplement desactive.Declenche par dv-webhook.service quand Radarr/Sonarr importent un film DV Profile 4 ou 7.
Flux :
Radarr/Sonarr import
|
v
dv_webhook_server.py (port 8787)
|-- cree session tracking via api.py
|-- passe DV_TRACKING_SESSION_ID en env
v
dv_convert.py
|-- 6 etapes avec metriques temps reel
|-- ffmpeg : frame / speed / size / time (parse stderr)
|-- dovi_tool: frames X/Y ou X% (parse stderr indicatif)
v
session tracking completee ou en erreur
Les 6 etapes trackees avec leurs metriques :
| Etape | Outil | Metriques affichees |
|---|---|---|
| 1/6 extraction HEVC | ffmpeg | frame / speed / size / time |
| 2/6 demux BL/EL | dovi_tool | frames X/Y (%), bl: X GB, el: X GB |
| 3/6 extraction RPU + conv P8 | dovi_tool | frames X/Y (%), RPU: X KB |
| 4/6 injection RPU P8 dans BL | dovi_tool | frames X/Y (%), P8 HEVC: X GB |
| 5/6 reconstruction timestamps | ffmpeg | frame / fps / size |
| 6/6 remuxage MKV final | ffmpeg | frame / speed / size |
La barre de progression globale avance en continu pendant chaque etape (pas par sauts de 1/6 a la fin de chaque etape).
Mode manuel :
# Fichier unique
python <media-server>/scripts/dv_convert.py /chemin/film.mkv
# Scan dossier
python <media-server>/scripts/dv_convert.py --scan /mnt/media/media/movies
En mode manuel, la session tracking est creee automatiquement dans process_file.
Scripts externes peuvent creer/modifier des sessions directement :
# Creer une session
curl -X POST http://127.0.0.1:8765/sessions \
-H "Content-Type: application/json" \
-d '{"name":"Mon operation","template":"free","total":100,"unit":"%"}'
# -> {"id": "a1b2c3d4"}
# Mettre a jour
curl -X PUT http://127.0.0.1:8765/sessions/a1b2c3d4 \
-H "Content-Type: application/json" \
-d '{"processed":45,"log":"Etape 2/5 en cours","extra":{"phase":"etape 2"}}'
# Mettre a jour un item
curl -X PUT http://127.0.0.1:8765/sessions/a1b2c3d4 \
-H "Content-Type: application/json" \
-d '{"item":{"name":"mon-item","status":"done","note":"100 frames speed: 2x"}}'
# Supprimer
curl -X DELETE http://127.0.0.1:8765/sessions/a1b2c3d4
# Lister
curl http://127.0.0.1:8765/sessions
Corps PUT complet (tous les champs optionnels) :
{
"processed": 45.0,
"total": 100.0,
"status": "running",
"extra": {"phase": "etape 2"},
"log": "message de log",
"item": {
"name": "nom-de-l-item",
"status": "running",
"note": "metriques ici",
"processed": 50.0,
"total": 100.0
}
}
tracking_createParametres:
name (str) Nom de la session
template (str) "download" | "machine" | "free" | "movie" | "lyra_task" |
"subtitles" | "series_episode" | "series_season" | template utilisateur
total (float) Valeur totale
unit (str, opt) Unite affichee (ex: " MB", " machines", "%")
items (list, opt) Liste d'elements a suivre
extra (dict, opt) Champs specifiques au template
Format items:
[{"name": "fichier.iso", "total": 5100, "unit": " MB", "note": "info"}]
Retourne: ID de session + etat initial formate
tracking_updateParametres:
session_id (str) ID de la session
processed (float, opt) Nouvelle valeur de progression
message (str, opt) Message de log
item_updates (list, opt) Mises a jour des items
extra (dict, opt) Champs extra a merger
Format item_updates:
[{"name": "item-1", "status": "done", "processed": 1200, "note": "detail"}]
Status: "pending" | "running" | "done" | "error"
tracking_logAjoute un log sans modifier la progression.
Parametres:
session_id (str)
message (str)
tracking_completeMarque done a 100%.
Parametres:
session_id (str)
message (str, opt)
tracking_errorMarque en erreur (prefixe "ERREUR:" auto, remonte dans colonne Erreurs).
Parametres:
session_id (str)
message (str)
tracking_stopArrete proprement une session (status -> paused). Reste visible dans le dashboard.
Parametres:
session_id (str)
message (str, opt)
tracking_killSupprime une session en force. Disparait immediatement du dashboard.
Parametres:
session_id (str)
tracking_getRetourne l'etat complet formate d'une session.
tracking_listParametres:
template (str, opt) Filtrer par template
status (str, opt) Filtrer par statut ("running", "done", "error", "paused")
tracking_deleteSupprime une session (equivalent de tracking_kill).
tracking_templatesAffiche la liste des templates et leurs champs.
open_tracking_uiOuvre le dashboard dans un terminal Kitty.
Parametres:
filter_template (str, opt) Template a afficher au lancement
downloadTelechargement de fichiers. Alimente automatiquement par qBittorrent via le poller.
Champs extra : speed, eta
Unite par defaut : MB
machineOperations sur des machines (update, clone, snapshot, deploy). Utilise par Lyra pour les operations VM/cluster.
Champs extra : operation, target
Unite par defaut : machines
freeFormat libre. Utilise par le poller pour les sous-titres Bazarr manquants.
Aucun champ extra impose, aucune unite par defaut.
lyra_taskOperations Lyra (VM clone, backup, update, snapshot).
Champs extra : operation, target, phase, eta
Unite par defaut : %
moviePipeline complet d'un film : telechargement -> conversion Dolby Vision. Alimente automatiquement par dv_convert.py quand Radarr/Sonarr importent un fichier DV P4/P7.
Champs extra : phase, quality, codec, audio, source, dv, speed, eta
Unite par defaut : %
Les 6 etapes DV trackees avec metriques temps reel :
"1/6 extraction HEVC"
"2/6 demux BL/EL"
"3/6 extraction RPU + conv P8"
"4/6 injection RPU P8 dans BL"
"5/6 reconstruction timestamps"
"6/6 remuxage MKV final"
Deux attaquants realistes sur une machine de bureau :
127.0.0.1:8765. Sans protection, elle pourrait creer des
sessions, en supprimer, et surtout demander l'envoi d'un signal a un processus.api.py ecoute uniquement sur 127.0.0.1:8765 -- inaccessible depuis le reseau$XDG_RUNTIME_DIR/tracking/token (droits 0600, donc illisible par un autre utilisateur), exige
en Authorization: Bearer ... sur POST, PUT et DELETE. Une page web ne peut pas le lire.Origin donne un 403, meme avec le jeton.Host verifie (boucle locale uniquement) et Content-Type: application/json exige
en ecriture.pid annonce existe et appartient au meme utilisateur, puis releve son
heure de demarrage. /stop et /kill refusent d'agir si cette empreinte a change (numero de
processus recycle par un autre programme) ou si le pid n'a jamais ete enregistre. Le pid ne
peut plus etre modifie par un PUT. Chaque signal envoye est journalise.ProtectSystem=strict, ProtectHome=read-only avec le seul etat
en ecriture, PrivateTmp, SystemCallFilter=@system-service, CapabilityBoundingSet= vide,
UMask=0077. Verifiable avec systemd-analyze security tracking-api.service.lyra-daemon garde un sudo NOPASSWD pour piloter la machine (services,
VMs, audio). Ce n'est pas le tracking qui l'accorde, et le durcir releve du projet Lyra ; tant
que ce daemon existe, un attaquant qui obtiendrait l'execution de code sous cet utilisateur
disposerait de ce pouvoir, independamment des protections ci-dessus.127.0.0.1:5678 dans docker-compose.ymldv_webhook_server.py ecoute sur 0.0.0.0:8787 (necessaire pour recevoir les webhooks Docker) -- proteger ce port avec un firewall si la machine est exposeeNoNewPrivileges=truepoller.py lit $CREDENTIALS_DIRECTORY (service user) ou dechiffre credentials/*.cred via systemd-creds decrypt --user (service systeme), avec repli sur les variables QBT_PASSWORD / BAZARR_KEY pour le debugtemplates.py et ajouter une entree dans TEMPLATES :"mon_template": {
"description": "Description courte",
"extra_fields": ["champ1", "champ2"],
"default_unit": " unites",
"example_extra": {"champ1": "valeur", "champ2": "valeur"},
},
_sim_mon_template() dans sim.py.Le template est immediatement disponible sans autre modification.
Sans toucher au code, un template peut aussi etre declare dans ~/.config/tracking/templates.json
(meme structure, cle = nom du template) ; il est charge au demarrage.
make test # unitaires (storage, metrics) + integration (API HTTP reelle sur port ephemere)
La fixture autouse de conftest.py redirige la persistence vers un tmp_path : les tests ne
touchent jamais l'etat de production.
| Dépôt | Rôle |
|---|---|
| lyra | French-first voice assistant : assistant DevOps vocal, local par défaut (AGPL-3.0) ; le français familier est compris par des règles avant même d'appeler un modèle, ce qui lui suffit d'un modèle de 0.5B |
| fedora-agents | MCP : machines virtuelles KVM et sauvegardes |
| mcp-tracking | MCP + API + tableau de bord des tâches longues |
| neutroncore | hub PWA du homelab |
| hue-mcp | MCP Philips Hue (fork de ThomasRohde/hue-mcp) |
| pylips-mcp | MCP TV Philips |
| denon-mcp | MCP ampli Denon |
| catt-mcp | MCP Chromecast et DLNA |