commit a3b853de0405ad101689e4130034b0bb965fd4a0 Author: Hermes Agent Date: Mon Jun 22 16:44:47 2026 -0400 chore: initialiser le projet imagod-web (Rust) - Structure du projet Cargo avec dépendances (clap, axum, reqwest, rusqlite, arboard, tray-icon) - CLI avec sous-commandes start, serve, stop, status, config, install, uninstall, logs - Module config: parsing TOML, chemins Windows/Linux - Module dedup: dédoublonnage SHA-256 avec cache SQLite - Module imago_client: client HTTP pour API Imago (upload, search, get) - Module watcher: boucle de polling stub - Module server: serveur web axum avec routes API REST stub - Platform-specific: clipboard, tray, service, window_info (Windows) - Frontend placeholder HTML - Config par défaut (imagod.toml) - README.md, LICENSE (MIT), .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e473ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Build +/target/ +.cargo/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Local data +imagod.db +imagod.log +*.local.toml diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..fd4e277 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,71 @@ +[package] +name = "imagod-web" +version = "0.1.0" +edition = "2021" +description = "Imago Daemon + Dashboard — Service de capture automatique de screenshots + Dashboard web embarqué" +authors = ["Bruno Charest "] +license = "MIT" +repository = "https://git.dracodev.net/Projets/imagod-web" +keywords = ["screenshot", "clipboard", "imago", "dashboard", "ai"] +categories = ["command-line-utilities", "web-programming"] + +[dependencies] +# CLI +clap = { version = "4", features = ["derive", "env"] } +# Async runtime +tokio = { version = "1", features = ["full"] } +# HTTP Client (API Imago) +reqwest = { version = "0.12", features = ["json", "multipart", "stream"] } +# Serialization +serde = { version = "1", features = ["derive"] } +serde_json = "1" +# Config +toml = "0.8" +# Hashing +sha2 = "0.10" +# SQLite +rusqlite = { version = "0.32", features = ["bundled"] } +# Time +chrono = { version = "0.4", features = ["serde"] } +# Logging +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +# Error handling +anyhow = "1" +thiserror = "2" +# Async trait +async-trait = "0.1" +# UUID for unique IDs +uuid = { version = "1", features = ["v4"] } + +# Web server (axum + tower) +axum = { version = "0.8", features = ["ws"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["cors", "fs", "trace"] } +# Frontend embedding +rust-embed = "8" + +# Platform-specific — Windows +[target.'cfg(windows)'.dependencies] +# Clipboard access +arboard = "3" +# System tray +tray-icon = "0.19" +# Windows API (pour titre fenêtre active) +windows-sys = { version = "0.59", features = [ + "Win32_UI_WindowsAndMessaging", + "Win32_Foundation", + "Win32_System_Threading", + "Win32_UI_TextServices", +] } + +# Platform-specific — Linux +[target.'cfg(unix)'.dependencies] +# Notify pour watcher filesystem (Linux) +notify = { version = "7", features = ["macos_kqueue"] } + +[profile.release] +opt-level = "z" # Optimize for size +lto = true # Link-time optimization +strip = true # Strip symbols +codegen-units = 1 # Better optimization diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8cdeac5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Bruno Charest + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8c55a09 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +# imagod-web + +**Imago Daemon + Dashboard** — Service de capture automatique de screenshots + Dashboard web embarqué. + +Un binaire **Rust** unique qui remplit deux rôles : + +| Mode | Plateforme | Rôle | +|------|-----------|------| +| `imagod-web start` | Windows | Watcher clipboard + Dashboard web :9090 | +| `imagod-web serve` | Linux/Raspi | Dashboard web uniquement (sans watcher) | + +## Fonctionnalités + +- **Capture automatique** : surveille le clipboard Windows, détecte les nouveaux screenshots +- **Dédoublonnage** : SHA-256 pour éviter les doublons +- **Upload Imago** : envoi vers le backend Imago pour OCR + AI Vision + Tags +- **Dashboard web embarqué** : galerie, recherche, preview, copie/partage +- **Cross-platform** : Windows (watcher+dashboard), Linux/Raspi (dashboard) +- **System tray** : icône native Windows avec menu contextuel +- **Service Windows** : installation via NSSM pour démarrage automatique + +## Prérequis + +- [Rust](https://rustup.rs) 1.80+ +- Un serveur [Imago](https://git.dracodev.net/Projets/Imago) fonctionnel +- Une clé API client Imago + +## Installation + +```bash +# Compiler depuis les sources +git clone https://git.dracodev.net/Projets/imagod-web.git +cd imagod-web +cargo build --release + +# Le binaire est dans target/release/imagod-web (ou imagod-web.exe) +``` + +### Cross-compilation + +```bash +# Windows +cargo build --release --target x86_64-pc-windows-msvc + +# Linux x86_64 (openclaw-1) +cargo build --release --target x86_64-unknown-linux-gnu + +# Linux ARM64 (Raspberry Pi) +cargo build --release --target aarch64-unknown-linux-gnu +``` + +## Utilisation + +```bash +# Windows — lancer le watcher + dashboard +imagod-web start --port 9090 --open-browser + +# Linux — dashboard uniquement +imagod-web serve --port 9090 --host 0.0.0.0 --no-watcher + +# Voir le statut +imagod-web status + +# Configurer +imagod-web config show +imagod-web config set imago.api_key "ik_..." + +# Installer comme service Windows (admin requis) +imagod-web install +``` + +## Configuration + +Fichier `imagod.toml` dans `%APPDATA%/imagod-web/` (Windows) ou `~/.config/imagod-web/` (Linux). + +```toml +[imago] +url = "http://imago.dracodev.net:8000" +api_key = "CHANGE_ME" + +[watcher] +capture_images = true +poll_interval_ms = 500 +``` + +## Architecture + +Voir [[imagod-web-architecture|Document d'architecture complet]] dans le vault Obsidian IT. + +## Licence + +MIT — Copyright (c) 2026 Bruno Charest diff --git a/config/imagod.toml b/config/imagod.toml new file mode 100644 index 0000000..11c46a6 --- /dev/null +++ b/config/imagod.toml @@ -0,0 +1,30 @@ +[imago] +# URL du serveur Imago +url = "http://imago.dracodev.net:8000" +# Clé API client Imago (à configurer) +api_key = "CHANGE_ME" + +[watcher] +capture_images = true +poll_interval_ms = 500 + +[watcher.filters] +# Applications à capturer (vide = tout) +only_apps = [] +# Applications à exclure +exclude_apps = ["mstsc"] + +[dedup] +# Méthode de dédoublonnage : sha256 | perceptual_hash +method = "sha256" +# Nombre de hashes en cache +cache_size = 10000 + +[ui] +show_notifications = true +show_tray_icon = true +notification_timeout_sec = 3 + +[logging] +# Niveau de log : trace | debug | info | warn | error +level = "info" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..e79c6ed --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,80 @@ +//! Parsing CLI avec clap + +use clap::{Parser, Subcommand}; + +#[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 :9090\n \ + Linux : imagod-web serve --port 9090 --no-watcher → dashboard uniquement" +)] +pub struct Args { + #[command(subcommand)] + pub command: Command, +} + +#[derive(Subcommand)] +pub enum Command { + /// Lancer le watcher + dashboard (Windows uniquement) + Start { + /// Port du dashboard web + #[arg(long, default_value = "9090")] + port: u16, + /// Ouvrir le dashboard dans le navigateur + #[arg(long)] + open_browser: bool, + }, + /// Lancer le dashboard web uniquement (Linux/serveur) + Serve { + /// Port du dashboard web + #[arg(long, default_value = "9090")] + port: u16, + /// Interface d'écoute + #[arg(long, default_value = "127.0.0.1")] + host: String, + /// Désactiver le watcher clipboard + #[arg(long)] + no_watcher: bool, + /// Ouvrir le dashboard dans le navigateur + #[arg(long)] + open_browser: bool, + }, + /// Arrêter le service + Stop, + /// Afficher l'état du service + Status, + /// Gérer la configuration + Config { + #[command(subcommand)] + action: ConfigAction, + }, + /// Installer comme service Windows + Install, + /// Désinstaller le service Windows + Uninstall, + /// Afficher les derniers logs + Logs, +} + +#[derive(Subcommand)] +pub enum ConfigAction { + /// Afficher la configuration actuelle + Show, + /// Modifier un paramètre + Set { + /// Clé de configuration (ex: imago.url) + key: String, + /// Valeur + value: String, + }, + /// Réinitialiser la configuration par défaut + Reset, +} + +pub fn parse() -> Args { + Args::parse() +} diff --git a/src/clipboard.rs b/src/clipboard.rs new file mode 100644 index 0000000..5038e37 --- /dev/null +++ b/src/clipboard.rs @@ -0,0 +1,16 @@ +//! Clipboard — Abstraction arboard (Windows) +//! +//! Phase 3: Lecture du clipboard Windows, détection d'images + +use anyhow::Result; + +/// Vérifie si le clipboard contient une image +pub fn has_image() -> Result { + // Phase 3: Implémentation arboard + todo!("Phase 3: Clipboard detection") +} + +/// Récupère l'image du clipboard en bytes +pub fn get_image_bytes() -> Result> { + todo!("Phase 3: Clipboard image extraction") +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..0c5587a --- /dev/null +++ b/src/config.rs @@ -0,0 +1,132 @@ +//! Gestion de la configuration TOML + +use anyhow::{Context, Result}; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +/// Configuration complète d'imagod-web +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + #[serde(default)] + pub imago: ImagoConfig, + #[serde(default)] + pub watcher: WatcherConfig, + #[serde(default)] + pub dedup: DedupConfig, + #[serde(default)] + pub ui: UiConfig, + #[serde(default)] + pub logging: LoggingConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImagoConfig { + pub url: String, + pub api_key: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WatcherConfig { + pub capture_images: bool, + pub poll_interval_ms: u64, + pub filters: WatcherFilters, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct WatcherFilters { + pub only_apps: Vec, + pub exclude_apps: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DedupConfig { + pub method: String, + pub cache_size: usize, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UiConfig { + pub show_notifications: bool, + pub show_tray_icon: bool, + pub notification_timeout_sec: u64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LoggingConfig { + pub level: String, +} + +impl Default for Config { + fn default() -> Self { + Self { + imago: ImagoConfig { + url: "http://imago.dracodev.net:8000".into(), + api_key: String::new(), + }, + watcher: WatcherConfig { + capture_images: true, + poll_interval_ms: 500, + filters: WatcherFilters { + only_apps: vec![], + exclude_apps: vec!["mstsc".into()], + }, + }, + dedup: DedupConfig { + method: "sha256".into(), + cache_size: 10000, + }, + ui: UiConfig { + show_notifications: true, + show_tray_icon: true, + notification_timeout_sec: 3, + }, + logging: LoggingConfig { + level: "info".into(), + }, + } + } +} + +impl Config { + /// Charge la configuration depuis `%APPDATA%/imagod-web/imagod.toml` + pub fn load() -> Result { + let path = Self::config_path()?; + if path.exists() { + 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 { + // Premier lancement — créer la config par défaut + let config = Config::default(); + config.save()?; + Ok(config) + } + } + + /// Sauvegarde la configuration + pub fn save(&self) -> Result<()> { + let path = Self::config_path()?; + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let content = toml::to_string_pretty(self)?; + std::fs::write(&path, content)?; + Ok(()) + } + + /// Chemin du fichier de configuration + fn config_path() -> Result { + let base = if cfg!(target_os = "windows") { + let appdata = std::env::var("APPDATA") + .context("Variable APPDATA non définie")?; + PathBuf::from(appdata) + } else { + let home = std::env::var("HOME") + .context("Variable HOME non définie")?; + PathBuf::from(home).join(".config") + }; + Ok(base.join("imagod-web").join("imagod.toml")) + } +} diff --git a/src/dedup.rs b/src/dedup.rs new file mode 100644 index 0000000..f743f16 --- /dev/null +++ b/src/dedup.rs @@ -0,0 +1,71 @@ +//! Dédoublonnage des images par hash SHA-256 + +use anyhow::Result; +use rusqlite::Connection; +use sha2::{Digest, Sha256}; +use std::path::Path; + +/// Gestionnaire de dédoublonnage +pub struct Dedup { + conn: Connection, + cache_size: usize, +} + +impl Dedup { + /// Ouvre ou crée la base de dédoublonnage locale + pub fn open(db_path: &Path, cache_size: usize) -> Result { + let conn = Connection::open(db_path)?; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS hashes ( + hash BLOB PRIMARY KEY, + image_id TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_hashes_created ON hashes(created_at);" + )?; + Ok(Self { conn, cache_size }) + } + + /// Calcule le hash SHA-256 d'un buffer + pub fn hash_bytes(data: &[u8]) -> Vec { + let mut hasher = Sha256::new(); + hasher.update(data); + hasher.finalize().to_vec() + } + + /// Vérifie si un hash a déjà été vu + pub fn already_seen(&self, hash: &[u8]) -> Result { + let exists: bool = self.conn.query_row( + "SELECT COUNT(*) > 0 FROM hashes WHERE hash = ?1", + rusqlite::params![hash], + |row| row.get(0), + )?; + Ok(exists) + } + + /// Marque un hash comme vu + pub fn mark_seen(&self, hash: &[u8], image_id: &str) -> Result<()> { + self.conn.execute( + "INSERT OR IGNORE INTO hashes (hash, image_id) VALUES (?1, ?2)", + rusqlite::params![hash, image_id], + )?; + // Nettoyage périodique si dépassement du cache + self.conn.execute( + "DELETE FROM hashes WHERE rowid NOT IN ( + SELECT rowid FROM hashes ORDER BY created_at DESC LIMIT ?1 + )", + rusqlite::params![self.cache_size], + )?; + Ok(()) + } + + /// Nombre de hashes en cache + pub fn count(&self) -> Result { + let count: usize = self.conn.query_row( + "SELECT COUNT(*) FROM hashes", + [], + |row| row.get(0), + )?; + Ok(count) + } +} diff --git a/src/frontend/index.html b/src/frontend/index.html new file mode 100644 index 0000000..ebcd1e9 --- /dev/null +++ b/src/frontend/index.html @@ -0,0 +1,38 @@ + + + + + + Imago Dashboard + + + +
+

