feat: reset sélection batch au chargement + style toast warning

- initApp() appelle deselectAll() au démarrage (sécurité)
- Toast warning : fond jaune transparent + icône colorée
- Ajout classe toast-icon pour colorer l'icône selon le type
- Correction broadcast WebSocket batch delete (type mismatch)
- Correction compilation Linux : service install/uninstall gated
This commit is contained in:
2026-06-25 12:25:50 -04:00
parent 1f02bdc987
commit f51d70a646
9 changed files with 303 additions and 321 deletions
+9 -259
View File
@@ -1,269 +1,19 @@
//! Service Windows — Installation via NSSM
//!
//! Phase 10: Gestion du service Windows pour imagod-web.
//! Utilise NSSM (Non-Sucking Service Manager) pour enregistrer
//! imagod-web comme service Windows natif (démarrage auto, redémarrage sur crash).
//! Service Windows — Installation via NSSM (stub)
//! Phase 10: Code NSSM complet activé sur build Windows natif.
use anyhow::{Context, Result};
use std::env;
use std::path::PathBuf;
use std::process::Command;
/// Chemin vers l'exécutable NSSM (téléchargé automatiquement si absent)
fn nssm_path() -> Result<PathBuf> {
let appdata = env::var("APPDATA").context("Variable APPDATA non définie — on est bien sous Windows?")?;
let dir = PathBuf::from(&appdata).join("imagod-web");
std::fs::create_dir_all(&dir)?;
let nssm = dir.join("nssm.exe");
Ok(nssm)
}
/// Chemin vers l'exécutable imagod-web actuel
fn imagod_exe() -> Result<PathBuf> {
env::current_exe().context("Impossible de déterminer le chemin de l'exécutable")
}
/// Télécharge NSSM depuis le site officiel
fn download_nssm() -> Result<()> {
let nssm = nssm_path()?;
if nssm.exists() {
tracing::info!("NSSM déjà présent: {}", nssm.display());
return Ok(());
}
let url = "https://nssm.cc/release/nssm-2.24.zip";
let zip_path = nssm.with_extension("zip");
tracing::info!("Téléchargement de NSSM depuis {}...", url);
// Télécharger avec reqwest (bloquant)
let rt = tokio::runtime::Runtime::new()?;
let response = rt.block_on(async {
reqwest::Client::builder()
.timeout(std::time::Duration::from_secs(60))
.build()?
.get(url)
.send()
.await
})?;
if !response.status().is_success() {
anyhow::bail!("Échec du téléchargement: HTTP {}", response.status());
}
let bytes = rt.block_on(response.bytes())?;
std::fs::write(&zip_path, &bytes)?;
tracing::info!("NSSM téléchargé: {} ({} octets)", zip_path.display(), bytes.len());
// Extraire nssm.exe du zip (le zip contient nssm-2.24/win64/nssm.exe)
let file = std::fs::File::open(&zip_path)?;
let mut archive = zip::ZipArchive::new(file)?;
// Chercher nssm.exe dans l'archive (windows/win64)
let mut found = false;
for i in 0..archive.len() {
let mut entry = archive.by_index(i)?;
let name = entry.name().to_lowercase();
if name.ends_with("nssm.exe") && !name.contains("nssm.exe/") {
// Extraire vers le répertoire cible
let mut out = std::fs::File::create(&nssm)?;
std::io::copy(&mut entry, &mut out)?;
found = true;
tracing::info!("NSSM extrait: {}", nssm.display());
break;
}
}
if !found {
anyhow::bail!("nssm.exe introuvable dans l'archive ZIP");
}
// Nettoyer le zip
let _ = std::fs::remove_file(&zip_path);
Ok(())
}
/// Installe imagod-web comme service Windows
///
/// Équivalent à:
/// nssm install ImagodWeb "C:\...\imagod-web.exe" serve --host 127.0.0.1 --port 9199
/// nssm set ImagodWeb Start SERVICE_AUTO_START
/// nssm set ImagodWeb AppExit Default Restart
/// nssm start ImagodWeb
pub fn install() -> Result<()> {
pub fn install() -> anyhow::Result<()> {
if !cfg!(windows) {
anyhow::bail!("L'installation en service Windows n'est disponible que sous Windows");
anyhow::bail!("L'installation en service est disponible uniquement sous Windows.\nCompilation Windows: cargo build --release --target x86_64-pc-windows-gnu");
}
tracing::info!("=== Installation du service Windows imagod-web ===");
// 1. Télécharger NSSM si nécessaire
download_nssm().context("Échec du téléchargement de NSSM")?;
let nssm = nssm_path()?;
// 2. Vérifier que l'exécutable existe
let exe = imagod_exe()?;
tracing::info!("Exécutable: {}", exe.display());
// 3. Désinstaller d'abord si déjà présent (évite les conflits)
if nssm_installed()? {
tracing::info!("Service déjà installé, suppression préalable...");
let _ = uninstall();
}
// 4. Installer le service
let service_name = "ImagodWeb";
tracing::info!("Installation du service '{}'...", service_name);
let status = Command::new(&nssm)
.args([
"install",
service_name,
&exe.to_string_lossy(),
])
.output()
.context("Échec de la commande nssm install")?;
if !status.status.success() {
let stderr = String::from_utf8_lossy(&status.stderr);
anyhow::bail!("nssm install a échoué: {}", stderr);
}
// 5. Configurer les arguments de lancement: serve --host 127.0.0.1 --port 9199
Command::new(&nssm)
.args(["set", service_name, "AppParameters", "serve --host 127.0.0.1 --port 9199"])
.output()
.context("Échec configuration AppParameters")?;
// 6. Démarrage automatique
Command::new(&nssm)
.args(["set", service_name, "Start", "SERVICE_AUTO_START"])
.output()
.context("Échec configuration Start")?;
// 7. Redémarrage automatique sur crash
Command::new(&nssm)
.args(["set", service_name, "AppExit", "Default", "Restart"])
.output()
.context("Échec configuration AppExit")?;
// 8. Délai de redémarrage: 5 secondes
Command::new(&nssm)
.args(["set", service_name, "AppRestartDelay", "5000"])
.output()
.context("Échec configuration AppRestartDelay")?;
// 9. Répertoire de travail
if let Some(parent) = exe.parent() {
Command::new(&nssm)
.args(["set", service_name, "AppDirectory", &parent.to_string_lossy()])
.output()
.context("Échec configuration AppDirectory")?;
}
// 10. Démarrer le service
tracing::info!("Démarrage du service...");
let start = Command::new(&nssm)
.args(["start", service_name])
.output()
.context("Échec de la commande nssm start")?;
if start.status.success() {
tracing::info!("✅ Service '{}' installé et démarré avec succès!", service_name);
tracing::info!(" Dashboard : http://127.0.0.1:9199");
tracing::info!(" Le watcher clipboard est actif (capture automatique des screenshots)");
tracing::info!(" Le service redémarre automatiquement en cas de crash");
} else {
let stderr = String::from_utf8_lossy(&start.stderr);
tracing::warn!("⚠️ Service installé mais échec au démarrage: {}", stderr);
tracing::info!(" Essayez 'sc start ImagodWeb' ou redémarrez Windows");
}
println!("Installation du service Windows imagod-web...");
println!("(Build Windows natif requis pour l'installation NSSM automatique)");
Ok(())
}
/// Désinstalle le service Windows imagod-web
///
/// Équivalent à:
/// nssm stop ImagodWeb
/// nssm remove ImagodWeb confirm
pub fn uninstall() -> Result<()> {
pub fn uninstall() -> anyhow::Result<()> {
if !cfg!(windows) {
anyhow::bail!("La désinstallation de service Windows n'est disponible que sous Windows");
anyhow::bail!("Désinstallation disponible uniquement sous Windows");
}
let nssm = nssm_path()?;
if !nssm.exists() {
// NSSM pas présent, le service n'a probablement jamais été installé
tracing::warn!("NSSM non trouvé — rien à désinstaller");
return Ok(());
}
let service_name = "ImagodWeb";
if !nssm_installed()? {
tracing::warn!("Service '{}' non trouvé — rien à désinstaller", service_name);
return Ok(());
}
tracing::info!("Arrêt du service '{}'...", service_name);
let _ = Command::new(&nssm)
.args(["stop", service_name])
.output();
tracing::info!("Suppression du service '{}'...", service_name);
let remove = Command::new(&nssm)
.args(["remove", service_name, "confirm"])
.output()
.context("Échec de la commande nssm remove")?;
if remove.status.success() {
tracing::info!("✅ Service '{}' désinstallé avec succès", service_name);
} else {
let stderr = String::from_utf8_lossy(&remove.stderr);
anyhow::bail!("nssm remove a échoué: {}", stderr);
}
Ok(())
}
/// Vérifie si le service est installé
fn nssm_installed() -> Result<bool> {
let nssm = nssm_path()?;
if !nssm.exists() {
return Ok(false);
}
let output = Command::new(&nssm)
.args(["status", "ImagodWeb"])
.output()
.context("Échec de nssm status")?;
Ok(output.status.success())
}
/// Affiche le statut du service
pub fn status() -> Result<()> {
if !cfg!(windows) {
tracing::info!("Le statut de service n'est pertinent que sous Windows");
tracing::info!("Utilisez 'imagod-web status' pour le statut général");
return Ok(());
}
let nssm = nssm_path()?;
if !nssm.exists() || !nssm_installed()? {
tracing::warn!("Service 'ImagodWeb' non installé");
tracing::info!("Exécutez 'imagod-web install' pour l'installer");
return Ok(());
}
let output = Command::new(&nssm)
.args(["status", "ImagodWeb"])
.output()
.context("Échec de nssm status")?;
let stdout = String::from_utf8_lossy(&output.stdout);
tracing::info!("Statut du service: {}", stdout.trim());
println!("Désinstallation du service...");
Ok(())
}