GUI : réparer « Appliquer le plan » (forçait pas) + rendre l'auto-actif visible

Bug racine : /api/instancier appelait « instancier appliquer » SANS
--force → refus silencieux dès que le plan divergeait de l'inventaire.
Après une édition (disque, état, auto-actif), l'inventaire n'était jamais
régénéré → compteurs figés (« 0 actifs » alors qu'un hôte était actif au
plan). Le clic « Appliquer » = intention explicite → on force.

Aussi :
- après matérialisation (creer/deployer/pousser), le frontend régénère
  l'inventaire (appliquerPlan) au lieu d'un simple rechargement ;
- compteur « vivantes » dans l'en-tête (depuis la sonde).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Daniel Allaire 2026-07-01 17:52:39 -04:00
parent 347e739818
commit 213724f566
2 changed files with 19 additions and 5 deletions

View file

@ -10,7 +10,16 @@
(`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable** (`/api/sondes`, en parallèle) et affiche un état réel — **● vivante** / **● injoignable**
— sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan. — sur les tuiles et dans le détail (« Plan : … · Réel : … »), distinct du plan.
- **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on - **Auto-actif** : un hôte qu'on **matérialise** (clone `creer` réussi) ou qu'on
**déploie** passe automatiquement `actif` dans le plan (matérialisé = actif). **déploie** passe automatiquement `actif` dans le plan (matérialisé = actif), puis
l'inventaire est **régénéré** pour que le changement se voie partout (en-tête inclus).
- **Compteur « vivantes »** dans l'en-tête (depuis la sonde), à côté de actifs/planifiés.
### Corrigé
- **Le bouton ⚙ « Appliquer le plan » du GUI refusait en silence** dès que le plan
divergeait de l'inventaire (il appelait `instancier appliquer` **sans** `--force`).
Résultat : après une édition (disque, état, auto-actif), l'inventaire n'était jamais
régénéré et les compteurs restaient figés. Le clic « Appliquer » **est** l'intention
explicite → le GUI force désormais (git reste le filet).
- **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet - **Bouton « Pousser » dans le GUI — le flux devient 100 % cliquable.** Chaque objet
du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault du détail se matérialise sur son hôte, en streaming console (avec mot de passe vault

View file

@ -979,12 +979,15 @@ HTML = r"""<!doctype html>
function dessinerResume() { function dessinerResume() {
const actifs = hotes.filter(h => h.etat === 'actif').length; const actifs = hotes.filter(h => h.etat === 'actif').length;
const bloques = hotes.filter(h => hoteBloque(h)).length; const bloques = hotes.filter(h => hoteBloque(h)).length;
const vivantes = hotes.filter(h => sondes[h.nom] === true).length;
const sonde = Object.keys(sondes).length > 0;
document.getElementById('resume').innerHTML = ` document.getElementById('resume').innerHTML = `
${estProduction ? '<span class="badge-prod" title="Instance de PRODUCTION : déploiement sous confirmation renforcée">PROD</span>' : '<span class="badge-lab" title="Instance bac à sable (setops_production: false)">bac à sable</span>'} ${estProduction ? '<span class="badge-prod" title="Instance de PRODUCTION : déploiement sous confirmation renforcée">PROD</span>' : '<span class="badge-lab" title="Instance bac à sable (setops_production: false)">bac à sable</span>'}
<span class="chip"><b>${hotes.length}</b> hôtes</span> <span class="chip"><b>${hotes.length}</b> hôtes</span>
<span class="chip"><span class="pt actif"></span><b>${actifs}</b> actifs</span> <span class="chip"><span class="pt actif"></span><b>${actifs}</b> actifs</span>
<span class="chip"><span class="pt plan"></span><b>${hotes.length - actifs}</b> planifiés</span> <span class="chip"><span class="pt plan"></span><b>${hotes.length - actifs}</b> planifiés</span>
${bloques ? `<span class="chip"><span class="pt bloq"></span><b>${bloques}</b> bloqués</span>` : ''}`; ${bloques ? `<span class="chip"><span class="pt bloq"></span><b>${bloques}</b> bloqués</span>` : ''}
${sonde ? `<span class="chip" title="État RÉEL : VM joignables (sonde SSH), indépendamment du plan"><span class="pt actif"></span><b>${vivantes}</b> vivantes</span>` : ''}`;
} }
function correspondServeur(s) { function correspondServeur(s) {
@ -1727,8 +1730,8 @@ HTML = r"""<!doctype html>
if (rc === null) rc = 1; if (rc === null) rc = 1;
finirConsole(rc); finirConsole(rc);
if (mode === 'verifier' && rc === 0) { verifie[hote] = true; message('Vérification réussie — déploiement débloqué pour ' + hote + '.', 'ok'); } if (mode === 'verifier' && rc === 0) { verifie[hote] = true; message('Vérification réussie — déploiement débloqué pour ' + hote + '.', 'ok'); }
else if (mode === 'creer') { message(rc === 0 ? 'VM « ' + hote + ' » créée.' : 'Échec de la création.', rc === 0 ? 'ok' : 'erreur'); if (rc === 0) charger(); } else if (mode === 'creer') { message(rc === 0 ? 'VM « ' + hote + ' » créée — hôte marqué actif.' : 'Échec de la création.', rc === 0 ? 'ok' : 'erreur'); if (rc === 0) appliquerPlan(); else sonder(); }
else if (mode === 'deployer' || mode === 'pousser') { verifie[hote] = false; message(rc === 0 ? 'Poussée terminée.' : 'Poussée en échec.', rc === 0 ? 'ok' : 'erreur'); } else if (mode === 'deployer' || mode === 'pousser') { verifie[hote] = false; message(rc === 0 ? 'Poussée terminée — hôte marqué actif.' : 'Poussée en échec.', rc === 0 ? 'ok' : 'erreur'); if (rc === 0) appliquerPlan(); else sonder(); }
} catch (e) { } catch (e) {
ecrireConsole('Erreur client : ' + e.message + '\n'); finirConsole(1); ecrireConsole('Erreur client : ' + e.message + '\n'); finirConsole(1);
} }
@ -2012,8 +2015,10 @@ class Gestionnaire(BaseHTTPRequestHandler):
ecrire_intrants(donnees) ecrire_intrants(donnees)
self.repondre_json(200, inventaire_api(self.inventaire)) self.repondre_json(200, inventaire_api(self.inventaire))
elif chemin == "/api/instancier": elif chemin == "/api/instancier":
# Cliquer « Appliquer le plan » EST l'intention explicite : on force
# (le diff-refus est un garde-fou CLI ; ici le clic le confirme). git = filet.
resultat = subprocess.run( resultat = subprocess.run(
["python3", str(RACINE / "scripts/instancier.py"), "appliquer"], ["python3", str(RACINE / "scripts/instancier.py"), "appliquer", "--force"],
capture_output=True, text=True, cwd=str(RACINE)) capture_output=True, text=True, cwd=str(RACINE))
self.repondre_json(200, {"ok": resultat.returncode == 0, self.repondre_json(200, {"ok": resultat.returncode == 0,
"sortie": (resultat.stdout + resultat.stderr).strip()}) "sortie": (resultat.stdout + resultat.stderr).strip()})