🖼️ Imago Dashboard

+

Dashboard web en construction — Phase 9

+
+ + diff --git a/src/imago_client.rs b/src/imago_client.rs new file mode 100644 index 0000000..494ea5b --- /dev/null +++ b/src/imago_client.rs @@ -0,0 +1,113 @@ +//! Client HTTP pour l'API Imago + +use anyhow::{Context, Result}; +use reqwest::{Client, multipart}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct UploadResponse { + pub id: String, + pub filename: String, + pub status: String, +} + +#[derive(Debug, Deserialize)] +pub struct ImageInfo { + pub id: String, + pub filename: String, + pub tags: Vec, + pub description: Option, + pub ocr_text: Option, + pub created_at: String, + pub width: Option, + pub height: Option, + pub size_bytes: Option, +} + +#[derive(Debug, Deserialize)] +pub struct SearchResponse { + pub results: Vec, + pub total: usize, +} + +/// Client pour l'API Imago +pub struct ImagoClient { + client: Client, + base_url: String, + api_key: String, +} + +impl ImagoClient { + pub fn new(base_url: String, api_key: String) -> Result { + let client = Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build()?; + Ok(Self { client, base_url, api_key }) + } + + /// Upload une image vers Imago + pub async fn upload_image( + &self, + data: Vec, + filename: &str, + window_title: Option<&str>, + ) -> Result { + let part = multipart::Part::bytes(data) + .file_name(filename.to_string()) + .mime_str("image/png")?; + + let mut form = multipart::Form::new() + .part("file", part); + + if let Some(title) = window_title { + form = form.text("window_title", title.to_string()); + } + + let response = self.client + .post(format!("{}/images/upload", self.base_url)) + .header("Authorization", format!("Bearer {}", self.api_key)) + .multipart(form) + .send() + .await?; + + let upload: UploadResponse = response + .error_for_status()? + .json() + .await?; + + Ok(upload) + } + + /// Recherche des images + pub async fn search(&self, query: &str, limit: usize) -> Result { + let response = self.client + .get(format!("{}/images/search", self.base_url)) + .header("Authorization", format!("Bearer {}", self.api_key)) + .query(&[("q", query), ("limit", &limit.to_string())]) + .send() + .await?; + + let result: SearchResponse = response + .error_for_status()? + .json() + .await?; + + Ok(result) + } + + /// Récupère les infos d'une image + pub async fn get_image(&self, id: &str) -> Result { + let response = self.client + .get(format!("{}/images/{}", self.base_url, id)) + .header("Authorization", format!("Bearer {}", self.api_key)) + .send() + .await?; + + let info: ImageInfo = response + .error_for_status()? + .json() + .await?; + + Ok(info) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..17104a3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,87 @@ +//! imagod-web — Imago Daemon + Dashboard +//! +//! Point d'entrée CLI. Dispatch vers les sous-commandes : +//! - `start` : Lance le watcher clipboard + dashboard web (Windows) +//! - `serve` : Lance le dashboard web uniquement (Linux/serveur) +//! - `stop` : Arrête le service +//! - `status`: Affiche l'état +//! - `config`: Gère la configuration +//! - `install` / `uninstall`: Service Windows + +mod cli; +mod config; +mod dedup; +mod imago_client; +mod watcher; + +#[cfg(target_os = "windows")] +mod clipboard; +#[cfg(target_os = "windows")] +mod tray; +#[cfg(target_os = "windows")] +mod service; +#[cfg(target_os = "windows")] +mod window_info; + +mod server; + +use anyhow::Result; +use tracing_subscriber::{fmt, EnvFilter}; + +fn setup_logging() { + let filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info")); + + fmt() + .with_env_filter(filter) + .with_target(true) + .init(); +} + +#[tokio::main] +async fn main() -> Result<()> { + setup_logging(); + + let args = cli::parse(); + + match args.command { + cli::Command::Start { port, open_browser } => { + tracing::info!("Démarrage imagod-web — watcher + dashboard"); + // Lance le watcher et le serveur web en parallèle + // ... + todo!("Phase 6-7: Watcher + serveur web") + } + cli::Command::Serve { port, host, no_watcher, open_browser } => { + tracing::info!("Démarrage dashboard web uniquement sur {}:{}", host, port); + // Lance uniquement le serveur web + // ... + todo!("Phase 9: Dashboard web") + } + cli::Command::Stop => { + tracing::info!("Arrêt du service"); + todo!("Phase 7: Stop") + } + cli::Command::Status => { + tracing::info!("Statut du service"); + todo!("Phase 7: Status") + } + cli::Command::Config { action } => { + tracing::info!("Configuration: {:?}", action); + todo!("Phase 2: Config") + } + cli::Command::Install => { + tracing::info!("Installation comme service Windows"); + todo!("Phase 10: Service install") + } + cli::Command::Uninstall => { + tracing::info!("Désinstallation du service Windows"); + todo!("Phase 10: Service uninstall") + } + cli::Command::Logs => { + tracing::info!("Logs"); + todo!("Phase 7: Logs") + } + } + + Ok(()) +} diff --git a/src/server/mod.rs b/src/server/mod.rs new file mode 100644 index 0000000..beab578 --- /dev/null +++ b/src/server/mod.rs @@ -0,0 +1,24 @@ +//! Serveur web — Dashboard embarqué (axum) +//! +//! Phase 9: API REST + WebSocket + frontend Svelte embarqué via rust-embed + +pub mod routes; +pub mod ws; + +use axum::Router; +use std::net::SocketAddr; + +/// Démarre le serveur web +pub async fn serve(host: &str, port: u16) -> anyhow::Result<()> { + let app = Router::new() + .merge(routes::api_routes()) + .merge(routes::static_routes()); + + let addr: SocketAddr = format!("{}:{}", host, port).parse()?; + tracing::info!("Dashboard web démarré sur http://{}", addr); + + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/src/server/routes.rs b/src/server/routes.rs new file mode 100644 index 0000000..f07c7ad --- /dev/null +++ b/src/server/routes.rs @@ -0,0 +1,37 @@ +//! Routes API REST du dashboard + +use axum::{Router, routing::get}; + +/// Routes de l'API REST +pub fn api_routes() -> Router { + Router::new() + .route("/api/health", get(health_check)) + .route("/api/images", get(list_images)) + .route("/api/images/search", get(search_images)) + .route("/api/stats", get(stats)) +} + +/// Routes statiques (frontend embarqué) +pub fn static_routes() -> Router { + // Phase 9: Servir le frontend Svelte via rust-embed + Router::new() +} + +async fn health_check() -> &'static str { + "OK" +} + +async fn list_images() -> &'static str { + // Phase 9: Liste paginée des images + "[]" +} + +async fn search_images() -> &'static str { + // Phase 9: Recherche full-text + "[]" +} + +async fn stats() -> &'static str { + // Phase 9: Statistiques + r#"{"total_images":0,"total_storage_bytes":0}"# +} diff --git a/src/server/ws.rs b/src/server/ws.rs new file mode 100644 index 0000000..e6708cb --- /dev/null +++ b/src/server/ws.rs @@ -0,0 +1,5 @@ +//! WebSocket — Mise à jour en temps réel du dashboard +//! +//! Phase 9: Notifie le frontend quand un nouveau screenshot est uploadé + +// Stub — à implémenter dans Phase 9 diff --git a/src/service.rs b/src/service.rs new file mode 100644 index 0000000..00bba06 --- /dev/null +++ b/src/service.rs @@ -0,0 +1,13 @@ +//! Service Windows (NSSM / windows-service) +//! +//! Phase 10: Installation comme service Windows + +use anyhow::Result; + +pub fn install() -> Result<()> { + todo!("Phase 10: Service install via NSSM") +} + +pub fn uninstall() -> Result<()> { + todo!("Phase 10: Service uninstall") +} diff --git a/src/tray.rs b/src/tray.rs new file mode 100644 index 0000000..669c68b --- /dev/null +++ b/src/tray.rs @@ -0,0 +1,10 @@ +//! System Tray (Windows) +//! +//! Phase 8: Icône system tray avec menu contextuel + +use anyhow::Result; + +pub async fn run() -> Result<()> { + // Phase 8: Icône tray avec Start/Stop/Status/Quit + todo!("Phase 8: System tray") +} diff --git a/src/watcher.rs b/src/watcher.rs new file mode 100644 index 0000000..5eb1f7a --- /dev/null +++ b/src/watcher.rs @@ -0,0 +1,54 @@ +//! Watcher — Boucle principale de polling du clipboard +//! +//! Surveille le clipboard à intervalle régulier, détecte les nouvelles images, +//! les dédoublonne et les envoie à Imago. + +use anyhow::Result; +use crate::config::Config; +use crate::dedup::Dedup; +use crate::imago_client::ImagoClient; +use std::path::PathBuf; +use std::time::Duration; + +pub struct Watcher { + config: Config, + dedup: Dedup, + imago: ImagoClient, +} + +impl Watcher { + pub async fn new(config: Config) -> Result { + let db_path = PathBuf::from("imagod.db"); + let dedup = Dedup::open(&db_path, config.dedup.cache_size)?; + let imago = ImagoClient::new( + config.imago.url.clone(), + config.imago.api_key.clone(), + )?; + + Ok(Self { config, dedup, imago }) + } + + /// Lance la boucle de polling + pub async fn run(&self) -> Result<()> { + let interval = Duration::from_millis(self.config.watcher.poll_interval_ms); + + tracing::info!( + "Watcher démarré — polling toutes les {}ms", + self.config.watcher.poll_interval_ms + ); + + // TODO: Phase 6 — Boucle principale + // loop { + // 1. Vérifier le clipboard (Windows: arboard, Linux: N/A) + // 2. Si nouvelle image → hash SHA-256 + // 3. Vérifier dédoublonnage + // 4. Si nouveau → upload vers Imago + // 5. Marquer comme vu + // sleep(interval) + // } + tracing::warn!("Watcher::run() — implémentation Phase 6 à venir"); + tokio::time::sleep(Duration::from_secs(1)).await; + + Ok(()) + } +} diff --git a/src/window_info.rs b/src/window_info.rs new file mode 100644 index 0000000..ab0a5d7 --- /dev/null +++ b/src/window_info.rs @@ -0,0 +1,11 @@ +//! Window Info — Titre de la fenêtre active (Windows API) +//! +//! Phase 6: Capture du contexte (titre fenêtre) pour enrichir les tags + +use anyhow::Result; + +/// Récupère le titre de la fenêtre active +pub fn get_active_window_title() -> Result> { + // Phase 6: Windows API — GetForegroundWindow + GetWindowText + todo!("Phase 6: Active window title") +}