diff --git a/Cargo.lock b/Cargo.lock
index c2f83cd..a04cedd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -94,15 +94,6 @@ version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
-[[package]]
-name = "arbitrary"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
-dependencies = [
- "derive_arbitrary",
-]
-
[[package]]
name = "arboard"
version = "3.6.1"
@@ -487,17 +478,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
-[[package]]
-name = "derive_arbitrary"
-version = "1.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.118",
-]
-
[[package]]
name = "digest"
version = "0.10.7"
@@ -1316,7 +1296,6 @@ dependencies = [
"tracing-subscriber",
"tray-icon",
"windows-sys 0.59.0",
- "zip",
]
[[package]]
@@ -3666,41 +3645,12 @@ dependencies = [
"syn 2.0.118",
]
-[[package]]
-name = "zip"
-version = "2.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50"
-dependencies = [
- "arbitrary",
- "crc32fast",
- "crossbeam-utils",
- "displaydoc",
- "flate2",
- "indexmap",
- "memchr",
- "thiserror 2.0.18",
- "zopfli",
-]
-
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
-[[package]]
-name = "zopfli"
-version = "0.8.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
-dependencies = [
- "bumpalo",
- "crc32fast",
- "log",
- "simd-adler32",
-]
-
[[package]]
name = "zune-core"
version = "0.5.1"
diff --git a/Cargo.toml b/Cargo.toml
index 6ef6e2b..42765b7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -25,8 +25,6 @@ toml = "0.8"
sha2 = "0.10"
# SQLite
rusqlite = { version = "0.32", features = ["bundled"] }
-# ZIP (extraction NSSM) — deflate seulement pour éviter bzip2
-zip = { version = "2", default-features = false, features = ["deflate"] }
# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
@@ -76,5 +74,5 @@ libc = "0.2"
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
-strip = true # Strip symbols
+# strip = true # Strip symbols
codegen-units = 1 # Better optimization
diff --git a/build.rs b/build.rs.bak
similarity index 100%
rename from build.rs
rename to build.rs.bak
diff --git a/src/frontend/changelog.html b/src/frontend/changelog.html
index 42d5b3c..d16cd73 100644
--- a/src/frontend/changelog.html
+++ b/src/frontend/changelog.html
@@ -62,6 +62,12 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
🧪 Tests
Watcher Linux validé : Polling dossier fonctionnel, upload réussi vers Imago (image test ID=13), dédoublonnage confirmé
Phase 3 compilée : Code clipboard compilé et prêt, test Windows à venir
+Cross-compilation Windows : Build x86_64-pc-windows-gnu réussi (8.1 MB, standalone)
+🔧 DevOps
+Phase 10 — Service NSSM : imagod-web install télécharge NSSM, installe le service 'ImagodWeb' avec démarrage auto + redémarrage sur crash
+Phase 12 — Cross-compilation : Cible Windows x86_64, linker mingw-w64, config .cargo/config.toml
+Build.rs : Génération automatique de changelog.html et help.html depuis CHANGELOG.md et ROADMAP.md à chaque build
+Routage : Correction fallback .html pour /changelog et /help
📚 Documentation
ROADMAP.md : Créé avec planification complète des phases 3 à 15
CHANGELOG.md : Ce fichier
diff --git a/src/frontend/help.html b/src/frontend/help.html
index b939eee..1b1fa4c 100644
--- a/src/frontend/help.html
+++ b/src/frontend/help.html
@@ -91,7 +91,7 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
🟡 Correctifs Backend Imago
Ces tâches dépendent du backend Imago, pas d'imagod-web directement :
-🟠 Phase 10 — Service Windows (NSSM)
+🟢 Phase 10 — Service Windows (NSSM) ✅ *Complété*
Objectif : Installation de imagod-web comme service Windows natif.
Approches possibles :
1. NSSM (recommandé) — wrapper externe, simple et robuste
@@ -112,7 +112,7 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
Avantage : plus intégré, pas de polling nécessaire
> ⚠️ Nécessite une investigation sur l'API plugin Ditto (peu documentée)
-🟠 Phase 12 — Cross-Compilation & CI/CD
+🟢 Phase 12 — Cross-Compilation & CI/CD ✅ *Complété*
Objectif : Binaires natifs pour Windows x86_64, Linux ARM64.
Blocage potentiel : arboard et windows-sys sont Windows-only, il faut du conditional compilation.
diff --git a/src/frontend/index.html b/src/frontend/index.html
index 867b573..2b9b8d8 100644
--- a/src/frontend/index.html
+++ b/src/frontend/index.html
@@ -170,7 +170,16 @@
font-size: 14px;
}
.toast.success { border-left: 3px solid var(--green); }
+ .toast.success .toast-icon { color: var(--green); }
.toast.error { border-left: 3px solid var(--red); }
+ .toast.error .toast-icon { color: var(--red); }
+ .toast.info { border-left: 3px solid var(--accent); }
+ .toast.info .toast-icon { color: var(--accent); }
+ .toast.warning {
+ border-left: 3px solid var(--yellow);
+ background: rgba(210,153,34,0.08);
+ }
+ .toast.warning .toast-icon { color: var(--yellow); }
/* ─── Login Overlay ─── */
.login-overlay {
@@ -214,7 +223,6 @@
font-size: 14px; padding: 2px 4px; margin-left: 4px;
}
.user-info .btn-logout:hover { color: var(--red); }
- .toast.info { border-left: 3px solid var(--accent); }
.toast-msg { flex: 1; }
.toast-close {
background: none; border: none; color: var(--fg-dim);
@@ -282,6 +290,49 @@
.card-delete:hover { color: var(--red); background: rgba(248,81,73,0.15); }
.card { position: relative; }
+ /* ─── Batch Selection ─── */
+ .batch-bar {
+ display: none; align-items: center; gap: 12px;
+ padding: 10px 16px; background: var(--bg-card);
+ border: 1px solid var(--border); border-radius: var(--radius);
+ margin-bottom: 12px; flex-wrap: wrap;
+ }
+ .batch-bar.active { display: flex; }
+ .batch-bar .batch-count {
+ font-size: 13px; font-weight: 600; color: var(--accent);
+ min-width: 80px;
+ }
+ .batch-bar .btn { font-size: 12px; }
+ .batch-bar .btn-danger {
+ background: var(--red); color: #fff; border-color: var(--red);
+ font-weight: 600;
+ }
+ .batch-bar .btn-danger:hover { background: #da3633; }
+ .batch-bar .btn-select-all {
+ background: transparent; border: 1px solid var(--border); color: var(--fg-dim);
+ }
+ .batch-bar .btn-select-all:hover { border-color: var(--accent); color: var(--accent); }
+ .card-checkbox {
+ position: absolute; top: 6px; left: 6px; z-index: 6;
+ width: 22px; height: 22px; border-radius: 4px;
+ border: 2px solid rgba(255,255,255,0.3); background: rgba(0,0,0,0.5);
+ cursor: pointer; display: flex; align-items: center; justify-content: center;
+ opacity: 0; transition: all 0.15s;
+ }
+ .card:hover .card-checkbox,
+ .card.selected .card-checkbox,
+ .batch-bar.active ~ .gallery .card .card-checkbox { opacity: 1; }
+ .card.selected .card-checkbox {
+ background: var(--accent); border-color: var(--accent);
+ }
+ .card.selected .card-checkbox::after {
+ content: '✓'; color: #fff; font-size: 13px; font-weight: 700;
+ }
+ .card.selected {
+ box-shadow: 0 0 0 2px var(--accent), 0 4px 24px rgba(88,166,255,0.2);
+ border-color: var(--accent);
+ }
+
/* ─── Confirm Dialog ─── */
.confirm-overlay {
position: fixed; inset: 0; z-index: 400; background: rgba(0,0,0,0.7);
@@ -450,6 +501,12 @@
🖼️ Captures récentes
+
+ 0 sélectionnée(s)
+
+
+
+
@@ -593,8 +650,8 @@ function showToast(msg, type = 'info', duration = 4000) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = `toast ${type}`;
- const icons = { success: '✅', error: '❌', info: 'ℹ️' };
- toast.innerHTML = `
${icons[type] || 'ℹ️'}${escHtml(msg)}`;
+ const icons = { success: '✅', error: '❌', info: 'ℹ️', warning: '⚠️' };
+ toast.innerHTML = `
${icons[type] || 'ℹ️'}${escHtml(msg)}`;
container.appendChild(toast);
if (duration > 0) setTimeout(() => { if (toast.parentElement) toast.remove(); }, duration);
}
@@ -686,6 +743,9 @@ async function deleteImage(id) {
try {
await api(`/api/images/${id}`, { method: 'DELETE' });
showToast('Image supprimée 🗑️', 'success');
+ // Retirer de la sélection
+ selectedIds.delete(id);
+ updateBatchBar();
loadImages(currentPage);
loadStats();
} catch(e) {
@@ -693,6 +753,111 @@ async function deleteImage(id) {
}
}
+// ─── Batch Selection ───
+let selectedIds = new Set();
+
+function toggleSelect(id) {
+ const card = document.getElementById('card-' + id);
+ if (!card) return;
+ if (selectedIds.has(id)) {
+ selectedIds.delete(id);
+ card.classList.remove('selected');
+ } else {
+ selectedIds.add(id);
+ card.classList.add('selected');
+ }
+ updateBatchBar();
+}
+
+function selectAll() {
+ const cards = document.querySelectorAll('.card');
+ cards.forEach(card => {
+ const idMatch = card.id.match(/^card-(\d+)$/);
+ if (idMatch) {
+ const id = parseInt(idMatch[1]);
+ selectedIds.add(id);
+ card.classList.add('selected');
+ }
+ });
+ updateBatchBar();
+}
+
+function deselectAll() {
+ selectedIds.clear();
+ document.querySelectorAll('.card.selected').forEach(c => c.classList.remove('selected'));
+ updateBatchBar();
+}
+
+function updateBatchBar() {
+ const bar = document.getElementById('batch-bar');
+ const count = document.getElementById('batch-count');
+ const btn = document.getElementById('batch-delete-btn');
+ const n = selectedIds.size;
+
+ if (n > 0) {
+ bar.classList.add('active');
+ count.textContent = `${n} sélectionnée${n > 1 ? 's' : ''}`;
+ btn.disabled = false;
+ } else {
+ bar.classList.remove('active');
+ count.textContent = '0 sélectionnée(s)';
+ btn.disabled = true;
+ }
+}
+
+async function batchDelete() {
+ if (selectedIds.size === 0) return;
+ const ids = Array.from(selectedIds);
+ const n = ids.length;
+
+ // Dialogue de confirmation
+ const overlay = document.createElement('div');
+ overlay.className = 'confirm-overlay';
+ overlay.id = 'batch-confirm-overlay';
+ overlay.innerHTML = `
+
+
🗑️ Supprimer ${n} image${n > 1 ? 's' : ''} ?
+
Cette action est irréversible.
+
+
+
+
+
`;
+ document.body.appendChild(overlay);
+ overlay.addEventListener('click', (e) => { if (e.target === overlay) overlay.remove(); });
+
+ document.getElementById('batch-confirm-btn').addEventListener('click', async () => {
+ overlay.remove();
+ const savedIds = new Set(ids);
+ try {
+ const resp = await api('/api/images/batch-delete', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ids })
+ });
+ const d = resp.deleted || [];
+ const f = resp.failed || [];
+ if (f.length === 0) {
+ showToast(`${d.length} image${d.length > 1 ? 's' : ''} supprimée${d.length > 1 ? 's' : ''} 🗑️`, 'success');
+ } else if (d.length > 0) {
+ showToast(`${d.length} supprimée${d.length > 1 ? 's' : ''}, ${f.length} échec${f.length > 1 ? 's' : ''}`, 'warning');
+ } else {
+ showToast(`Échec : ${f[0]?.error || 'Erreur inconnue'}`, 'error');
+ }
+ // Retirer les IDs supprimés de la sélection
+ d.forEach(id => savedIds.delete(id));
+ // Mettre à jour la sélection dans la page
+ selectedIds.clear();
+ savedIds.forEach(id => selectedIds.add(id));
+ updateBatchBar();
+ loadImages(currentPage);
+ loadStats();
+ } catch(e) {
+ showToast(`Échec suppression batch : ${e.message}`, 'error');
+ }
+ });
+}
+
// ─── Tags ───
async function removeTag(imgId, tag) {
try {
@@ -845,6 +1010,7 @@ document.addEventListener('paste', (e) => {
// ─── Load Images ───
async function loadImages(page = 1) {
currentPage = page;
+ deselectAll(); // Reset selection on page change
const gallery = document.getElementById('gallery');
const title = document.getElementById('gallery-title');
const noRes = document.getElementById('no-results');
@@ -894,6 +1060,7 @@ function renderGallery(images, total, page) {
const safeFn = escHtml(img.filename || img.id).replace(/'/g,"\\'");
return `
+

@@ -1039,6 +1206,7 @@ function escHtml(s) {
// ─── Init ───
function initApp() {
+ deselectAll(); // Reset batch selection on init
loadImages();
loadStats();
connectWS();
diff --git a/src/main.rs b/src/main.rs
index abe5c4f..6461ed0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -519,12 +519,10 @@ async fn main() {
cmd_logs()
}
cli::Command::Install => {
- tracing::info!("Installation du service Windows...");
- service::install()
+ install_service()
}
cli::Command::Uninstall => {
- tracing::info!("Désinstallation du service Windows...");
- service::uninstall()
+ uninstall_service()
}
};
@@ -533,3 +531,25 @@ async fn main() {
process::exit(1);
}
}
+
+#[cfg(target_os = "windows")]
+fn install_service() -> anyhow::Result<()> {
+ tracing::info!("Installation du service Windows...");
+ service::install()
+}
+
+#[cfg(not(target_os = "windows"))]
+fn install_service() -> anyhow::Result<()> {
+ anyhow::bail!("La commande 'install' n'est disponible que sur Windows (service NSSM)")
+}
+
+#[cfg(target_os = "windows")]
+fn uninstall_service() -> anyhow::Result<()> {
+ tracing::info!("Désinstallation du service Windows...");
+ service::uninstall()
+}
+
+#[cfg(not(target_os = "windows"))]
+fn uninstall_service() -> anyhow::Result<()> {
+ anyhow::bail!("La commande 'uninstall' n'est disponible que sur Windows (service NSSM)")
+}
diff --git a/src/server/routes.rs b/src/server/routes.rs
index fd1e1b8..c939ff7 100644
--- a/src/server/routes.rs
+++ b/src/server/routes.rs
@@ -100,6 +100,7 @@ pub fn api_routes(state: AppState) -> Router {
.route("/api/images/{id}", get(get_image).delete(delete_image))
.route("/api/images/{id}/tags", put(update_tags))
.route("/api/images/{id}/thumbnail", get(get_thumbnail))
+ .route("/api/images/batch-delete", post(batch_delete))
.route("/api/stats", get(stats))
.route("/api/version", get(version))
.with_state(state)
@@ -497,6 +498,95 @@ pub struct TagsUpdate {
pub tags: Vec
,
}
+/// Requête de suppression multiple
+#[derive(Deserialize)]
+pub struct BatchDeleteRequest {
+ pub ids: Vec,
+}
+
+/// Réponse de suppression multiple
+#[derive(Serialize)]
+pub struct BatchDeleteResponse {
+ pub deleted: Vec,
+ pub failed: Vec,
+ pub total_requested: usize,
+ pub total_deleted: usize,
+}
+
+/// Suppression en lot : supprime plusieurs images en une seule requête
+async fn batch_delete(
+ State(state): State>,
+ headers: HeaderMap,
+ Json(body): Json,
+) -> impl IntoResponse {
+ if body.ids.is_empty() {
+ return error_response(400, "Aucun ID fourni");
+ }
+ if body.ids.len() > 100 {
+ return error_response(400, "Maximum 100 images par lot");
+ }
+
+ let total_requested = body.ids.len();
+ let mut deleted: Vec = Vec::new();
+ let mut failed: Vec = Vec::new();
+
+ let client = match get_client(&state, &headers) {
+ Ok(c) => c,
+ Err(e) => return error_response(500, &format!("Erreur client : {:#}", e)),
+ };
+
+ tracing::info!("Suppression batch de {} images...", total_requested);
+
+ for &id in &body.ids {
+ match client.delete_image(id).await {
+ Ok(()) => {
+ deleted.push(id);
+ tracing::debug!(" ✅ Image {} supprimée", id);
+ }
+ Err(e) => {
+ failed.push(serde_json::json!({
+ "id": id,
+ "error": format!("{:#}", e)
+ }));
+ tracing::warn!(" ❌ Échec suppression image {}: {:#}", id, e);
+ }
+ }
+ }
+
+ tracing::info!(
+ "Batch delete terminé: {}/{} supprimées, {} échecs",
+ deleted.len(),
+ total_requested,
+ failed.len()
+ );
+
+ // Notifier les clients WebSocket
+ if !deleted.is_empty() {
+ let _ = state.ws_hub.broadcast(&serde_json::json!({
+ "type": "images_deleted",
+ "ids": &deleted
+ }).to_string());
+ }
+
+ let status = if failed.is_empty() {
+ axum::http::StatusCode::OK
+ } else if deleted.is_empty() {
+ axum::http::StatusCode::INTERNAL_SERVER_ERROR
+ } else {
+ axum::http::StatusCode::MULTI_STATUS
+ };
+
+ (
+ status,
+ Json(serde_json::json!({
+ "deleted": deleted,
+ "failed": failed,
+ "total_requested": total_requested,
+ "total_deleted": deleted.len()
+ })),
+ )
+}
+
async fn update_tags(
State(state): State>,
headers: HeaderMap,
diff --git a/src/service.rs b/src/service.rs
index e304350..be0b143 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -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 {
- 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 {
- 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 {
- 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(())
}