2026-06-24 20:17:46 -04:00
#!/usr/bin/env python3
""" Interface web locale pour gerer un inventaire Set-OPS. """
from __future__ import annotations
from http . server import BaseHTTPRequestHandler , ThreadingHTTPServer
import argparse
import json
import os
import re
import secrets
2026-07-01 17:39:44 -04:00
import socket
2026-06-24 20:17:46 -04:00
import subprocess
import tempfile
2026-07-07 12:49:17 -04:00
import time
2026-06-24 20:17:46 -04:00
import threading
2026-07-01 17:39:44 -04:00
from concurrent . futures import ThreadPoolExecutor
2026-06-24 20:17:46 -04:00
from pathlib import Path
from urllib . parse import urlparse
import yaml
2026-07-07 15:57:47 -04:00
import devis_reseau
2026-06-24 20:17:46 -04:00
from inventory_rules import (
GROUPE_HOTES_ACTIFS ,
GROUPE_HOTES_PLANIFIES ,
GROUPES_ETAT_HOTE ,
chaine_connexion ,
charger_applications ,
charger_bases_donnees ,
charger_dependances ,
charger_domaines ,
charger_nomenclature ,
charger_serveurs ,
est_groupe_operationnel ,
groupes_operationnels_connus ,
reconcilier_serveur ,
valider_applications ,
2026-07-22 21:32:42 -04:00
catalogue_liens ,
2026-06-24 20:17:46 -04:00
valider_bases ,
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
valider_domaines ,
2026-06-24 20:17:46 -04:00
valider_serveurs ,
)
RACINE = Path ( __file__ ) . resolve ( ) . parents [ 1 ]
INSTANCE = Path ( os . environ . get ( " SETOPS_INSTANCE " ) or ( RACINE / " instance " ) )
2026-06-30 16:08:25 -04:00
def _inventaire ( instance : Path , * noms : str ) - > Path :
""" Inventaire de l ' instance, détection rétro-compatible (premier existant). """
for nom in noms :
p = instance / " inventories " / nom / " hosts.yml "
if p . exists ( ) :
return p
Mise en conformité prouvable : registre d'affirmations + make prouver
Le dépôt fait / explique / prouve ce qu'il affirme, vérifiable en une commande.
- Phase 1 : docs/audit/affirmations.md — 54 affirmations publiques tracées vers
une commande de preuve et un statut (✅/🟡/❌/⚪).
- Phase 2 : CLAUDE.md réduit à un pointeur mince ; contradiction SSH levée (le
code applique déjà PasswordAuthentication no + AuthenticationMethods publickey,
conforme à AGENTS.md) ; section AGENTS « Codex » → « agents IA ».
- Phase 3 : parcours démarrage réparé (QUICKSTART renvoyait à un modèle absent,
chemins de voûte faux, commandes make périmées) ; make verifier vert
(ansible-lint 33 → 0 : site.yml généré nommé, pipefail, name[template]) ;
voûte Proxmox unifiée lue par le clonage (all/vault.yml).
- Phase 4 : make prouver → docs/audit/preuve-<date>.md, harnais rejouable qui
rappelle l'outillage existant (aucune validation réimplémentée).
- Phase 5 : parcours QUICKSTART prouvé hors-ligne sur le socle ; modèle socle
rendu valide (autorite interne → auto-heberge) ; split-brain d'inventaire
corrigé (repli sur le répertoire existant, pas principal/).
make prouver : 15 OK, 0 échec, 1 sautée (voûte). ansible-lint : 0 failure.
Écarts découverts en cours de traitement (AFF-097..100) : tous résolus.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 19:53:18 -04:00
# Aucun hosts.yml encore généré : viser le répertoire d'inventaire déjà présent
# (ses group_vars y sont), sinon le premier nom par défaut.
for nom in noms :
if ( instance / " inventories " / nom ) . is_dir ( ) :
return instance / " inventories " / nom / " hosts.yml "
2026-06-30 16:08:25 -04:00
return instance / " inventories " / noms [ 0 ] / " hosts.yml "
# Un seul inventaire par instance (modèle « séparation par instance ») ; on accepte
# l'ancien découpage lab/production en rétro-compatibilité. SETOPS_INVENTAIRE force.
_force = os . environ . get ( " SETOPS_INVENTAIRE " )
INVENTAIRE_DEFAUT = Path ( _force ) if _force else _inventaire ( INSTANCE , " principal " , " production " )
INVENTAIRE_PRODUCTION = INVENTAIRE_DEFAUT . resolve ( )
# Inventaire qui porte la config Proxmox / le clonage (modèle) : lab > principal > production.
INVENTAIRE_MODELE = _inventaire ( INSTANCE , " lab " , " principal " , " production " )
2026-06-24 20:17:46 -04:00
DOSSIER_PLAYBOOKS_GROUPES = RACINE / " playbooks/groupes "
FICHIER_DEPENDANCES = RACINE / " docs/dependances-groupes.yml "
FICHIER_NOMENCLATURE = INSTANCE / " plan/nomenclature.yml "
FICHIER_BASES = INSTANCE / " plan/bases-donnees.yml "
FICHIER_APPLICATIONS = INSTANCE / " plan/applications.yml "
FICHIER_DOMAINES = INSTANCE / " plan/domaines.yml "
FICHIER_SERVEURS = INSTANCE / " plan/serveurs.yml "
2026-06-30 10:07:17 -04:00
# Intrants de base (panneau GUI). Cibles d'ecriture possedees par le GUI.
2026-06-30 16:31:42 -04:00
# Identite : group_vars/all/10-intrants.yml de l'inventaire. Selon l'instance c'est
# un fichier reel (modele « par instance ») ou un symlink vers une source partagee
# (ecrire suit le symlink). Proxmox : group_vars de l'inventaire modele. Les SECRETS
# n'y entrent jamais.
INTRANTS_IDENTITE = INVENTAIRE_DEFAUT . parent / " group_vars/all/10-intrants.yml "
2026-06-30 16:08:25 -04:00
INTRANTS_PROXMOX = INVENTAIRE_MODELE . parent / " group_vars/proxmox.yml "
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
# Le seed reseau (`index`) vit dans la nomenclature (le plan reseau) — d'ou tout
# l'adressage se derive. Le panneau Intrants le lit/ecrit la, chirurgicalement, sans
# reformater le fichier (cf. _ecrire_index_nomenclature).
FICHIERS_INTRANTS = { " identite " : INTRANTS_IDENTITE , " proxmox " : INTRANTS_PROXMOX ,
" reseau " : FICHIER_NOMENCLATURE }
2026-06-30 10:07:17 -04:00
# (variable, fichier, classe, section, libelle, type)
INTRANTS_SCHEMA = [
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
( " index " , " reseau " , " constante " , " Réseau " , " Index d ' instance dans la fédération (seed : dérive supernet 10.(10+index), VLAN 1000+index× 10+zone, VMID) " , " int " ) ,
2026-06-30 10:07:17 -04:00
( " domaine_interne " , " identite " , " constante " , " Identité " , " Domaine DNS interne " , " str " ) ,
2026-07-05 00:47:44 -04:00
( " fuseau_horaire " , " identite " , " defaut " , " Identité " , " Fuseau horaire " , " str " ) ,
( " organisation " , " identite " , " defaut " , " Identité " , " Organisation (annuaire LDAP, certificats) " , " str " ) ,
( " identite_realm " , " identite " , " defaut " , " Identité " , " Realm SSO (Keycloak) " , " str " ) ,
2026-07-07 11:24:56 -04:00
( " nftables_admin_ssh " , " identite " , " catalogue " , " Sécurité " , " Sources SSH d ' administration toujours autorisées (garde anti-lockout du pare-feu) " , " liste " ) ,
2026-06-30 10:07:17 -04:00
( " proxmox_api_host " , " proxmox " , " constante " , " Proxmox " , " Hôte API Proxmox " , " str " ) ,
( " proxmox_api_user " , " proxmox " , " constante " , " Proxmox " , " Utilisateur API " , " str " ) ,
( " proxmox_api_port " , " proxmox " , " constante " , " Proxmox " , " Port API " , " str " ) ,
( " proxmox_validate_certs " , " proxmox " , " constante " , " Proxmox " , " Valider les certificats TLS " , " bool " ) ,
( " proxmox_clone_source_nom " , " proxmox " , " constante " , " Proxmox " , " Nom du golden template " , " str " ) ,
( " proxmox_clone_vmid_modele " , " proxmox " , " constante " , " Proxmox " , " VMID du golden template " , " int " ) ,
( " proxmox_clone_noeud " , " proxmox " , " defaut " , " Proxmox " , " Nœud Proxmox (défaut) " , " str " ) ,
( " proxmox_clone_stockage " , " proxmox " , " defaut " , " Proxmox " , " Stockage (défaut) " , " str " ) ,
( " proxmox_clone_pont " , " proxmox " , " defaut " , " Proxmox " , " Pont réseau (défaut) " , " str " ) ,
2026-06-30 12:36:51 -04:00
( " proxmox_noeuds " , " proxmox " , " catalogue " , " Proxmox " , " Nœuds disponibles (liste) " , " liste " ) ,
( " proxmox_stockages " , " proxmox " , " catalogue " , " Proxmox " , " Stockages disponibles (liste) " , " liste " ) ,
( " proxmox_ponts " , " proxmox " , " catalogue " , " Proxmox " , " Ponts réseau disponibles (liste) " , " liste " ) ,
2026-06-30 10:07:17 -04:00
]
# Cles secretes : interdites a l'ecriture par le GUI (garde-fou).
INTRANTS_CLES_INTERDITES = {
" proxmox_api_token_id " , " proxmox_api_token_secret " , " ansible_become_pass " , " ansible_password " ,
}
# Rappel en lecture seule (jamais de valeur) : secrets a fournir via Ansible Vault.
SECRETS_ATTENDUS = [
2026-07-07 12:07:39 -04:00
# Accès Proxmox (proxmox.vault.yml)
2026-06-30 10:07:17 -04:00
" proxmox_api_token_id " , " proxmox_api_token_secret " ,
2026-07-07 12:07:39 -04:00
# PKI (step-ca)
2026-07-07 11:24:56 -04:00
" vault_step_ca_password " , " vault_step_ca_provisioner_password " ,
2026-07-07 12:07:39 -04:00
# Identité (LDAP / Keycloak)
2026-06-30 10:07:17 -04:00
" vault_openldap_admin " , " vault_ldap_sssd " , " vault_keycloak_admin " ,
2026-07-07 12:07:39 -04:00
# Bases de données
" vault_postgresql_keycloak " , " vault_bd_keycloak " , " vault_bd_forgejo " ,
" vault_bd_icingadb " , " vault_bd_nextcloud " ,
# Forge (Forgejo)
" vault_forgejo_admin " , " vault_forgejo_secret_key " , " vault_forgejo_internal_token " ,
# Observabilité / cache
" vault_grafana_admin " , " vault_redis " ,
# Applications (admin)
" vault_nextcloud_admin " ,
# Secrets OIDC des clients SSO (un par app fédérée)
" vault_grafana_oidc " , " vault_forgejo_oidc " , " vault_icingaweb2_oidc " ,
" vault_nextcloud_oidc " , " vault_oauth2_cookie " ,
# Sauvegarde (restic hors-nœud)
" vault_restic_password " , " vault_backup_ssh_privkey " ,
2026-06-30 10:07:17 -04:00
]
2026-06-24 20:17:46 -04:00
# Garde-fous des executions (verifier / deployer) depuis l'interface.
JETON = secrets . token_urlsafe ( 18 )
VERROU = threading . Lock ( )
VERIF_OK : dict [ str , bool ] = { }
MOTIF_HOTE = re . compile ( r " ^[A-Za-z0-9._-] { 1,63}$ " )
# Champs de provisioning persistes comme variables d'hote dans l'inventaire.
# (cle_gui, variable_inventaire, type)
CHAMPS_PROVISION = [
( " adresse_ip " , " ansible_host " , " str " ) ,
( " utilisateur_ansible " , " ansible_user " , " str " ) ,
( " cidr " , " proxmox_cidr " , " int " ) ,
( " passerelle " , " proxmox_passerelle " , " str " ) ,
( " vlan " , " proxmox_vlan " , " int " ) ,
( " pont " , " proxmox_pont " , " str " ) ,
( " dns " , " proxmox_dns " , " str " ) ,
( " vmid " , " proxmox_vmid " , " int " ) ,
( " noeud " , " proxmox_noeud " , " str " ) ,
( " stockage " , " proxmox_stockage " , " str " ) ,
( " disque_taille " , " proxmox_disque_taille " , " str " ) ,
( " memoire " , " proxmox_memoire " , " int " ) ,
( " coeurs " , " proxmox_coeurs " , " int " ) ,
]
def charger_yaml ( path : Path ) - > dict :
if not path . exists ( ) :
return { " all " : { " children " : { } } }
with path . open ( " r " , encoding = " utf-8 " ) as fichier :
data = yaml . safe_load ( fichier ) or { }
if not isinstance ( data , dict ) :
raise ValueError ( f " { path } ne contient pas une table YAML. " )
data . setdefault ( " all " , { } )
data [ " all " ] . setdefault ( " children " , { } )
return data
def ecrire_yaml ( path : Path , data : dict ) - > None :
with path . open ( " w " , encoding = " utf-8 " ) as fichier :
yaml . safe_dump ( data , fichier , default_flow_style = False , sort_keys = False , allow_unicode = True )
def enfants ( data : dict ) - > dict :
return data . setdefault ( " all " , { } ) . setdefault ( " children " , { } )
def groupes_disponibles ( data : dict ) - > list [ str ] :
return groupes_operationnels_connus ( enfants ( data ) , DOSSIER_PLAYBOOKS_GROUPES )
def roles_des_groupes ( ) - > dict :
""" Pour chaque playbook de groupe, extrait les roles appliques (chaine groupe -> roles). """
resultat : dict = { }
for chemin in sorted ( DOSSIER_PLAYBOOKS_GROUPES . glob ( " *.yml " ) ) :
roles : list [ str ] = [ ]
try :
with chemin . open ( " r " , encoding = " utf-8 " ) as fichier :
docs = yaml . safe_load ( fichier ) or [ ]
plays = docs if isinstance ( docs , list ) else [ docs ]
for play in plays :
if not isinstance ( play , dict ) :
continue
for role in play . get ( " roles " , [ ] ) or [ ] :
if isinstance ( role , str ) :
roles . append ( role )
elif isinstance ( role , dict ) :
nom = role . get ( " role " ) or role . get ( " name " )
if nom :
roles . append ( str ( nom ) )
except ( OSError , yaml . YAMLError ) :
pass
resultat [ chemin . stem ] = { " roles " : roles , " stub " : not roles }
return resultat
def hotes_actifs_fichier ( path : Path ) - > set [ str ] :
data = charger_yaml ( path )
return set ( enfants ( data ) . get ( GROUPE_HOTES_ACTIFS , { } ) . get ( " hosts " , { } ) or { } )
def variables_hote ( data : dict , hote : str ) - > dict :
resultat : dict = { }
for groupe_data in enfants ( data ) . values ( ) :
hosts = groupe_data . get ( " hosts " , { } )
valeurs = hosts . get ( hote )
if isinstance ( valeurs , dict ) :
resultat . update ( valeurs )
return resultat
def liste_hotes ( data : dict ) - > list [ dict ] :
noms = set ( )
for groupe_data in enfants ( data ) . values ( ) :
noms . update ( groupe_data . get ( " hosts " , { } ) )
hotes = [ ]
for nom in sorted ( noms ) :
groupes = [ ]
etat = " planifie "
visible = False
for groupe , groupe_data in enfants ( data ) . items ( ) :
if nom not in groupe_data . get ( " hosts " , { } ) :
continue
if groupe == GROUPE_HOTES_ACTIFS :
etat = " actif "
visible = True
elif groupe in GROUPES_ETAT_HOTE :
visible = True
continue
elif est_groupe_operationnel ( groupe ) :
groupes . append ( groupe )
visible = True
if not visible :
continue
variables = variables_hote ( data , nom )
hote = { " nom " : nom , " etat " : etat , " groupes " : sorted ( groupes ) }
for cle , var , _type in CHAMPS_PROVISION :
valeur = variables . get ( var , " " )
hote [ cle ] = " " if valeur is None else valeur
if not str ( hote . get ( " utilisateur_ansible " , " " ) ) . strip ( ) :
hote [ " utilisateur_ansible " ] = " ansible "
hotes . append ( hote )
return hotes
2026-07-01 17:39:44 -04:00
def _joignable ( ip : str , port : int = 22 , timeout : float = 1.5 ) - > bool :
if not ip :
return False
try :
with socket . create_connection ( ( ip , port ) , timeout = timeout ) :
return True
except OSError :
return False
def sonder_hotes ( path : Path ) - > dict :
""" Etat REEL (vs plan declaratif) : la VM de chaque hote repond-elle sur SSH ? """
cibles = [ ( h . get ( " nom " ) , h . get ( " adresse_ip " ) ) for h in liste_hotes ( charger_yaml ( path ) ) ]
with ThreadPoolExecutor ( max_workers = 16 ) as executeur :
resultats = executeur . map ( lambda c : ( c [ 0 ] , _joignable ( c [ 1 ] ) ) , cibles )
return { nom : ok for nom , ok in resultats if nom }
def marquer_serveur_actif ( hote : str ) - > None :
""" Auto-actif : un hote materialise (clone/deploye) passe ' actif ' dans le plan. """
registre = charger_serveurs ( FICHIER_SERVEURS )
serveurs = registre . get ( " serveurs " ) or { }
if hote in serveurs and serveurs [ hote ] . get ( " etat " ) != " actif " :
serveurs [ hote ] [ " etat " ] = " actif "
ecrire_serveurs ( FICHIER_SERVEURS , registre )
2026-06-24 20:17:46 -04:00
def serveurs_reconcilies ( data : dict ) - > list :
""" Serveurs du plan + valeurs derivees + statut de reconciliation (lecture). """
registre = charger_serveurs ( FICHIER_SERVEURS )
nomenclature = charger_nomenclature ( FICHIER_NOMENCLATURE )
hotes = { h [ " nom " ] : h for h in liste_hotes ( data ) if h . get ( " nom " ) }
resultat = [ ]
for nom , srv in ( registre . get ( " serveurs " ) or { } ) . items ( ) :
rec = reconcilier_serveur ( nom , srv , hotes . get ( nom ) , nomenclature )
d = rec [ " derive " ]
statut = " absent " if rec [ " absent_inventaire " ] else ( " divergence " if rec [ " divergences " ] else " reconcilie " )
resultat . append ( {
" nom " : nom , " fonction " : srv . get ( " fonction " , " " ) , " etat " : srv . get ( " etat " , " " ) ,
" noeud " : srv . get ( " noeud " , " " ) , " stockage " : srv . get ( " stockage " , " " ) ,
" disque " : srv . get ( " disque " , " " ) , " memoire " : srv . get ( " memoire " , " " ) , " coeurs " : srv . get ( " coeurs " , " " ) ,
" integrations " : srv . get ( " integrations " , [ ] ) ,
" vmid " : d . get ( " vmid " , " " ) , " adresse_ip " : d . get ( " adresse_ip " , " " ) , " vlan " : d . get ( " vlan " , " " ) ,
" statut " : statut , " divergences " : rec [ " divergences " ] ,
} )
return resultat
2026-07-07 11:34:55 -04:00
def flux_api ( ) - > list :
""" Matrice des flux réseau (meta/flux.yml de tous les rôles) — lecture seule. """
try :
import resoudre_flux
flux = resoudre_flux . charger_flux ( )
except Exception :
return [ ]
lignes = [ ]
for role in sorted ( flux ) :
for fl in flux [ role ] :
pair = fl . get ( " pair " )
pair = " , " . join ( str ( p ) for p in pair ) if isinstance ( pair , list ) else str ( pair )
port = fl . get ( " port " )
port = " , " . join ( str ( p ) for p in port ) if isinstance ( port , list ) else str ( port )
lignes . append ( {
" role " : role , " sens " : fl . get ( " sens " , " " ) , " port " : port ,
" protocole " : fl . get ( " protocole " , " " ) , " pair " : pair ,
" chiffrement " : fl . get ( " chiffrement " , " " ) , " raison " : fl . get ( " raison " , " " ) ,
} )
return lignes
def couches_api ( ) - > list :
""" Ordre de déploiement en couches (couches-deploiement.yml + graphe) — lecture seule. """
try :
import orchestrer
return [ { " nom " : nom , " groupes " : list ( groupes ) } for nom , groupes in orchestrer . ordre_deploiement ( ) ]
except Exception :
return [ ]
2026-06-24 20:17:46 -04:00
def inventaire_api ( path : Path ) - > dict :
data = charger_yaml ( path )
dependencies = charger_dependances ( FICHIER_DEPENDANCES )
return {
2026-07-07 11:34:55 -04:00
" flux " : flux_api ( ) ,
" couches " : couches_api ( ) ,
2026-06-24 20:17:46 -04:00
" inventaire " : os . path . relpath ( path , RACINE ) ,
2026-06-30 16:20:18 -04:00
" production " : instance_est_production ( path ) ,
2026-06-24 20:17:46 -04:00
" groupes " : groupes_disponibles ( data ) ,
" dependances " : dependencies ,
" nomenclature " : charger_nomenclature ( FICHIER_NOMENCLATURE ) ,
" chaine " : roles_des_groupes ( ) ,
" bases " : charger_bases_donnees ( FICHIER_BASES ) ,
" applications " : charger_applications ( FICHIER_APPLICATIONS ) ,
" domaines " : charger_domaines ( FICHIER_DOMAINES ) ,
" serveurs " : serveurs_reconcilies ( data ) ,
" hotes " : liste_hotes ( data ) ,
2026-06-30 10:07:17 -04:00
" intrants " : intrants_api ( ) ,
2026-06-30 12:36:51 -04:00
" integrations_disponibles " : integrations_disponibles ( ) ,
2026-07-22 21:32:42 -04:00
" liens_acceptes " : catalogue_liens ( RACINE ) ,
2026-06-30 10:07:17 -04:00
}
def _lire_yaml_dict ( path : Path ) - > dict :
if not path . exists ( ) :
return { }
with path . open ( " r " , encoding = " utf-8 " ) as fichier :
data = yaml . safe_load ( fichier ) or { }
return data if isinstance ( data , dict ) else { }
2026-06-30 16:20:18 -04:00
def instance_est_production ( path : Path ) - > bool :
""" Cette instance est-elle la PRODUCTION ? (garde-fou de prudence, pas un blocage)
2026-06-30 15:59:37 -04:00
2026-06-30 16:20:18 -04:00
Le déploiement est permis sur toute instance ( lab compris , vers son infra ) ;
ce drapeau ne sert qu ' à exiger une confirmation renforcée côté GUI quand c ' est
la prod . Repère explicite : ` setops_production ` dans ` < inventaire > / group_vars / all / ` .
Rétro - compatibilité : à défaut , ancien repère ( inventaire nommé « production » ) .
2026-06-30 15:59:37 -04:00
"""
base = path . parent / " group_vars " / " all "
fichiers = sorted ( base . glob ( " *.yml " ) ) if base . is_dir ( ) else [ ]
plat = path . parent / " group_vars " / " all.yml "
if plat . exists ( ) :
fichiers . append ( plat )
for f in fichiers :
d = _lire_yaml_dict ( f )
if isinstance ( d , dict ) and " setops_production " in d :
return bool ( d [ " setops_production " ] )
return path . resolve ( ) == INVENTAIRE_PRODUCTION
2026-06-30 10:07:17 -04:00
def intrants_courants ( ) - > dict :
cache : dict = { }
valeurs : dict = { }
2026-06-30 12:36:51 -04:00
for variable , fcle , _classe , _section , _libelle , typ in INTRANTS_SCHEMA :
2026-06-30 10:07:17 -04:00
if fcle not in cache :
cache [ fcle ] = _lire_yaml_dict ( FICHIERS_INTRANTS [ fcle ] )
2026-06-30 12:36:51 -04:00
v = cache [ fcle ] . get ( variable )
if typ == " liste " :
items = v if isinstance ( v , list ) else [ ]
valeurs [ variable ] = [ str ( x ) . strip ( ) for x in items if str ( x ) . strip ( ) ]
else :
valeurs [ variable ] = " " if v is None else v
2026-06-30 10:07:17 -04:00
return valeurs
2026-07-22 21:32:42 -04:00
# --- Couverture du schema du plan par le GUI -------------------------------------
#
# Ce que les fonctions de sauvegarde JS ecrivent REELLEMENT, registre par registre.
# Source unique verifiee par `scripts/couverture_gui.py` : tout champ present dans
# un plan reel (instance ou modele) mais absent d'ici signale un trou de l'interface
# — l'operateur devrait editer le YAML a la main, ce que la doctrine refuse.
#
# A TENIR A JOUR avec sauvegarderServeurs / sauvegarderApplications /
# sauvegarderBases / sauvegarderDomaines. Le verificateur controle aussi que chaque
# champ declare ici apparait bien dans le source du GUI (garde anti-derive).
CHAMPS_ECRITS_PAR_GUI = {
" serveurs " : { " fonction " , " etat " , " noeud " , " stockage " , " disque " , " memoire " ,
" coeurs " , " integrations " } ,
" applications " : { " groupe " , " hote " , " port " , " requiert " , " expose " , " liens " , " websocket " } ,
" bases_donnees " : { " serveur " , " base " , " proprietaire " , " secret " , " consommateur " ,
" portee " , " usage " } ,
" serveurs_bd " : { " type " , " hote " , " port " , " groupe " } ,
" domaines_publics " : { " autorite " , " edge " , " secondaires " , " dnssec " , " mail " } ,
# nomenclature : AUCUN champ. Le GUI la LIT (liste des fonctions) sans jamais
# l'ecrire — ajouter une fonction exige d'editer le YAML. Trou connu et assume ;
# `couverture_gui.py` le signale tant qu'il dure.
" nomenclature " : set ( ) ,
}
2026-06-30 12:36:51 -04:00
def integrations_disponibles ( ) - > list :
""" Roles client_* presents dans roles/ : source des integrations selectionnables. """
base = RACINE / " roles "
return sorted ( p . name for p in base . glob ( " client_* " ) if p . is_dir ( ) )
2026-06-30 10:07:17 -04:00
def intrants_api ( ) - > dict :
schema = [ { " variable " : v , " fichier " : f , " classe " : c , " section " : s ,
" libelle " : l , " type " : t } for ( v , f , c , s , l , t ) in INTRANTS_SCHEMA ]
return {
" schema " : schema ,
" valeurs " : intrants_courants ( ) ,
" secrets_attendus " : SECRETS_ATTENDUS ,
" fichiers " : { k : os . path . relpath ( v , RACINE ) for k , v in FICHIERS_INTRANTS . items ( ) } ,
2026-06-24 20:17:46 -04:00
}
2026-06-30 10:07:17 -04:00
def _coercer_intrant ( typ : str , brut : object ) - > object :
2026-06-30 12:36:51 -04:00
if typ == " liste " :
items = brut if isinstance ( brut , list ) else str ( brut ) . replace ( " \n " , " , " ) . split ( " , " )
# dedoublonne en preservant l'ordre
vus , sortie = set ( ) , [ ]
for x in items :
s = str ( x ) . strip ( )
if s and s not in vus :
vus . add ( s ) ; sortie . append ( s )
return sortie
2026-06-30 10:07:17 -04:00
s = str ( brut ) . strip ( )
if typ == " bool " :
return s . lower ( ) in ( " true " , " 1 " , " oui " , " yes " , " on " )
if typ == " int " :
return int ( s ) if s . lstrip ( " - " ) . isdigit ( ) else s
return s
def _ecrire_intrants_fichier ( path : Path , valeurs : dict , entete : str ) - > None :
interdits = sorted ( set ( valeurs ) & INTRANTS_CLES_INTERDITES )
if interdits :
raise ValueError ( f " Refus d ' ecrire des cles sensibles via le GUI: { ' , ' . join ( interdits ) } " )
path . parent . mkdir ( parents = True , exist_ok = True )
with path . open ( " w " , encoding = " utf-8 " ) as fichier :
fichier . write ( entete )
yaml . safe_dump ( valeurs , fichier , default_flow_style = False , sort_keys = True , allow_unicode = True )
def ecrire_intrants ( payload : dict ) - > None :
valeurs = payload . get ( " valeurs " , { } ) or { }
if not str ( valeurs . get ( " domaine_interne " , " " ) ) . strip ( ) :
raise ValueError ( " domaine_interne ne peut pas etre vide (cle de voute de l ' ecosysteme). " )
types = { v : t for ( v , f , c , s , l , t ) in INTRANTS_SCHEMA }
fichier_de = { v : f for ( v , f , c , s , l , t ) in INTRANTS_SCHEMA }
par_fichier : dict = { }
for variable , valeur in valeurs . items ( ) :
if variable not in fichier_de :
continue
par_fichier . setdefault ( fichier_de [ variable ] , { } ) [ variable ] = _coercer_intrant ( types [ variable ] , valeur )
if " identite " in par_fichier :
_ecrire_intrants_fichier (
INTRANTS_IDENTITE , par_fichier [ " identite " ] ,
" # Intrants d ' IDENTITE de l ' instance — SOURCE UNIQUE, partagee par tous les envs. \n "
" # Edite par le panneau « Intrants de base » du GUI (make inventaire-ui). \n "
" # Reference via symlink depuis inventories/<env>/group_vars/all/10-intrants.yml. \n --- \n " )
if " proxmox " in par_fichier :
fusion = _lire_yaml_dict ( INTRANTS_PROXMOX ) # preserve les cles non gerees
fusion . update ( par_fichier [ " proxmox " ] )
_ecrire_intrants_fichier (
INTRANTS_PROXMOX , fusion ,
" # Parametres Proxmox non sensibles (les secrets vont dans proxmox.vault.yml). \n "
" # Cles d ' intrants editees par le panneau « Intrants de base » du GUI. \n --- \n " )
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
if " reseau " in par_fichier and " index " in par_fichier [ " reseau " ] :
_ecrire_index_nomenclature ( int ( par_fichier [ " reseau " ] [ " index " ] ) )
def _ecrire_index_nomenclature ( index : int ) - > None :
""" Met a jour `index:` dans plan/nomenclature.yml SANS reformater le reste.
Le fichier garde ses commentaires et son formatage compact ( l ' adressage se derive
de ce seul seed ) . Remplacement de la ligne existante , ou insertion en tete a defaut .
"""
path = FICHIER_NOMENCLATURE
texte = path . read_text ( encoding = " utf-8 " ) if path . exists ( ) else " --- \n "
nouveau , remplace = re . subn ( r " (?m)^index:.*$ " , f " index: { index } " , texte )
if not remplace :
nouveau = re . sub ( r " (?m)^(--- \ n) " , rf " \ g<1>index: { index } \ n " , texte , count = 1 ) or texte
path . write_text ( nouveau , encoding = " utf-8 " )
2026-06-30 10:07:17 -04:00
2026-06-24 20:17:46 -04:00
def ecrire_bases ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des bases de donnees Set-OPS (serveurs_bd + bases_donnees). \n "
" # Edite par make inventaire-ui ou scripts/bases_donnees.py. \n "
" # ' portee ' (application|groupe|hote) interprete ' consommateur ' ; defaut groupe. \n "
" # ' secret ' nomme une variable Ansible Vault (jamais le mot de passe). \n "
" --- \n "
)
contenu = {
" serveurs_bd " : registre . get ( " serveurs_bd " , { } ) or { } ,
" bases_donnees " : registre . get ( " bases_donnees " , { } ) or { } ,
}
with path . open ( " w " , encoding = " utf-8 " ) as fichier :
fichier . write ( entete )
yaml . safe_dump ( contenu , fichier , default_flow_style = False , sort_keys = False , allow_unicode = True )
def ecrire_applications ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des applications Set-OPS (application = entite pivot). \n "
" # Edite par make inventaire-ui ou scripts/applications.py. \n "
" # Une VM peut porter plusieurs applications ; une base se lie a une application. \n "
2026-07-22 21:32:42 -04:00
" # ' liens ' = bindings declaratifs [ { vers, role}] resolus par scripts/instancier.py ; \n "
" # les roles acceptes sont ceux de roles/<groupe>/meta/liens.yml. \n "
2026-06-24 20:17:46 -04:00
" --- \n "
)
with path . open ( " w " , encoding = " utf-8 " ) as fichier :
fichier . write ( entete )
yaml . safe_dump ( { " applications " : registre . get ( " applications " , { } ) or { } } ,
fichier , default_flow_style = False , sort_keys = False , allow_unicode = True )
def ecrire_serveurs ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des serveurs (VM) du plan Set-OPS. \n "
" # Edite par make inventaire-ui ou scripts/serveurs.py. \n "
" # VMID/IP/VLAN/passerelle sont DERIVES de la fonction via instance/plan/nomenclature.yml. \n "
" --- \n "
)
with path . open ( " w " , encoding = " utf-8 " ) as fichier :
fichier . write ( entete )
yaml . safe_dump ( { " serveurs " : registre . get ( " serveurs " , { } ) or { } } ,
fichier , default_flow_style = False , sort_keys = False , allow_unicode = True )
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
def ecrire_domaines ( path : Path , registre : dict ) - > None :
entete = (
" # Registre des domaines publics (zones DNS de l ' instance). \n "
" # Edite par make inventaire-ui ou scripts/domaines.py. \n "
" # autorite : primaire-cache | auto-heberge | delegue (label descriptif). \n "
" --- \n "
)
with path . open ( " w " , encoding = " utf-8 " ) as fichier :
fichier . write ( entete )
yaml . safe_dump ( { " domaines_publics " : registre . get ( " domaines_publics " , { } ) or { } } ,
fichier , default_flow_style = False , sort_keys = False , allow_unicode = True )
2026-07-07 13:31:02 -04:00
def _extraire_echec ( ligne : str , tache : str ) - > dict | None :
""" D ' une ligne Ansible fatal/unreachable, tire un échec en LANGAGE CLAIR
( tâche · hôte · type · message ) — pour communiquer entre humains . """
m = re . search ( r ' (?:fatal|failed): \ s* \ [([^ \ ]]+) \ ]: \ s*(FAILED!|UNREACHABLE!)? \ s*=> \ s*( \ { .* \ }) ' , ligne )
if not m :
return None
hote = m . group ( 1 )
typ = " injoignable (SSH) " if m . group ( 2 ) == " UNREACHABLE! " else " échec "
detail : dict = { }
try :
detail = json . loads ( m . group ( 3 ) )
except Exception :
pass
if detail . get ( " censored " ) :
message = " sortie masquée (no_log) — souvent une tâche qui manipule un secret "
else :
msg = str ( detail . get ( " msg " , " " ) ) . strip ( )
# Pour un échec de commande, msg est générique ; la vraie cause est dans stderr.
stderr = " "
for cle in ( " stderr " , " module_stderr " , " stdout " ) :
valeur = str ( detail . get ( cle , " " ) ) . strip ( )
if valeur :
lignes = [ x for x in valeur . splitlines ( ) if x . strip ( ) ]
stderr = lignes [ 0 ] if lignes else valeur # l'erreur mène, l'indice suit
break
if msg and msg . lower ( ) not in ( " non-zero return code " , " module failure " , " " ) :
message = msg
elif stderr :
message = stderr
else :
message = msg or " échec sans message explicite (voir le journal complet) "
message = " " . join ( str ( message ) . split ( ) ) # une ligne propre
return { " tache " : ( tache or " ? " ) . strip ( ) , " hote " : hote , " type " : typ , " message " : message [ : 400 ] }
2026-06-24 20:17:46 -04:00
def donnees_hote ( hote : dict ) - > dict :
variables : dict = { }
for cle , var , typ in CHAMPS_PROVISION :
brut = str ( hote . get ( cle , " " ) ) . strip ( )
if not brut :
continue
if typ == " int " and brut . lstrip ( " - " ) . isdigit ( ) :
variables [ var ] = int ( brut )
else :
variables [ var ] = brut
return variables
def construire_inventaire ( payload : dict , groupes_connus : list [ str ] ) - > dict :
enfants_data : dict = {
" modeles_vm " : { " hosts " : { } } ,
GROUPE_HOTES_ACTIFS : { " hosts " : { } } ,
GROUPE_HOTES_PLANIFIES : { " hosts " : { } } ,
}
for groupe in groupes_connus :
enfants_data . setdefault ( groupe , { " hosts " : { } } )
enfants_data . setdefault ( " hotes_proxmox " , { " hosts " : { } } )
for hote in payload . get ( " hotes " , [ ] ) :
nom = str ( hote . get ( " nom " , " " ) ) . strip ( )
if not nom :
continue
variables = donnees_hote ( hote )
groupe_etat = GROUPE_HOTES_ACTIFS if hote . get ( " etat " ) == " actif " else GROUPE_HOTES_PLANIFIES
enfants_data [ groupe_etat ] [ " hosts " ] [ nom ] = variables
for groupe in hote . get ( " groupes " , [ ] ) :
if groupe not in groupes_connus :
continue
enfants_data [ groupe ] [ " hosts " ] [ nom ] = { }
return { " all " : { " children " : enfants_data } }
def valider_dependances_payload ( payload : dict , dependencies : dict ) - > None :
hotes = payload . get ( " hotes " , [ ] )
active_groups : set [ str ] = set ( )
for hote in hotes :
if hote . get ( " etat " ) != " actif " :
continue
active_groups . update ( hote . get ( " groupes " , [ ] ) )
missing : list [ str ] = [ ]
for hote in hotes :
if hote . get ( " etat " ) != " actif " :
continue
nom = str ( hote . get ( " nom " , " " ) ) . strip ( )
for groupe in hote . get ( " groupes " , [ ] ) :
for required_group in dependencies . get ( groupe , { } ) . get ( " requiert_groupes_actifs " , [ ] ) :
if required_group not in active_groups :
missing . append ( f " { nom } : { groupe } requiert { required_group } actif " )
if missing :
raise ValueError ( " Dependances manquantes: " + " ; " . join ( missing ) )
def valider_payload ( payload : dict , groupes_connus : list [ str ] , dependencies : dict ) - > None :
noms : set [ str ] = set ( )
vmids : dict [ str , str ] = { }
for hote in payload . get ( " hotes " , [ ] ) :
nom = str ( hote . get ( " nom " , " " ) ) . strip ( )
if not nom :
raise ValueError ( " Un hote a un nom vide. " )
if nom in noms :
raise ValueError ( f " Hote en double: { nom } " )
noms . add ( nom )
if hote . get ( " etat " ) == " actif " and not str ( hote . get ( " adresse_ip " , " " ) ) . strip ( ) :
raise ValueError ( f " Hote actif sans adresse IP: { nom } " )
groupes = hote . get ( " groupes " , [ ] )
if not groupes :
raise ValueError ( f " Aucun groupe operationnel pour: { nom } " )
inconnus = sorted ( set ( groupes ) - set ( groupes_connus ) )
if inconnus :
raise ValueError ( f " Groupes inconnus pour { nom } : { ' , ' . join ( inconnus ) } " )
vlan = str ( hote . get ( " vlan " , " " ) ) . strip ( )
if vlan and ( not vlan . isdigit ( ) or not 1 < = int ( vlan ) < = 4094 ) :
raise ValueError ( f " VLAN invalide pour { nom } : { vlan } (attendu 1-4094) " )
cidr = str ( hote . get ( " cidr " , " " ) ) . strip ( )
if cidr and ( not cidr . isdigit ( ) or not 0 < = int ( cidr ) < = 32 ) :
raise ValueError ( f " CIDR invalide pour { nom } : { cidr } (attendu 0-32) " )
for cle , libelle in ( ( " memoire " , " Memoire " ) , ( " coeurs " , " Coeurs " ) ) :
valeur = str ( hote . get ( cle , " " ) ) . strip ( )
if valeur and ( not valeur . isdigit ( ) or int ( valeur ) < = 0 ) :
raise ValueError ( f " { libelle } invalide pour { nom } : { valeur } (entier positif attendu) " )
vmid = str ( hote . get ( " vmid " , " " ) ) . strip ( )
if not vmid :
continue
if vmid in vmids :
raise ValueError ( f " VMID en double: { vmid } pour { vmids [ vmid ] } et { nom } " )
vmids [ vmid ] = nom
valider_dependances_payload ( payload , dependencies )
HTML = r """ <!doctype html>
< html lang = " fr " >
< head >
< meta charset = " utf-8 " >
< meta name = " viewport " content = " width=device-width, initial-scale=1 " >
< title > Set - OPS · Votre artisan numérique < / title >
< style >
: root {
color - scheme : dark ;
- - fond : #0c1119;
- - fond - degrade : radial - gradient ( 1100 px 540 px at 100 % - 8 % , #142033 0, transparent 55%), var(--fond);
- - surface : #151d2b;
- - surface - 2 : #1a2333;
- - surface - 3 : #212d41;
- - texte : #e8edf6;
- - muted : #93a1bb;
- - faint : #67738c;
- - ligne : #27324a;
- - ligne - forte : #36445f;
- - primaire : #3b82f6;
- - primaire - fort : #2f6fe0;
- - teal : #2dd4bf;
- - vert : #34d399; --vert-fond:rgba(52,211,153,.13);
- - bleu : #60a5fa; --bleu-fond:rgba(96,165,250,.13);
- - rouge : #f87171; --rouge-fond:rgba(248,113,113,.13);
- - ambre : #fbbf24; --ambre-fond:rgba(251,191,36,.13);
- - ombre : 0 1 px 2 px rgba ( 0 , 0 , 0 , .35 ) , 0 10 px 30 px - 14 px rgba ( 0 , 0 , 0 , .7 ) ;
- - ombre - forte : 0 18 px 50 px - 18 px rgba ( 0 , 0 , 0 , .8 ) ;
- - rayon : 11 px ; - - rayon - sm : 8 px ;
}
* { box - sizing : border - box ; }
body {
margin : 0 ; background : var ( - - fond - degrade ) ; color : var ( - - texte ) ; min - height : 100 vh ;
font - family : ui - sans - serif , system - ui , - apple - system , " Segoe UI " , Roboto , sans - serif ;
font - size : 14 px ; line - height : 1.5 ; - webkit - font - smoothing : antialiased ;
}
: : selection { background : color - mix ( in srgb , var ( - - teal ) 35 % , transparent ) ; }
* { scrollbar - width : thin ; scrollbar - color : var ( - - ligne - forte ) transparent ; }
header {
position : sticky ; top : 0 ; z - index : 20 ;
display : flex ; align - items : center ; gap : 16 px ; flex - wrap : wrap ;
padding : 12 px 22 px ; border - bottom : 1 px solid var ( - - ligne ) ;
background : color - mix ( in srgb , var ( - - surface ) 82 % , transparent ) ;
backdrop - filter : saturate ( 150 % ) blur ( 12 px ) ;
}
. marque { display : flex ; align - items : center ; gap : 9 px ; min - width : 0 ; }
. marque . pastille { width : 9 px ; height : 9 px ; border - radius : 50 % ; background : var ( - - teal ) ; box - shadow : 0 0 0 4 px color - mix ( in srgb , var ( - - teal ) 18 % , transparent ) ; flex : none ; }
. marque . titre { display : flex ; flex - direction : column ; line - height : 1.15 ; min - width : 0 ; }
h1 { margin : 0 ; font - size : 15 px ; font - weight : 800 ; letter - spacing : - .01 em ; white - space : nowrap ; }
. slogan { font - size : 10.5 px ; color : var ( - - muted ) ; font - weight : 600 ; letter - spacing : .02 em ; white - space : nowrap ; }
. chemin { color : var ( - - muted ) ; font - size : 12 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; white - space : nowrap ; overflow : hidden ; text - overflow : ellipsis ; }
. grandit { flex : 1 1 auto ; min - width : 0 ; }
. resume { display : flex ; gap : 6 px ; flex - wrap : wrap ; }
. chip { display : inline - flex ; align - items : center ; gap : 6 px ; border : 1 px solid var ( - - ligne ) ; border - radius : 999 px ; padding : 3 px 10 px ; background : var ( - - surface - 2 ) ; color : var ( - - muted ) ; font - size : 11.5 px ; font - weight : 650 ; }
. chip b { color : var ( - - texte ) ; font - weight : 800 ; }
. pt { width : 7 px ; height : 7 px ; border - radius : 50 % ; flex : none ; }
. pt . actif { background : var ( - - vert ) ; } . pt . plan { background : var ( - - bleu ) ; } . pt . bloq { background : var ( - - rouge ) ; }
. barre { display : flex ; gap : 8 px ; align - items : center ; flex - wrap : wrap ; }
. recherche { position : relative ; }
. recherche input { padding - left : 30 px ; min - width : 180 px ; }
. recherche : : before { content : " ⌕ " ; position : absolute ; left : 10 px ; top : 50 % ; transform : translateY ( - 50 % ) ; color : var ( - - faint ) ; font - size : 16 px ; }
button {
border : 1 px solid var ( - - ligne - forte ) ; background : var ( - - surface - 2 ) ; color : var ( - - texte ) ;
padding : 8 px 13 px ; border - radius : var ( - - rayon - sm ) ; cursor : pointer ; font : inherit ; font - weight : 700 ;
transition : background .15 s , border - color .15 s , transform .05 s , box - shadow .15 s , opacity .15 s , color .15 s ;
}
button : hover : not ( : disabled ) { border - color : var ( - - teal ) ; }
button : active : not ( : disabled ) { transform : translateY ( 1 px ) ; }
button : disabled { opacity : .4 ; cursor : not - allowed ; }
button . primaire { background : var ( - - primaire ) ; color : #fff; border-color: var(--primaire); box-shadow: var(--ombre); }
button . primaire : hover : not ( : disabled ) { background : var ( - - primaire - fort ) ; border - color : var ( - - primaire - fort ) ; }
button . fantome { background : transparent ; }
button . attention { border - color : var ( - - ambre ) ; color : var ( - - ambre ) ; }
button . danger { color : var ( - - rouge ) ; border - color : transparent ; background : transparent ; }
button . danger : hover : not ( : disabled ) { background : var ( - - rouge - fond ) ; }
input , select {
width : 100 % ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon - sm ) ;
padding : 8 px 10 px ; background : var ( - - surface ) ; color : var ( - - texte ) ; font : inherit ;
transition : border - color .15 s , box - shadow .15 s ;
}
input : : placeholder { color : var ( - - faint ) ; }
input : focus , select : focus { outline : none ; border - color : var ( - - teal ) ; box - shadow : 0 0 0 3 px color - mix ( in srgb , var ( - - teal ) 22 % , transparent ) ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
main { padding : 16 px 22 px 44 px ; display : grid ; grid - template - columns : minmax ( 0 , 1.6 fr ) minmax ( 420 px , 1 fr ) ; gap : 20 px ; align - items : start ; max - width : 1760 px ; margin : 0 auto ; }
2026-06-30 12:19:26 -04:00
. toasts { position : fixed ; top : 14 px ; right : 14 px ; z - index : 60 ; display : flex ; flex - direction : column ; gap : 8 px ; max - width : min ( 380 px , 92 vw ) ; pointer - events : none ; }
. toast { pointer - events : auto ; background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne - forte ) ; border - left : 3 px solid var ( - - bleu ) ; border - radius : var ( - - rayon - sm ) ; box - shadow : var ( - - ombre - forte ) ; padding : 9 px 12 px ; font - size : 13 px ; font - weight : 600 ; color : var ( - - texte ) ; display : flex ; gap : 9 px ; align - items : flex - start ; animation : toast - in .18 s ease - out ; }
. toast . ok { border - left - color : var ( - - vert ) ; } . toast . erreur { border - left - color : var ( - - rouge ) ; }
. toast . sortant { opacity : 0 ; transform : translateX ( 12 px ) ; transition : opacity .25 s , transform .25 s ; }
. toast . x { margin - left : auto ; cursor : pointer ; opacity : .5 ; font - weight : 800 ; background : none ; border : 0 ; color : inherit ; padding : 0 2 px ; font - size : 15 px ; line - height : 1 ; }
. toast . x : hover { opacity : 1 ; }
@keyframes toast - in { from { opacity : 0 ; transform : translateX ( 12 px ) ; } }
input . invalide , select . invalide { border - color : var ( - - rouge ) ! important ; box - shadow : 0 0 0 1 px var ( - - rouge ) ; }
2026-06-24 20:17:46 -04:00
. section - grille { margin - bottom : 22 px ; }
. section - tete { display : flex ; align - items : center ; gap : 10 px ; margin : 0 0 11 px ; color : var ( - - faint ) ; font - size : 11 px ; font - weight : 800 ; letter - spacing : .09 em ; text - transform : uppercase ; }
. section - tete . compte { color : var ( - - muted ) ; }
. section - tete : : after { content : " " ; height : 1 px ; background : var ( - - ligne ) ; flex : 1 ; }
. grille - cartes { display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 190 px , 1 fr ) ) ; gap : 12 px ; }
. carte {
position : relative ; text - align : left ; display : flex ; flex - direction : column ; gap : 8 px ;
background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ;
padding : 12 px 13 px ; cursor : pointer ; font : inherit ; color : var ( - - texte ) ;
transition : border - color .14 s , box - shadow .14 s , transform .05 s , background .14 s ;
}
. carte : : before { content : " " ; position : absolute ; left : 0 ; top : 11 px ; bottom : 11 px ; width : 3 px ; border - radius : 0 3 px 3 px 0 ; background : var ( - - bleu ) ; }
. carte . actif : : before { background : var ( - - vert ) ; }
. carte . bloque : : before { background : var ( - - rouge ) ; }
. carte : hover { border - color : var ( - - ligne - forte ) ; box - shadow : var ( - - ombre ) ; }
. carte . selectionnee { border - color : var ( - - teal ) ; box - shadow : 0 0 0 1 px var ( - - teal ) , var ( - - ombre ) ; background : var ( - - surface - 2 ) ; }
. carte - haut { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; }
. carte - vmid { color : var ( - - faint ) ; font - size : 11 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; }
. carte - nom { font - weight : 800 ; font - size : 14 px ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. carte - nom . sans { color : var ( - - faint ) ; font - style : italic ; font - weight : 600 ; }
. carte - ip { color : var ( - - muted ) ; font - size : 12 px ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; }
. carte - grps { display : flex ; gap : 5 px ; flex - wrap : wrap ; margin - top : 1 px ; }
. tag { font - size : 10.5 px ; font - weight : 700 ; padding : 2 px 7 px ; border - radius : 999 px ; background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; white - space : nowrap ; }
. tag . plus { background : transparent ; color : var ( - - faint ) ; }
. badge - statut { display : inline - flex ; align - items : center ; gap : 5 px ; border - radius : 999 px ; padding : 3 px 9 px ; font - size : 10.5 px ; font - weight : 800 ; }
. badge - statut . ok { color : var ( - - vert ) ; background : var ( - - vert - fond ) ; }
. badge - statut . attente { color : var ( - - bleu ) ; background : var ( - - bleu - fond ) ; }
. badge - statut . bloque { color : var ( - - rouge ) ; background : var ( - - rouge - fond ) ; }
aside { position : sticky ; top : 70 px ; max - height : calc ( 100 vh - 90 px ) ; overflow : auto ; }
. detail { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon ) ; box - shadow : var ( - - ombre ) ; overflow : hidden ; }
. detail . bloque { border - color : color - mix ( in srgb , var ( - - rouge ) 50 % , var ( - - ligne ) ) ; }
. detail - tete { display : flex ; align - items : center ; gap : 10 px ; padding : 14 px 16 px 12 px ; border - bottom : 1 px solid var ( - - ligne ) ; flex - wrap : wrap ; }
. detail - nom { flex : 1 1 150 px ; min - width : 0 ; }
. zone - hint { display : inline - block ; margin : 4 px 0 0 8 px ; font - size : 11 px ; font - weight : 700 ; color : var ( - - teal ) ; }
. stats { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 64 px , 1 fr ) ) ; gap : 8 px ; padding : 14 px 16 px ; }
. stat { background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; padding : 9 px 10 px ; min - width : 0 ; }
. stat . v { font - size : 18 px ; font - weight : 800 ; line - height : 1.1 ; overflow : hidden ; text - overflow : ellipsis ; white - space : nowrap ; }
. stat . v . vide { color : var ( - - faint ) ; }
. stat . l { font - size : 9.5 px ; text - transform : uppercase ; letter - spacing : .06 em ; color : var ( - - faint ) ; font - weight : 800 ; margin - top : 3 px ; }
. grille { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 135 px , 1 fr ) ) ; gap : 11 px 12 px ; }
. champ { display : flex ; flex - direction : column ; gap : 4 px ; min - width : 0 ; }
. champ > span { color : var ( - - muted ) ; font - size : 11 px ; font - weight : 700 ; }
. champ . unite { color : var ( - - faint ) ; font - weight : 600 ; }
2026-06-30 10:07:17 -04:00
. champ > span . a - aide { cursor : help ; border - bottom : 1 px dotted var ( - - ligne - forte ) ; width : fit - content ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
. aide - modele { margin : 0 2 px 14 px ; background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - left : 3 px solid var ( - - bleu ) ; border - radius : var ( - - rayon - sm ) ; }
. aide - modele > summary { cursor : pointer ; padding : 10 px 13 px ; font - size : 12.5 px ; font - weight : 750 ; color : var ( - - bleu ) ; list - style : none ; }
. aide - modele > summary : : - webkit - details - marker { display : none ; }
. aide - modele > summary : : before { content : " ⓘ " ; }
. aide - corps { padding : 2 px 15 px 12 px ; color : var ( - - muted ) ; font - size : 12.5 px ; line - height : 1.55 ; }
. aide - corps p { margin : 6 px 0 ; } . aide - corps ul { margin : 6 px 0 ; padding - left : 18 px ; } . aide - corps li { margin : 3 px 0 ; }
. aide - corps b { color : var ( - - texte ) ; }
. aide - ro { border - top : 1 px solid var ( - - ligne ) ; padding - top : 8 px ; }
2026-06-30 12:19:26 -04:00
. detail - nom - txt { font - weight : 800 ; font - size : 17 px ; padding : 4 px 7 px ; display : inline - block ; overflow - wrap : anywhere ; }
2026-06-30 10:07:17 -04:00
#infobulle {
position : fixed ; z - index : 200 ; max - width : 260 px ; pointer - events : none ;
background : var ( - - surface - 3 ) ; color : var ( - - texte ) ;
border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon - sm ) ;
padding : 7 px 10 px ; font - size : 12 px ; font - weight : 600 ; line - height : 1.4 ;
box - shadow : var ( - - ombre ) ; opacity : 0 ; transform : translateY ( 2 px ) ;
transition : opacity .12 s ease , transform .12 s ease ;
}
#infobulle.on { opacity: 1; transform: none; }
2026-06-24 20:17:46 -04:00
. pied { padding : 12 px 16 px ; border - top : 1 px solid var ( - - ligne ) ; display : flex ; gap : 8 px ; align - items : center ; }
. pied . grandit { flex : 1 ; }
. pied . hint - dep { color : var ( - - faint ) ; font - size : 12 px ; font - style : italic ; }
. dep - bloc { margin - top : 4 px ; }
. dep - bloc > summary { list - style : none ; cursor : pointer ; padding : 10 px 13 px ; display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; font - weight : 800 ; font - size : 12.5 px ; background : var ( - - surface ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; }
. dep - bloc > summary : : - webkit - details - marker { display : none ; }
. dep - bloc > summary : : after { content : " ▾ " ; color : var ( - - faint ) ; }
. dep - bloc [ open ] > summary { border - radius : var ( - - rayon - sm ) var ( - - rayon - sm ) 0 0 ; }
. dep - corps { border : 1 px solid var ( - - ligne ) ; border - top : none ; border - radius : 0 0 var ( - - rayon - sm ) var ( - - rayon - sm ) ; padding : 11 px ; display : grid ; grid - template - columns : repeat ( auto - fill , minmax ( 220 px , 1 fr ) ) ; gap : 8 px ; }
. carte - dep { border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; padding : 9 px 10 px ; background : var ( - - surface - 2 ) ; }
. carte - dep . ok { border - color : color - mix ( in srgb , var ( - - vert ) 45 % , var ( - - ligne ) ) ; }
. carte - dep . attente { border - color : color - mix ( in srgb , var ( - - ambre ) 45 % , var ( - - ligne ) ) ; }
. carte - dep . bloque { border - color : color - mix ( in srgb , var ( - - rouge ) 50 % , var ( - - ligne ) ) ; }
. carte - dep . haut { display : flex ; align - items : center ; justify - content : space - between ; gap : 8 px ; margin - bottom : 4 px ; }
. carte - dep . nom { font - weight : 800 ; font - size : 12 px ; overflow - wrap : anywhere ; }
. carte - dep . txt { color : var ( - - muted ) ; font - size : 11 px ; line - height : 1.4 ; }
. vide { color : var ( - - muted ) ; padding : 40 px 24 px ; text - align : center ; }
. vide b { color : var ( - - texte ) ; display : block ; margin - bottom : 4 px ; font - size : 15 px ; }
. vue - bascule { display : inline - flex ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : 999 px ; padding : 2 px ; background : var ( - - surface - 3 ) ; gap : 2 px ; }
. vue - bascule button { border : none ; background : transparent ; border - radius : 999 px ; padding : 6 px 13 px ; font - size : 12.5 px ; font - weight : 750 ; color : var ( - - muted ) ; box - shadow : none ; }
. vue - bascule button : hover { color : var ( - - texte ) ; background : transparent ; }
. vue - bascule button . on { background : var ( - - surface ) ; color : var ( - - teal ) ; box - shadow : var ( - - ombre ) ; }
. mono { font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; color : var ( - - teal ) ; font - size : 12 px ; }
. ch - stub { color : var ( - - ambre ) ; font - size : 11.5 px ; font - style : italic ; margin - top : 5 px ; }
. ch - bases { display : grid ; gap : 4 px ; margin : 6 px 0 0 23 px ; }
. ch - base { font - size : 11.5 px ; color : var ( - - muted ) ; overflow - wrap : anywhere ; }
. badge - bd { display : inline - block ; font - size : 10 px ; font - weight : 800 ; padding : 1 px 6 px ; border - radius : 999 px ; background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
. modale { position : fixed ; inset : 0 ; z - index : 50 ; display : flex ; align - items : center ; justify - content : center ; background : rgba ( 0 , 0 , 0 , .55 ) ; }
. modale [ hidden ] { display : none ; }
. modale - boite { background : var ( - - surface ) ; border : 1 px solid var ( - - ligne - forte ) ; border - radius : var ( - - rayon ) ; box - shadow : var ( - - ombre - forte ) ; padding : 18 px 20 px ; width : min ( 440 px , 92 vw ) ; display : grid ; gap : 12 px ; }
. modale - titre { font - weight : 800 ; font - size : 15 px ; }
. modale - texte { color : var ( - - muted ) ; font - size : 13 px ; line - height : 1.5 ; }
. modale - actions { display : flex ; justify - content : flex - end ; gap : 8 px ; margin - top : 4 px ; }
2026-06-30 10:07:17 -04:00
. modale - boite . large { width : min ( 760 px , 94 vw ) ; max - height : 88 vh ; overflow : auto ; }
. intrants - corps { display : grid ; gap : 16 px ; }
. intr - section { display : grid ; gap : 9 px ; }
. intr - tete { font - weight : 800 ; font - size : 13 px ; color : var ( - - bleu ) ; border - bottom : 1 px solid var ( - - ligne ) ; padding - bottom : 5 px ; }
. intr - grille { display : grid ; grid - template - columns : repeat ( auto - fit , minmax ( 220 px , 1 fr ) ) ; gap : 11 px ; }
. badge - classe { display : inline - block ; font - size : 9.5 px ; font - weight : 800 ; letter - spacing : .04 em ; text - transform : uppercase ; padding : 1 px 6 px ; border - radius : 999 px ; margin - left : 6 px ; vertical - align : middle ; }
. badge - classe . const { background : color - mix ( in srgb , var ( - - ambre ) 20 % , transparent ) ; color : var ( - - ambre ) ; }
. badge - classe . def { background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
2026-06-30 12:36:51 -04:00
. badge - classe . cat { background : color - mix ( in srgb , var ( - - teal ) 20 % , transparent ) ; color : var ( - - teal ) ; }
2026-06-30 16:20:18 -04:00
. badge - prod { display : inline - flex ; align - items : center ; font - size : 11 px ; font - weight : 800 ; letter - spacing : .06 em ; padding : 3 px 9 px ; border - radius : 999 px ; background : var ( - - rouge ) ; color : #fff; }
. badge - lab { display : inline - flex ; align - items : center ; font - size : 11 px ; font - weight : 800 ; letter - spacing : .04 em ; padding : 3 px 9 px ; border - radius : 999 px ; background : color - mix ( in srgb , var ( - - teal ) 22 % , transparent ) ; color : var ( - - teal ) ; }
2026-06-30 12:36:51 -04:00
. integ - cases { display : flex ; flex - wrap : wrap ; gap : 6 px 12 px ; padding : 4 px 2 px ; }
. integ - case { display : inline - flex ; align - items : center ; gap : 5 px ; font - size : 12 px ; font - weight : 600 ; color : var ( - - texte ) ; cursor : pointer ; }
. integ - case input { width : 14 px ; height : 14 px ; flex : none ; accent - color : var ( - - teal ) ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
. lien - objet { background : none ; border : 0 ; padding : 0 ; color : var ( - - bleu ) ; font : inherit ; font - weight : 700 ; cursor : pointer ; text - decoration : underline dotted ; }
. lien - objet : hover { color : var ( - - teal ) ; }
2026-06-30 10:07:17 -04:00
. intr - ro { color : var ( - - muted ) ; font - size : 12 px ; line - height : 1.55 ; }
. intr - ro b { color : var ( - - texte ) ; font - weight : 700 ; }
. intr - secrets { display : flex ; flex - wrap : wrap ; gap : 6 px ; margin - top : 6 px ; }
. intr - secrets code { font - size : 11 px ; background : var ( - - surface - 2 ) ; border : 1 px solid var ( - - ligne ) ; border - radius : var ( - - rayon - sm ) ; padding : 1 px 6 px ; color : var ( - - muted ) ; }
2026-06-24 20:17:46 -04:00
. console { position : fixed ; left : 0 ; right : 0 ; bottom : 0 ; height : 44 vh ; background : var ( - - surface ) ; border - top : 1 px solid var ( - - ligne - forte ) ; box - shadow : 0 - 16 px 50 px - 18 px rgba ( 0 , 0 , 0 , .8 ) ; z - index : 40 ; display : flex ; flex - direction : column ; }
. console [ hidden ] { display : none ; }
. console - tete { display : flex ; align - items : center ; gap : 12 px ; padding : 9 px 16 px ; border - bottom : 1 px solid var ( - - ligne ) ; background : var ( - - surface - 2 ) ; }
. console - titre { font - weight : 800 ; font - size : 13 px ; }
. console - etat { font - size : 12 px ; font - weight : 800 ; }
. console - etat . actif { color : var ( - - bleu ) ; animation : pulse 1.2 s ease - in - out infinite ; }
. console - etat . ok { color : var ( - - vert ) ; } . console - etat . echec { color : var ( - - rouge ) ; }
@keyframes pulse { 50 % { opacity : .4 ; } }
. console pre { margin : 0 ; padding : 12 px 16 px ; overflow : auto ; flex : 1 ; font - family : ui - monospace , SFMono - Regular , Menlo , monospace ; font - size : 12 px ; line - height : 1.45 ; white - space : pre - wrap ; word - break : break - word ; color : #cdd6e6; }
@media ( max - width : 1000 px ) {
main { grid - template - columns : 1 fr ; }
aside { position : static ; max - height : none ; order : - 1 ; }
}
2026-07-07 11:34:55 -04:00
/ * Vues lecture seule : Flux + Couches * /
. tbl - lecture { width : 100 % ; border - collapse : collapse ; font - size : 12.5 px ; }
. tbl - lecture th { text - align : left ; padding : 7 px 10 px ; color : var ( - - muted ) ; font - weight : 750 ; border - bottom : 1 px solid var ( - - ligne - forte ) ; position : sticky ; top : 0 ; background : var ( - - surface ) ; }
. tbl - lecture td { padding : 6 px 10 px ; border - bottom : 1 px solid var ( - - ligne ) ; vertical - align : top ; }
. tbl - lecture tr : hover td { background : var ( - - surface - 2 ) ; }
. flux - raison { color : var ( - - muted ) ; max - width : 44 ch ; }
. sens { font - size : 11 px ; font - weight : 750 ; padding : 1 px 7 px ; border - radius : 999 px ; }
. sens - ingress { background : color - mix ( in srgb , var ( - - bleu ) 16 % , transparent ) ; color : var ( - - bleu ) ; }
. sens - egress { background : color - mix ( in srgb , var ( - - ambre ) 18 % , transparent ) ; color : var ( - - ambre ) ; }
. chip . chi - tls - requis { background : color - mix ( in srgb , var ( - - teal ) 22 % , transparent ) ; color : var ( - - teal ) ; }
. chip . chi - tls { background : color - mix ( in srgb , var ( - - teal ) 13 % , transparent ) ; color : var ( - - teal ) ; }
. chip . chi - starttls { background : color - mix ( in srgb , var ( - - ambre ) 18 % , transparent ) ; color : var ( - - ambre ) ; }
. chip . chi - ssh { background : color - mix ( in srgb , var ( - - bleu ) 18 % , transparent ) ; color : var ( - - bleu ) ; }
. chip . chi - tls - cible { background : color - mix ( in srgb , var ( - - ambre ) 12 % , transparent ) ; color : var ( - - muted ) ; }
. chip . chi - clair { background : var ( - - surface - 3 ) ; color : var ( - - muted ) ; }
. couches - pile { display : flex ; flex - direction : column ; gap : 0 ; padding : 6 px ; }
. couche { border : 1 px solid var ( - - ligne - forte ) ; border - radius : 10 px ; padding : 9 px 12 px ; background : var ( - - surface - 2 ) ; }
. couche - tete { display : flex ; align - items : center ; gap : 8 px ; margin - bottom : 6 px ; }
. couche - idx { display : inline - flex ; width : 20 px ; height : 20 px ; align - items : center ; justify - content : center ; border - radius : 999 px ; background : var ( - - teal ) ; color : var ( - - surface ) ; font - size : 11 px ; font - weight : 800 ; }
. couche - groupes { display : flex ; flex - wrap : wrap ; gap : 5 px ; }
. couche - fleche { text - align : center ; color : var ( - - muted ) ; font - size : 14 px ; line - height : 1.5 ; }
. legende { display : flex ; flex - direction : column ; gap : 6 px ; font - size : 12 px ; color : var ( - - muted ) ; }
. legende . chip { margin - right : 4 px ; }
2026-07-07 13:31:02 -04:00
/ * Résumé d ' erreur en langage clair (console) */
#console-resume { padding: 12px 16px; background: color-mix(in srgb, var(--rouge) 10%, var(--surface)); border-bottom: 1px solid var(--ligne); }
. re - tete { display : flex ; align - items : center ; gap : 10 px ; font - weight : 800 ; color : var ( - - rouge ) ; font - size : 13 px ; margin - bottom : 6 px ; }
. re - tete button { margin - left : auto ; }
. re - liste { margin : 0 ; padding - left : 18 px ; display : flex ; flex - direction : column ; gap : 7 px ; }
. re - liste li { font - size : 12.5 px ; }
. re - hote { color : var ( - - teal ) ; font - weight : 700 ; }
. re - type { color : var ( - - muted ) ; font - size : 11 px ; }
. re - msg { background : var ( - - surface - 3 ) ; border - radius : 6 px ; padding : 4 px 8 px ; margin - top : 3 px ; font - family : ui - monospace , monospace ; font - size : 11.5 px ; white - space : pre - wrap ; word - break : break - word ; }
2026-07-07 15:57:47 -04:00
/ * Devis réseau ( config switch ) * /
. devis - reseau { margin : 8 px 0 0 ; padding : 12 px 14 px ; background : var ( - - surface - 3 ) ; border - radius : 8 px ; font - family : ui - monospace , monospace ; font - size : 11.5 px ; line - height : 1.5 ; white - space : pre ; overflow : auto ; max - height : calc ( 100 vh - 210 px ) ; }
2026-06-24 20:17:46 -04:00
< / style >
< / head >
< body >
2026-06-30 12:19:26 -04:00
< div class = " toasts " id = " toasts " aria - live = " polite " > < / div >
2026-06-24 20:17:46 -04:00
< header >
< div class = " marque " > < span class = " pastille " > < / span > < div class = " titre " > < h1 > Set - OPS < / h1 > < span class = " slogan " > Votre artisan numérique < / span > < / div > < / div >
< span class = " chemin " id = " chemin " > < / span >
< div class = " grandit " > < / div >
< div class = " resume " id = " resume " > < / div >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< div class = " barre " title = " Raccourcis clavier — 1-3 : changer de vue · j/k : naviguer les serveurs · v : vérifier · d : déployer · / : filtrer · Ctrl+S : sauvegarder la vue " >
2026-06-24 20:17:46 -04:00
< div class = " vue-bascule " >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< button type = " button " id = " btn-vue-serveurs " class = " on " onclick = " setVue( ' serveurs ' ) " > Serveurs < / button >
2026-06-24 20:17:46 -04:00
< button type = " button " id = " btn-vue-applications " onclick = " setVue( ' applications ' ) " > Applications < / button >
< button type = " button " id = " btn-vue-bases " onclick = " setVue( ' bases ' ) " > Bases < / button >
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
< button type = " button " id = " btn-vue-domaines " onclick = " setVue( ' domaines ' ) " > Domaines < / button >
2026-07-07 11:34:55 -04:00
< button type = " button " id = " btn-vue-flux " onclick = " setVue( ' flux ' ) " > Flux < / button >
< button type = " button " id = " btn-vue-couches " onclick = " setVue( ' couches ' ) " > Couches < / button >
2026-07-07 15:57:47 -04:00
< button type = " button " id = " btn-vue-reseau " onclick = " setVue( ' reseau ' ) " > Réseau < / button >
2026-06-24 20:17:46 -04:00
< / div >
< span class = " recherche " > < input id = " filtre " type = " search " placeholder = " Filtrer… " oninput = " filtrer(this.value) " > < / span >
2026-06-30 10:07:17 -04:00
< button type = " button " class = " fantome " onclick = " ouvrirIntrants() " > ⚙ Intrants < / button >
2026-06-24 20:17:46 -04:00
< button type = " button " class = " fantome " onclick = " charger() " > Recharger < / button >
2026-06-30 12:19:26 -04:00
< button type = " button " class = " primaire " id = " btn-sauver " onclick = " sauvegarderVue() " > Sauvegarder < / button >
2026-06-24 20:17:46 -04:00
< / div >
< / header >
< main >
< section >
< div id = " grilles " > < / div >
< details class = " dep-bloc " id = " panneau-dep " >
< summary > Dépendances causales < span id = " dep-resume " class = " chip " > < / span > < / summary >
< div class = " dep-corps " id = " dependances " > < / div >
< / details >
< / section >
< aside > < div id = " detail " > < / div > < / aside >
< / main >
< div class = " modale " id = " modale " hidden >
< div class = " modale-boite " >
< div class = " modale-titre " id = " modale-titre " > < / div >
< div class = " modale-texte " id = " modale-texte " > < / div >
< label class = " champ " > < span > Mot de passe du vault Ansible ( laisser vide si non requis ) < / span >
< input type = " password " id = " modale-vault " autocomplete = " off " placeholder = " •••••••• " onkeydown = " if(event.key=== ' Enter ' )confirmerModale() " > < / label >
< div class = " modale-actions " >
< button type = " button " onclick = " annulerModale() " > Annuler < / button >
< button type = " button " class = " primaire " onclick = " confirmerModale() " > Confirmer < / button >
< / div >
< / div >
< / div >
2026-06-30 10:07:17 -04:00
< div class = " modale " id = " intrants " hidden >
< div class = " modale-boite large " >
< div class = " modale-titre " > ⚙ Intrants de base de l ' écosystème</div>
< div class = " modale-texte " > Saisis ici les valeurs communes à tout l ' écosystème, depuis un endroit unique.
Les < b > constantes < / b > sont une valeur unique non surchargeable & nbsp ; ; les < b > défauts < / b > resurgissent
comme valeurs proposées là où ils réapparaissent dans les instances déclarées .
Les secrets ne sont jamais saisis ici & nbsp ; : ils restent dans le Vault Ansible ( CLI ) . < / div >
< div class = " intrants-corps " id = " intrants-corps " > < / div >
< div class = " modale-actions " >
< button type = " button " onclick = " fermerIntrants() " > Fermer < / button >
< button type = " button " class = " primaire " onclick = " enregistrerIntrants() " > Enregistrer < / button >
< / div >
< / div >
< / div >
2026-06-24 20:17:46 -04:00
< div class = " console " id = " console " hidden >
< div class = " console-tete " >
< span class = " console-titre " id = " console-titre " > < / span >
< span class = " console-etat " id = " console-etat " > < / span >
< span class = " grandit " > < / span >
< button type = " button " id = " btn-fermer-console " onclick = " fermerConsole() " > Fermer < / button >
< / div >
2026-07-07 13:31:02 -04:00
< div id = " console-resume " hidden > < / div >
2026-06-24 20:17:46 -04:00
< pre id = " console-sortie " > < / pre >
< / div >
< script >
const JETON = " __JETON__ " ;
let groupes = [ ] ;
let dependances = { } ;
let nomenclature = { } ;
let chaine = { } ;
let hotes = [ ] ;
let estProduction = false ;
let filtre = ' ' ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
let vuePrincipale = ' serveurs ' ;
2026-07-07 11:34:55 -04:00
let fluxRegistre = [ ] ; / / matrice des flux réseau ( lecture seule )
let couchesRegistre = [ ] ; / / ordre de déploiement en couches ( lecture seule )
2026-06-24 20:17:46 -04:00
let bdServeurs = [ ] ;
let bdApplis = [ ] ;
let basesModifie = false ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
let selApp = null ; / / index de l ' application sélectionnée (vue Applications)
let selServeur = null ; / / index du serveur sélectionné ( vue Serveurs , maître - détail fusionné )
let selBase = null ; / / clé de l ' item sélectionné (vue Bases) : " srv:<i> " ou " bd:<i> "
let hoteOpCourant = ' ' ; / / nom de l ' hôte dont les boutons Vérifier/Déployer sont affichés
2026-06-24 20:17:46 -04:00
let applications = [ ] ;
let applicationsModifie = false ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
let domaines = [ ] ; / / registre des domaines ( éditable )
let selDomaine = null ; / / index du domaine sélectionné
let domainesModifie = false ;
const AUTORITES_DNS = [ ' primaire-cache ' , ' auto-heberge ' , ' delegue ' ] ;
2026-06-24 20:17:46 -04:00
let serveurs = [ ] ;
let serveursModifie = false ;
let ongletActif = ' reseau ' ;
let occupe = false ;
let verifie = { } ;
let motVault = ' ' ;
let modaleResolveur = null ;
2026-06-30 10:07:17 -04:00
let intrants = null ;
2026-06-30 12:36:51 -04:00
let integrationsDispo = [ ] ;
2026-07-22 21:32:42 -04:00
let liensAcceptes = { } ;
2026-07-01 17:39:44 -04:00
let sondes = { } ; / / état RÉEL par hôte : joignable ( SSH ) ou non — distinct du plan
2026-06-30 12:19:26 -04:00
let chronoConsole = null ;
let consoleDebut = 0 ;
2026-06-24 20:17:46 -04:00
2026-06-30 12:19:26 -04:00
let toastSeq = 0 ;
function message ( texte , type = ' ' , opts = { } ) {
const conteneur = document . getElementById ( ' toasts ' ) ;
if ( ! conteneur ) return null ;
const t = document . createElement ( ' div ' ) ;
t . className = ' toast ' + ( type ? ' ' + type : ' ' ) ;
t . id = ' toast- ' + ( + + toastSeq ) ;
const corps = document . createElement ( ' span ' ) ;
corps . textContent = texte ;
t . appendChild ( corps ) ;
const x = document . createElement ( ' button ' ) ;
x . type = ' button ' ; x . className = ' x ' ; x . textContent = ' × ' ;
x . onclick = ( ) = > retirerToast ( t ) ;
t . appendChild ( x ) ;
conteneur . appendChild ( t ) ;
while ( conteneur . children . length > 6 ) conteneur . removeChild ( conteneur . firstChild ) ;
const duree = opts . persistant ? 0 : ( opts . duree != null ? opts . duree : ( type == = ' erreur ' ? 7000 : 3500 ) ) ;
if ( duree > 0 ) setTimeout ( ( ) = > retirerToast ( t ) , duree ) ;
return t ; / / renvoie le nœud pour permettre de le mettre à jour ( opérations longues )
}
function majToast ( t , texte , type , dureeFin ) {
if ( ! t ) return ;
if ( type != = undefined ) t . className = ' toast ' + ( type ? ' ' + type : ' ' ) ;
const corps = t . querySelector ( ' span ' ) ; if ( corps ) corps . textContent = texte ;
if ( dureeFin ) setTimeout ( ( ) = > retirerToast ( t ) , dureeFin ) ;
}
function retirerToast ( t ) {
if ( ! t | | ! t . parentNode ) return ;
t . classList . add ( ' sortant ' ) ;
setTimeout ( ( ) = > { if ( t . parentNode ) t . parentNode . removeChild ( t ) ; } , 260 ) ;
2026-06-24 20:17:46 -04:00
}
async function charger ( ) {
const reponse = await fetch ( ' /api/inventaire ' ) ;
const data = await reponse . json ( ) ;
groupes = data . groupes ;
dependances = data . dependances | | { } ;
nomenclature = data . nomenclature | | { } ;
chaine = data . chaine | | { } ;
2026-07-07 11:34:55 -04:00
fluxRegistre = data . flux | | [ ] ;
couchesRegistre = data . couches | | [ ] ;
2026-06-30 10:07:17 -04:00
intrants = data . intrants | | null ;
2026-06-30 12:36:51 -04:00
integrationsDispo = data . integrations_disponibles | | [ ] ;
2026-07-22 21:32:42 -04:00
liensAcceptes = data . liens_acceptes | | { } ;
2026-06-24 20:17:46 -04:00
chargerBases ( data ) ;
chargerApplications ( data ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
chargerDomaines ( data ) ;
2026-06-24 20:17:46 -04:00
chargerServeurs ( data ) ;
hotes = data . hotes ;
estProduction = ! ! data . production ;
2026-06-30 12:19:26 -04:00
verifie = { } ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' chemin ' ) . textContent = data . inventaire ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( selServeur == null | | selServeur > = serveurs . length ) selServeur = serveurs . length ? 0 : null ;
2026-06-24 20:17:46 -04:00
dessiner ( ) ;
message ( ' Inventaire chargé. ' , ' ok ' ) ;
2026-07-01 17:39:44 -04:00
sonder ( ) ; / / état réel ( joignabilité ) en arrière - plan , non bloquant
}
async function sonder ( ) {
try {
const r = await fetch ( ' /api/sondes ' ) ;
if ( r . ok ) { sondes = await r . json ( ) ; dessiner ( ) ; }
} catch ( e ) { / * silencieux * / }
2026-06-24 20:17:46 -04:00
}
function setOnglet ( o ) { ongletActif = o ; dessinerDetail ( ) ; }
function filtrer ( valeur ) { filtre = ( valeur | | ' ' ) . trim ( ) . toLowerCase ( ) ; dessinerGrilles ( ) ; }
function groupesActifs ( ) { const a = new Set ( ) ; hotes . forEach ( h = > { if ( h . etat == = ' actif ' ) ( h . groupes | | [ ] ) . forEach ( g = > a . add ( g ) ) ; } ) ; return a ; }
function groupesPlanifies ( ) { const p = new Set ( ) ; hotes . forEach ( h = > ( h . groupes | | [ ] ) . forEach ( g = > p . add ( g ) ) ) ; return p ; }
function statutDependance ( groupe ) {
const requis = ( ( dependances [ groupe ] | | { } ) . requiert_groupes_actifs | | [ ] ) ;
if ( ! requis . length ) return ' neutre ' ;
const actifs = groupesActifs ( ) , planifies = groupesPlanifies ( ) ;
const manquants = requis . filter ( i = > ! actifs . has ( i ) ) ;
if ( ! manquants . length ) return ' ok ' ;
if ( manquants . every ( i = > planifies . has ( i ) ) ) return ' attente ' ;
return ' bloque ' ;
}
function manquantsPourGroupe ( groupe ) { const a = groupesActifs ( ) ; return ( ( dependances [ groupe ] | | { } ) . requiert_groupes_actifs | | [ ] ) . filter ( i = > ! a . has ( i ) ) ; }
function hoteBloque ( h ) { if ( ! h | | h . etat != = ' actif ' ) return false ; return ( h . groupes | | [ ] ) . some ( g = > manquantsPourGroupe ( g ) . length ) ; }
function libelleStatut ( s ) { return { ok : ' actif ' , attente : ' planifié ' , bloque : ' bloqué ' , neutre : ' libre ' } [ s ] | | s ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function dessiner ( ) { dessinerResume ( ) ; dessinerGrilles ( ) ; dessinerDependances ( ) ; dessinerDetail ( ) ; majIndicateurSauvegarde ( ) ; }
2026-06-24 20:17:46 -04:00
function majIndicateurSauvegarde ( ) {
const b = document . getElementById ( ' btn-sauver ' ) ;
2026-06-30 12:19:26 -04:00
if ( ! b ) return ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
const editable = vuePrincipale == = ' serveurs ' | | vuePrincipale == = ' applications ' | | vuePrincipale == = ' bases ' | | vuePrincipale == = ' domaines ' ;
2026-06-30 12:19:26 -04:00
const sale = ( vuePrincipale == = ' serveurs ' & & serveursModifie )
| | ( vuePrincipale == = ' applications ' & & applicationsModifie )
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
| | ( vuePrincipale == = ' bases ' & & basesModifie )
| | ( vuePrincipale == = ' domaines ' & & domainesModifie ) ;
2026-06-30 12:19:26 -04:00
b . classList . toggle ( ' attention ' , sale ) ;
b . textContent = sale ? ' Sauvegarder • ' : ' Sauvegarder ' ;
b . disabled = ! editable ;
b . title = editable ? ' Sauvegarder cette vue (Ctrl+S) ' : ' Lecture seule — édite dans Serveurs / Applications / Bases ' ;
2026-06-24 20:17:46 -04:00
}
function dessinerResume ( ) {
const actifs = hotes . filter ( h = > h . etat == = ' actif ' ) . length ;
const bloques = hotes . filter ( h = > hoteBloque ( h ) ) . length ;
2026-07-01 17:52:39 -04:00
const vivantes = hotes . filter ( h = > sondes [ h . nom ] == = true ) . length ;
const sonde = Object . keys ( sondes ) . length > 0 ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' resume ' ) . innerHTML = `
2026-06-30 16:20:18 -04:00
$ { 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> ' }
2026-06-24 20:17:46 -04:00
< 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 plan " > < / span > < b > $ { hotes . length - actifs } < / b > planifiés < / span >
2026-07-01 17:52:39 -04:00
$ { 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 > ` : ' ' } ` ;
2026-06-24 20:17:46 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function correspondServeur ( s ) {
if ( ! filtre ) return true ;
return [ s . nom , s . fonction , s . adresse_ip , s . vmid , s . etat ] . join ( ' ' ) . toLowerCase ( ) . includes ( filtre ) ;
2026-06-24 20:17:46 -04:00
}
function setVue ( v ) {
vuePrincipale = v ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( v == = ' serveurs ' & & selServeur == null & & serveurs . length ) selServeur = 0 ;
if ( v == = ' applications ' & & selApp == null & & applications . length ) selApp = 0 ;
if ( v == = ' bases ' & & selBase == null ) selBase = bdServeurs . length ? ' srv:0 ' : ( bdApplis . length ? ' bd:0 ' : null ) ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' btn-vue-serveurs ' ) . classList . toggle ( ' on ' , v == = ' serveurs ' ) ;
document . getElementById ( ' btn-vue-applications ' ) . classList . toggle ( ' on ' , v == = ' applications ' ) ;
document . getElementById ( ' btn-vue-bases ' ) . classList . toggle ( ' on ' , v == = ' bases ' ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
document . getElementById ( ' btn-vue-domaines ' ) . classList . toggle ( ' on ' , v == = ' domaines ' ) ;
2026-07-07 11:34:55 -04:00
document . getElementById ( ' btn-vue-flux ' ) . classList . toggle ( ' on ' , v == = ' flux ' ) ;
document . getElementById ( ' btn-vue-couches ' ) . classList . toggle ( ' on ' , v == = ' couches ' ) ;
2026-07-07 15:57:47 -04:00
document . getElementById ( ' btn-vue-reseau ' ) . classList . toggle ( ' on ' , v == = ' reseau ' ) ;
2026-06-24 20:17:46 -04:00
dessinerGrilles ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
dessinerDetail ( ) ; / / le panneau droit reflète la sélection de la vue courante
2026-06-30 12:19:26 -04:00
majIndicateurSauvegarde ( ) ;
2026-06-24 20:17:46 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const BANNIERE_MODELE = ` < details class = " aide-modele " >
< summary > Comment lire ce parc — serveur · hôte · groupe · application · base < / summary >
< div class = " aide-corps " >
< p > < b > Serveur < / b > ( ce que tu écris ici ) → < b > « Appliquer le plan » < / b > → < b > Hôte < / b > ( la VM générée ) . C ' est le même objet, à deux étapes.</p>
< ul >
< li > < b > Serveur / Hôte < / b > — une VM ; elle < b > appartient à des groupes < / b > . < / li >
< li > < b > Groupe < / b > — un rôle appliqué à l ' hôte (ex. <span class= " mono " >serveur_nginx</span>, <span class= " mono " >client_dns</span>).</li>
< li > < b > Application < / b > — un service qui < b > tourne sur < / b > un hôte , < b > via < / b > un groupe , et < b > expose < / b > un FQDN . < / li >
< li > < b > Base < / b > — une donnée < b > hébergée sur < / b > un hôte et < b > consommée par < / b > une application . < / li >
< / ul >
< p class = " aide-ro " > Saisis à < b > gauche < / b > ( tuiles ) , édite à < b > droite < / b > ; les dérivés ( VMID / IP / VLAN / groupes ) se calculent à « Appliquer le plan » . < / p >
< / div > < / details > ` ;
2026-06-24 20:17:46 -04:00
function dessinerGrilles ( ) {
if ( vuePrincipale == = ' applications ' ) { dessinerApplications ( ) ; return ; }
if ( vuePrincipale == = ' bases ' ) { dessinerBases ( ) ; return ; }
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( vuePrincipale == = ' domaines ' ) { dessinerDomaines ( ) ; return ; }
2026-07-07 11:34:55 -04:00
if ( vuePrincipale == = ' flux ' ) { dessinerFlux ( ) ; return ; }
if ( vuePrincipale == = ' couches ' ) { dessinerCouches ( ) ; return ; }
2026-07-07 15:57:47 -04:00
if ( vuePrincipale == = ' reseau ' ) { dessinerReseau ( ) ; return ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
dessinerServeurs ( ) ;
2026-06-24 20:17:46 -04:00
}
2026-07-07 11:34:55 -04:00
function legendeLecture ( ) {
const d = document . getElementById ( ' detail ' ) ;
if ( vuePrincipale == = ' flux ' ) {
d . innerHTML = ` < div class = " detail " > < div class = " section-tete " > Chiffrement < / div >
< div class = " legende " >
< div > < span class = " chip chi-tls-requis " > tls - requis < / span > chiffré + pair vérifié < / div >
< div > < span class = " chip chi-tls " > tls < / span > chiffré < / div >
< div > < span class = " chip chi-starttls " > starttls < / span > mise à niveau opportuniste < / div >
< div > < span class = " chip chi-ssh " > ssh < / span > transport SSH < / div >
< div > < span class = " chip chi-tls-cible " > tls - cible < / span > TLS visé ( roadmap ) < / div >
< div > < span class = " chip chi-clair " > clair < / span > non chiffré ( local / edge ) < / div > < / div >
< div class = " hint-dep " style = " margin-top:10px " > Lecture seule . Éditer les < code > meta / flux . yml < / code > des rôles , puis < code > make flux < / code > . < / div > < / div > ` ;
2026-07-07 15:57:47 -04:00
} else if ( vuePrincipale == = ' reseau ' ) {
Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
d . innerHTML = ` < div class = " detail " > < div class = " section-tete " > Réseau & amp ; fédération < / div >
< div class = " hint-dep " > Chaque instance fédérée dérive son adressage de son seed < code > index < / code > ( VLAN = 1000 + index × 10 + zone , unique sur le trunk ) . La vue liste la flotte , signale toute collision d ' index, et génère le devis des switches.</div>
< div class = " hint-dep " style = " margin-top:10px " > Lecture seule , régénéré du plan — < code > make instances < / code > / < code > make devis - reseau < / code > . Le devis est à adapter à ta plateforme ( NX - OS / IOS - XE , HSRP , route par défaut vers OPNsense , port de trunk ) . < / div > < / div > ` ;
2026-07-07 11:34:55 -04:00
} else {
d . innerHTML = ` < div class = " detail " > < div class = " section-tete " > Reconstruction < / div >
< div class = " hint-dep " > L ' ordre part du socle et remonte : chaque couche suppose la précédente debout. Le tri intra-couche vient du graphe des dépendances.</div>
< div class = " hint-dep " style = " margin-top:10px " > Lecture seule . Éditer < code > docs / couches - deploiement . yml < / code > ; généré dans < code > playbooks / site . yml < / code > par < code > make site < / code > . < / div > < / div > ` ;
}
}
function dessinerFlux ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
if ( ! fluxRegistre . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucun flux (meta/flux.yml).</div> ' ; return ; }
const lignes = fluxRegistre . map ( f = > ` < tr >
< td class = " mono " > $ { echapper ( f . role ) } < / td >
< td > < span class = " sens sens-$ { echapper(f.sens)} " > $ { echapper ( f . sens ) } < / span > < / td >
< td class = " mono " > $ { echapper ( f . port ) } / $ { echapper ( f . protocole ) } < / td >
< td class = " mono " > $ { echapper ( f . pair ) } < / td >
< td > < span class = " chip chi-$ { echapper(f.chiffrement)} " > $ { echapper ( f . chiffrement ) } < / span > < / td >
< td class = " flux-raison " > $ { echapper ( f . raison ) } < / td > < / tr > ` ) . join ( ' ' ) ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Flux réseau — matrice d ' audit <span class= " compte " >$ {fluxRegistre.length} </span>
< span class = " hint-dep " style = " margin-left:auto " > ingress = le rôle écoute · egress = il se connecte · source de nftables + audit < / span > < / div >
< table class = " tbl-lecture " > < thead > < tr > < th > Rôle < / th > < th > Sens < / th > < th > Port < / th > < th > Pair < / th > < th > Chiffrement < / th > < th > Raison < / th > < / tr > < / thead >
< tbody > $ { lignes } < / tbody > < / table > < / div > ` ;
}
2026-07-07 15:57:47 -04:00
let devisReseauTexte = ' ' ;
Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
function tableauFlotte ( inst , coll ) {
if ( ! inst . length ) return ' <div class= " vide " style= " padding:14px " >Aucune instance découverte (dépôts frères avec plan/nomenclature.yml).</div> ' ;
const banniere = Object . keys ( coll | | { } ) . length
? ` < div class = " hint-dep " style = " color:var(--danger);border:1px solid var(--danger);border-radius:8px;padding:8px 10px;margin-bottom:8px " > ⚠ COLLISION d ' index entre instances fédérées : $ { Object.entries(coll).map(([i, n]) => ' index ' + i + ' → ' + n.map(echapper).join( ' , ' )).join( ' ; ' )} — mêmes VLAN/VMID sur le trunk.</div>`
: ' ' ;
const lignes = inst . map ( i = > {
const vlan = i . vlans ? ( i . vlans [ 0 ] + ' - ' + i . vlans [ 1 ] ) : ' — ' ;
const fed = i . federe ? ' <span class= " chip " >fédérée</span> ' : ' <span class= " chip " style= " opacity:.6 " >local</span> ' ;
const prod = i . production ? ' <span class= " chip chi-tls-requis " >prod</span> ' : ' ' ;
return ` < tr >
< td class = " mono " > $ { i . actif ? ' ★ ' : ' ' } $ { echapper ( i . nom ) } < / td >
< td class = " mono " > $ { i . index } < / td >
< td class = " mono " > $ { vlan } < / td >
< td > $ { fed } $ { prod } < / td > < / tr > ` ;
} ) . join ( ' ' ) ;
return ` $ { banniere } < table class = " tbl-lecture " > < thead > < tr > < th > Instance < / th > < th > Index < / th > < th > VLAN < / th > < th > Statut < / th > < / tr > < / thead > < tbody > $ { lignes } < / tbody > < / table >
< div class = " hint-dep " style = " margin-top:6px " > ★ = active ( symlink < code > instance < / code > ) . Basculer en CLI : < code > make instance - utiliser NOM = & lt ; dépôt & gt ; < / code > . < / div > ` ;
}
2026-07-07 15:57:47 -04:00
async function dessinerReseau ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Lecture de la fédération…</div> ' ;
2026-07-07 15:57:47 -04:00
try {
Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
const [ ri , rd ] = await Promise . all ( [
fetch ( ' /api/instances ' , { headers : { ' X-Jeton ' : JETON } } ) ,
fetch ( ' /api/devis-reseau ' , { headers : { ' X-Jeton ' : JETON } } ) ,
] ) ;
const di = await ri . json ( ) , data = await rd . json ( ) ;
if ( ! rd . ok ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " > ' + echapper ( data . erreur | | ' Indisponible. ' ) + ' </div> ' ; return ; }
2026-07-07 15:57:47 -04:00
devisReseauTexte = data . config | | ' ' ;
Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
const flotte = ` < div class = " section-grille " > < div class = " section-tete " > Flotte d ' instances <span class= " compte " >$ { (di.instances || []).length}</span></div>$ { tableauFlotte(di.instances || [], di.collisions || {} )}</div>`;
const devis = devisReseauTexte
? ` < div class = " section-grille " style = " margin-top:12px " > < div class = " section-tete " > Devis config réseau — switches ( instances fédérées )
< button type = " button " class = " fantome " style = " margin-left:auto " onclick = " copierDevis() " > Copier < / button > < / div >
< pre class = " devis-reseau " > $ { echapper ( devisReseauTexte ) } < / pre > < / div > `
: ' <div class= " hint-dep " style= " margin-top:12px " >Aucune instance fédérée : pas de devis convergé à générer.</div> ' ;
cible . innerHTML = flotte + devis ;
2026-07-07 15:57:47 -04:00
} catch ( e ) {
cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Erreur : ' + echapper ( e . message ) + ' </div> ' ;
}
}
function copierDevis ( ) {
if ( navigator . clipboard ) navigator . clipboard . writeText ( devisReseauTexte )
. then ( ( ) = > message ( ' Devis réseau copié — prêt à coller sur le switch. ' , ' ok ' ) , ( ) = > { } ) ;
}
2026-07-07 11:34:55 -04:00
function dessinerCouches ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
if ( ! couchesRegistre . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Ordre de déploiement indisponible.</div> ' ; return ; }
const blocs = couchesRegistre . map ( ( c , i ) = > ` < div class = " couche " >
< div class = " couche-tete " > < span class = " couche-idx " > $ { i } < / span > < b > $ { echapper ( c . nom ) } < / b > < span class = " compte " > $ { c . groupes . length } < / span > < / div >
< div class = " couche-groupes " > $ { c . groupes . map ( g = > ` < span class = " chip mono " > $ { echapper ( g ) } < / span > ` ) . join ( ' ' ) } < / div > < / div > `
) . join ( ' <div class= " couche-fleche " >↓</div> ' ) ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Ordre de déploiement — couches < span class = " compte " > $ { couchesRegistre . length } < / span >
< span class = " hint-dep " style = " margin-left:auto " > make deployer - tout / myDay déroule dans cet ordre · tri topo intra - couche < / span > < / div >
< div class = " couches-pile " > $ { blocs } < / div > < / div > ` ;
}
2026-06-24 20:17:46 -04:00
function basesDeApp ( app , hoteNom ) {
return bdApplis . filter ( a = > {
const p = a . portee | | ' groupe ' ;
return ( p == = ' application ' & & a . consommateur == = app . id )
| | ( p == = ' groupe ' & & a . consommateur == = app . groupe )
| | ( p == = ' hote ' & & a . consommateur == = hoteNom ) ;
} ) ;
}
function chargerBases ( data ) {
const b = data . bases | | { serveurs_bd : { } , bases_donnees : { } } ;
bdServeurs = Object . entries ( b . serveurs_bd | | { } ) . map ( ( [ nom , s ] ) = > ( {
nom , type : s . type | | ' postgres ' , hote : s . hote | | ' ' , port : s . port | | 5432 , groupe : s . groupe | | ' ' } ) ) ;
bdApplis = Object . entries ( b . bases_donnees | | { } ) . map ( ( [ cle , x ] ) = > ( {
cle , serveur : x . serveur | | ' ' , base : x . base | | ' ' , proprietaire : x . proprietaire | | ' ' , secret : x . secret | | ' ' ,
consommateur : x . consommateur | | ' ' , portee : x . portee | | ' groupe ' , usage : x . usage | | ' principale ' } ) ) ;
basesModifie = false ;
}
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
function chargerDomaines ( data ) {
const dp = ( ( data . domaines | | { } ) . domaines_publics ) | | { } ;
domaines = Object . entries ( dp ) . map ( ( [ nom , x ] ) = > ( {
nom , autorite : x . autorite | | ' auto-heberge ' , edge : x . edge | | ' serveur_nginx ' ,
dnssec : ! ! x . dnssec , secondaires : ( x . secondaires | | [ ] ) . join ( ' , ' ) , mail : x . mail | | ' ' } ) ) ;
domainesModifie = false ;
}
function marquerDomainesModifie ( ) { domainesModifie = true ; majIndicateurSauvegarde ( ) ; }
function choisirDomaine ( i ) { selDomaine = i ; dessiner ( ) ; }
function definirDomaine ( champ , v ) {
const d = domaines [ selDomaine ] ; if ( ! d ) return ;
d [ champ ] = v ; marquerDomainesModifie ( ) ;
if ( champ == = ' nom ' ) { const t = document . querySelector ( ' #tuile-dom- ' + selDomaine + ' .carte-nom ' ) ; if ( t ) { t . textContent = v | | ' (sans nom) ' ; t . className = ' carte-nom ' + ( v ? ' ' : ' sans ' ) ; } }
else if ( champ == = ' autorite ' | | champ == = ' edge ' ) dessinerDetail ( ) ;
}
function ajouterDomaine ( ) {
const edgeDefaut = groupes . includes ( ' serveur_nginx ' ) ? ' serveur_nginx ' : ( groupes [ 0 ] | | ' ' ) ;
domaines . unshift ( { nom : ' ' , autorite : ' auto-heberge ' , edge : edgeDefaut , dnssec : false , secondaires : ' ' , mail : ' ' } ) ;
selDomaine = 0 ; marquerDomainesModifie ( ) ; dessiner ( ) ;
const c = document . querySelector ( ' #detail .grille input ' ) ; if ( c ) c . focus ( ) ;
}
function retirerDomaine ( ) {
const d = domaines [ selDomaine ] ; if ( ! d ) return ;
if ( d . nom & & ! confirm ( ' Retirer le domaine « ' + d . nom + ' » ? ' ) ) return ;
domaines . splice ( selDomaine , 1 ) ;
selDomaine = domaines . length ? Math . min ( selDomaine , domaines . length - 1 ) : null ;
marquerDomainesModifie ( ) ; dessiner ( ) ;
}
function carteDomaine ( d , i ) {
return ` < button type = " button " class = " carte $ { i === selDomaine ? ' selectionnee ' : ' ' } " id = " tuile-dom-$ {i} " onclick = " choisirDomaine($ {i} ) " >
< div class = " carte-haut " > < span class = " badge-bd " > zone < / span > $ { d . dnssec ? ' <span class= " carte-vmid " >dnssec</span> ' : ' ' } < / div >
< div class = " carte-nom $ { d.nom ? ' ' : ' sans ' } " > $ { echapper ( d . nom | | ' (sans nom) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( d . autorite | | ' — ' ) } < / div >
< / button > ` ;
}
function dessinerDomaines ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
const tuiles = domaines . length
? ` < div class = " grille-cartes " > $ { domaines . map ( ( d , i ) = > carteDomaine ( d , i ) ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Aucun domaine. Une zone DNS = parent des expositions (FQDN) derrière l \' edge.</div> ' ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Domaines < span class = " compte " > $ { domaines . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterDomaine() " > + Domaine < / button > < / div >
$ { tuiles } < / div > ` ;
}
function expositionsSous ( zone ) {
if ( ! zone ) return [ ] ;
return applications . flatMap ( a = > listeDepuisTexte ( a . expose ) ) . filter ( f = > f == = zone | | f . endsWith ( ' . ' + zone ) ) ;
}
function inspecteurDomaine ( ) {
const cible = document . getElementById ( ' detail ' ) ;
if ( selDomaine == null | | ! domaines [ selDomaine ] ) {
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun domaine sélectionné</b>Choisis une tuile, ou ajoute un domaine.</div></div> ' ;
return ;
}
const d = domaines [ selDomaine ] ;
const optAut = AUTORITES_DNS . map ( a = > ` < option value = " $ {a} " $ { a == = d . autorite ? ' selected ' : ' ' } > $ { a } < / option > ` ) . join ( ' ' ) ;
const grpEdge = groupes . filter ( g = > g . startsWith ( ' serveur_nginx ' ) ) ;
if ( d . edge & & ! grpEdge . includes ( d . edge ) ) grpEdge . push ( d . edge ) ;
const optEdge = grpEdge . map ( g = > ` < option value = " $ { echapper(g)} " $ { g == = d . edge ? ' selected ' : ' ' } > $ { echapper ( g ) } < / option > ` ) . join ( ' ' ) ;
const fqdns = expositionsSous ( d . nom ) ;
const liens = fqdns . length
? ` < div class = " ch-bases " > $ { fqdns . map ( f = > ` < div class = " ch-base " > < span class = " mono " > $ { echapper ( f ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune exposition sous cette zone</div> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( d . nom | | ' (sans nom) ' ) } < / span > < / div >
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer ce domaine " onclick = " retirerDomaine() " > ✕ < / button > < / div >
< div class = " grille " >
< label class = " champ " style = " grid-column:1/-1 " > < span > Zone DNS < / span > < input value = " $ { echapper(d.nom)} " placeholder = " exemple.ca " oninput = " definirDomaine( ' nom ' , this.value) " > < / label >
< label class = " champ " > < span > Autorité < / span > < select onchange = " definirDomaine( ' autorite ' , this.value) " > $ { optAut } < / select > < / label >
< label class = " champ " > < span > Edge < / span > < select onchange = " definirDomaine( ' edge ' , this.value) " > $ { optEdge } < / select > < / label >
< label class = " champ " > < span > DNSSEC < / span > < select onchange = " definirDomaine( ' dnssec ' , this.value === ' oui ' ) " > < option value = " non " $ { d . dnssec ? ' ' : ' selected ' } > non < / option > < option value = " oui " $ { d . dnssec ? ' selected ' : ' ' } > oui < / option > < / select > < / label >
< label class = " champ " > < span > Mail ( optionnel ) < / span > < input value = " $ { echapper(d.mail)} " placeholder = " — " oninput = " definirDomaine( ' mail ' , this.value) " > < / label >
< label class = " champ " style = " grid-column:1/-1 " > < span > Secondaires ( DNS ) < / span > < input value = " $ { echapper(d.secondaires)} " placeholder = " ns2.exemple.ca, … " oninput = " definirDomaine( ' secondaires ' , this.value) " > < / label >
< / div >
< div class = " section-tete " style = " margin:12px 2px 4px " > Expositions sous cette zone < / div >
$ { liens }
< / article > ` ;
}
async function sauvegarderDomaines ( ) {
const dp = { } ;
domaines . forEach ( d = > {
if ( ! String ( d . nom ) . trim ( ) ) return ;
const o = { autorite : d . autorite , edge : d . edge , secondaires : listeDepuisTexte ( d . secondaires ) , dnssec : ! ! d . dnssec } ;
if ( String ( d . mail ) . trim ( ) ) o . mail = d . mail ;
dp [ d . nom ] = o ;
} ) ;
const rep = await fetch ( ' /api/domaines ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { domaines_publics : dp } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerDomaines ( data ) ; dessinerDomaines ( ) ;
message ( ' Domaines sauvegardés. ' , ' ok ' ) ;
}
2026-06-24 20:17:46 -04:00
function chargerApplications ( data ) {
const a = ( data . applications | | { } ) . applications | | { } ;
applications = Object . entries ( a ) . map ( ( [ id , x ] ) = > ( {
id , groupe : x . groupe | | ' ' , hote : x . hote | | ' ' , port : x . port | | ' ' ,
2026-07-22 21:32:42 -04:00
requiert : ( x . requiert | | [ ] ) . join ( ' , ' ) , expose : ( x . expose | | [ ] ) . join ( ' , ' ) ,
websocket : ! ! x . websocket ,
liens : ( x . liens | | [ ] ) . map ( l = > ( { vers : l . vers | | ' ' , role : l . role | | ' ' } ) ) } ) ) ;
2026-06-24 20:17:46 -04:00
applicationsModifie = false ;
}
function dsnBase ( a ) {
const s = bdServeurs . find ( x = > x . nom == = a . serveur ) ;
if ( ! s ) return ' (serveur inconnu : ' + echapper ( a . serveur | | ' — ' ) + ' ) ' ;
return ` $ { s . type } : / / $ { a . proprietaire | | ' ? ' } : * * * * @ $ { s . hote | | ' ? ' } : $ { s . port | | ' ? ' } / $ { a . base | | ' ? ' } ` ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function marquerBasesModifie ( ) { basesModifie = true ; majIndicateurSauvegarde ( ) ; }
function selBaseInfo ( ) { if ( ! selBase ) return null ; const p = selBase . split ( ' : ' ) ; return { kind : p [ 0 ] , i : + p [ 1 ] } ; }
function choisirBase ( key ) { selBase = key ; dessiner ( ) ; }
function definirServeurBd ( champ , v ) {
const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' srv ' ) return ;
const s = bdServeurs [ info . i ] ; if ( ! s ) return ;
s [ champ ] = v ; marquerBasesModifie ( ) ;
if ( champ == = ' nom ' ) { const t = document . querySelector ( ' #tuile-base-srv- ' + info . i + ' .carte-nom ' ) ; if ( t ) t . textContent = v | | ' (sans nom) ' ; }
}
function definirBaseBd ( champ , v ) {
const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' bd ' ) return ;
const a = bdApplis [ info . i ] ; if ( ! a ) return ;
a [ champ ] = v ; marquerBasesModifie ( ) ;
if ( champ == = ' cle ' ) { const t = document . querySelector ( ' #tuile-base-bd- ' + info . i + ' .carte-nom ' ) ; if ( t ) t . textContent = v | | ' (sans id) ' ; }
else if ( champ == = ' serveur ' | | champ == = ' portee ' ) dessinerDetail ( ) ;
}
function ajouterServeurBd ( ) { bdServeurs . unshift ( { nom : ' ' , type : ' postgres ' , hote : ' ' , port : 5432 , groupe : ' ' } ) ; selBase = ' srv:0 ' ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function retirerServeurBd ( ) { const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' srv ' ) return ; bdServeurs . splice ( info . i , 1 ) ; selBase = null ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function ajouterBaseBd ( ) { bdApplis . unshift ( { cle : ' ' , serveur : ( bdServeurs [ 0 ] | | { } ) . nom | | ' ' , base : ' ' , proprietaire : ' ' , secret : ' ' , consommateur : ' ' , portee : ' groupe ' , usage : ' principale ' } ) ; selBase = ' bd:0 ' ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function retirerBaseBd ( ) { const info = selBaseInfo ( ) ; if ( ! info | | info . kind != = ' bd ' ) return ; bdApplis . splice ( info . i , 1 ) ; selBase = null ; marquerBasesModifie ( ) ; dessiner ( ) ; }
function carteBaseServeur ( s , i ) {
const key = ' srv: ' + i ;
return ` < button type = " button " class = " carte $ { selBase === key ? ' selectionnee ' : ' ' } " id = " tuile-base-srv-$ {i} " onclick = " choisirBase( ' $ {key} ' ) " >
< div class = " carte-haut " > < span class = " badge-bd " > $ { echapper ( s . type | | ' bd ' ) } < / span > $ { s . port ? ` < span class = " carte-vmid " > : $ { echapper ( s . port ) } < / span > ` : ' ' } < / div >
< div class = " carte-nom $ { s.nom ? ' ' : ' sans ' } " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( s . hote | | ' — hôte ? ' ) } < / div >
< / button > ` ;
}
function carteBaseDonnee ( a , i ) {
const key = ' bd: ' + i ;
return ` < button type = " button " class = " carte $ { selBase === key ? ' selectionnee ' : ' ' } " id = " tuile-base-bd-$ {i} " onclick = " choisirBase( ' $ {key} ' ) " >
< div class = " carte-haut " > < span class = " badge-bd " > $ { echapper ( a . portee | | ' groupe ' ) } < / span > < / div >
< div class = " carte-nom $ { a.cle ? ' ' : ' sans ' } " > $ { echapper ( a . cle | | ' (sans id) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( a . base | | ' — ' ) } @ $ { echapper ( a . serveur | | ' ? ' ) } < / div >
< / button > ` ;
2026-06-24 20:17:46 -04:00
}
function dessinerBases ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const srv = bdServeurs . length ? ` < div class = " grille-cartes " > $ { bdServeurs . map ( ( s , i ) = > carteBaseServeur ( s , i ) ) . join ( ' ' ) } < / div > ` : ' <div class= " vide " style= " padding:12px " >Aucun serveur de BD.</div> ' ;
const bd = bdApplis . length ? ` < div class = " grille-cartes " > $ { bdApplis . map ( ( a , i ) = > carteBaseDonnee ( a , i ) ) . join ( ' ' ) } < / div > ` : ' <div class= " vide " style= " padding:12px " >Aucune base.</div> ' ;
cible . innerHTML = ` < div class = " section-grille " > < div class = " section-tete " > Serveurs de bases < span class = " compte " > $ { bdServeurs . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterServeurBd() " > + Serveur BD < / button > < / div > $ { srv } < / div >
< div class = " section-grille " > < div class = " section-tete " > Bases applicatives < span class = " compte " > $ { bdApplis . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterBaseBd() " > + Base < / button > < / div > $ { bd } < / div > ` ;
}
function inspecteurBase ( ) {
const cible = document . getElementById ( ' detail ' ) ;
const info = selBaseInfo ( ) ;
if ( ! info ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun élément sélectionné</b>Choisis un serveur de base ou une base à gauche.</div></div> ' ; return ; }
if ( info . kind == = ' srv ' ) {
const s = bdServeurs [ info . i ] ;
if ( ! s ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " >Introuvable.</div></div> ' ; return ; }
const optHote = ' <option value= " " >—</option> ' + hotes . map ( h = > h . nom ) . filter ( Boolean ) . map ( n = > ` < option value = " $ { echapper(n)} " $ { n == = s . hote ? ' selected ' : ' ' } > $ { echapper ( n ) } < / option > ` ) . join ( ' ' ) ;
const optGroupe = ' <option value= " " >—</option> ' + groupes . map ( g = > ` < option value = " $ { echapper(g)} " $ { g == = s . groupe ? ' selected ' : ' ' } > $ { echapper ( g ) } < / option > ` ) . join ( ' ' ) ;
const ici = bdApplis . filter ( a = > a . serveur == = s . nom ) ;
const iciHtml = ici . length ? ` < div class = " ch-bases " > $ { ici . map ( a = > ` < div class = " ch-base " > < span class = " badge-bd " > $ { echapper ( a . portee | | ' groupe ' ) } < / span > < span class = " mono " > $ { echapper ( a . base | | ' ? ' ) } < / span > ( $ { echapper ( a . cle ) } ) < / div > ` ) . join ( ' ' ) } < / div > ` : ' <div class= " ch-stub " >aucune base sur ce serveur</div> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / span > < / div > < span class = " grandit " > < / span > < span class = " badge-bd " > $ { echapper ( s . type | | ' bd ' ) } < / span > < button type = " button " class = " danger " title = " Retirer " onclick = " retirerServeurBd() " > ✕ < / button > < / div >
< div class = " grille " >
< label class = " champ " > < span > Nom < / span > < input value = " $ { echapper(s.nom)} " placeholder = " pg-principal " oninput = " definirServeurBd( ' nom ' , this.value) " > < / label >
< label class = " champ " > < span > Type < / span > < input value = " $ { echapper(s.type)} " placeholder = " postgres " oninput = " definirServeurBd( ' type ' , this.value) " > < / label >
< label class = " champ " > < span > Hôte ( VM ) < / span > < select onchange = " definirServeurBd( ' hote ' , this.value) " > $ { optHote } < / select > < / label >
< label class = " champ " > < span > Port < / span > < input type = " number " value = " $ { echapper(s.port)} " oninput = " definirServeurBd( ' port ' , this.value) " > < / label >
< label class = " champ " > < span > Groupe Ansible < / span > < select onchange = " definirServeurBd( ' groupe ' , this.value) " > $ { optGroupe } < / select > < / label >
< / div >
< div class = " section-tete " style = " margin:12px 2px 2px " > Bases hébergées ici < / div > $ { iciHtml }
2026-07-01 15:58:32 -04:00
$ { ( ( ) = > {
const hS = hotes . find ( h = > h . nom == = s . hote ) ;
if ( hS & & hS . etat == = ' actif ' ) { const hiS = hotes . indexOf ( hS ) ;
2026-07-04 14:40:02 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > Déployer applique les rôles sur l ' hôte « $ { echapper(s.hote)} ».</span><span class= " grandit " ></span>$ { estProduction ? ' < span class = " badge-prod " > PROD < / span > ' : ' ' }<button type= " button " class= " $ { estProduction ? ' danger ' : ' primaire ' } " onclick= " lancer( ' pousser ' , $ {hiS} ) " title= " Déployer l ' hôte du serveur de BD " >Déployer</button></div>`; }
2026-07-01 15:58:32 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > $ { s . hote ? ` Active + pousse d ' abord l ' hôte « $ { echapper ( s . hote ) } » . ` : ' Aucun hôte assigné. ' } < / span > < / div > ` ;
} ) ( ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
} else {
const a = bdApplis [ info . i ] ;
if ( ! a ) { cible . innerHTML = ' <div class= " detail " ><div class= " vide " >Introuvable.</div></div> ' ; return ; }
const source = a . portee == = ' hote ' ? hotes . map ( h = > h . nom ) . filter ( Boolean ) : a . portee == = ' application ' ? applications . map ( x = > x . id ) : groupes ;
const optConso = ' <option value= " " >—</option> ' + source . map ( v = > ` < option value = " $ { echapper(v)} " $ { v == = a . consommateur ? ' selected ' : ' ' } > $ { echapper ( v ) } < / option > ` ) . join ( ' ' ) ;
const optPortee = [ ' groupe ' , ' application ' , ' hote ' ] . map ( p = > ` < option value = " $ {p} " $ { p == = ( a . portee | | ' groupe ' ) ? ' selected ' : ' ' } > $ { p } < / option > ` ) . join ( ' ' ) ;
const optServeur = ' <option value= " " >—</option> ' + bdServeurs . map ( x = > ` < option value = " $ { echapper(x.nom)} " $ { x . nom == = a . serveur ? ' selected ' : ' ' } > $ { echapper ( x . nom ) } < / option > ` ) . join ( ' ' ) ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( a . cle | | ' (sans id) ' ) } < / span > < / div > < span class = " grandit " > < / span > < button type = " button " class = " danger " title = " Retirer " onclick = " retirerBaseBd() " > ✕ < / button > < / div >
< div class = " grille " >
< label class = " champ " > < span > Identifiant < / span > < input value = " $ { echapper(a.cle)} " placeholder = " bd_forgejo " oninput = " definirBaseBd( ' cle ' , this.value) " > < / label >
< label class = " champ " > < span > Portée < / span > < select onchange = " definirBaseBd( ' portee ' , this.value) " > $ { optPortee } < / select > < / label >
< label class = " champ " > < span > Consommateur < / span > < select onchange = " definirBaseBd( ' consommateur ' , this.value) " > $ { optConso } < / select > < / label >
< label class = " champ " > < span > Serveur < / span > < select onchange = " definirBaseBd( ' serveur ' , this.value) " > $ { optServeur } < / select > < / label >
< label class = " champ " > < span > Base < / span > < input value = " $ { echapper(a.base)} " placeholder = " forgejo " oninput = " definirBaseBd( ' base ' , this.value) " > < / label >
< label class = " champ " > < span > Propriétaire < / span > < input value = " $ { echapper(a.proprietaire)} " oninput = " definirBaseBd( ' proprietaire ' , this.value) " > < / label >
< label class = " champ " > < span > Usage < / span > < input value = " $ { echapper(a.usage)} " placeholder = " principale " oninput = " definirBaseBd( ' usage ' , this.value) " > < / label >
< label class = " champ " > < span > Secret ( Vault ) < / span > < input value = " $ { echapper(a.secret)} " placeholder = " vault_bd_app " oninput = " definirBaseBd( ' secret ' , this.value) " > < / label >
< / div >
< div class = " dsn mono " style = " margin-top:8px " > $ { echapper ( dsnBase ( a ) ) } < / div >
2026-07-01 15:58:32 -04:00
$ { ( ( ) = > {
const srv = bdServeurs . find ( x = > x . nom == = a . serveur ) ;
const hB = srv ? hotes . find ( h = > h . nom == = srv . hote ) : null ;
if ( hB & & hB . etat == = ' actif ' ) { const hiB = hotes . indexOf ( hB ) ;
2026-07-04 14:40:02 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > Déployer applique les rôles sur « $ { echapper ( srv . hote ) } » ( le serveur de BD qui héberge cette base ) . < / span > < span class = " grandit " > < / span > $ { estProduction ? ' <span class= " badge-prod " >PROD</span> ' : ' ' } < button type = " button " class = " $ { estProduction ? ' danger ' : ' primaire ' } " onclick = " lancer( ' pousser ' , $ {hiB} ) " title = " Déployer le serveur de BD (crée cette base) " > Déployer < / button > < / div > ` ; }
2026-07-01 15:58:32 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > $ { ( srv & & srv . hote ) ? ` Active + pousse d ' abord l ' hôte « $ { echapper ( srv . hote ) } » . ` : ' Serveur de BD sans hôte assigné. ' } < / span > < / div > ` ;
} ) ( ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
}
2026-06-24 20:17:46 -04:00
}
async function sauvegarderBases ( ) {
const sd = { } ;
bdServeurs . forEach ( s = > { if ( String ( s . nom ) . trim ( ) ) sd [ s . nom ] = { type : s . type , hote : s . hote , port : parseInt ( s . port , 10 ) | | s . port , . . . ( s . groupe ? { groupe : s . groupe } : { } ) } ; } ) ;
const bd = { } ;
bdApplis . forEach ( a = > { if ( String ( a . cle ) . trim ( ) ) bd [ a . cle ] = { serveur : a . serveur , base : a . base , proprietaire : a . proprietaire , secret : a . secret , consommateur : a . consommateur , portee : a . portee | | ' groupe ' , usage : a . usage | | ' principale ' } ; } ) ;
const rep = await fetch ( ' /api/bases ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { serveurs_bd : sd , bases_donnees : bd } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerBases ( data ) ;
dessinerBases ( ) ;
message ( ' Bases de données sauvegardées. ' , ' ok ' ) ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function marquerApplicationsModifie ( ) { applicationsModifie = true ; majIndicateurSauvegarde ( ) ; }
function choisirApp ( i ) { selApp = i ; dessiner ( ) ; }
function definirApplication ( champ , v ) {
const a = applications [ selApp ] ; if ( ! a ) return ;
a [ champ ] = v ; marquerApplicationsModifie ( ) ;
if ( champ == = ' id ' ) { const t = document . querySelector ( ' #tuile-app- ' + selApp + ' .carte-nom ' ) ; if ( t ) { t . textContent = v | | ' (sans id) ' ; t . className = ' carte-nom ' + ( v ? ' ' : ' sans ' ) ; } }
2026-07-22 21:32:42 -04:00
else if ( champ == = ' groupe ' ) {
const ok = Object . keys ( liensAcceptes [ a . groupe ] | | { } ) ;
( a . liens | | [ ] ) . forEach ( l = > { if ( l . role & & ! ok . includes ( l . role ) ) l . role = ' ' ; } ) ;
dessinerDetail ( ) ;
}
else if ( champ == = ' hote ' | | champ == = ' port ' ) dessinerDetail ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
}
function ajouterApplication ( ) {
2026-07-22 21:32:42 -04:00
applications . unshift ( { id : ' ' , groupe : ( groupes [ 0 ] | | ' ' ) , hote : ' ' , port : ' ' , requiert : ' ' , expose : ' ' , websocket : false , liens : [ ] } ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
selApp = 0 ; marquerApplicationsModifie ( ) ; dessiner ( ) ;
const c = document . querySelector ( ' #detail .grille input ' ) ; if ( c ) c . focus ( ) ;
}
function retirerApplication ( ) {
const a = applications [ selApp ] ; if ( ! a ) return ;
if ( a . id & & ! confirm ( ' Retirer l \' application « ' + a . id + ' » ? ' ) ) return ;
applications . splice ( selApp , 1 ) ;
selApp = applications . length ? Math . min ( selApp , applications . length - 1 ) : null ;
marquerApplicationsModifie ( ) ; dessiner ( ) ;
}
2026-07-22 21:32:42 -04:00
function ajouterLien ( ) {
const a = applications [ selApp ] ; if ( ! a ) return ;
if ( ! a . liens ) a . liens = [ ] ;
const roles = Object . keys ( liensAcceptes [ a . groupe ] | | { } ) ;
a . liens . push ( { vers : ' ' , role : roles . length == = 1 ? roles [ 0 ] : ' ' } ) ;
marquerApplicationsModifie ( ) ; dessinerDetail ( ) ;
}
function retirerLien ( i ) {
const a = applications [ selApp ] ; if ( ! a | | ! a . liens ) return ;
a . liens . splice ( i , 1 ) ; marquerApplicationsModifie ( ) ; dessinerDetail ( ) ;
}
function definirLien ( i , champ , v ) {
const a = applications [ selApp ] ; if ( ! a | | ! a . liens | | ! a . liens [ i ] ) return ;
a . liens [ i ] [ champ ] = v ; marquerApplicationsModifie ( ) ;
if ( champ == = ' role ' ) dessinerDetail ( ) ;
}
function blocLiens ( a ) {
const accepte = liensAcceptes [ a . groupe ] | | { } ;
const roles = Object . keys ( accepte ) ;
if ( ! roles . length ) {
return ` < div class = " ch-stub " > Le rôle « $ { echapper ( a . groupe | | ' — ' ) } » n ' accepte aucun lien
( aucun < span class = " mono " > roles / $ { echapper ( a . groupe | | ' ? ' ) } / meta / liens . yml < / span > ) . < / div > ` ;
}
const cibles = applications . map ( x = > x . id ) . filter ( x = > x & & x != = a . id ) ;
const lignes = ( a . liens | | [ ] ) . map ( ( l , i ) = > {
const spec = accepte [ l . role ] | | { } ;
const vars = Object . keys ( spec . variables | | { } ) ;
const optV = ' <option value= " " >— cible —</option> ' + cibles . map ( c = >
` < option value = " $ { echapper(c)} " $ { c == = l . vers ? ' selected ' : ' ' } > $ { echapper ( c ) } < / option > ` ) . join ( ' ' ) ;
const optR = ' <option value= " " >— rôle —</option> ' + roles . map ( r = >
` < option value = " $ { echapper(r)} " $ { r == = l . role ? ' selected ' : ' ' } > $ { echapper ( r ) } < / option > ` ) . join ( ' ' ) ;
const inconnue = l . vers & & ! cibles . includes ( l . vers ) ;
return ` < div class = " ch-base " style = " gap:8px;align-items:center " >
< select style = " max-width:12rem " onchange = " definirLien($ {i} , ' role ' , this.value) " > $ { optR } < / select >
< span class = " mono " > → < / span >
< select style = " max-width:12rem " onchange = " definirLien($ {i} , ' vers ' , this.value) " > $ { optV } < / select >
$ { inconnue ? ` < span class = " tag " style = " color:#b45309 " > cible « $ { echapper ( l . vers ) } » inconnue < / span > ` : ' ' }
$ { vars . length ? ` < span class = " tag " title = " Variables injectées sur cette application " > $ { echapper ( vars . join ( ' , ' ) ) } < / span > ` : ' ' }
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer ce lien " onclick = " retirerLien($ {i} ) " > ✕ < / button >
< / div > ` ;
} ) . join ( ' ' ) ;
return ` $ { lignes | | ' <div class= " ch-stub " >aucun lien déclaré</div> ' }
< div style = " margin-top:6px " > < button type = " button " class = " fantome " onclick = " ajouterLien() " > + Lien < / button > < / div > ` ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function carteApp ( a , i ) {
const exp = listeDepuisTexte ( a . expose ) ;
return ` < button type = " button " class = " carte $ { i === selApp ? ' selectionnee ' : ' ' } " id = " tuile-app-$ {i} " onclick = " choisirApp($ {i} ) " >
< div class = " carte-haut " > < span class = " badge-bd " > app < / span > $ { a . port ? ` < span class = " carte-vmid " > : $ { echapper ( a . port ) } < / span > ` : ' ' } < / div >
< div class = " carte-nom $ { a.id ? ' ' : ' sans ' } " > $ { echapper ( a . id | | ' (sans id) ' ) } < / div >
< div class = " carte-ip " > $ { echapper ( a . hote | | ' — hôte ? ' ) } < / div >
$ { exp . length ? ` < div class = " carte-grps " > < span class = " tag " > $ { echapper ( exp [ 0 ] ) } < / span > $ { exp . length > 1 ? ` < span class = " tag plus " > + $ { exp . length - 1 } < / span > ` : ' ' } < / div > ` : ' ' }
< / button > ` ;
2026-06-24 20:17:46 -04:00
}
function dessinerApplications ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const tuiles = applications . length
? ` < div class = " grille-cartes " > $ { applications . map ( ( a , i ) = > carteApp ( a , i ) ) . join ( ' ' ) } < / div > `
: ' <div class= " vide " style= " padding:14px " >Aucune application. Une VM peut en porter plusieurs.</div> ' ;
cible . innerHTML = ` < div class = " section-grille " >
< div class = " section-tete " > Applications < span class = " compte " > $ { applications . length } < / span > < button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterApplication() " > + Application < / button > < / div >
$ { tuiles } < / div > ` ;
}
function inspecteurApplication ( ) {
const cible = document . getElementById ( ' detail ' ) ;
if ( selApp == null | | ! applications [ selApp ] ) {
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucune application sélectionnée</b>Choisis une tuile à gauche, ou ajoute une application.</div></div> ' ;
return ;
}
const a = applications [ selApp ] ;
const optGroupe = groupes . map ( g = > ` < option value = " $ { echapper(g)} " $ { g == = a . groupe ? ' selected ' : ' ' } > $ { echapper ( g ) } < / option > ` ) . join ( ' ' ) ;
const optHote = ' <option value= " " >—</option> ' + hotes . map ( h = > h . nom ) . filter ( Boolean ) . map ( n = > ` < option value = " $ { echapper(n)} " $ { n == = a . hote ? ' selected ' : ' ' } > $ { echapper ( n ) } < / option > ` ) . join ( ' ' ) ;
const dsns = basesDeApp ( a , a . hote ) ;
const liens = dsns . length
? ` < div class = " ch-bases " > $ { dsns . map ( b = > ` < div class = " ch-base " > < span class = " badge-bd " > $ { echapper ( b . portee | | ' groupe ' ) } < / span > < span class = " mono " > $ { echapper ( dsnBase ( b ) ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune base liée</div> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( a . id | | ' (sans id) ' ) } < / span > < / div >
< span class = " grandit " > < / span >
< button type = " button " class = " danger " title = " Retirer cette application " onclick = " retirerApplication() " > ✕ < / button > < / div >
< div class = " grille " >
< label class = " champ " > < span > Identifiant < / span > < input value = " $ { echapper(a.id)} " placeholder = " boutique " oninput = " definirApplication( ' id ' , this.value) " > < / label >
< label class = " champ " > < span > Groupe ( rôle ) < / span > < select onchange = " definirApplication( ' groupe ' , this.value) " > $ { optGroupe } < / select > < / label >
< label class = " champ " > < span > Hôte ( VM ) < / span > < select onchange = " definirApplication( ' hote ' , this.value) " > $ { optHote } < / select > < / label >
< label class = " champ " > < span > Port < / span > < input type = " number " value = " $ { echapper(a.port)} " placeholder = " 3000 " oninput = " definirApplication( ' port ' , this.value) " > < / label >
2026-07-22 21:32:42 -04:00
< label class = " champ " > < span > WebSocket < / span > < span style = " display:flex;align-items:center;gap:8px;height:2.1rem " > < input type = " checkbox " $ { a . websocket ? ' checked ' : ' ' } onchange = " definirApplication( ' websocket ' , this.checked) " > < span class = " hint-dep " > vhost compatible WebSocket ( Collabora … ) < / span > < / span > < / label >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< label class = " champ " style = " grid-column:1/-1 " > < span > Requiert ( applis ) < / span > < input value = " $ { echapper(a.requiert)} " placeholder = " pg-principal, … " oninput = " definirApplication( ' requiert ' , this.value) " > < / label >
< label class = " champ " style = " grid-column:1/-1 " > < span > Expose ( FQDN ) < / span > < input value = " $ { echapper(a.expose)} " placeholder = " forge.exemple.ca " oninput = " definirApplication( ' expose ' , this.value) " > < / label >
2026-06-24 20:17:46 -04:00
< / div >
2026-07-22 21:32:42 -04:00
< div class = " section-tete " style = " margin:12px 2px 4px " > Liens ( bindings )
< span class = " compte " > $ { ( a . liens | | [ ] ) . length } < / span > < / div >
$ { blocLiens ( a ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< div class = " section-tete " style = " margin:12px 2px 4px " > Bases consommées < / div >
$ { liens }
2026-07-01 15:58:32 -04:00
$ { ( ( ) = > {
const hA = hotes . find ( x = > x . nom == = a . hote ) ;
if ( hA & & hA . etat == = ' actif ' ) {
const hiA = hotes . indexOf ( hA ) ;
2026-07-04 14:40:02 -04:00
return ` < div class = " pied " > < span class = " hint-dep " > Déployer applique les rôles sur l ' hôte porteur « $ { echapper(a.hote)} ».</span><span class= " grandit " ></span>$ { estProduction ? ' < span class = " badge-prod " > PROD < / span > ' : ' ' }<button type= " button " class= " $ { estProduction ? ' danger ' : ' primaire ' } " onclick= " lancer( ' pousser ' , $ {hiA} ) " title= " Déployer l ' hôte porteur ( rend l ' application vivante) " >Déployer</button></div>`;
2026-07-01 15:58:32 -04:00
}
return ` < div class = " pied " > < span class = " hint-dep " > $ { a . hote ? ` Active + pousse d ' abord l ' hôte « $ { echapper ( a . hote ) } » . ` : ' Aucun hôte assigné. ' } < / span > < / div > ` ;
} ) ( ) }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< / article > ` ;
2026-06-24 20:17:46 -04:00
}
function listeDepuisTexte ( v ) { return String ( v | | ' ' ) . split ( ' , ' ) . map ( s = > s . trim ( ) ) . filter ( Boolean ) ; }
async function sauvegarderApplications ( ) {
const apps = { } ;
applications . forEach ( a = > {
if ( ! String ( a . id ) . trim ( ) ) return ;
const o = { groupe : a . groupe , hote : a . hote } ;
if ( String ( a . port ) . trim ( ) ) o . port = parseInt ( a . port , 10 ) | | a . port ;
const req = listeDepuisTexte ( a . requiert ) ; if ( req . length ) o . requiert = req ;
const exp = listeDepuisTexte ( a . expose ) ; if ( exp . length ) o . expose = exp ;
2026-07-22 21:32:42 -04:00
if ( a . websocket ) o . websocket = true ;
const lns = ( a . liens | | [ ] )
. filter ( l = > String ( l . vers ) . trim ( ) & & String ( l . role ) . trim ( ) )
. map ( l = > ( { vers : l . vers , role : l . role } ) ) ;
if ( lns . length ) o . liens = lns ;
2026-06-24 20:17:46 -04:00
apps [ a . id ] = o ;
} ) ;
const rep = await fetch ( ' /api/applications ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { applications : apps } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerApplications ( data ) ;
dessinerApplications ( ) ;
message ( ' Applications sauvegardées. ' , ' ok ' ) ;
}
function chargerServeurs ( data ) {
serveurs = ( data . serveurs | | [ ] ) . map ( s = > ( { . . . s , integrations : ( s . integrations | | [ ] ) . join ( ' , ' ) } ) ) ;
serveursModifie = false ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function marquerServeursModifie ( ) { serveursModifie = true ; majIndicateurSauvegarde ( ) ; }
function choisirServeur ( i ) { selServeur = i ; dessiner ( ) ; }
function definirServeur ( champ , v ) {
const s = serveurs [ selServeur ] ; if ( ! s ) return ;
s [ champ ] = v ; marquerServeursModifie ( ) ;
if ( champ == = ' nom ' ) { const t = document . querySelector ( ' #tuile-srv- ' + selServeur + ' .carte-nom ' ) ; if ( t ) { t . textContent = v | | ' (sans nom) ' ; t . className = ' carte-nom ' + ( v ? ' ' : ' sans ' ) ; } }
else if ( champ == = ' fonction ' | | champ == = ' etat ' ) dessiner ( ) ;
2026-06-24 20:17:46 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function basculerIntegration ( role , actif ) {
const s = serveurs [ selServeur ] ; if ( ! s ) return ;
const set = new Set ( listeDepuisTexte ( s . integrations ) ) ;
2026-06-30 12:36:51 -04:00
if ( actif ) set . add ( role ) ; else set . delete ( role ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
s . integrations = Array . from ( set ) . join ( ' , ' ) ; marquerServeursModifie ( ) ;
}
function ajouterServeur ( ) {
serveurs . unshift ( { nom : ' ' , fonction : ( Object . keys ( nomenclature . fonctions | | { } ) [ 0 ] | | ' ' ) , etat : ' planifie ' , noeud : ' ' , stockage : ' ' , disque : ' ' , memoire : ' ' , coeurs : ' ' , integrations : ' ' , statut : ' absent ' , vmid : ' ' , adresse_ip : ' ' , vlan : ' ' } ) ;
selServeur = 0 ; marquerServeursModifie ( ) ; dessiner ( ) ;
const c = document . querySelector ( ' #detail .grille input ' ) ; if ( c ) c . focus ( ) ;
}
function retirerServeur ( ) {
const s = serveurs [ selServeur ] ; if ( ! s ) return ;
if ( s . nom & & ! confirm ( ' Retirer le serveur « ' + s . nom + ' » du plan ? ' ) ) return ;
serveurs . splice ( selServeur , 1 ) ;
selServeur = serveurs . length ? Math . min ( selServeur , serveurs . length - 1 ) : null ;
marquerServeursModifie ( ) ; dessiner ( ) ;
}
function ouvrirApp ( id ) { const i = applications . findIndex ( a = > a . id == = id ) ; selApp = i > = 0 ? i : null ; setVue ( ' applications ' ) ; }
function ouvrirBaseServeur ( nom ) { const i = bdServeurs . findIndex ( x = > x . nom == = nom ) ; selBase = i > = 0 ? ' srv: ' + i : null ; setVue ( ' bases ' ) ; }
2026-07-01 17:39:44 -04:00
function pointVie ( nom ) {
const v = sondes [ nom ] ;
return v == = true ? ' <span class= " pt actif " title= " VM joignable (SSH) " ></span> '
: v == = false ? ' <span class= " pt bloq " title= " Injoignable " ></span> ' : ' ' ;
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function carteServeur ( s , i ) {
const actif = s . etat == = ' actif ' ;
const recon = s . statut == = ' divergence ' ? ' <span class= " tag plus " title= " Le plan diverge de l \' inventaire généré " >⚠ divergent</span> '
: s . statut == = ' absent ' ? ' <span class= " tag " title= " Déclaré, pas encore instancié " >○ non instancié</span> ' : ' ' ;
return ` < button type = " button " class = " carte $ { actif ? ' actif ' : ' attente ' } $ { i === selServeur ? ' selectionnee ' : ' ' } " id = " tuile-srv-$ {i} " onclick = " choisirServeur($ {i} ) " >
< div class = " carte-haut " > < span class = " badge-statut $ { actif ? ' ok ' : ' attente ' } " > $ { actif ? ' actif ' : ' planifié ' } < / span > $ { s . vmid ? ` < span class = " carte-vmid " > #${echapper(s.vmid)}</span>` : ''}</div>
< div class = " carte-nom $ { s.nom ? ' ' : ' sans ' } " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / div >
2026-07-01 17:39:44 -04:00
< div class = " carte-ip " > $ { pointVie ( s . nom ) } $ { echapper ( s . adresse_ip | | ' — ' ) } < / div >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
$ { recon ? ` < div class = " carte-grps " > $ { recon } < / div > ` : ' ' }
< / button > ` ;
2026-06-30 12:36:51 -04:00
}
2026-06-24 20:17:46 -04:00
function dessinerServeurs ( ) {
const cible = document . getElementById ( ' grilles ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const entete = BANNIERE_MODELE + ` < div class = " section-grille " > < div class = " section-tete " > Serveurs < span class = " compte " > $ { serveurs . length } < / span >
< button type = " button " class = " fantome " style = " margin-left:auto " onclick = " ajouterServeur() " > + Serveur < / button >
< button type = " button " class = " fantome " onclick = " appliquerPlan() " > ⚙ Appliquer le plan < / button > < / div > < / div > ` ;
if ( ! serveurs . length ) { cible . innerHTML = entete + ' <div class= " vide " style= " padding:14px " >Aucun serveur. « + Serveur » ou <span class= " mono " >make serveurs-bootstrap</span>.</div> ' ; return ; }
const idx = serveurs . map ( ( s , i ) = > [ s , i ] ) . filter ( ( [ s ] ) = > correspondServeur ( s ) ) ;
const sections = [ [ ' Actifs ' , idx . filter ( ( [ s ] ) = > s . etat == = ' actif ' ) ] , [ ' Planifiés ' , idx . filter ( ( [ s ] ) = > s . etat != = ' actif ' ) ] ] ;
cible . innerHTML = entete + sections . filter ( ( [ , l ] ) = > l . length ) . map ( ( [ t , l ] ) = >
` < div class = " section-grille " > < div class = " section-tete " > $ { t } < span class = " compte " > $ { l . length } < / span > < / div > < div class = " grille-cartes " > $ { l . map ( ( [ s , i ] ) = > carteServeur ( s , i ) ) . join ( ' ' ) } < / div > < / div > ` ) . join ( ' ' ) ;
}
function inspecteurServeur ( ) {
const cible = document . getElementById ( ' detail ' ) ;
if ( selServeur == null | | ! serveurs [ selServeur ] ) {
hoteOpCourant = ' ' ;
cible . innerHTML = ' <div class= " detail " ><div class= " vide " ><b>Aucun serveur sélectionné</b>Choisis une tuile à gauche, ou ajoute un serveur.</div></div> ' ;
return ;
}
const s = serveurs [ selServeur ] ;
2026-06-30 12:36:51 -04:00
const vi = ( intrants & & intrants . valeurs ) | | { } ;
const libDef = ( d ) = > ' (défaut ' + ( d ? ' : ' + d : ' ' ) + ' ) ' ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const optFonction = Object . keys ( nomenclature . fonctions | | { } ) . map ( f = > ` < option value = " $ { echapper(f)} " $ { f == = s . fonction ? ' selected ' : ' ' } > $ { echapper ( f ) } < / option > ` ) . join ( ' ' ) ;
const optEtat = [ ' planifie ' , ' actif ' ] . map ( e = > ` < option value = " $ {e} " $ { e == = s . etat ? ' selected ' : ' ' } > $ { e } < / option > ` ) . join ( ' ' ) ;
const optDeListe = ( liste , sel , lib ) = > ` < option value = " " $ { sel ? ' ' : ' selected ' } > $ { echapper ( lib ) } < / option > `
+ [ . . . new Set ( [ . . . ( liste | | [ ] ) , . . . ( sel ? [ sel ] : [ ] ) ] ) ] . map ( v = > ` < option value = " $ { echapper(v)} " $ { v == = sel ? ' selected ' : ' ' } > $ { echapper ( v ) } < / option > ` ) . join ( ' ' ) ;
const set = new Set ( listeDepuisTexte ( s . integrations ) ) ;
const tousRoles = [ . . . new Set ( [ . . . integrationsDispo , . . . set ] ) ] ;
const cases = tousRoles . length
? tousRoles . map ( role = > ` < label class = " integ-case " title = " $ { echapper(role)} " > < input type = " checkbox " $ { set . has ( role ) ? ' checked ' : ' ' } onchange = " basculerIntegration( ' $ { echapper(role)} ' , this.checked) " > < span > $ { echapper ( role . replace ( / ^ client_ / , ' ' ) ) } < / span > < / label > ` ) . join ( ' ' )
: ' <span class= " dsn " >aucun rôle client_* disponible</span> ' ;
const info = infoFonction ( s . fonction ) ;
const zoneHint = info ? ` < span class = " zone-hint " title = " Dérivé de la nomenclature " > $ { echapper ( info . libelle ) } · VLAN $ { info . vlan } < / span > ` : ' ' ;
const h = hotes . find ( x = > x . nom == = s . nom ) ;
const apps = applications . filter ( a = > a . hote == = s . nom ) ;
const appsHtml = apps . length
? ` < div class = " ch-bases " > $ { apps . map ( a = > ` < div class = " ch-base " > < span class = " badge-bd " > app < / span > < button type = " button " class = " lien-objet " onclick = " ouvrirApp( ' $ { echapper(a.id)} ' ) " > $ { echapper ( a . id | | ' (sans id) ' ) } < / button > $ { a . expose ? ` < span class = " mono " > → $ { echapper ( listeDepuisTexte ( a . expose ) . join ( ' , ' ) ) } < / span > ` : ' ' } < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucune application sur cet hôte</div> ' ;
const bds = bdServeurs . filter ( x = > x . hote == = s . nom ) ;
const bdHtml = bds . length
? ` < div class = " ch-bases " > $ { bds . map ( x = > ` < div class = " ch-base " > < span class = " badge-bd " > $ { echapper ( x . type ) } < / span > < button type = " button " class = " lien-objet " onclick = " ouvrirBaseServeur( ' $ { echapper(x.nom)} ' ) " > $ { echapper ( x . nom ) } < / button > < span class = " mono " > : $ { echapper ( x . port ) } < / span > < / div > ` ) . join ( ' ' ) } < / div > `
: ' <div class= " ch-stub " >aucun serveur de base ici</div> ' ;
hoteOpCourant = s . nom ;
2026-07-01 15:58:32 -04:00
const hi = h ? hotes . indexOf ( h ) : - 1 ;
let boutons = ' ' ;
2026-07-04 14:40:02 -04:00
if ( h ) boutons + = ` < button type = " button " title = " Cloner la VM depuis le golden template (Proxmox) " onclick = " lancer( ' creer ' , $ {hi} ) " > 🖥 Créer la VM < / button > ` ;
2026-06-30 16:20:18 -04:00
if ( h & & h . etat == = ' actif ' & & ! hoteBloque ( h ) ) {
const clsDep = estProduction ? ' danger ' : ' primaire ' ;
2026-07-01 15:58:32 -04:00
boutons + = ` < button type = " button " id = " btn-verifier " onclick = " lancer( ' verifier ' , $ {hi} ) " > Vérifier < / button > `
+ ` < button type = " button " id = " btn-deployer " class = " $ {clsDep} " onclick = " lancer( ' deployer ' , $ {hi} ) " > Déployer < / button > ` ;
}
let pied ;
if ( boutons ) {
const bloque = h & & h . etat == = ' actif ' & & hoteBloque ( h ) ;
const hint = bloque ? ` < span class = " hint-dep " > Prérequis manquant : $ { [ . . . new Set ( ( h . groupes | | [ ] ) . flatMap ( g = > manquantsPourGroupe ( g ) ) ) ] . join ( ' , ' ) } < / span > ` : ' ' ;
pied = ` < div class = " pied " > $ { estProduction ? ' <span class= " badge-prod " >PROD</span> ' : ' ' } $ { hint } < span class = " grandit " > < / span > $ { boutons } < / div > ` ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
} else {
2026-07-01 15:58:32 -04:00
pied = ` < div class = " pied " > < span class = " hint-dep " > Applique le plan pour instancier cet hôte ( ⚙ Appliquer le plan ) < / span > < / div > ` ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
}
const statBadge = s . statut == = ' divergence ' ? ' <span class= " badge-statut bloque " >divergent</span> '
: s . statut == = ' absent ' ? ' <span class= " badge-statut attente " >non instancié</span> '
: ' <span class= " badge-statut ok " >réconcilié</span> ' ;
cible . innerHTML = ` < article class = " detail " >
< div class = " detail-tete " > < div class = " detail-nom " > < span class = " detail-nom-txt " > $ { echapper ( s . nom | | ' (sans nom) ' ) } < / span > $ { zoneHint } < / div >
< span class = " grandit " > < / span > $ { statBadge }
< button type = " button " class = " danger " title = " Retirer ce serveur " onclick = " retirerServeur() " > ✕ < / button > < / div >
< div class = " stats " > $ { tuile ( s . vmid , ' VMID ' ) } $ { tuile ( s . adresse_ip , ' IP ' ) } $ { tuile ( s . vlan , ' VLAN ' ) } < / div >
2026-07-01 17:39:44 -04:00
< div class = " dsn " style = " padding:5px 2px " > < b > Plan < / b > : $ { echapper ( s . etat ) } & nbsp ; · & nbsp ; < b > Réel < / b > : $ { sondes [ s . nom ] == = true ? ' <span class= " pt actif " ></span> vivante ' + ( s . adresse_ip ? ' ( ' + echapper ( s . adresse_ip ) + ' ) ' : ' ' ) : sondes [ s . nom ] == = false ? ' <span class= " pt bloq " ></span> injoignable ' : ' <span class= " pt plan " ></span> non sondé ' } < / div >
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
< div class = " section-tete " style = " margin:4px 2px 2px " > Identité — saisie du plan ( surcharge les défauts ) < / div >
< div class = " grille " >
< label class = " champ " > < span > Serveur ( fonction - NN ) < / span > < input data - v = " nom " value = " $ { echapper(s.nom)} " placeholder = " web-frontal-03 " oninput = " definirServeur( ' nom ' , this.value) " > < / label >
< label class = " champ " > < span > Fonction < / span > < select onchange = " definirServeur( ' fonction ' , this.value) " > $ { optFonction } < / select > < / label >
< label class = " champ " > < span > État < / span > < select onchange = " definirServeur( ' etat ' , this.value) " > $ { optEtat } < / select > < / label >
< label class = " champ " > < span > Nœud < / span > < select onchange = " definirServeur( ' noeud ' , this.value) " > $ { optDeListe ( vi . proxmox_noeuds , s . noeud , libDef ( vi . proxmox_clone_noeud ) ) } < / select > < / label >
< label class = " champ " > < span > Stockage < / span > < select onchange = " definirServeur( ' stockage ' , this.value) " > $ { optDeListe ( vi . proxmox_stockages , s . stockage , libDef ( vi . proxmox_clone_stockage ) ) } < / select > < / label >
< label class = " champ " > < span > Disque < / span > < input data - v = " disque " value = " $ { echapper(s.disque)} " placeholder = " 16G " oninput = " definirServeur( ' disque ' , this.value) " > < / label >
< label class = " champ " > < span > Mémoire < / span > < input data - v = " memoire " type = " number " value = " $ { echapper(s.memoire)} " placeholder = " 2048 " oninput = " definirServeur( ' memoire ' , this.value) " > < / label >
< label class = " champ " > < span > Cœurs < / span > < input data - v = " coeurs " type = " number " value = " $ { echapper(s.coeurs)} " placeholder = " 2 " oninput = " definirServeur( ' coeurs ' , this.value) " > < / label >
< label class = " champ " style = " grid-column:1/-1 " > < span > Intégrations ( rôles client ) < / span > < div class = " integ-cases " > $ { cases } < / div > < / label >
< / div >
< div class = " section-tete " style = " margin:12px 2px 2px " > Applications ici < / div > $ { appsHtml }
< div class = " section-tete " style = " margin:12px 2px 2px " > Bases ici < / div > $ { bdHtml }
< div class = " dsn " style = " padding:6px 2px;color:#94a3b8 " > VMID / IP / VLAN sont < b > dérivés < / b > . Sauvegarde ( Ctrl + S ) puis « Appliquer le plan » pour régénérer l ' inventaire.</div>
$ { pied }
< / article > ` ;
majBoutonsDeploiement ( ) ;
2026-06-24 20:17:46 -04:00
}
2026-06-30 12:19:26 -04:00
function serveurValeurValide ( cle , v ) {
const s = String ( v == null ? ' ' : v ) . trim ( ) ;
if ( s == = ' ' ) return true ; / / vide = dérivé / défaut , autorisé
if ( cle == = ' nom ' ) return / ^ [ a - z0 - 9 ] + ( - [ a - z0 - 9 ] + ) * - \d + $ / . test ( s ) ;
if ( cle == = ' memoire ' ) return / ^ \d + $ / . test ( s ) & & + s > = 64 & & + s < = 1048576 ;
if ( cle == = ' coeurs ' ) return / ^ \d + $ / . test ( s ) & & + s > = 1 & & + s < = 256 ;
if ( cle == = ' disque ' ) return / ^ \d + \s * [ GMgm ] ? $ / . test ( s ) ;
return true ;
}
function validerChampServeur ( el ) {
const ok = serveurValeurValide ( el . getAttribute ( ' data-v ' ) , el . value ) ;
el . classList . toggle ( ' invalide ' , ! ok ) ;
return ok ;
}
2026-06-24 20:17:46 -04:00
async function sauvegarderServeurs ( ) {
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const invalides = Array . from ( document . querySelectorAll ( ' #detail [data-v] ' ) ) . filter ( el = > ! validerChampServeur ( el ) ) ;
2026-06-30 12:19:26 -04:00
if ( invalides . length ) {
message ( ' Corrige les ' + invalides . length + ' champ(s) en rouge avant de sauvegarder. ' , ' erreur ' ) ;
invalides [ 0 ] . focus ( ) ;
return ;
}
2026-06-24 20:17:46 -04:00
const srv = { } ;
serveurs . forEach ( s = > {
if ( ! String ( s . nom ) . trim ( ) ) return ;
const o = { fonction : s . fonction , etat : s . etat | | ' planifie ' } ;
[ ' noeud ' , ' stockage ' , ' disque ' ] . forEach ( k = > { if ( String ( s [ k ] | | ' ' ) . trim ( ) ) o [ k ] = s [ k ] ; } ) ;
[ ' memoire ' , ' coeurs ' ] . forEach ( k = > { if ( String ( s [ k ] | | ' ' ) . trim ( ) ) o [ k ] = parseInt ( s [ k ] , 10 ) | | s [ k ] ; } ) ;
const integ = listeDepuisTexte ( s . integrations ) ; if ( integ . length ) o . integrations = integ ;
srv [ s . nom ] = o ;
} ) ;
const rep = await fetch ( ' /api/serveurs ' , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : JSON . stringify ( { serveurs : srv } )
} ) ;
const data = await rep . json ( ) ;
if ( ! rep . ok ) { message ( data . erreur | | ' Sauvegarde refusée. ' , ' erreur ' ) ; return ; }
chargerServeurs ( data ) ; dessinerServeurs ( ) ;
message ( ' Serveurs (plan) sauvegardés. Pense à « Appliquer le plan ». ' , ' ok ' ) ;
}
async function appliquerPlan ( ) {
2026-06-30 12:19:26 -04:00
const t = message ( ' Application du plan… ' , ' ' , { persistant : true } ) ;
try {
const rep = await fetch ( ' /api/instancier ' , { method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } , body : ' {} ' } ) ;
const data = await rep . json ( ) ;
const derniere = ( data . sortie | | ' ' ) . split ( ' \n ' ) . pop ( ) ;
majToast ( t , ( data . ok ? ' Plan appliqué : ' : ' Échec : ' ) + derniere , data . ok ? ' ok ' : ' erreur ' , data . ok ? 4000 : 8000 ) ;
if ( data . ok ) charger ( ) ;
} catch ( e ) {
majToast ( t , ' Erreur réseau : ' + e , ' erreur ' , 8000 ) ;
}
2026-06-24 20:17:46 -04:00
}
function dessinerDependances ( ) {
const cible = document . getElementById ( ' dependances ' ) ;
const noms = Object . keys ( dependances ) . sort ( ) ;
const bloques = noms . filter ( g = > statutDependance ( g ) == = ' bloque ' ) . length ;
const resume = document . getElementById ( ' dep-resume ' ) ;
resume . textContent = bloques ? bloques + ' bloqués ' : noms . length + ' suivis ' ;
if ( ! noms . length ) { cible . innerHTML = ' <div class= " vide " style= " padding:14px " >Aucune dépendance.</div> ' ; return ; }
cible . innerHTML = noms . map ( groupe = > {
const config = dependances [ groupe ] | | { } ;
const requis = config . requiert_groupes_actifs | | [ ] ;
const statut = statutDependance ( groupe ) ;
const manquants = manquantsPourGroupe ( groupe ) ;
const detail = requis . length ? ` requiert $ { requis . join ( ' , ' ) } ` : ' aucun prérequis ' ;
const manque = requis . length & & manquants . length ? ` manquant : $ { manquants . join ( ' , ' ) } ` : ' ' ;
return `
< div class = " carte-dep $ {statut} " title = " $ { echapper(config.raison || ' ' )} " >
< div class = " haut " > < span class = " nom " > $ { echapper ( groupe ) } < / span > < span class = " chip " > < span class = " pt $ { statut === ' ok ' ? ' actif ' : statut === ' bloque ' ? ' bloq ' : ' plan ' } " > < / span > $ { libelleStatut ( statut ) } < / span > < / div >
< div class = " txt " > $ { echapper ( detail ) } < / div >
$ { manque ? ` < div class = " txt " > $ { echapper ( manque ) } < / div > ` : ' ' }
< / div > ` ;
} ) . join ( ' ' ) ;
}
function tuile ( valeur , libelle ) {
const v = ( valeur == = ' ' | | valeur == null ) ? ' — ' : String ( valeur ) ;
return ` < div class = " stat " > < div class = " v $ { v === ' — ' ? ' vide ' : ' ' } " > $ { echapper ( v ) } < / div > < div class = " l " > $ { echapper ( libelle ) } < / div > < / div > ` ;
}
function dessinerDetail ( ) {
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
/ / Le panneau de droite reflète la sélection de la VUE COURANTE .
2026-07-07 15:57:47 -04:00
if ( vuePrincipale == = ' flux ' | | vuePrincipale == = ' couches ' | | vuePrincipale == = ' reseau ' ) return legendeLecture ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( vuePrincipale == = ' applications ' ) return inspecteurApplication ( ) ;
if ( vuePrincipale == = ' bases ' ) return inspecteurBase ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( vuePrincipale == = ' domaines ' ) return inspecteurDomaine ( ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
return inspecteurServeur ( ) ;
2026-06-24 20:17:46 -04:00
}
function majBoutonsDeploiement ( ) {
const bv = document . getElementById ( ' btn-verifier ' ) ;
const bd = document . getElementById ( ' btn-deployer ' ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const hote = hoteOpCourant | | ' ' ;
2026-06-30 12:19:26 -04:00
if ( bv ) { bv . disabled = occupe ; bv . title = occupe ? ' Exécution en cours ' : ' Dry-run (--check --diff), aucune modification ' ; }
if ( bd ) { bd . disabled = occupe | | ! verifie [ hote ] ; bd . title = ! verifie [ hote ] ? ' Vérifiez (dry-run) d \' abord ' : ' Applique les playbooks sur la VM réelle ' ; }
2026-06-24 20:17:46 -04:00
}
function fonctionDe ( nom ) {
const m = ( nom | | ' ' ) . match ( / ^ ( . + ) - ( \d + ) $ / ) ;
return m ? { fonction : m [ 1 ] , seq : parseInt ( m [ 2 ] , 10 ) } : { fonction : nom | | ' ' , seq : null } ;
}
function infoFonction ( fonction ) {
const d = ( nomenclature . fonctions | | { } ) [ fonction ] ;
if ( ! d ) return null ;
const c = ( nomenclature . categories | | { } ) [ d . categorie ] ;
if ( ! c ) return null ;
Adressage derive du seul seed index (rupture, mode compact retire)
Principe : les valeurs de configuration se derivent des intrants, elles ne se
reecrivent pas a la main. La nomenclature dupliquait ce qu'index determine deja
(supernet, sous-reseaux, passerelles, VLAN). Corrige en rupture nette.
- inventory_rules : source unique de derivation — supernet_de, base3_de,
sous_reseau_de, passerelle_de, vlan_de. Modele 6 zones encode une fois
(2e octet = 10+index, 3e octet zone = 15+categorie, VLAN = 1000+index*10+zone).
deriver_nomenclature ne lit plus aucun adressage stocke ; mode compact supprime.
- devis_reseau : importe ces helpers (fin de la duplication) ; decouvre les
tenants sur `index` present (filtre vmid_schema retire).
- GUI : `index` devient un INTRANT (section Reseau). Il vit dans la nomenclature
(plan reseau uniforme, contrairement aux intrants des modeles heterogenes) et
le GUI l'ecrit chirurgicalement (une ligne, sans reformater). Le miroir JS
derive le VLAN du seed (fin de la lecture de c.vlan stocke).
- socle public : nomenclature au format maigre.
Preuve P20 (preuve_nomenclature_derivee) : aucune nomenclature ne stocke
d'adressage — garde-fou permanent, teste en negatif.
Valide : DIFF VIDE sur les 3 instances (la derivation reproduit exactement
l'adressage stocke), 7 modeles valident, devis_reseau genere les memes VLAN
(1011-1016 derives), make verifier rc=0 CONFORME 20/20, node --check du GUI OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 02:58:15 -04:00
/ / VLAN derive du seed index ( miroir de inventory_rules . vlan_de ) : 1000 + index * 10 + zone .
const idx = ( nomenclature . index != null ) ? nomenclature . index : 9 ;
const vlan = 1000 + idx * 10 + Number ( d . categorie ) ;
return { categorie : d . categorie , service : d . service , libelle : c . libelle , vlan } ;
2026-06-24 20:17:46 -04:00
}
function echapper ( valeur ) {
return String ( valeur ) . replace ( / [ & < > " ' ]/g, c => ( { ' & ' : ' & ' , ' < ' : ' < ' , ' > ' : ' > ' , ' " ' : ' & quot ; ' , " ' " : ' ' ' }[c]));
}
function ouvrirConsole ( titre , hote ) {
document . getElementById ( ' console-titre ' ) . textContent = titre + ' · ' + hote ;
document . getElementById ( ' console-sortie ' ) . textContent = ' ' ;
2026-07-07 13:31:02 -04:00
const res = document . getElementById ( ' console-resume ' ) ; res . hidden = true ; res . innerHTML = ' ' ;
2026-06-24 20:17:46 -04:00
const etat = document . getElementById ( ' console-etat ' ) ;
etat . className = ' console-etat actif ' ; etat . textContent = ' ● en cours ' ;
document . getElementById ( ' btn-fermer-console ' ) . disabled = true ;
document . getElementById ( ' console ' ) . hidden = false ;
2026-06-30 12:19:26 -04:00
consoleDebut = Date . now ( ) ;
clearInterval ( chronoConsole ) ;
chronoConsole = setInterval ( ( ) = > {
const s = Math . round ( ( Date . now ( ) - consoleDebut ) / 1000 ) ;
etat . textContent = ' ● en cours · ' + s + ' s ' ;
} , 1000 ) ;
2026-06-24 20:17:46 -04:00
}
function ecrireConsole ( t ) { const p = document . getElementById ( ' console-sortie ' ) ; p . textContent + = t ; p . scrollTop = p . scrollHeight ; }
2026-07-07 13:31:02 -04:00
let dernierResumeTexte = ' ' ;
function afficherResumeErreur ( resume ) {
const el = document . getElementById ( ' console-resume ' ) ;
const echecs = ( resume & & resume . echecs ) | | [ ] ;
if ( ! echecs . length ) { el . hidden = true ; return ; }
const lignes = echecs . map ( e = >
` < li > < b > $ { echapper ( e . tache ) } < / b > — < span class = " re-hote " > $ { echapper ( e . hote ) } < / span > < span class = " re-type " > $ { echapper ( e . type ) } < / span >
< div class = " re-msg " > $ { echapper ( e . message ) } < / div > < / li > ` ) . join ( ' ' ) ;
dernierResumeTexte = echecs . map ( e = > ` • [ $ { e . type } ] $ { e . tache } @ $ { e . hote } : $ { e . message } ` ) . join ( ' \n ' ) ;
el . innerHTML = ` < div class = " re-tete " > ❌ Échec — $ { echecs . length } tâche ( s ) en erreur
< button type = " button " class = " fantome " onclick = " copierResume() " title = " Copier un résumé clair à transmettre " > Copier < / button > < / div >
< ul class = " re-liste " > $ { lignes } < / ul > ` ;
el . hidden = false ;
}
function copierResume ( ) {
if ( navigator . clipboard ) navigator . clipboard . writeText ( dernierResumeTexte )
. then ( ( ) = > message ( ' Résumé copié — à transmettre au mainteneur. ' , ' ok ' ) , ( ) = > { } ) ;
}
2026-06-24 20:17:46 -04:00
function finirConsole ( rc ) {
2026-06-30 12:19:26 -04:00
clearInterval ( chronoConsole ) ; chronoConsole = null ;
const s = Math . round ( ( Date . now ( ) - consoleDebut ) / 1000 ) ;
2026-06-24 20:17:46 -04:00
const etat = document . getElementById ( ' console-etat ' ) ;
etat . className = ' console-etat ' + ( rc == = 0 ? ' ok ' : ' echec ' ) ;
2026-06-30 12:19:26 -04:00
etat . textContent = ( rc == = 0 ? ' ✓ succès ' : ' ✗ échec (rc= ' + rc + ' ) ' ) + ' · ' + s + ' s ' ;
2026-06-24 20:17:46 -04:00
document . getElementById ( ' btn-fermer-console ' ) . disabled = false ;
}
function fermerConsole ( ) { if ( ! occupe ) document . getElementById ( ' console ' ) . hidden = true ; }
function ouvrirModale ( titre , texte ) {
document . getElementById ( ' modale-titre ' ) . textContent = titre ;
document . getElementById ( ' modale-texte ' ) . textContent = texte ;
const champ = document . getElementById ( ' modale-vault ' ) ;
champ . value = motVault | | ' ' ;
document . getElementById ( ' modale ' ) . hidden = false ;
setTimeout ( ( ) = > champ . focus ( ) , 0 ) ;
return new Promise ( res = > { modaleResolveur = res ; } ) ;
}
function confirmerModale ( ) {
const v = document . getElementById ( ' modale-vault ' ) . value ;
document . getElementById ( ' modale ' ) . hidden = true ;
const r = modaleResolveur ; modaleResolveur = null ;
if ( r ) r ( v ) ;
}
function annulerModale ( ) {
document . getElementById ( ' modale ' ) . hidden = true ;
const r = modaleResolveur ; modaleResolveur = null ;
if ( r ) r ( null ) ;
}
2026-06-30 10:07:17 -04:00
function ouvrirIntrants ( ) {
if ( ! intrants ) { message ( ' Intrants indisponibles (rechargez). ' , ' erreur ' ) ; return ; }
rendreIntrants ( ) ;
document . getElementById ( ' intrants ' ) . hidden = false ;
}
function fermerIntrants ( ) { document . getElementById ( ' intrants ' ) . hidden = true ; }
function rendreIntrants ( ) {
const corps = document . getElementById ( ' intrants-corps ' ) ;
const schema = intrants . schema | | [ ] ;
const valeurs = intrants . valeurs | | { } ;
const sections = [ ] ;
const parSection = { } ;
schema . forEach ( ch = > {
if ( ! parSection [ ch . section ] ) { parSection [ ch . section ] = [ ] ; sections . push ( ch . section ) ; }
parSection [ ch . section ] . push ( ch ) ;
} ) ;
let html = ' ' ;
sections . forEach ( sec = > {
html + = ' <div class= " intr-section " ><div class= " intr-tete " > ' + echapper ( sec ) + ' </div><div class= " intr-grille " > ' ;
parSection [ sec ] . forEach ( ch = > {
const v = valeurs [ ch . variable ] ;
2026-06-30 12:36:51 -04:00
let badge ;
if ( ch . classe == = ' constante ' ) badge = ' <span class= " badge-classe const " title= " Valeur unique, non surchargeable " >Constante</span> ' ;
else if ( ch . classe == = ' catalogue ' ) badge = ' <span class= " badge-classe cat " title= " Liste de valeurs autorisées, proposée dans les menus déroulants " >Catalogue</span> ' ;
else badge = ' <span class= " badge-classe def " title= " Valeur par défaut, surchargeable dans les instances " >Défaut</span> ' ;
2026-06-30 10:07:17 -04:00
const lib = echapper ( ch . libelle ) + badge ;
if ( ch . type == = ' bool ' ) {
const coche = ( v == = true | | v == = ' true ' | | v == = ' True ' ) ? ' checked ' : ' ' ;
html + = ' <label class= " champ " ><span> ' + lib + ' </span> '
+ ' <input type= " checkbox " data-intrant= " ' + echapper ( ch . variable ) + ' " data-type= " bool " ' + coche + ' ></label> ' ;
2026-06-30 12:36:51 -04:00
} else if ( ch . type == = ' liste ' ) {
const txt = Array . isArray ( v ) ? v . join ( ' , ' ) : ( v == null ? ' ' : v ) ;
html + = ' <label class= " champ " style= " grid-column:1/-1 " ><span> ' + lib + ' <span class= " unite " >· séparés par des virgules</span></span> '
+ ' <input type= " text " data-intrant= " ' + echapper ( ch . variable ) + ' " data-type= " liste " '
+ ' placeholder= " ex. asgard, pve-02, pve-03 " value= " ' + echapper ( txt ) + ' " ></label> ' ;
2026-06-30 10:07:17 -04:00
} else {
const t = ch . type == = ' int ' ? ' number ' : ' text ' ;
html + = ' <label class= " champ " ><span> ' + lib + ' </span> '
+ ' <input type= " ' + t + ' " data-intrant= " ' + echapper ( ch . variable ) + ' " data-type= " ' + echapper ( ch . type ) + ' " '
+ ' value= " ' + echapper ( v == null ? ' ' : v ) + ' " ></label> ' ;
}
} ) ;
html + = ' </div></div> ' ;
} ) ;
/ / Nomenclature en lecture seule ( lue ailleurs : vue Nomenclature )
const n = nomenclature | | { } ;
if ( Object . keys ( n ) . length ) {
html + = ' <div class= " intr-section " ><div class= " intr-tete " >Nomenclature (lecture seule)</div> '
+ ' <div class= " intr-ro " > ' ;
if ( n . supernet ) html + = ' Supernet : <b> ' + echapper ( n . supernet ) + ' </b><br> ' ;
if ( n . domaine | | n . domaine_interne ) html + = ' Domaine : <b> ' + echapper ( n . domaine | | n . domaine_interne ) + ' </b><br> ' ;
const nbCat = n . categories ? Object . keys ( n . categories ) . length : 0 ;
const nbFon = n . fonctions ? Object . keys ( n . fonctions ) . length : 0 ;
html + = nbCat + ' catégorie(s), ' + nbFon + ' fonction(s) déclarée(s). ' ;
html + = ' <br><span style= " font-style:italic " >Modifiable dans le plan (nomenclature.yml), pas ici.</span> ' ;
html + = ' </div></div> ' ;
}
/ / Secrets attendus : informatif , jamais saisis ici
const secrets = intrants . secrets_attendus | | [ ] ;
if ( secrets . length ) {
html + = ' <div class= " intr-section " ><div class= " intr-tete " >Secrets attendus (Vault — CLI)</div> '
+ ' <div class= " intr-ro " >Ces clés ne sont jamais saisies ni stockées ici. Édite-les via <b>ansible-vault</b>. '
+ ' <div class= " intr-secrets " > '
+ secrets . map ( s = > ' <code> ' + echapper ( s ) + ' </code> ' ) . join ( ' ' )
+ ' </div></div></div> ' ;
}
const f = intrants . fichiers | | { } ;
const chemins = Object . values ( f ) . map ( x = > echapper ( x ) ) . join ( ' , ' ) ;
if ( chemins ) html + = ' <div class= " intr-ro " style= " margin-top:2px " >Écrit dans : <b> ' + chemins + ' </b></div> ' ;
corps . innerHTML = html ;
}
async function enregistrerIntrants ( ) {
const valeurs = { } ;
document . querySelectorAll ( ' #intrants-corps [data-intrant] ' ) . forEach ( el = > {
const k = el . getAttribute ( ' data-intrant ' ) ;
valeurs [ k ] = el . type == = ' checkbox ' ? el . checked : el . value ;
} ) ;
const ancienDomaine = ( intrants . valeurs | | { } ) . domaine_interne ;
const nouveauDomaine = valeurs . domaine_interne ;
if ( nouveauDomaine != = undefined & & nouveauDomaine != = ancienDomaine ) {
if ( ! confirm ( ' Changer le domaine interne (« ' + ( ancienDomaine | | ' ∅ ' ) + ' » → « ' + ( nouveauDomaine | | ' ∅ ' )
+ ' ») impacte tout l \' écosystème (DNS, certificats, noms d \' hôtes). Continuer ? ' ) ) return ;
}
try {
const reponse = await fetch ( ' /api/intrants ' , {
method : ' POST ' ,
headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( { valeurs } ) ,
} ) ;
if ( ! reponse . ok ) {
let detail = ' ' ;
try { detail = ( await reponse . json ( ) ) . erreur | | ' ' ; } catch ( e ) { detail = await reponse . text ( ) ; }
message ( ' Échec de l \' enregistrement des intrants : ' + detail , ' erreur ' ) ;
return ;
}
const data = await reponse . json ( ) ;
intrants = data . intrants | | intrants ;
nomenclature = data . nomenclature | | nomenclature ;
fermerIntrants ( ) ;
message ( ' Intrants de base enregistrés. ' , ' ok ' ) ;
} catch ( e ) {
message ( ' Erreur réseau : ' + e , ' erreur ' ) ;
}
}
2026-06-24 20:17:46 -04:00
async function lancer ( mode , index ) {
const hote = hotes [ index ] ? hotes [ index ] . nom : ' ' ;
2026-06-30 12:19:26 -04:00
if ( ! hote ) { message ( ' Hôte introuvable. ' , ' erreur ' ) ; return ; }
2026-06-24 20:17:46 -04:00
if ( occupe ) return ;
let vault = ' ' ;
2026-07-04 14:40:02 -04:00
if ( ( mode == = ' deployer ' | | mode == = ' pousser ' ) & & ! verifie [ hote ] ) { message ( ' Vérifiez (dry-run) d \' abord. ' , ' erreur ' ) ; return ; }
2026-07-01 21:18:10 -04:00
if ( mode == = ' creer ' | | mode == = ' deployer ' | | mode == = ' pousser ' | | mode == = ' verifier ' ) {
2026-07-01 15:58:32 -04:00
if ( estProduction & & ( mode == = ' deployer ' | | mode == = ' pousser ' ) ) {
const saisi = prompt ( ' ⚠️ PRODUCTION — action réelle sur « ' + hote + ' ». \n '
2026-06-30 16:20:18 -04:00
+ ' Tape le nom exact de l \' hôte pour confirmer : ' ) ;
if ( saisi == = null ) return ;
if ( saisi . trim ( ) != = hote ) { message ( ' Confirmation annulée : le nom ne concorde pas. ' , ' erreur ' ) ; return ; }
}
2026-07-01 15:58:32 -04:00
const av = estProduction ? ' ⚠️ PRODUCTION. ' : ' ' ;
const txt = mode == = ' creer '
? ' Clone la VM « ' + hote + ' » depuis le golden template sur Proxmox (création réelle). '
: mode == = ' pousser '
? av + ' Déploie l \' hôte « ' + hote + ' » (applique ses playbooks, en root). Non transactionnel. '
2026-07-01 21:18:10 -04:00
: mode == = ' verifier '
? ' Dry-run (--check), AUCUNE modification. Mot de passe vault pour déchiffrer les secrets — laisse VIDE si cet hôte n \' a pas de secrets. '
2026-07-01 15:58:32 -04:00
: av + ' Applique les playbooks de groupes sur la VM réelle, en root (sudo). Non transactionnel. ' ;
2026-07-04 14:40:02 -04:00
const titre = ( mode == = ' creer ' ? ' Créer la VM — ' : mode == = ' pousser ' ? ' Déployer — ' : mode == = ' verifier ' ? ' Vérifier — ' : ' Déployer — ' ) + hote ;
2026-07-01 15:58:32 -04:00
vault = await ouvrirModale ( titre , txt ) ;
2026-06-24 20:17:46 -04:00
if ( vault == = null ) return ;
motVault = vault ;
}
occupe = true ; majBoutonsDeploiement ( ) ;
2026-07-04 14:40:02 -04:00
const titreConsole = { verifier : ' Vérification (dry-run) ' , deployer : ' Déploiement ' , creer : ' Création VM (clone) ' , pousser : ' Déploiement ' } [ mode ] | | mode ;
2026-07-01 15:58:32 -04:00
ouvrirConsole ( titreConsole , hote ) ;
2026-06-24 20:17:46 -04:00
try {
const rep = await fetch ( ' /api/ ' + mode , {
method : ' POST ' , headers : { ' Content-Type ' : ' application/json ' , ' X-Jeton ' : JETON } ,
body : JSON . stringify ( vault ? { hote , vault } : { hote } )
} ) ;
if ( ! rep . ok | | ! rep . body ) {
const d = await rep . json ( ) . catch ( ( ) = > ( { } ) ) ;
ecrireConsole ( ( d . erreur | | ' Refusé. ' ) + ' \n ' ) ; finirConsole ( 1 ) ;
occupe = false ; majBoutonsDeploiement ( ) ; return ;
}
const lecteur = rep . body . getReader ( ) ;
const dec = new TextDecoder ( ) ;
2026-07-07 13:31:02 -04:00
let rc = null ; let brut = ' ' ;
2026-06-24 20:17:46 -04:00
while ( true ) {
const { value , done } = await lecteur . read ( ) ;
if ( done ) break ;
let texte = dec . decode ( value , { stream : true } ) ;
2026-07-07 13:31:02 -04:00
brut + = texte ;
2026-06-24 20:17:46 -04:00
const m = texte . match ( / __FIN__ rc = ( - ? \d + ) / ) ;
2026-07-07 13:31:02 -04:00
if ( m ) { rc = parseInt ( m [ 1 ] , 10 ) ; }
texte = texte . replace ( / \n ? __RESUME__ . * \n ? / , ' ' ) . replace ( / \n ? __FIN__ rc = - ? \d + \n ? / , ' ' ) ;
2026-06-24 20:17:46 -04:00
if ( texte ) ecrireConsole ( texte ) ;
}
if ( rc == = null ) rc = 1 ;
2026-07-07 13:31:02 -04:00
const mr = brut . match ( / __RESUME__ ( . + ) / ) ;
if ( rc != = 0 & & mr ) { try { afficherResumeErreur ( JSON . parse ( mr [ 1 ] ) ) ; } catch ( e ) { } }
2026-06-24 20:17:46 -04:00
finirConsole ( rc ) ;
if ( mode == = ' verifier ' & & rc == = 0 ) { verifie [ hote ] = true ; message ( ' Vérification réussie — déploiement débloqué pour ' + hote + ' . ' , ' ok ' ) ; }
2026-07-01 17:52:39 -04:00
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 ( ) ; }
2026-07-04 14:40:02 -04:00
else if ( mode == = ' deployer ' | | mode == = ' pousser ' ) { verifie [ hote ] = false ; message ( rc == = 0 ? ' Déploiement terminé — hôte marqué actif. ' : ' Déploiement en échec. ' , rc == = 0 ? ' ok ' : ' erreur ' ) ; if ( rc == = 0 ) appliquerPlan ( ) ; else sonder ( ) ; }
2026-06-24 20:17:46 -04:00
} catch ( e ) {
ecrireConsole ( ' Erreur client : ' + e . message + ' \n ' ) ; finirConsole ( 1 ) ;
}
occupe = false ; majBoutonsDeploiement ( ) ;
}
2026-06-30 12:19:26 -04:00
function sauvegarderVue ( ) {
if ( occupe ) return ;
if ( vuePrincipale == = ' serveurs ' ) sauvegarderServeurs ( ) ;
else if ( vuePrincipale == = ' applications ' ) sauvegarderApplications ( ) ;
else if ( vuePrincipale == = ' bases ' ) sauvegarderBases ( ) ;
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
else if ( vuePrincipale == = ' domaines ' ) sauvegarderDomaines ( ) ;
else message ( ' Vue en lecture seule (Flux / Couches) — édite dans Serveurs / Applications / Bases / Domaines. ' ) ;
2026-06-30 12:19:26 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function ordreServeursVisibles ( ) {
const vis = serveurs . map ( ( s , i ) = > [ s , i ] ) . filter ( ( [ s ] ) = > correspondServeur ( s ) ) ;
return [ . . . vis . filter ( ( [ s ] ) = > s . etat == = ' actif ' ) , . . . vis . filter ( ( [ s ] ) = > s . etat != = ' actif ' ) ] . map ( ( [ , i ] ) = > i ) ;
2026-06-30 12:19:26 -04:00
}
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
function naviguerServeurs ( delta ) {
const ordre = ordreServeursVisibles ( ) ;
2026-06-30 12:19:26 -04:00
if ( ! ordre . length ) return ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
let pos = ordre . indexOf ( selServeur ) ;
2026-06-30 12:19:26 -04:00
pos = pos < 0 ? ( delta > 0 ? 0 : ordre . length - 1 )
: Math . min ( ordre . length - 1 , Math . max ( 0 , pos + delta ) ) ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
choisirServeur ( ordre [ pos ] ) ;
const c = document . getElementById ( ' tuile-srv- ' + ordre [ pos ] ) ;
2026-06-30 12:19:26 -04:00
if ( c ) c . scrollIntoView ( { block : ' nearest ' , behavior : ' smooth ' } ) ;
2026-06-24 20:17:46 -04:00
}
document . addEventListener ( ' keydown ' , e = > {
if ( e . key == = ' Escape ' & & modaleResolveur ) { annulerModale ( ) ; return ; }
2026-06-30 12:19:26 -04:00
if ( ( e . ctrlKey | | e . metaKey ) & & ( e . key == = ' s ' | | e . key == = ' S ' ) ) { e . preventDefault ( ) ; sauvegarderVue ( ) ; return ; }
const dansSaisie = / ^ ( INPUT | SELECT | TEXTAREA ) $ / . test ( ( document . activeElement | | { } ) . tagName | | ' ' ) ;
if ( dansSaisie | | e . ctrlKey | | e . metaKey | | e . altKey ) return ;
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
const vues = { ' 1 ' : ' serveurs ' , ' 2 ' : ' applications ' , ' 3 ' : ' bases ' } ;
2026-06-30 12:19:26 -04:00
if ( vues [ e . key ] ) { setVue ( vues [ e . key ] ) ; return ; }
if ( e . key == = ' / ' ) { e . preventDefault ( ) ; const f = document . getElementById ( ' filtre ' ) ; if ( f ) f . focus ( ) ; return ; }
GUI : atelier maître-détail unifié + fusion Serveur/Hôte
Toutes les vues adoptent le même motif que l'Inventaire : tuiles à
gauche, détail + saisie à droite, panneau droit contextuel (fin du
panneau figé au changement de vue). Applications et Bases passent en
maître-détail ; liens cliquables entre objets.
Les vues Inventaire (hôtes) et Serveurs (plan) faisaient doublon : elles
sont fusionnées en une vue Serveurs unique — tuile avec statut de
réconciliation, détail réunissant identité éditable, dérivés, groupes,
applications/bases hébergées et Vérifier/Déployer. Navigation clavier et
filtre rebranchés sur les serveurs ; bandeau « Comment lire ce parc ».
Code mort retiré (carte, renduChaine, ONGLETS, champLecture, sélection
d'hôte, chips de filtre).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:22:11 -04:00
if ( vuePrincipale != = ' serveurs ' ) return ;
if ( e . key == = ' j ' ) { naviguerServeurs ( 1 ) ; return ; }
if ( e . key == = ' k ' ) { naviguerServeurs ( - 1 ) ; return ; }
if ( e . key == = ' v ' ) { const b = document . getElementById ( ' btn-verifier ' ) ; if ( b & & ! b . disabled ) b . click ( ) ; return ; }
if ( e . key == = ' d ' ) { const b = document . getElementById ( ' btn-deployer ' ) ; if ( b & & ! b . disabled ) b . click ( ) ; return ; }
2026-06-30 12:19:26 -04:00
} ) ;
/ / Validation inline des champs du plan ( vue Serveurs ) — liseré rouge avant l ' envoi.
document . addEventListener ( ' input ' , e = > {
if ( e . target & & e . target . matches & & e . target . matches ( ' [data-v] ' ) ) validerChampServeur ( e . target ) ;
} ) ;
/ / Garde - fou : prévenir la perte d ' éditions de plan non sauvegardées.
window . addEventListener ( ' beforeunload ' , e = > {
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
if ( serveursModifie | | applicationsModifie | | basesModifie | | domainesModifie ) { e . preventDefault ( ) ; e . returnValue = ' ' ; }
2026-06-24 20:17:46 -04:00
} ) ;
2026-06-30 10:07:17 -04:00
/ / - - - - - - Info - bulles d ' aide au survol de la description d ' un champ - - - - - -
( function ( ) {
const AIDES = {
' Adresse IP ' : " Adresse IPv4 de la VM sur son VLAN. Ex. 10.10.12.11. Souvent dérivée — clic « Proposer ». " ,
' CIDR ' : " Masque réseau en bits. 24 = /24 (255.255.255.0). " ,
' Passerelle ' : " Routeur de sortie du VLAN. En général le .1 du sous-réseau. " ,
' VLAN ' : " Identifiant 802.1Q (1– 4094). Dérivé de la fonction via la nomenclature. " ,
' Pont ' : " Pont réseau Proxmox. Ex. vmbr0. " ,
' DNS ' : " Résolveur DNS de la VM. En général le DNS interne de l ' écosystème. " ,
' Utilisateur Ansible ' : " Compte technique utilisé par Ansible. Par défaut : ansible. " ,
' VMID ' : " Identifiant unique Proxmox. Dérivé (catégorie + service + rang). " ,
' Nœud ' : " Hôte Proxmox cible. Ex. pve-01. " ,
' Stockage ' : " Pool de stockage Proxmox du disque. Ex. local-zfs. " ,
' Disque ' : " Taille du disque système. Ex. 32G. Dérivée des logiciels hébergés + socle. " ,
' Mémoire ' : " RAM en Mo. Dérivée des logiciels hébergés + socle (avec marge). " ,
' Cœurs ' : " vCPU alloués. Dérivés des logiciels hébergés (maximum, pas additif). " ,
' Nom ' : " Identifiant unique du serveur de base, sans espace. " ,
' Type ' : " Moteur de la base. Ex. postgres, redis. " ,
' Hôte ' : " VM qui héberge ce service (nom du serveur). " ,
' Hôte (VM) ' : " VM cible où l ' application est déployée. " ,
' Port ' : " Port d ' écoute du service. " ,
' Groupe Ansible ' : " Groupe/rôle Set-OPS appliqué. Ex. serveur_postgresql. " ,
' Groupe (rôle) ' : " Rôle Set-OPS appliqué à l ' application. " ,
' Identifiant ' : " Clé courte et unique, sans espace. " ,
' Portée ' : " À qui la base est offerte : groupe, hôte ou application. " ,
' Consommateur ' : " L ' entité (groupe / hôte / application) qui utilise la base. " ,
' Usage ' : " Étiquette libre. Ex. principale. " ,
' Serveur ' : " Serveur de base de données qui héberge cette base. " ,
' Base ' : " Nom de la base de données. " ,
' Propriétaire ' : " Rôle SQL propriétaire de la base. " ,
' Secret (Vault) ' : " Nom de la variable Ansible Vault contenant le mot de passe. " ,
' Requiert (applis) ' : " Applications prérequises, séparées par des virgules. " ,
' Expose (FQDN) ' : " Nom(s) public(s) exposé(s). Ex. forge.exemple.ca. " ,
' Serveur (fonction-NN) ' : " Nom = fonction + rang. Ex. web-frontal-03. " ,
} ;
const bulle = document . createElement ( ' div ' ) ;
bulle . id = ' infobulle ' ;
bulle . setAttribute ( ' role ' , ' tooltip ' ) ;
document . body . appendChild ( bulle ) ;
function libelleDe ( el ) {
const n = el . firstChild ;
return ( ( n & & n . nodeType == = 3 ? n . textContent : el . textContent ) | | ' ' ) . trim ( ) ;
}
function aideDe ( el ) {
if ( ! el | | ! el . matches | | ! el . matches ( ' .champ > span ' ) ) return null ;
return AIDES [ libelleDe ( el ) ] | | null ;
}
function placer ( el ) {
const r = el . getBoundingClientRect ( ) ;
bulle . style . left = Math . max ( 8 , Math . min ( r . left , innerWidth - bulle . offsetWidth - 8 ) ) + ' px ' ;
let haut = r . top - bulle . offsetHeight - 8 ;
if ( haut < 8 ) haut = r . bottom + 8 ; / / bascule sous le champ si pas la place
bulle . style . top = haut + ' px ' ;
}
document . addEventListener ( ' mouseover ' , e = > {
const aide = aideDe ( e . target ) ;
if ( ! aide ) return ;
bulle . textContent = aide ;
bulle . classList . add ( ' on ' ) ;
placer ( e . target ) ;
} ) ;
document . addEventListener ( ' mouseout ' , e = > {
if ( e . target & & e . target . matches & & e . target . matches ( ' .champ > span ' ) ) bulle . classList . remove ( ' on ' ) ;
} ) ;
/ / Affordance : souligné pointillé sur les descriptions qui ont une aide .
function marquer ( racine ) {
if ( ! racine . querySelectorAll ) return ;
racine . querySelectorAll ( ' .champ > span ' ) . forEach ( s = > {
if ( AIDES [ libelleDe ( s ) ] ) s . classList . add ( ' a-aide ' ) ;
} ) ;
}
new MutationObserver ( muts = > muts . forEach ( m = >
m . addedNodes . forEach ( n = > { if ( n . nodeType == = 1 ) marquer ( n ) ; } )
) ) . observe ( document . body , { childList : true , subtree : true } ) ;
marquer ( document ) ;
} ) ( ) ;
2026-06-24 20:17:46 -04:00
charger ( ) . catch ( err = > message ( err . message , ' erreur ' ) ) ;
< / script >
< / body >
< / html >
"""
class Gestionnaire ( BaseHTTPRequestHandler ) :
inventaire : Path = INVENTAIRE_DEFAUT
def log_message ( self , * args ) - > None : # silence par defaut
pass
def repondre ( self , status : int , contenu : bytes , content_type : str ) - > None :
try :
self . send_response ( status )
self . send_header ( " Content-Type " , content_type )
self . send_header ( " Content-Length " , str ( len ( contenu ) ) )
self . end_headers ( )
self . wfile . write ( contenu )
except ( BrokenPipeError , ConnectionResetError ) :
pass # le client a ferme la connexion : rien a faire
def repondre_json ( self , status : int , data : dict ) - > None :
self . repondre ( status , json . dumps ( data , ensure_ascii = False ) . encode ( " utf-8 " ) , " application/json; charset=utf-8 " )
def _ecrire_flux ( self , texte : str ) - > bool :
try :
self . wfile . write ( texte . encode ( " utf-8 " , " replace " ) )
self . wfile . flush ( )
return True
except ( BrokenPipeError , ConnectionResetError ) :
return False
def executer_flux ( self , hote : str , mode : str , vault : str | None = None ) - > None :
if not MOTIF_HOTE . match ( hote or " " ) :
self . repondre_json ( 400 , { " erreur " : " Nom d ' hote invalide. " } )
return
2026-07-01 15:58:32 -04:00
tous_hotes = { h . get ( " nom " ) for h in liste_hotes ( charger_yaml ( self . inventaire ) ) }
if hote not in tous_hotes :
self . repondre_json ( 409 , { " erreur " : f " { hote } est inconnu de l ' inventaire (applique le plan d ' abord). " } )
return
# 'creer' (clone) est permis meme sur un hote planifie ; les autres exigent actif.
if mode in ( " verifier " , " deployer " , " pousser " ) and hote not in hotes_actifs_fichier ( self . inventaire ) :
2026-06-24 20:17:46 -04:00
self . repondre_json ( 409 , { " erreur " : f " { hote } n ' est pas un hote actif sauvegarde. " } )
return
if mode == " deployer " and not VERIF_OK . get ( hote ) :
self . repondre_json ( 409 , { " erreur " : " Verification (dry-run) requise avant de deployer. " } )
return
if not VERROU . acquire ( blocking = False ) :
self . repondre_json ( 409 , { " erreur " : " Une execution est deja en cours. " } )
return
fichier_vault = None
2026-07-07 12:49:17 -04:00
journal = None
2026-06-24 20:17:46 -04:00
try :
2026-07-01 15:58:32 -04:00
cible = { " deployer " : " deployer " , " pousser " : " deployer " ,
" creer " : " creer-vm " } . get ( mode , " verifier-deploiement " )
2026-06-24 20:17:46 -04:00
env = dict ( os . environ , PYTHONUNBUFFERED = " 1 " , ANSIBLE_FORCE_COLOR = " 0 " )
if vault :
fd , fichier_vault = tempfile . mkstemp ( prefix = " setops-vault- " ) # 0600 par defaut
os . write ( fd , str ( vault ) . encode ( " utf-8 " ) )
os . close ( fd )
env [ " ANSIBLE_VAULT_PASSWORD_FILE " ] = fichier_vault
self . send_response ( 200 )
self . send_header ( " Content-Type " , " text/plain; charset=utf-8 " )
self . send_header ( " Cache-Control " , " no-cache " )
self . end_headers ( )
2026-07-07 12:49:17 -04:00
# Persistance : un journal horodaté par exécution, en plus du streaming live.
dossier_logs = self . inventaire . parents [ 2 ] / " logs "
dossier_logs . mkdir ( parents = True , exist_ok = True )
chemin_log = dossier_logs / f " { hote } - { cible } - { time . strftime ( ' % Y % m %d T % H % M % S ' ) } .log "
journal = open ( chemin_log , " w " , encoding = " utf-8 " )
entete = f " $ make { cible } HOTE= { hote } \n \n "
journal . write ( entete )
self . _ecrire_flux ( entete )
2026-07-07 13:31:02 -04:00
tache_courante = " "
echecs : list [ dict ] = [ ]
2026-06-24 20:17:46 -04:00
proc = subprocess . Popen (
[ " make " , cible , f " HOTE= { hote } " ] ,
cwd = str ( RACINE ) , env = env ,
stdin = subprocess . DEVNULL ,
stdout = subprocess . PIPE , stderr = subprocess . STDOUT , text = True , bufsize = 1 ,
)
try :
for ligne in proc . stdout :
2026-07-07 12:49:17 -04:00
journal . write ( ligne )
2026-07-07 13:31:02 -04:00
if ligne . startswith ( " TASK [ " ) :
deb , fin = ligne . find ( " [ " ) , ligne . find ( " ] " )
if - 1 < deb < fin :
tache_courante = ligne [ deb + 1 : fin ]
elif ( " FAILED! " in ligne or " UNREACHABLE! " in ligne ) and \
( " fatal: " in ligne or ligne . lstrip ( ) . startswith ( " failed: " ) ) :
echec = _extraire_echec ( ligne , tache_courante )
if echec :
echecs . append ( echec )
2026-06-24 20:17:46 -04:00
if not self . _ecrire_flux ( ligne ) :
2026-07-07 12:49:17 -04:00
# Navigateur déconnecté : on continue à journaliser jusqu'au bout.
for reste in proc . stdout :
journal . write ( reste )
2026-06-24 20:17:46 -04:00
break
finally :
rc = proc . wait ( )
if mode == " verifier " and rc == 0 :
VERIF_OK [ hote ] = True
2026-07-01 15:58:32 -04:00
elif mode in ( " deployer " , " pousser " ) :
2026-06-24 20:17:46 -04:00
VERIF_OK . pop ( hote , None )
2026-07-01 17:39:44 -04:00
# Auto-actif : un hote qu'on vient de materialiser (clone reussi) ou de
# deployer passe 'actif' dans le plan. Reflet fidele : materialise = actif.
if rc == 0 and mode in ( " creer " , " deployer " , " pousser " ) :
try :
marquer_serveur_actif ( hote )
except Exception :
pass
2026-07-07 13:31:02 -04:00
if rc != 0 and echecs :
self . _ecrire_flux ( " __RESUME__ " + json . dumps ( { " echecs " : echecs [ : 6 ] } , ensure_ascii = False ) + " \n " )
2026-07-07 12:49:17 -04:00
journal . write ( f " \n __FIN__ rc= { rc } \n " )
self . _ecrire_flux ( f " \n [journal : { os . path . relpath ( chemin_log , RACINE ) } ] \n __FIN__ rc= { rc } \n " )
2026-06-24 20:17:46 -04:00
finally :
2026-07-07 12:49:17 -04:00
if journal :
try :
journal . close ( )
except OSError :
pass
2026-06-24 20:17:46 -04:00
if fichier_vault :
try :
os . remove ( fichier_vault )
except OSError :
pass
VERROU . release ( )
def do_GET ( self ) - > None :
chemin = urlparse ( self . path ) . path
try :
if chemin == " / " :
page = HTML . replace ( " __JETON__ " , JETON )
self . repondre ( 200 , page . encode ( " utf-8 " ) , " text/html; charset=utf-8 " )
elif chemin == " /favicon.ico " :
self . repondre ( 204 , b " " , " image/x-icon " )
elif chemin == " /api/inventaire " :
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
2026-07-01 17:39:44 -04:00
elif chemin == " /api/sondes " :
self . repondre_json ( 200 , sonder_hotes ( self . inventaire ) )
2026-07-07 15:57:47 -04:00
elif chemin == " /api/devis-reseau " :
tenants = devis_reseau . decouvrir ( )
self . repondre_json ( 200 , {
" config " : devis_reseau . generer ( tenants ) if tenants else " " ,
" tenants " : [ { " nom " : nom , " index " : n [ " index " ] } for nom , _ , n in tenants ] ,
} )
Aligne le GUI et la doc sur le multi-instances
GUI (vue Réseau) :
- nouvelle vue FLOTTE (endpoint /api/instances) : liste les instances, marque
l'active (star), montre index / plage VLAN / statut federe-local / prod, et
affiche une banniere de COLLISION d'index — le pendant visuel de make instances
et de la preuve P21.
- le devis n'affiche plus que les instances FEDEREES (coherent avec le filtre
federe ; le labo local en est ecarte).
- retire les deux mentions perimees de « ip-miroir » (concept supprime a la
rupture ; la decouverte se fait desormais sur `index`).
La bascule d'instance et l'edition de `federe` restent au CLI (chirurgie de
symlink / drapeau rarement change) : plan de controle gele.
docs/multi-instances.md :
- section « Gerer la flotte » (make instances, bascule, les deux reflexes).
- section « Comment le moteur decouvre les instances » : convention de dossiers
freres (glob ../*/plan/nomenclature.yml), pas de registre.
- « Adressage federe » reecrit : il mentait encore (VMID compact 21101/1CSNN,
sandbox « sans index », plafond 9). Remplace par le modele derive du seed
(tableau des formules, VMID ip-miroir, drapeau federe: false, plafond reel 245).
- corrige les mentions residuelles de `supernet` a saisir (c'est `index`).
Valide : node --check du GUI, make verifier rc=0 CONFORME 21/21.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 11:57:59 -04:00
elif chemin == " /api/instances " :
import instances as _inst
liste = _inst . decouvrir ( )
self . repondre_json ( 200 , {
" instances " : liste ,
" collisions " : { str ( k ) : v for k , v in _inst . collisions ( liste ) . items ( ) } ,
} )
2026-06-24 20:17:46 -04:00
else :
self . repondre_json ( 404 , { " erreur " : " Introuvable " } )
except Exception as exc :
self . repondre_json ( 500 , { " erreur " : str ( exc ) } )
def do_POST ( self ) - > None :
chemin = urlparse ( self . path ) . path
if self . headers . get ( " X-Jeton " ) != JETON :
self . repondre_json ( 403 , { " erreur " : " Jeton manquant ou invalide. " } )
return
longueur = int ( self . headers . get ( " Content-Length " , " 0 " ) or 0 )
corps = self . rfile . read ( longueur ) . decode ( " utf-8 " ) if longueur else " "
try :
donnees = json . loads ( corps ) if corps else { }
except json . JSONDecodeError :
self . repondre_json ( 400 , { " erreur " : " Corps JSON invalide. " } )
return
try :
if chemin == " /api/inventaire " :
self . repondre_json ( 409 , { " erreur " : " hosts.yml est genere depuis le plan. "
" Edite instance/plan/serveurs.yml et instance/plan/applications.yml "
" (vues Serveurs / Applications), puis « Appliquer le plan ». " } )
elif chemin == " /api/bases " :
valider_bases ( donnees , charger_applications ( FICHIER_APPLICATIONS ) )
ecrire_bases ( FICHIER_BASES , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
elif chemin == " /api/applications " :
2026-07-22 21:32:42 -04:00
valider_applications ( donnees , charger_domaines ( FICHIER_DOMAINES ) ,
charger_serveurs ( FICHIER_SERVEURS ) )
2026-06-24 20:17:46 -04:00
ecrire_applications ( FICHIER_APPLICATIONS , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
elif chemin == " /api/serveurs " :
valider_serveurs ( donnees , charger_nomenclature ( FICHIER_NOMENCLATURE ) )
ecrire_serveurs ( FICHIER_SERVEURS , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
GUI : éditeur Domaines (6e onglet éditable)
Le registre des domaines publics — seul sans éditeur — a désormais son onglet :
ajouter/retirer une zone, autorité (sélecteur), edge, DNSSEC, mail, secondaires,
et affichage des expositions (FQDN) sous la zone. Backend : route /api/domaines,
ecrire_domaines, validation valider_domaines (rejette une autorité inconnue).
Éprouvé : round-trip backend (valide/écrit/rejette interne), node --check, smoke serveur.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 13:08:15 -04:00
elif chemin == " /api/domaines " :
valider_domaines ( donnees )
ecrire_domaines ( FICHIER_DOMAINES , donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
2026-06-30 10:07:17 -04:00
elif chemin == " /api/intrants " :
ecrire_intrants ( donnees )
self . repondre_json ( 200 , inventaire_api ( self . inventaire ) )
2026-06-24 20:17:46 -04:00
elif chemin == " /api/instancier " :
2026-07-01 17:52:39 -04:00
# 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.
2026-06-24 20:17:46 -04:00
resultat = subprocess . run (
2026-07-01 17:52:39 -04:00
[ " python3 " , str ( RACINE / " scripts/instancier.py " ) , " appliquer " , " --force " ] ,
2026-06-24 20:17:46 -04:00
capture_output = True , text = True , cwd = str ( RACINE ) )
self . repondre_json ( 200 , { " ok " : resultat . returncode == 0 ,
" sortie " : ( resultat . stdout + resultat . stderr ) . strip ( ) } )
elif chemin == " /api/verifier " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " verifier " , donnees . get ( " vault " ) )
elif chemin == " /api/deployer " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " deployer " , donnees . get ( " vault " ) )
2026-07-01 15:58:32 -04:00
elif chemin == " /api/creer " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " creer " , donnees . get ( " vault " ) )
elif chemin == " /api/pousser " :
self . executer_flux ( str ( donnees . get ( " hote " , " " ) ) , " pousser " , donnees . get ( " vault " ) )
2026-06-24 20:17:46 -04:00
else :
self . repondre_json ( 404 , { " erreur " : " Introuvable " } )
except Exception as exc :
self . repondre_json ( 400 , { " erreur " : str ( exc ) } )
def main ( ) - > int :
parser = argparse . ArgumentParser ( description = " Interface web locale pour gerer un inventaire Set-OPS. " )
parser . add_argument ( " --inventaire " , type = Path , default = INVENTAIRE_DEFAUT )
parser . add_argument ( " --hote " , default = " 127.0.0.1 " )
parser . add_argument ( " --port " , type = int , default = 8765 )
args = parser . parse_args ( )
Gestionnaire . inventaire = args . inventaire . resolve ( )
serveur = ThreadingHTTPServer ( ( args . hote , args . port ) , Gestionnaire )
print ( f " Interface inventaire: http:// { args . hote } : { args . port } / " )
print ( f " Inventaire: { Gestionnaire . inventaire } " )
serveur . serve_forever ( )
return 0
if __name__ == " __main__ " :
raise SystemExit ( main ( ) )