2 Commits
Author SHA1 Message Date
bruno d05bcc0ef7 chore: structure Nexus version/OS/, CHANGELOG v0.2.1+0.2.2, README
- CHANGELOG.md : ajout entrées v0.2.1 (update, install auto-config, NSSM fixes)
  et v0.2.2 (fix --config ignoré, Config::load_from)
- Nexus : structure v0.2.2/windows/ et v0.2.2/linux/
- README.md + CHANGELOG.md uploadés à la racine imagod-web/
- Anciens fichiers plats supprimés (v0.2.2/*.zip, SHA256SUMS)
2026-06-30 09:45:34 -04:00
bruno 08dca81e7c fix: --config flag ignoré par la commande serve + Config::load_from()
- Ajout de Config::load_from(path) pour charger depuis un chemin custom
- cmd_serve() accepte maintenant un Option<PathBuf> pour --config
- Le flag --config du CLI est maintenant passé à cmd_serve()
- Version bump 0.2.1 → 0.2.2

Corrige le bug où le service Windows NSSM passait --config mais
la commande serve lisait toujours depuis %APPDATA%/imagod-web/imagod.toml
2026-06-30 09:33:22 -04:00
13 changed files with 297 additions and 72 deletions
+25
View File
@@ -4,6 +4,31 @@ Toutes les modifications notables de imagod-web sont documentées ici.
---
## [v0.2.2] — 2026-06-30
### 🐛 Corrections
- **Fix `--config` ignoré par `serve`** : Le flag `--config` était parsé par le CLI mais jamais passé à `cmd_serve()`. Le service NSSM passait `serve --config <path>` mais la commande lisait toujours depuis `%APPDATA%/imagod-web/imagod.toml`. Résultat : le service Windows crashait au démarrage (SERVICE_PAUSED).
- **Ajout `Config::load_from(path)`** : Nouvelle méthode pour charger la config depuis un chemin arbitraire, utilisée quand `--config` est spécifié.
### 🔧 Améliorations
- Structure Nexus : `v0.2.2/windows/` et `v0.2.2/linux/` au lieu d'un fichier plat
- Builds Linux + Windows systématiques
---
## [v0.2.1] — 2026-06-28
### 🚀 Ajouts
- **Commande `update`** : Self-update depuis Nexus (`imagod-web update`), téléchargement + vérification SHA256 + remplacement du binaire
- **`install` auto-config** : Génération automatique de `imagod.toml` avec `--api-key`, plus besoin de créer le fichier manuellement
- **NSSM AppDirectory** : Le service Windows définit maintenant le répertoire de travail correctement
### 🐛 Corrections
- **Service NSSM** : `AppDirectory` pointe maintenant vers le dossier du binaire (et non `C:\Windows\System32`)
- **Redémarrage auto** : Configuration `AppExit Default Restart` pour le service Windows
---
## [v0.2.0] — 2026-06-25
### 🚀 Ajouts majeurs
Generated
+3 -1
View File
@@ -667,6 +667,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@@ -1285,7 +1286,7 @@ dependencies = [
[[package]]
name = "imagod-web"
version = "0.2.0"
version = "0.2.2"
dependencies = [
"anyhow",
"arboard",
@@ -2237,6 +2238,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
dependencies = [
"base64",
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"http",
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "imagod-web"
version = "0.2.0"
version = "0.2.2"
edition = "2021"
description = "Imago Daemon + Dashboard — Service de capture automatique de screenshots + Dashboard web embarqué"
authors = ["Bruno Charest <[email protected]>"]
@@ -15,7 +15,7 @@ clap = { version = "4", features = ["derive", "env"] }
# Async runtime
tokio = { version = "1", features = ["full"] }
# HTTP Client (API Imago) — rustls pour éviter OpenSSL
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "stream", "rustls-tls"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "multipart", "stream", "rustls-tls", "blocking"] }
# Serialization
serde = { version = "1", features = ["derive"] }
serde_json = "1"
Binary file not shown.
Binary file not shown.
+60 -35
View File
@@ -1,80 +1,105 @@
//! Parsing CLI avec clap
use clap::{Parser, Subcommand};
use std::path::PathBuf;
#[derive(Parser)]
#[command(
name = "imagod-web",
version = env!("CARGO_PKG_VERSION"),
about = "Imago Daemon + Dashboard — Capture automatique de screenshots + Dashboard web embarqué",
long_about = "Service de capture automatique de screenshots vers Imago avec dashboard web embarqué.\n\n\
Modes :\n \
Windows : imagod-web start → watcher clipboard + dashboard\n \
Linux : imagod-web serve → watcher dossier + dashboard"
version = env!("IMAGOD_BUILD_VERSION"),
about = "Imago Daemon + Dashboard — Watcher clipboard et dashboard web",
long_about = "Companion multiplateforme pour le backend Imago. Dashboard web, watcher clipboard, gestionnaire d'upload."
)]
pub struct Args {
pub struct Cli {
#[command(subcommand)]
pub command: Command,
pub cmd: Command,
}
#[derive(Subcommand)]
pub enum Command {
/// Lancer le watcher + dashboard (Windows uniquement)
/// Lancer le watcher + dashboard
Start {
/// Port du dashboard web
#[arg(long, default_value = "9090")]
/// Port d'écoute
#[arg(long, default_value = "9199")]
port: u16,
/// Ouvrir le dashboard dans le navigateur
/// Ouvrir le navigateur
#[arg(long)]
open_browser: bool,
},
/// Lancer le dashboard web uniquement (Linux/serveur)
/// Lancer le dashboard web (+ watcher dossier sur Linux)
Serve {
/// Port du dashboard web
#[arg(long, default_value = "9090")]
/// Port d'écoute
#[arg(long, default_value = "9199")]
port: u16,
/// Interface d'écoute
/// Adresse d'écoute
#[arg(long, default_value = "127.0.0.1")]
host: String,
/// Désactiver le watcher clipboard
/// Désactiver le watcher (surveillance du presse-papier)
#[arg(long)]
no_watcher: bool,
/// Ouvrir le dashboard dans le navigateur
/// Ouvrir le navigateur
#[arg(long)]
open_browser: bool,
/// Chemin du fichier de configuration
#[arg(long)]
config: Option<PathBuf>,
},
/// Arrêter le service
Stop,
/// Afficher l'état du service
/// Afficher l'état
Status,
/// Gérer la configuration
Config {
#[command(subcommand)]
action: ConfigAction,
},
/// Installer comme service Windows
Install,
/// Installer en tant que service Windows
Install {
/// Chemin du fichier de configuration
#[arg(short, long)]
config: Option<PathBuf>,
/// Clé API Imago
#[arg(short, long)]
api_key: Option<String>,
},
/// Désinstaller le service Windows
Uninstall,
/// Afficher les derniers logs
Logs,
/// Mettre à jour depuis Nexus
Update {
/// Forcer même si déjà à jour
#[arg(short, long)]
force: bool,
/// URL du serveur Nexus
#[arg(short, long, default_value = "https://nexus.dracodev.net")]
nexus_url: String,
},
}
#[derive(Subcommand, Debug)]
#[derive(Subcommand)]
pub enum ConfigAction {
/// Afficher la configuration actuelle
/// Afficher la configuration
Show,
/// Modifier un paramètre
Set {
/// Clé de configuration (ex: imago.url)
key: String,
/// Valeur
value: String,
},
/// Réinitialiser la configuration par défaut
/// Modifier une clé
Set { key: String, value: String },
/// Réinitialiser
Reset,
}
pub fn parse() -> Args {
Args::parse()
/// Parse les arguments CLI
pub fn parse() -> Command {
<Cli as clap::Parser>::parse().cmd
}
+12 -2
View File
@@ -160,15 +160,25 @@ impl Config {
/// Charge la configuration depuis `%APPDATA%/imagod-web/imagod.toml`
pub fn load() -> Result<Self> {
let path = Self::config_path()?;
Self::load_from(&path)
}
/// Charge la configuration depuis un chemin spécifique
pub fn load_from(path: &PathBuf) -> Result<Self> {
if path.exists() {
let content = std::fs::read_to_string(&path)
let content = std::fs::read_to_string(path)
.with_context(|| format!("Impossible de lire {}", path.display()))?;
let config: Config = toml::from_str(&content)
.with_context(|| "Erreur de parsing du fichier de configuration")?;
Ok(config)
} else {
let config = Config::default();
config.save()?;
// Sauvegarder au chemin spécifié
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let content = toml::to_string_pretty(&config)?;
std::fs::write(path, &content)?;
Ok(config)
}
}
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Changelog — imagod-web v0.2.0+build.32-fb292ea-dirty</title>
<title>Changelog — imagod-web v0.2.2+build.35-08dca81</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
@@ -37,7 +37,7 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
</head>
<body>
<header>
<h1>📋 imagod-web <span class="version-badge">v0.2.0+build.32-fb292ea-dirty</span></h1>
<h1>📋 imagod-web <span class="version-badge">v0.2.2+build.35-08dca81</span></h1>
<nav>
<a href="/">🖼️ Galerie</a>
<a href="/changelog" class="active">📋 Changelog</a>
@@ -107,6 +107,6 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
<h2>Légende des tags</h2>
</main>
<footer>imagod-web v0.2.0+build.32-fb292ea-dirty — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
<footer>imagod-web v0.2.2+build.35-08dca81 — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
</body>
</html>
+3 -3
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aide & Roadmap — imagod-web v0.2.0+build.32-fb292ea-dirty</title>
<title>Aide & Roadmap — imagod-web v0.2.2+build.35-08dca81</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🖼️</text></svg>">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
@@ -37,7 +37,7 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
</head>
<body>
<header>
<h1>❓ imagod-web <span class="version-badge">v0.2.0+build.32-fb292ea-dirty</span></h1>
<h1>❓ imagod-web <span class="version-badge">v0.2.2+build.35-08dca81</span></h1>
<nav>
<a href="/">🖼️ Galerie</a>
<a href="/changelog">📋 Changelog</a>
@@ -169,6 +169,6 @@ footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
<p>5. <strong>Corriger le backend Imago</strong> (thumbnails, upload, stockage)</p>
</main>
<footer>imagod-web v0.2.0+build.32-fb292ea-dirty — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
<footer>imagod-web v0.2.2+build.35-08dca81 — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
</body>
</html>
+1 -1
View File
@@ -465,7 +465,7 @@
<button class="btn" id="clear-btn" style="display:none">✕</button>
</div>
<button class="btn accent" id="header-upload-btn" onclick="document.getElementById('drop-zone').scrollIntoView({behavior:'smooth'}); document.getElementById('file-input').click()">📤 Upload</button>
<span class="version-badge" id="version-badge">v0.2.0+build.32-fb292ea-dirty</span>
<span class="version-badge" id="version-badge">v0.2.2+build.35-08dca81</span>
</div>
<!-- ─── Navigation ─── -->
+32 -21
View File
@@ -21,13 +21,11 @@ mod watcher;
mod clipboard;
#[cfg(target_os = "windows")]
mod tray;
#[cfg(target_os = "windows")]
mod service;
#[cfg(target_os = "windows")]
mod window_info;
mod server;
#[cfg(windows)]
mod service;
mod update;
mod window_info;
use anyhow::{Context, Result};
use std::path::PathBuf;
use std::process;
@@ -212,7 +210,7 @@ async fn cmd_start(port: u16, open_browser: bool) -> Result<()> {
}
/// `serve` — Lance le dashboard web uniquement
async fn cmd_serve(host: &str, port: u16, no_watcher: bool, open_browser: bool) -> Result<()> {
async fn cmd_serve(host: &str, port: u16, no_watcher: bool, open_browser: bool, config_path: Option<PathBuf>) -> Result<()> {
// Vérifier qu'il n'y a pas déjà une instance
if let Some(pid) = is_running() {
anyhow::bail!(
@@ -222,7 +220,12 @@ async fn cmd_serve(host: &str, port: u16, no_watcher: bool, open_browser: bool)
);
}
let cfg = config::Config::load()?;
let cfg = if let Some(ref path) = config_path {
tracing::info!("📄 Configuration chargée depuis : {}", path.display());
config::Config::load_from(path)?
} else {
config::Config::load()?
};
// Écrire le PID
write_pid()?;
@@ -498,13 +501,12 @@ async fn main() {
setup_logging();
let args = cli::parse();
let result = match args.command {
let result = match args {
cli::Command::Start { port, open_browser } => {
cmd_start(port, open_browser).await
}
cli::Command::Serve { port, host, no_watcher, open_browser } => {
cmd_serve(&host, port, no_watcher, open_browser).await
cli::Command::Serve { port, host, no_watcher, open_browser, config } => {
cmd_serve(&host, port, no_watcher, open_browser, config).await
}
cli::Command::Stop => {
cmd_stop()
@@ -515,17 +517,16 @@ async fn main() {
cli::Command::Config { action } => {
cmd_config(&action)
}
cli::Command::Logs => {
cmd_logs()
}
cli::Command::Install => {
install_service()
cli::Command::Install { config, api_key } => {
install_service(config, api_key)
}
cli::Command::Uninstall => {
uninstall_service()
}
cli::Command::Update { force, nexus_url } => {
update_binary(force, &nexus_url)
}
};
if let Err(e) = result {
eprintln!("❌ Erreur : {:#}", e);
process::exit(1);
@@ -533,13 +534,13 @@ async fn main() {
}
#[cfg(target_os = "windows")]
fn install_service() -> anyhow::Result<()> {
fn install_service(config: Option<PathBuf>, api_key: Option<String>) -> anyhow::Result<()> {
tracing::info!("Installation du service Windows...");
service::install()
service::install(config, api_key)
}
#[cfg(not(target_os = "windows"))]
fn install_service() -> anyhow::Result<()> {
fn install_service(_config: Option<PathBuf>, _api_key: Option<String>) -> anyhow::Result<()> {
anyhow::bail!("La commande 'install' n'est disponible que sur Windows (service NSSM)")
}
@@ -549,6 +550,16 @@ fn uninstall_service() -> anyhow::Result<()> {
service::uninstall()
}
#[cfg(windows)]
fn update_binary(_force: bool, _nexus_url: &str) -> anyhow::Result<()> {
crate::update::check_and_update(_force, _nexus_url)
}
#[cfg(not(windows))]
fn update_binary(_force: bool, _nexus_url: &str) -> anyhow::Result<()> {
anyhow::bail!("La commande 'update' n'est disponible que sur Windows (remplacement du binaire + service NSSM)")
}
#[cfg(not(target_os = "windows"))]
fn uninstall_service() -> anyhow::Result<()> {
anyhow::bail!("La commande 'uninstall' n'est disponible que sur Windows (service NSSM)")
+46 -4
View File
@@ -6,7 +6,8 @@
//! NSSM doit être présent dans le même dossier que imagod-web.exe
//! ou dans le PATH. Téléchargeable sur https://nssm.cc/download
use std::path::PathBuf;
use anyhow::Context;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
/// Trouve l'exécutable NSSM
@@ -29,7 +30,7 @@ fn own_exe_path() -> anyhow::Result<String> {
.to_string())
}
pub fn install() -> anyhow::Result<()> {
pub fn install(config_override: Option<PathBuf>, api_key: Option<String>) -> anyhow::Result<()> {
if !cfg!(windows) {
anyhow::bail!("L'installation en service est disponible uniquement sous Windows");
}
@@ -49,6 +50,44 @@ pub fn install() -> anyhow::Result<()> {
let exe_path = own_exe_path()?;
let service_name = "imagod-web";
let exe_dir = Path::new(&exe_path).parent().unwrap_or(Path::new("."));
// --- Config auto-génération ---
let config_path = if let Some(ref cfg) = config_override {
cfg.clone()
} else {
let default_config = exe_dir.join("imagod.toml");
if !default_config.exists() {
let key = api_key.unwrap_or_default();
let config_content = format!(
r#"[imago]
base_url = "http://imago.dracodev.net"
api_key = "{}"
[server]
host = "127.0.0.1"
port = 9199
"#,
key
);
std::fs::write(&default_config, config_content)?;
println!("📝 Config créée : {}", default_config.display());
if key.is_empty() {
println!("⚠️ Clé API non fournie — éditez imagod.toml pour ajouter votre clé Imago");
}
}
default_config
};
// Vérifier que le fichier de config existe
if !config_path.exists() {
anyhow::bail!(
"Fichier de configuration introuvable : {}\n\
Créez un fichier imagod.toml ou utilisez --config <chemin>",
config_path.display()
);
}
println!("🔧 Installation du service Windows '{}'...", service_name);
println!(" Binaire : {}", exe_path);
println!(" NSSM : {}", nssm.display());
@@ -70,9 +109,12 @@ pub fn install() -> anyhow::Result<()> {
}
// Configurer le service
let config_arg = format!("serve --config {}", config_path.display());
let settings: &[(&[&str], &str)] = &[
// Commande : "serve" (watcher + dashboard)
(&["set", service_name, "AppParameters", "serve"], "Paramètres de l'application"),
// Commande : "serve --config <path>"
(&["set", service_name, "AppParameters", &config_arg], "Paramètres de l'application"),
// Répertoire de travail = là où est le binaire
(&["set", service_name, "AppDirectory", &exe_dir.to_string_lossy()], "Répertoire de travail"),
// Démarrage automatique
(&["set", service_name, "Start", "SERVICE_AUTO_START"], "Type de démarrage"),
// Nom affiché
+110
View File
@@ -0,0 +1,110 @@
//! Module de mise à jour automatique depuis Nexus (Windows uniquement)
#[cfg(windows)]
mod imp {
use std::path::Path;
use std::process::Command;
pub fn check_and_update(force: bool, nexus_url: &str) -> anyhow::Result<()> {
let current_version = env!("CARGO_PKG_VERSION");
let platform = "windows";
let exe_path = std::env::current_exe()?;
let exe_dir = exe_path.parent().unwrap_or(Path::new("."));
// 1. Récupérer la version distante
let version_url = format!(
"{}/repository/raw-hosted/imagod-web/{}/latest/version.txt",
nexus_url.trim_end_matches('/'),
platform
);
println!("🔍 Vérification de la version sur Nexus...");
println!(" Actuelle : v{}", current_version);
let remote_version = match reqwest::blocking::get(&version_url) {
Ok(resp) if resp.status().is_success() => resp.text()?.trim().to_string(),
Ok(resp) => anyhow::bail!("Impossible de récupérer la version distante (HTTP {})", resp.status()),
Err(e) => anyhow::bail!("Erreur réseau : {}", e),
};
println!(" Distante : v{}", remote_version);
if !force && remote_version == current_version {
println!("✅ Vous utilisez déjà la dernière version.");
return Ok(());
}
println!("📦 Mise à jour : v{} → v{}", current_version, remote_version);
// 2. Télécharger
let bin_name = "imagod-web.exe";
let bin_url = format!(
"{}/repository/raw-hosted/imagod-web/{}/latest/{}",
nexus_url.trim_end_matches('/'), platform, bin_name
);
println!("⬇️ Téléchargement...");
let response = reqwest::blocking::get(&bin_url)?;
if !response.status().is_success() {
anyhow::bail!("Téléchargement échoué (HTTP {})", response.status());
}
let new_binary = response.bytes()?;
println!(" Téléchargé : {:.1} MB", new_binary.len() as f64 / 1_048_576.0);
// 3. Vérifier SHA256
let sha_url = format!("{}.sha256", bin_url);
let expected_sha = reqwest::blocking::get(&sha_url)?
.text()?
.split_whitespace()
.next()
.unwrap_or("")
.to_string();
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(&new_binary);
let actual_sha = format!("{:x}", hasher.finalize());
if expected_sha.is_empty() {
println!("⚠️ Pas de checksum — vérification ignorée.");
} else if actual_sha != expected_sha {
anyhow::bail!("❌ SHA256 mismatch!\n Attendu: {}\n Obtenu: {}", expected_sha, actual_sha);
} else {
println!("✅ SHA256 vérifié");
}
// 4. Remplacer
let temp_path = exe_dir.join(format!("{}.new", bin_name));
std::fs::write(&temp_path, &new_binary)?;
let _ = Command::new("nssm").args(["stop", "imagod-web"]).status();
std::thread::sleep(std::time::Duration::from_secs(2));
let backup_path = exe_dir.join(format!("{}.old", bin_name));
let _ = std::fs::remove_file(&backup_path);
std::fs::rename(&exe_path, &backup_path)?;
std::fs::rename(&temp_path, &exe_path)?;
// 5. Redémarrer
let start = Command::new("nssm").args(["start", "imagod-web"]).status();
match start {
Ok(s) if s.success() => println!("🚀 Service redémarré !"),
_ => println!("⚠️ Redémarrage manuel requis : nssm start imagod-web"),
}
println!("🎉 Mise à jour v{} → v{} terminée !", current_version, remote_version);
Ok(())
}
}
#[cfg(not(windows))]
mod imp {
pub fn check_and_update(_force: bool, _nexus_url: &str) -> anyhow::Result<()> {
anyhow::bail!("La commande 'update' n'est disponible que sur Windows")
}
}
pub fn check_and_update(force: bool, nexus_url: &str) -> anyhow::Result<()> {
imp::check_and_update(force, nexus_url)
}