From baea6ccdd15cabb4df5fe96077fb3aa4203ad36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Sun, 21 Jun 2026 08:48:59 +0200 Subject: [PATCH] docs(merlin): transfers viz spec + routines note Merlin Studio v2: in-progress + completed transfer journal (duration, throughput, retry failed) alongside the routines tab. --- .../2026-06-21-lisael-box-routines-design.md | 3 + .../2026-06-21-merlin-transfers-viz-design.md | 124 ++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-21-merlin-transfers-viz-design.md diff --git a/docs/superpowers/specs/2026-06-21-lisael-box-routines-design.md b/docs/superpowers/specs/2026-06-21-lisael-box-routines-design.md index 6af0092..bdee9b8 100644 --- a/docs/superpowers/specs/2026-06-21-lisael-box-routines-design.md +++ b/docs/superpowers/specs/2026-06-21-lisael-box-routines-design.md @@ -3,6 +3,9 @@ - **Date** : 2026-06-21 - **Repo** : `electron-rare/lisael-box` (firmware) + Tower `lisael-content` + Merlin Studio (`merlin.saillant.cc`) - **Statut** : design validé, prêt pour le plan d'implémentation +- **Livraison** : 2 lots — **Firmware Mode Routine** + chantier **Merlin Studio v2** (onglet + Routines ici, et la [visu des transferts](2026-06-21-merlin-transfers-viz-design.md) livrée + dans le même chantier web/Tower). ## 1. But diff --git a/docs/superpowers/specs/2026-06-21-merlin-transfers-viz-design.md b/docs/superpowers/specs/2026-06-21-merlin-transfers-viz-design.md new file mode 100644 index 0000000..d8c401c --- /dev/null +++ b/docs/superpowers/specs/2026-06-21-merlin-transfers-viz-design.md @@ -0,0 +1,124 @@ +# Merlin Studio — Visualisation des transferts (design) + +- **Date** : 2026-06-21 +- **Composants** : Tower `lisael-content` (`lisael_content.py`, `lisael_server.py`) + UI web `merlin_ui.html` +- **Statut** : design validé (variante « plus complète »), prêt pour le plan +- **Lié à** : [2026-06-21-lisael-box-routines-design.md](2026-06-21-lisael-box-routines-design.md) — + livré dans le même chantier **Merlin Studio v2** (onglet Routines + onglet Transferts). + +## 1. But + +Sur `merlin.saillant.cc`, voir **les transferts en cours et ceux effectués** : quand +Tower pousse du contenu vers la box (au boot/`register` ou via « Pousser maintenant »), +on veut un retour visuel — quel fichier part maintenant, combien restent, et l'historique +des push passés (réussis/échoués) avec **durée** et **débit moyen**, plus un bouton pour +**relancer les transferts échoués**. + +## 2. Contexte (état actuel) + +- Le push tourne dans un thread de fond : `try_push(ip, delta)` → `push_batch` → `push_file` + (POST `urllib` bloquant vers `http://:8080/put?name=podcasts/`, timeout 300 s). +- Aucun journal n'est exposé : l'onglet **Box** montre l'état (`box-state.json`) et le *delta* + restant, mais pas le déroulé des transferts ni l'historique. +- Contrainte : `push_file` est un POST bloquant → **pas de % intra-fichier** réaliste. On + expose donc le **statut par fichier** + l'**avancement du lot** (X/N). + +## 3. Décisions (verrouillées) + +| Sujet | Décision | +|---|---| +| Contenu | Transferts **en cours** + **effectués** | +| Granularité | **Par fichier**, groupés par **lot de push** (un `register` ou un `/api/push`) | +| Métriques | taille, **durée** par fichier, **débit moyen** (taille/durée), statut, horodatage | +| Échecs | raison courte + **bouton « relancer les échecs »** (re-push des fichiers `failed`) | +| Historique | **persistant**, ~200 derniers fichiers (`transfers.json`, écriture atomique) | +| Emplacement | **onglet dédié « 📤 Transferts »** dans Merlin | +| Rafraîchissement | **auto-refresh ~2 s** tant qu'un transfert est actif, sinon au clic | +| Pas de | % intra-fichier (limite POST bloquant) | + +## 4. Architecture + +``` +push_batch/push_file ──maj──▶ journal (mémoire: lot actif) + transfers.json (historique) + (lisael_content.py) │ + GET /api/transfers ──▶ onglet "Transferts" + POST /api/transfers/retry (auto-refresh 2s) + (lisael_server.py) (merlin_ui.html) +``` + +## 5. Modèle de données + +Un **enregistrement de transfert** (par fichier) : +```json +{ "name":"matin_xx_a48.mp3", "dir":"routines", "size":428111, + "status":"done", "started":"2026-06-21T09:12:03", "ended":"2026-06-21T09:12:07", + "duration_s":4.1, "rate_bps":104417, "error":null, "box_ip":"192.168.0.250" } +``` +- `status` ∈ `pending | sending | done | failed`. +- **Lot actif** (en mémoire, un seul à la fois — `try_push` est déjà sérialisé par IP via `_busy`) : +```json +{ "box_ip":"192.168.0.250", "started":"...", "total":15, "current":3, + "files":[ {name,dir,size,status,...}, ... ] } +``` +- **Historique** : `transfers.json` = liste des enregistrements `done|failed` terminés, + tronquée aux ~200 derniers, écrite via `_atomic_write_json` (pattern existant). + +## 6. Tower — instrumentation (`lisael_content.py`) + +- Un module d'état `transfers` : `transfers_begin(box_ip, files)` (initialise le lot actif + avec tous les fichiers en `pending` + leur taille via `os.path.getsize`), + `transfers_mark(name, status, error=None)` (passe `sending` puis `done`/`failed`, + calcule `duration_s` et `rate_bps` à la fin), `transfers_end()` (vide le lot actif, + *flush* les enregistrements terminés dans `transfers.json` borné à 200), + `transfers_active()` / `transfers_history(limit)` (lecture pour l'API). +- `push_batch` appelle `transfers_begin` au début, `transfers_mark(name,"sending")` avant + chaque `push_file`, `transfers_mark(name,"done"/"failed",err)` après, `transfers_end` à la fin. +- `push_file` mesure le temps (monotone) et lève/retourne l'info de succès pour le mark. +- **Retry** : `retry_failed(box_ip)` = collecter les `failed` du dernier lot (ou de l'historique + récent pour cette box) → `try_push(ip, names)`. + +## 7. API (`lisael_server.py`) + +- `GET /api/transfers` → `{ "active": , "history": [<~200 derniers>] }` + (sous la Basic Auth existante). +- `POST /api/transfers/retry` → relance les fichiers `failed` connus pour la box + enregistrée (`box-state.json` → ip), `202` + `{started, count}` ; `409` si pas d'IP / + rien à relancer. + +## 8. UI (`merlin_ui.html`) — onglet « 📤 Transferts » + +- 4ᵉ onglet (bouton + panneau + entrée dans `TABS` + branche `showTab`), suivant le style + exact des onglets existants (helper `api()`, rendu de liste, bannière d'erreur). +- **En cours** : si `active` → carte « Vers — fichier / » + barre de + progression (X/N) + nom du fichier `sending` ; sinon « Aucun transfert en cours ». +- **Effectués** : table (fichier · dossier · taille · durée · débit · statut ✓/✗ · heure), + ligne `failed` en rouge avec la raison ; bouton **« 🔁 Relancer les échecs »** + (`POST /api/transfers/retry`) actif s'il existe ≥1 `failed`. +- **Auto-refresh** : `setInterval` ~2 s qui appelle `loadTransfers()` tant que `active != null` ; + arrêt quand plus de lot actif (et un dernier refresh pour figer l'historique). + +## 9. Gestion d'erreurs + +- `transfers.json` absent/corrompu → historique vide, on repart proprement (try/except). +- Restart du service en plein push → le lot actif (mémoire) est perdu ; l'historique persistant + garde les fichiers déjà terminés ; le prochain `register` de la box re-déclenche le delta. +- Retry sans box enregistrée → `409` + message UI clair. +- Double-push évité : `try_push` garde déjà l'anti-concurrence par IP (`_busy`). + +## 10. Tests + +- **Hôte** (Python, pattern `test_delta.py`, asserts purs) : `transfers_begin/mark/end` + (statuts, calcul `duration_s`/`rate_bps`, troncature à 200), `retry_failed` (sélection des + `failed`). Pas de réseau réel : injecter une fonction de push factice / utiliser un fichier + `transfers.json` temporaire. + +## 11. Critères d'acceptation + +1. Lancer un push (bouton « Pousser maintenant » ou boot box) → l'onglet Transferts montre le + lot **en cours** (box, X/N, fichier courant) qui progresse, puis bascule en **historique**. +2. L'historique affiche fichier/taille/**durée**/**débit**/statut/heure et **survit à un + restart** du service `lisael-content`. +3. Un fichier en échec apparaît en rouge avec sa raison ; **« Relancer les échecs »** le + re-pousse et il repasse ✓. +4. L'onglet s'**auto-rafraîchit** pendant un transfert et se calme une fois fini. +5. Test hôte vert (journal + retry).