docs(merlin): transfers viz spec + routines note
Merlin Studio v2: in-progress + completed transfer journal (duration, throughput, retry failed) alongside the routines tab.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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://<box>:8080/put?name=podcasts/<f>`, 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": <lot actif|null>, "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 <box_ip> — fichier <current>/<total> » + 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).
|
||||
Reference in New Issue
Block a user