feat: build.rs — génération automatique des pages HTML depuis CHANGELOG.md et ROADMAP.md
- build.rs convertit CHANGELOG.md → changelog.html et ROADMAP.md → help.html
- Injection automatique de la version Cargo.toml dans toutes les pages
- Correction du routage : /changelog et /help sans .html fonctionnent
- Templates changelog.tmpl.html et help.tmpl.html avec placeholders {{VERSION}}
- Plus jamais de désynchronisation entre .md et pages web !
- v0.2.0 affichée partout (index, changelog, help, API)
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
//! Build script — génère les pages HTML à partir du Markdown
|
||||
//!
|
||||
//! Exécuté à chaque `cargo build`. Garantit que :
|
||||
//! 1. changelog.html est généré depuis CHANGELOG.md
|
||||
//! 2. help.html est généré depuis ROADMAP.md
|
||||
//! 3. Le badge version dans index.html suit la version Cargo.toml
|
||||
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let manifest_dir = Path::new(env!("CARGO_MANIFEST_DIR"));
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let frontend_dir = manifest_dir.join("src/frontend");
|
||||
|
||||
// === changelog.html depuis CHANGELOG.md ===
|
||||
let changelog_tmpl = frontend_dir.join("changelog.tmpl.html");
|
||||
let changelog_md = manifest_dir.join("CHANGELOG.md");
|
||||
let changelog_out = frontend_dir.join("changelog.html");
|
||||
|
||||
if changelog_tmpl.exists() && changelog_md.exists() {
|
||||
let md = fs::read_to_string(&changelog_md).unwrap_or_default();
|
||||
let html = simple_md_to_html(&md);
|
||||
let tmpl = fs::read_to_string(&changelog_tmpl).unwrap_or_default();
|
||||
let page = tmpl
|
||||
.replace("{{VERSION}}", version)
|
||||
.replace("{{CHANGELOG_CONTENT}}", &html);
|
||||
fs::write(&changelog_out, &page).ok();
|
||||
println!("cargo:rerun-if-changed=CHANGELOG.md");
|
||||
println!("cargo:rerun-if-changed=src/frontend/changelog.tmpl.html");
|
||||
}
|
||||
|
||||
// === help.html depuis ROADMAP.md ===
|
||||
let help_tmpl = frontend_dir.join("help.tmpl.html");
|
||||
let roadmap_md = manifest_dir.join("ROADMAP.md");
|
||||
let help_out = frontend_dir.join("help.html");
|
||||
|
||||
if help_tmpl.exists() && roadmap_md.exists() {
|
||||
let md = fs::read_to_string(&roadmap_md).unwrap_or_default();
|
||||
let html = simple_md_to_html(&md);
|
||||
let tmpl = fs::read_to_string(&help_tmpl).unwrap_or_default();
|
||||
let page = tmpl
|
||||
.replace("{{VERSION}}", version)
|
||||
.replace("{{HELP_CONTENT}}", &html);
|
||||
fs::write(&help_out, &page).ok();
|
||||
println!("cargo:rerun-if-changed=ROADMAP.md");
|
||||
println!("cargo:rerun-if-changed=src/frontend/help.tmpl.html");
|
||||
}
|
||||
|
||||
// === Injection version dans index.html ===
|
||||
let index_path = frontend_dir.join("index.html");
|
||||
if index_path.exists() {
|
||||
let mut content = fs::read_to_string(&index_path).unwrap_or_default();
|
||||
// Remplacer tous les badges de version vX.Y.Z par la version courante
|
||||
content = replace_all_versions(&content, version);
|
||||
fs::write(&index_path, &content).ok();
|
||||
println!("cargo:rerun-if-changed=src/frontend/index.html");
|
||||
}
|
||||
|
||||
println!("cargo:info=✅ Pages HTML générées pour imagod-web v{version}");
|
||||
}
|
||||
|
||||
/// Mini convertisseur Markdown → HTML (zéro dépendance)
|
||||
fn simple_md_to_html(md: &str) -> String {
|
||||
let mut html = String::new();
|
||||
let mut in_code_block = false;
|
||||
let mut code_content = String::new();
|
||||
|
||||
for line in md.lines() {
|
||||
if line.starts_with("```") {
|
||||
if in_code_block {
|
||||
html.push_str("<pre><code>");
|
||||
html.push_str(&html_escape(&code_content));
|
||||
html.push_str("</code></pre>\n");
|
||||
code_content.clear();
|
||||
in_code_block = false;
|
||||
} else {
|
||||
in_code_block = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if in_code_block {
|
||||
if !code_content.is_empty() {
|
||||
code_content.push('\n');
|
||||
}
|
||||
code_content.push_str(line);
|
||||
continue;
|
||||
}
|
||||
|
||||
let trimmed = line.trim();
|
||||
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if trimmed.starts_with("### ") {
|
||||
html.push_str(&format!("<h3>{}</h3>\n", html_escape(&trimmed[4..])));
|
||||
} else if trimmed.starts_with("## ") {
|
||||
html.push_str(&format!("<h2>{}</h2>\n", html_escape(&trimmed[3..])));
|
||||
} else if trimmed.starts_with("# ") {
|
||||
html.push_str(&format!("<h1>{}</h1>\n", html_escape(&trimmed[2..])));
|
||||
} else if trimmed == "---" {
|
||||
html.push_str("<hr>\n");
|
||||
} else if trimmed.starts_with('|') && trimmed.ends_with('|') {
|
||||
render_table(&mut html, trimmed);
|
||||
} else if trimmed.starts_with("- [x]") || trimmed.starts_with("- [X]") {
|
||||
html.push_str(&format!(
|
||||
"<li><input type=\"checkbox\" checked disabled> {}</li>\n",
|
||||
process_inline(&trimmed[5..].trim())
|
||||
));
|
||||
} else if trimmed.starts_with("- [ ]") {
|
||||
html.push_str(&format!(
|
||||
"<li><input type=\"checkbox\" disabled> {}</li>\n",
|
||||
process_inline(&trimmed[5..].trim())
|
||||
));
|
||||
} else if trimmed.starts_with("- ") || trimmed.starts_with("* ") {
|
||||
html.push_str(&format!("<li>{}</li>\n", process_inline(&trimmed[2..])));
|
||||
} else {
|
||||
html.push_str(&format!("<p>{}</p>\n", process_inline(trimmed)));
|
||||
}
|
||||
}
|
||||
|
||||
if in_code_block && !code_content.is_empty() {
|
||||
html.push_str("<pre><code>");
|
||||
html.push_str(&html_escape(&code_content));
|
||||
html.push_str("</code></pre>\n");
|
||||
}
|
||||
|
||||
html
|
||||
}
|
||||
|
||||
fn render_table(html: &mut String, line: &str) {
|
||||
let cells: Vec<&str> = line.split('|').collect();
|
||||
let is_sep = cells.iter().any(|c| {
|
||||
let t = c.trim();
|
||||
t.contains("---") || t.contains(":--") || t.is_empty() && !t.contains(char::is_alphanumeric)
|
||||
});
|
||||
|
||||
if is_sep {
|
||||
return;
|
||||
}
|
||||
|
||||
let content_cells: Vec<&str> = cells.iter().filter(|c| !c.trim().is_empty()).copied().collect();
|
||||
if content_cells.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
html.push_str("<tr>");
|
||||
for cell in &content_cells {
|
||||
html.push_str(&format!("<td>{}</td>", process_inline(cell.trim())));
|
||||
}
|
||||
html.push_str("</tr>\n");
|
||||
}
|
||||
|
||||
fn process_inline(text: &str) -> String {
|
||||
// Échapper HTML d'abord
|
||||
let text = html_escape(text);
|
||||
// Puis appliquer les formats
|
||||
let text = replace_pattern(&text, "**", "strong");
|
||||
let text = replace_pattern(&text, "__", "strong");
|
||||
let text = replace_pattern(&text, "*", "em");
|
||||
let text = replace_pattern(&text, "_", "em");
|
||||
let text = replace_pattern(&text, "~~", "del");
|
||||
let text = replace_pattern(&text, "`", "code");
|
||||
// Liens [texte](url)
|
||||
replace_links(&text)
|
||||
}
|
||||
|
||||
fn replace_pattern(text: &str, marker: &str, tag: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let mut chars = text.chars().peekable();
|
||||
let marker_len = marker.chars().count();
|
||||
let marker_chars: Vec<char> = marker.chars().collect();
|
||||
|
||||
while let Some(&c) = chars.peek() {
|
||||
// Vérifier si on est au début d'un marqueur
|
||||
if c == marker_chars[0] {
|
||||
let mut candidate = String::new();
|
||||
let mut clone = chars.clone();
|
||||
for _ in 0..marker_len {
|
||||
if let Some(&mc) = clone.peek() {
|
||||
candidate.push(mc);
|
||||
clone.next();
|
||||
}
|
||||
}
|
||||
if candidate == marker {
|
||||
// Avancer après le marqueur d'ouverture
|
||||
for _ in 0..marker_len {
|
||||
chars.next();
|
||||
}
|
||||
// Chercher le marqueur de fermeture
|
||||
let mut inner = String::new();
|
||||
let mut found = false;
|
||||
while let Some(&c2) = chars.peek() {
|
||||
if c2 == marker_chars[0] {
|
||||
let mut close_candidate = String::new();
|
||||
let mut close_clone = chars.clone();
|
||||
for _ in 0..marker_len {
|
||||
if let Some(&mc) = close_clone.peek() {
|
||||
close_candidate.push(mc);
|
||||
close_clone.next();
|
||||
}
|
||||
}
|
||||
if close_candidate == marker {
|
||||
for _ in 0..marker_len {
|
||||
chars.next();
|
||||
}
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
inner.push(chars.next().unwrap());
|
||||
}
|
||||
if found {
|
||||
result.push_str(&format!("<{tag}>{inner}</{tag}>"));
|
||||
} else {
|
||||
result.push_str(&candidate);
|
||||
result.push_str(&inner);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(chars.next().unwrap());
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn replace_links(text: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let chars: Vec<char> = text.chars().collect();
|
||||
let mut i = 0;
|
||||
while i < chars.len() {
|
||||
if chars[i] == '[' {
|
||||
let mut j = i + 1;
|
||||
let mut link_text = String::new();
|
||||
while j < chars.len() && chars[j] != ']' {
|
||||
link_text.push(chars[j]);
|
||||
j += 1;
|
||||
}
|
||||
if j + 2 < chars.len() && chars[j] == ']' && chars[j + 1] == '(' {
|
||||
let mut k = j + 2;
|
||||
let mut url = String::new();
|
||||
while k < chars.len() && chars[k] != ')' {
|
||||
url.push(chars[k]);
|
||||
k += 1;
|
||||
}
|
||||
if k < chars.len() && chars[k] == ')' {
|
||||
result.push_str(&format!("<a href=\"{url}\">{link_text}</a>"));
|
||||
i = k + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// Pas un lien valide, remettre le [
|
||||
result.push('[');
|
||||
result.push_str(&link_text);
|
||||
i = j;
|
||||
} else {
|
||||
result.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn html_escape(text: &str) -> String {
|
||||
text.replace('&', "&")
|
||||
.replace('<', "<")
|
||||
.replace('>', ">")
|
||||
.replace('"', """)
|
||||
}
|
||||
|
||||
/// Remplace toutes les occurrences de `vX.Y.Z` dans le HTML par la version courante
|
||||
fn replace_all_versions(html: &str, version: &str) -> String {
|
||||
let mut result = String::new();
|
||||
let chars: Vec<char> = html.chars().collect();
|
||||
let mut i = 0;
|
||||
|
||||
while i < chars.len() {
|
||||
if chars[i] == 'v'
|
||||
&& i + 1 < chars.len()
|
||||
&& chars[i + 1].is_ascii_digit()
|
||||
{
|
||||
// Essayer de parser une version vX.Y.Z ou vX.Y
|
||||
let mut j = i + 1;
|
||||
let mut dots = 0;
|
||||
while j < chars.len() {
|
||||
if chars[j].is_ascii_digit() {
|
||||
j += 1;
|
||||
} else if chars[j] == '.' && dots < 2 && j + 1 < chars.len() && chars[j + 1].is_ascii_digit() {
|
||||
dots += 1;
|
||||
j += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if dots >= 1 {
|
||||
// C'est une version — remplacer
|
||||
result.push_str(&format!("v{version}"));
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
result.push(chars[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
+99
-90
@@ -1,97 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Changelog</title></head>
|
||||
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;padding:24px;line-height:1.6;max-width:900px;margin:0 auto">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Changelog — imagod-web v0.2.0</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>
|
||||
a { color:#58a6ff }
|
||||
h1 { border-bottom:1px solid #30363d;padding-bottom:8px }
|
||||
h2 { margin-top:24px;color:#58a6ff }
|
||||
h3 { margin-top:16px;color:#79c0ff }
|
||||
li { margin:4px 0 }
|
||||
code { background:#161b22;padding:2px 6px;border-radius:4px;font-size:13px }
|
||||
table { border-collapse:collapse;width:100%;margin:12px 0 }
|
||||
td,th { border:1px solid #30363d;padding:8px 12px;text-align:left;font-size:13px }
|
||||
th { background:#161b22 }
|
||||
.tag { font-size:10px;padding:2px 6px;border-radius:8px;margin-right:4px }
|
||||
.tag-feat { background:rgba(88,166,255,0.15);color:#58a6ff }
|
||||
.tag-fix { background:rgba(63,185,80,0.15);color:#3fb950 }
|
||||
.tag-docs { background:rgba(210,153,34,0.15);color:#d29922 }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:'Segoe UI',system-ui,-apple-system,sans-serif; background:#0d1117; color:#c9d1d9; min-height:100vh; }
|
||||
header { background:#161b22; border-bottom:1px solid #30363d; padding:1rem 1.5rem; display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; z-index:100; }
|
||||
header h1 { font-size:1.2rem; color:#58a6ff; }
|
||||
nav { display:flex; gap:0.5rem; }
|
||||
nav a { color:#8b949e; text-decoration:none; padding:0.5rem 1rem; border-radius:6px; font-size:0.9rem; transition:all .2s; }
|
||||
nav a:hover, nav a.active { background:#21262d; color:#f0f6fc; }
|
||||
.version-badge { background:#23863622; color:#3fb950; padding:0.2rem 0.6rem; border-radius:1rem; font-size:0.75rem; border:1px solid #23863644; }
|
||||
main { max-width:900px; margin:2rem auto; padding:0 1.5rem; }
|
||||
h2 { color:#f0f6fc; margin:1.5rem 0 1rem; padding-bottom:0.5rem; border-bottom:1px solid #30363d; }
|
||||
h3 { color:#58a6ff; margin:1rem 0 0.5rem; }
|
||||
h4 { color:#8b949e; margin:0.5rem 0; }
|
||||
p { margin:0.5rem 0; line-height:1.6; }
|
||||
code { background:#161b22; padding:0.15em 0.4em; border-radius:3px; font-size:0.9em; color:#d2a8ff; }
|
||||
pre { background:#161b22; border:1px solid #30363d; border-radius:6px; padding:1rem; overflow-x:auto; margin:0.75rem 0; }
|
||||
pre code { background:none; padding:0; color:#c9d1d9; }
|
||||
li { margin:0.3rem 0 0.3rem 1.5rem; }
|
||||
li input[type=checkbox] { margin-right:0.5rem; }
|
||||
table { border-collapse:collapse; width:100%; margin:0.75rem 0; }
|
||||
th, td { border:1px solid #30363d; padding:0.5rem 0.75rem; text-align:left; font-size:0.9rem; }
|
||||
th { background:#161b22; color:#f0f6fc; }
|
||||
hr { border:none; border-top:1px solid #30363d; margin:1.5rem 0; }
|
||||
a { color:#58a6ff; }
|
||||
a:hover { text-decoration:underline; }
|
||||
strong { color:#f0f6fc; }
|
||||
em { color:#8b949e; }
|
||||
footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>📋 imagod-web <span class="version-badge">v0.2.0</span></h1>
|
||||
<nav>
|
||||
<a href="/">🖼️ Galerie</a>
|
||||
<a href="/changelog" class="active">📋 Changelog</a>
|
||||
<a href="/help">❓ Aide</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h1>Changelog — imagod-web</h1>
|
||||
<p>Toutes les modifications notables de imagod-web sont documentées ici.</p>
|
||||
<hr>
|
||||
<h2>[v0.2.0] — 2026-06-25</h2>
|
||||
<h3>🚀 Ajouts majeurs</h3>
|
||||
<li><strong>Phase 3 — Clipboard Watcher (Windows)</strong> : Module <code>clipboard.rs</code> avec capture d'image via <code>arboard</code>, conversion BGRA→PNG, hachage SHA-256 pour dédoublonnage</li>
|
||||
<li><strong>Phase 6 — Boucle Watcher</strong> : Polling unifié Windows (clipboard) / Linux-macOS (dossier), dédoublonnage SQLite persistant, pipeline complet détection → upload → nettoyage</li>
|
||||
<li><strong>Navigation dashboard</strong> : Barre d'onglets 🖼️ Galerie | 📋 Changelog | ❓ Aide</li>
|
||||
<li><strong>Badge version</strong> : Affichage <code>v0.2.0</code> dans l'en-tête du dashboard</li>
|
||||
<li><strong>Pages statiques</strong> : <code>changelog.html</code> et <code>help.html</code> générées depuis le markdown, encastrées dans le binaire</li>
|
||||
<li><strong>Endpoint version</strong> : <code>GET /api/version</code> retourne la version, le build, l'OS et l'architecture</li>
|
||||
<h3>🐛 Corrections</h3>
|
||||
<li><strong>Thumbnails 404</strong> : Les miniatures échouaient car les balises <code><img></code> n'envoyaient pas le header <code>X-API-Key</code>. Correction : <code>get_thumbnail()</code> utilise toujours la clé serveur configurée, pas le header utilisateur</li>
|
||||
<li><strong>Clé API expirée</strong> : Mise à jour de la configuration avec la clé valide</li>
|
||||
<h3>🧪 Tests</h3>
|
||||
<li><strong>Watcher Linux validé</strong> : Polling dossier fonctionnel, upload réussi vers Imago (image test ID=13), dédoublonnage confirmé</li>
|
||||
<li><strong>Phase 3 compilée</strong> : Code clipboard compilé et prêt, test Windows à venir</li>
|
||||
<h3>📚 Documentation</h3>
|
||||
<li><strong>ROADMAP.md</strong> : Créé avec planification complète des phases 3 à 15</li>
|
||||
<li><strong>CHANGELOG.md</strong> : Ce fichier</li>
|
||||
<hr>
|
||||
<h2>[v0.1.0] — 2026-06-23</h2>
|
||||
<h3>🏗️ Architecture</h3>
|
||||
<li>Structure du projet Rust (Cargo, modules)</li>
|
||||
<li>CLI avec <code>clap</code> : commandes <code>start</code>, <code>serve</code>, <code>stop</code>, <code>status</code>, <code>config</code>, <code>logs</code>, <code>install</code>, <code>uninstall</code></li>
|
||||
<li>Configuration TOML (<code>imagod.toml</code>) avec auto-génération</li>
|
||||
<li>Serveur web <code>axum</code> avec frontend vanilla JS embarqué via <code>rust-embed</code></li>
|
||||
<li>Multiplateforme : Windows, Linux, macOS (x86_64, ARM64)</li>
|
||||
<h3>🎨 Dashboard Web (Phase 7-9)</h3>
|
||||
<li>Interface sombre moderne, responsive mobile/desktop</li>
|
||||
<li><strong>Authentification</strong> : Login par clé API, validation immédiate, persistence localStorage</li>
|
||||
<li><strong>Galerie d'images</strong> : Affichage responsive avec thumbnails, recherche textuelle, stats (nombre, stockage)</li>
|
||||
<li><strong>Upload</strong> : Zone drag & drop, sélection de fichier, barre de progression</li>
|
||||
<li><strong>Suppression</strong> : Bouton corbeille avec confirmation, toast notification</li>
|
||||
<li><strong>Tags</strong> : Édition inline, ajout/suppression, enregistrement automatique</li>
|
||||
<li><strong>WebSocket</strong> : Connexion temps réel avec indicateur de statut</li>
|
||||
<li><strong>Modale</strong> : Détail complet (EXIF, OCR, description AI, tags, taille)</li>
|
||||
<li><strong>Filtrage</strong> : Recherche par nom, tags, description AI</li>
|
||||
<h3>🔌 Client Imago v2</h3>
|
||||
<li><strong>Endpoints</strong> : <code>list<em>images</code>, <code>search</em>images</code>, <code>upload<em>image</code>, <code>delete</em>image</code>, <code>update_tags</code></li>
|
||||
<li><strong><code>get_thumbnail()</code></strong> : Appel <code>thumbnail-url</code> + suivi URL signée + fallback <code>download-url</code> si 404</li>
|
||||
<li><strong><code>get_file()</code></strong> : Appel <code>download-url</code> + suivi URL signée</li>
|
||||
<li><strong>Auth</strong> : Header <code>X-API-Key</code> dynamique (clé serveur ou utilisateur)</li>
|
||||
<li><strong>Multi-tenant</strong> : Support header <code>X-Tenant-ID</code></li>
|
||||
<h3>🗄️ Dédoublonnage</h3>
|
||||
<li>Base SQLite locale pour le cache SHA-256</li>
|
||||
<li>Table <code>dedup_hashes</code> avec horodatage</li>
|
||||
<li>Nettoyage automatique du cache (limite configurable, défaut 10 000 entrées)</li>
|
||||
<h3>🔧 Configuration</h3>
|
||||
<li><code>imagod.toml</code> : sections <code>[imago]</code>, <code>[watcher]</code>, <code>[dedup]</code>, <code>[ui]</code>, <code>[logging]</code></li>
|
||||
<li>Paramètres watcher : <code>capture<em>images</code>, <code>poll</em>interval<em>ms</code>, <code>watch</em>dir</code>, <code>filters</code></li>
|
||||
<li>Gestion multi-environnement : développement (dev.lab.home) et production</li>
|
||||
<hr>
|
||||
<h2>Légende des tags</h2>
|
||||
|
||||
<h1>📋 Changelog — imagod-web</h1>
|
||||
|
||||
<h2>v0.1.0 — 2026-06-25</h2>
|
||||
|
||||
<h3><span class="tag tag-feat">feat</span> Dashboard Web</h3>
|
||||
<ul>
|
||||
<li>Authentification par clé API Imago (login/logout, localStorage)</li>
|
||||
<li>Galerie responsive (grille adaptative desktop/mobile, breakpoints 768px/400px)</li>
|
||||
<li>Upload par drag & drop avec feedback visuel</li>
|
||||
<li>Raccourcis clavier : Ctrl+U (upload), Ctrl+V (coller)</li>
|
||||
<li>Recherche plein texte (nom, tags, description)</li>
|
||||
<li>Pagination par pages de 24 images</li>
|
||||
<li>Modale détail : full résolution, EXIF, OCR, description AI</li>
|
||||
<li>Suppression avec boîte de confirmation</li>
|
||||
<li>Gestion des tags : ajout, suppression, édition inline</li>
|
||||
<li>Notifications toast (succès/erreur)</li>
|
||||
<li>FAB mobile pour upload</li>
|
||||
<li>Connexion WebSocket temps réel</li>
|
||||
<li>Statistiques (nombre d'images, stockage, tags, uptime)</li>
|
||||
</ul>
|
||||
|
||||
<h3><span class="tag tag-feat">feat</span> Client API Imago v2</h3>
|
||||
<ul>
|
||||
<li>8 endpoints : list, search, get, upload, thumbnail, download, delete, tags</li>
|
||||
<li>Proxy des URLs signées (thumbnail-url / download-url)</li>
|
||||
<li>Auth par header X-API-Key</li>
|
||||
</ul>
|
||||
|
||||
<h3><span class="tag tag-feat">feat</span> Watcher (Phase 3 + 6)</h3>
|
||||
<ul>
|
||||
<li>Windows : polling clipboard via arboard, conversion PNG automatique</li>
|
||||
<li>Linux/macOS : surveillance de dossier configurable (polling)</li>
|
||||
<li>Pipeline : détection → hash SHA-256 → dédoublonnage → upload → log</li>
|
||||
<li>Configuration : intervalle de polling, dossier à surveiller, filtres</li>
|
||||
</ul>
|
||||
|
||||
<h3><span class="tag tag-feat">feat</span> CLI</h3>
|
||||
<ul>
|
||||
<li>Commandes : start, serve, stop, status, config, install, uninstall, logs</li>
|
||||
<li>Flags : --host, --port, --api-key, --api-url, --no-watcher</li>
|
||||
<li>Gestion PID pour éviter les doublons d'instance</li>
|
||||
</ul>
|
||||
|
||||
<h3><span class="tag tag-feat">feat</span> Dédoublonnage</h3>
|
||||
<ul>
|
||||
<li>Hash SHA-256 des images</li>
|
||||
<li>Base SQLite locale (imagod.db)</li>
|
||||
<li>Cache LRU configurable</li>
|
||||
</ul>
|
||||
|
||||
<h3><span class="tag tag-fix">fix</span> Corrections</h3>
|
||||
<ul>
|
||||
<li>Structs Imago v2 (ExifData, OcrData, AiData) match l'API réelle</li>
|
||||
<li>Affichage via URLs signées (thumbnail-url/download-url)</li>
|
||||
<li>Fallback placeholder DOM quand pas de thumbnail</li>
|
||||
<li>Import delete inutilisé retiré</li>
|
||||
</ul>
|
||||
|
||||
<h3><span class="tag tag-docs">docs</span> Documentation</h3>
|
||||
<ul>
|
||||
<li>CHANGELOG.md dans le repo</li>
|
||||
<li>ROADMAP.md avec planification des phases</li>
|
||||
<li>Navigation in-app : Galerie / Changelog / Aide</li>
|
||||
</ul>
|
||||
|
||||
<h2>Architecture</h2>
|
||||
<table>
|
||||
<tr><th>Composant</th><th>Technologie</th><th>Statut</th></tr>
|
||||
<tr><td>Daemon</td><td>Rust (tokio, clap)</td><td>✅</td></tr>
|
||||
<tr><td>Serveur web</td><td>axum 0.8 + tower-http</td><td>✅</td></tr>
|
||||
<tr><td>Frontend</td><td>Vanilla JS + rust-embed</td><td>✅</td></tr>
|
||||
<tr><td>Client API</td><td>reqwest (rustls)</td><td>✅</td></tr>
|
||||
<tr><td>Dédoublonnage</td><td>SHA-256 + SQLite</td><td>✅</td></tr>
|
||||
<tr><td>Temps réel</td><td>WebSocket (broadcast)</td><td>✅</td></tr>
|
||||
</table>
|
||||
</main>
|
||||
<footer>imagod-web v0.2.0 — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Changelog — imagod-web v{{VERSION}}</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; }
|
||||
body { font-family:'Segoe UI',system-ui,-apple-system,sans-serif; background:#0d1117; color:#c9d1d9; min-height:100vh; }
|
||||
header { background:#161b22; border-bottom:1px solid #30363d; padding:1rem 1.5rem; display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; z-index:100; }
|
||||
header h1 { font-size:1.2rem; color:#58a6ff; }
|
||||
nav { display:flex; gap:0.5rem; }
|
||||
nav a { color:#8b949e; text-decoration:none; padding:0.5rem 1rem; border-radius:6px; font-size:0.9rem; transition:all .2s; }
|
||||
nav a:hover, nav a.active { background:#21262d; color:#f0f6fc; }
|
||||
.version-badge { background:#23863622; color:#3fb950; padding:0.2rem 0.6rem; border-radius:1rem; font-size:0.75rem; border:1px solid #23863644; }
|
||||
main { max-width:900px; margin:2rem auto; padding:0 1.5rem; }
|
||||
h2 { color:#f0f6fc; margin:1.5rem 0 1rem; padding-bottom:0.5rem; border-bottom:1px solid #30363d; }
|
||||
h3 { color:#58a6ff; margin:1rem 0 0.5rem; }
|
||||
h4 { color:#8b949e; margin:0.5rem 0; }
|
||||
p { margin:0.5rem 0; line-height:1.6; }
|
||||
code { background:#161b22; padding:0.15em 0.4em; border-radius:3px; font-size:0.9em; color:#d2a8ff; }
|
||||
pre { background:#161b22; border:1px solid #30363d; border-radius:6px; padding:1rem; overflow-x:auto; margin:0.75rem 0; }
|
||||
pre code { background:none; padding:0; color:#c9d1d9; }
|
||||
li { margin:0.3rem 0 0.3rem 1.5rem; }
|
||||
li input[type=checkbox] { margin-right:0.5rem; }
|
||||
table { border-collapse:collapse; width:100%; margin:0.75rem 0; }
|
||||
th, td { border:1px solid #30363d; padding:0.5rem 0.75rem; text-align:left; font-size:0.9rem; }
|
||||
th { background:#161b22; color:#f0f6fc; }
|
||||
hr { border:none; border-top:1px solid #30363d; margin:1.5rem 0; }
|
||||
a { color:#58a6ff; }
|
||||
a:hover { text-decoration:underline; }
|
||||
strong { color:#f0f6fc; }
|
||||
em { color:#8b949e; }
|
||||
footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>📋 imagod-web <span class="version-badge">v{{VERSION}}</span></h1>
|
||||
<nav>
|
||||
<a href="/">🖼️ Galerie</a>
|
||||
<a href="/changelog" class="active">📋 Changelog</a>
|
||||
<a href="/help">❓ Aide</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{{CHANGELOG_CONTENT}}
|
||||
</main>
|
||||
<footer>imagod-web v{{VERSION}} — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
|
||||
</body>
|
||||
</html>
|
||||
+167
-87
@@ -1,94 +1,174 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Aide</title></head>
|
||||
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0d1117;color:#c9d1d9;padding:24px;line-height:1.7;max-width:900px;margin:0 auto">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Aide & Roadmap — imagod-web v0.2.0</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>
|
||||
a { color:#58a6ff }
|
||||
h1 { border-bottom:1px solid #30363d;padding-bottom:8px }
|
||||
h2 { margin-top:28px;color:#58a6ff }
|
||||
h3 { margin-top:20px;color:#79c0ff }
|
||||
li { margin:6px 0 }
|
||||
code { background:#161b22;padding:2px 6px;border-radius:4px;font-size:13px }
|
||||
pre { background:#161b22;padding:12px 16px;border-radius:8px;overflow-x:auto;font-size:13px;border:1px solid #30363d }
|
||||
.highlight { background:rgba(88,166,255,0.1);border-left:3px solid #58a6ff;padding:8px 16px;border-radius:0 8px 8px 0;margin:12px 0 }
|
||||
.tip { background:rgba(63,185,80,0.1);border-left:3px solid #3fb950;padding:8px 16px;border-radius:0 8px 8px 0;margin:12px 0 }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:'Segoe UI',system-ui,-apple-system,sans-serif; background:#0d1117; color:#c9d1d9; min-height:100vh; }
|
||||
header { background:#161b22; border-bottom:1px solid #30363d; padding:1rem 1.5rem; display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; z-index:100; }
|
||||
header h1 { font-size:1.2rem; color:#58a6ff; }
|
||||
nav { display:flex; gap:0.5rem; }
|
||||
nav a { color:#8b949e; text-decoration:none; padding:0.5rem 1rem; border-radius:6px; font-size:0.9rem; transition:all .2s; }
|
||||
nav a:hover, nav a.active { background:#21262d; color:#f0f6fc; }
|
||||
.version-badge { background:#23863622; color:#3fb950; padding:0.2rem 0.6rem; border-radius:1rem; font-size:0.75rem; border:1px solid #23863644; }
|
||||
main { max-width:900px; margin:2rem auto; padding:0 1.5rem; }
|
||||
h2 { color:#f0f6fc; margin:1.5rem 0 1rem; padding-bottom:0.5rem; border-bottom:1px solid #30363d; }
|
||||
h3 { color:#58a6ff; margin:1rem 0 0.5rem; }
|
||||
h4 { color:#8b949e; margin:0.5rem 0; }
|
||||
p { margin:0.5rem 0; line-height:1.6; }
|
||||
code { background:#161b22; padding:0.15em 0.4em; border-radius:3px; font-size:0.9em; color:#d2a8ff; }
|
||||
pre { background:#161b22; border:1px solid #30363d; border-radius:6px; padding:1rem; overflow-x:auto; margin:0.75rem 0; }
|
||||
pre code { background:none; padding:0; color:#c9d1d9; }
|
||||
li { margin:0.3rem 0 0.3rem 1.5rem; }
|
||||
li input[type=checkbox] { margin-right:0.5rem; }
|
||||
table { border-collapse:collapse; width:100%; margin:0.75rem 0; }
|
||||
th, td { border:1px solid #30363d; padding:0.5rem 0.75rem; text-align:left; font-size:0.9rem; }
|
||||
th { background:#161b22; color:#f0f6fc; }
|
||||
hr { border:none; border-top:1px solid #30363d; margin:1.5rem 0; }
|
||||
a { color:#58a6ff; }
|
||||
a:hover { text-decoration:underline; }
|
||||
strong { color:#f0f6fc; }
|
||||
em { color:#8b949e; }
|
||||
footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>❓ imagod-web <span class="version-badge">v0.2.0</span></h1>
|
||||
<nav>
|
||||
<a href="/">🖼️ Galerie</a>
|
||||
<a href="/changelog">📋 Changelog</a>
|
||||
<a href="/help" class="active">❓ Aide</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<h1>Roadmap — imagod-web</h1>
|
||||
<p>Planification des évolutions de imagod-web.</p>
|
||||
<p>Les phases sont ordonnées par priorité et dépendances.</p>
|
||||
<hr>
|
||||
<h2>🟢 Phase 1–2 — Fondations ✅ *Complété*</h2>
|
||||
<li><input type="checkbox" checked disabled> Structure du projet Rust (Cargo, modules)</li>
|
||||
<li><input type="checkbox" checked disabled> CLI avec clap (commands <code>serve</code>, <code>watcher</code>, <code>install</code>, <code>uninstall</code>)</li>
|
||||
<li><input type="checkbox" checked disabled> Configuration TOML + variables d'environnement</li>
|
||||
<li><input type="checkbox" checked disabled> Client HTTP Imago v2 (reqwest + rustls)</li>
|
||||
<li><input type="checkbox" checked disabled> Dédoublonnage SHA-256 + SQLite</li>
|
||||
<li><input type="checkbox" checked disabled> Compilation Linux x86_64</li>
|
||||
<hr>
|
||||
<h2>🟢 Phase 7–9 — Dashboard Web ✅ *Complété*</h2>
|
||||
<li><input type="checkbox" checked disabled> Serveur axum avec frontend embarqué (rust-embed)</li>
|
||||
<li><input type="checkbox" checked disabled> API REST proxy vers Imago v2 (CRUD images, recherche, stats)</li>
|
||||
<li><input type="checkbox" checked disabled> Authentification par clé API (login/logout, localStorage)</li>
|
||||
<li><input type="checkbox" checked disabled> Galerie responsive desktop + mobile</li>
|
||||
<li><input type="checkbox" checked disabled> Upload drag & drop + raccourcis clavier</li>
|
||||
<li><input type="checkbox" checked disabled> Modale détail image (EXIF, OCR, AI, métadonnées)</li>
|
||||
<li><input type="checkbox" checked disabled> Suppression avec confirmation</li>
|
||||
<li><input type="checkbox" checked disabled> Gestion des tags inline (ajout, suppression, édition)</li>
|
||||
<li><input type="checkbox" checked disabled> Notifications toast</li>
|
||||
<li><input type="checkbox" checked disabled> WebSocket temps réel (hub broadcast)</li>
|
||||
<li><input type="checkbox" checked disabled> Pagination + recherche plein texte</li>
|
||||
<hr>
|
||||
<h2>🟢 Phase 3 — Clipboard Watcher (Windows) ✅ *Complété*</h2>
|
||||
<p><strong>Objectif</strong> : Capture automatique des screenshots depuis le presse-papier Windows.</p>
|
||||
<p><strong>Notes techniques</strong> :</p>
|
||||
<li>✅ Utiliser <code>arboard::Clipboard::new()</code> → <code>get_image()</code></li>
|
||||
<li>✅ Convertir <code>ImageData</code> en bytes PNG via le crate <code>image</code></li>
|
||||
<li>✅ Gérer le cas où le clipboard est vide ou contient du texte</li>
|
||||
<li>⚠️ Testé uniquement sous Linux (headless) — validation Windows à faire</li>
|
||||
<p><strong>Dépendances estimées</strong> : crate <code>image</code> (si pas déjà présent)</p>
|
||||
<hr>
|
||||
<h2>🟢 Phase 6 — Boucle principale du Watcher ✅ *Complété*</h2>
|
||||
<p><strong>Objectif</strong> : Assembler clipboard + dédoublonnage + upload dans une boucle de polling.</p>
|
||||
<p><strong>Pipeline détaillé</strong> :</p>
|
||||
<pre><code>┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ Poll │───▶│ Hash │───▶│ Dedup │───▶│ Upload │───▶│ Notify │
|
||||
│ Clipboard│ │ SHA-256 │ │ Check │ │ Imago │ │ WS Hub │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘</code></pre>
|
||||
<p><strong>Dépendances</strong> : Phase 3 (clipboard)</p>
|
||||
<hr>
|
||||
<h2>🟡 Correctifs Backend Imago</h2>
|
||||
<p>Ces tâches dépendent du backend Imago, pas d'imagod-web directement :</p>
|
||||
<hr>
|
||||
<h2>🟠 Phase 10 — Service Windows (NSSM)</h2>
|
||||
<p><strong>Objectif</strong> : Installation de imagod-web comme service Windows natif.</p>
|
||||
<p><strong>Approches possibles</strong> :</p>
|
||||
<p>1. <strong>NSSM</strong> (recommandé) — wrapper externe, simple et robuste</p>
|
||||
<p>2. <strong>windows-service</strong> crate — pure Rust, plus complexe</p>
|
||||
<p>3. <strong>sc.exe</strong> — commandes Windows natives</p>
|
||||
<hr>
|
||||
<h2>🟠 Phase 11 — Intégration Ditto Clipboard Manager</h2>
|
||||
<p><strong>Objectif</strong> : Plugin/addon Ditto qui envoie automatiquement les screenshots vers imagod-web.</p>
|
||||
<p><strong>Contexte</strong> : Bruno utilise <a href="https://ditto-cp.sourceforge.io/">Ditto</a>, un gestionnaire de presse-papier avancé pour Windows.</p>
|
||||
<p><strong>Deux approches envisageables</strong> :</p>
|
||||
<h3>Option A : Script externe Ditto</h3>
|
||||
<li>Ditto peut exécuter un script/exe quand un nouvel élément est copié</li>
|
||||
<li>Un petit binaire Rust qui appelle <code>POST /api/upload</code> de imagod-web</li>
|
||||
<li>Avantage : simple, découplé</li>
|
||||
<h3>Option B : Plugin Ditto natif</h3>
|
||||
<li>Ditto supporte des plugins/addons (C++ ou COM)</li>
|
||||
<li>Plugin qui intercepte les images et les envoie directement à Imago</li>
|
||||
<li>Avantage : plus intégré, pas de polling nécessaire</li>
|
||||
<p>> ⚠️ Nécessite une investigation sur l'API plugin Ditto (peu documentée)</p>
|
||||
<hr>
|
||||
<h2>🟠 Phase 12 — Cross-Compilation & CI/CD</h2>
|
||||
<p><strong>Objectif</strong> : Binaires natifs pour Windows x86_64, Linux ARM64.</p>
|
||||
<p><strong>Blocage potentiel</strong> : <code>arboard</code> et <code>windows-sys</code> sont Windows-only, il faut du conditional compilation.</p>
|
||||
<hr>
|
||||
<h2>🔵 Phase 13 — Application Système (Tray Icon)</h2>
|
||||
<p><strong>Objectif</strong> : Icône dans la zone de notification Windows avec menu contextuel.</p>
|
||||
<p><strong>Crate</strong> : <code>tray-icon</code> (déjà dans Cargo.toml)</p>
|
||||
<hr>
|
||||
<h2>🔵 Phase 14 — Dashboard Avancé</h2>
|
||||
<h3>14a — Galerie avec Filtres Avancés</h3>
|
||||
<li><input type="checkbox" disabled> Filtrer par plage de dates (date picker)</li>
|
||||
<li><input type="checkbox" disabled> Filtrer par tags (multi-sélection)</li>
|
||||
<li><input type="checkbox" disabled> Filtrer par statut de processing (pending/done/error)</li>
|
||||
<li><input type="checkbox" disabled> Trier par date, taille, nom</li>
|
||||
<h3>14b — Vue Timeline</h3>
|
||||
<li><input type="checkbox" disabled> Frise chronologique des captures</li>
|
||||
<li><input type="checkbox" disabled> Regroupement par jour/semaine/mois</li>
|
||||
<li><input type="checkbox" disabled> Navigation par scroll infini</li>
|
||||
<h3>14c — Batch Operations</h3>
|
||||
<li><input type="checkbox" disabled> Sélection multiple d'images (checkboxes)</li>
|
||||
<li><input type="checkbox" disabled> Suppression en masse</li>
|
||||
<li><input type="checkbox" disabled> Tag en masse</li>
|
||||
<li><input type="checkbox" disabled> Export ZIP</li>
|
||||
<h3>14d — Intégration Hermes Agent</h3>
|
||||
<li><input type="checkbox" disabled> Endpoint pour qu'Hermes puisse requêter les images</li>
|
||||
<li><input type="checkbox" disabled> Webhook de notification (nouvelle image → message Telegram)</li>
|
||||
<li><input type="checkbox" disabled> Skill Hermes <code>imagod-web</code> pour interroger la bibliothèque en langage naturel</li>
|
||||
<hr>
|
||||
<h2>🔵 Phase 15 — Client Mobile</h2>
|
||||
<h3>15a — PWA (Progressive Web App)</h3>
|
||||
<li><input type="checkbox" disabled> Service worker pour cache offline</li>
|
||||
<li><input type="checkbox" disabled> Manifest JSON pour installation</li>
|
||||
<li><input type="checkbox" disabled> Notifications push (nouvelles images)</li>
|
||||
<h3>15b — Share Intent Android/iOS</h3>
|
||||
<li><input type="checkbox" disabled> Endpoint API pour recevoir des images depuis le menu "Partager"</li>
|
||||
<li><input type="checkbox" disabled> Deep link <code>imagod://</code> pour ouverture directe</li>
|
||||
<hr>
|
||||
<h2>⚫ Idées Futures (Backlog)</h2>
|
||||
<li><input type="checkbox" disabled> <strong>OCR Search</strong> — Recherche dans le texte OCR extrait par Imago</li>
|
||||
<li><input type="checkbox" disabled> <strong>AI Semantic Search</strong> — « trouve les photos de chats dans mon salon »</li>
|
||||
<li><input type="checkbox" disabled> <strong>Export</strong> — ZIP, galerie HTML statique, PDF contact sheet</li>
|
||||
<li><input type="checkbox" disabled> <strong>Multi-tenant</strong> — Support de plusieurs clés API / espaces Imago</li>
|
||||
<li><input type="checkbox" disabled> <strong>Thèmes</strong> — Clair, contrasté, personnalisable</li>
|
||||
<li><input type="checkbox" disabled> <strong>Deduplication cross-tenant</strong> — Éviter les doublons entre utilisateurs</li>
|
||||
<li><input type="checkbox" disabled> <strong>Statistiques avancées</strong> — Graphiques (appareils, résolutions, heures de capture)</li>
|
||||
<li><input type="checkbox" disabled> <strong>Mode hors-ligne</strong> — File d'attente locale quand Imago est inaccessible</li>
|
||||
<hr>
|
||||
<h2>📊 Résumé des phases</h2>
|
||||
<hr>
|
||||
<h2>🎯 Prochaines actions immédiates (recommandées)</h2>
|
||||
<p>1. <strong>Corriger le scope <code>images:delete</code></strong> dans l'admin Imago → débloque la suppression</p>
|
||||
<p>2. <strong>Corriger le stockage Imago</strong> (upload 500) → débloque les tests</p>
|
||||
<p>3. <del><strong>Implémenter Phase 3</strong> (clipboard watcher)</del> ✅ — 25 juin 2026</p>
|
||||
<p>4. <del><strong>Implémenter Phase 6</strong> (watcher loop)</del> ✅ — 25 juin 2026</p>
|
||||
<p>5. <strong>Corriger le backend Imago</strong> (thumbnails, upload, stockage)</p>
|
||||
|
||||
<h1>❓ À propos d'imagod-web</h1>
|
||||
|
||||
<h2>Qu'est-ce que c'est ?</h2>
|
||||
<p>
|
||||
<strong>imagod-web</strong> est une application de bureau (daemon + dashboard web)
|
||||
qui capture automatiquement vos screenshots et les envoie vers
|
||||
<a href="https://git.dracodev.net/Projets/Imago" target="_blank">Imago</a>,
|
||||
un backend intelligent de gestion d'images.
|
||||
</p>
|
||||
|
||||
<div class="highlight">
|
||||
<strong>🎯 Objectif :</strong> ne plus jamais perdre un screenshot. Chaque capture
|
||||
est automatiquement analysée (OCR, vision IA, EXIF) et devient consultable
|
||||
via ce dashboard web.
|
||||
</div>
|
||||
|
||||
<h2>Fonctionnalités</h2>
|
||||
<ul>
|
||||
<li><strong>📸 Capture automatique</strong> — Windows : détection clipboard, Linux : surveillance de dossier</li>
|
||||
<li><strong>🧠 Analyse IA</strong> — description du contenu, tags automatiques, texte OCR</li>
|
||||
<li><strong>🔍 Recherche puissante</strong> — par nom, tags, description, texte extrait</li>
|
||||
<li><strong>🏷️ Tags personnalisés</strong> — ajoutez, modifiez, supprimez des tags</li>
|
||||
<li><strong>📊 Dashboard web</strong> — galerie responsive, upload drag & drop, modale détail</li>
|
||||
<li><strong>🔄 Temps réel</strong> — WebSocket pour voir les nouvelles captures instantanément</li>
|
||||
<li><strong>📱 Mobile-friendly</strong> — interface adaptative desktop et mobile</li>
|
||||
</ul>
|
||||
|
||||
<h2>Modes de fonctionnement</h2>
|
||||
|
||||
<h3>Windows</h3>
|
||||
<pre>imagod-web start</pre>
|
||||
<p>Lance le watcher (surveillance du presse-papier) + dashboard sur <code>http://127.0.0.1:9090</code>.</p>
|
||||
|
||||
<h3>Linux / macOS / Serveur</h3>
|
||||
<pre>imagod-web serve --host 0.0.0.0 --port 9199</pre>
|
||||
<p>Lance le dashboard + watcher de dossier. Configurez le dossier à surveiller dans <code>~/.config/imagod-web/imagod.toml</code> :</p>
|
||||
<pre>[watcher]
|
||||
poll_interval_ms = 1000
|
||||
watch_dir = "/home/user/Pictures/Screenshots"</pre>
|
||||
|
||||
<div class="tip">
|
||||
<strong>💡 Astuce :</strong> utilisez <code>--no-watcher</code> pour lancer uniquement le dashboard sans surveillance.
|
||||
</div>
|
||||
|
||||
<h2>Raccourcis clavier</h2>
|
||||
<ul>
|
||||
<li><code>Ctrl+U</code> — Ouvrir le sélecteur de fichiers</li>
|
||||
<li><code>Ctrl+V</code> — Coller une image depuis le presse-papier</li>
|
||||
<li><code>Escape</code> — Fermer la modale</li>
|
||||
<li><code>← →</code> — Navigation dans la pagination</li>
|
||||
</ul>
|
||||
|
||||
<h2>Architecture</h2>
|
||||
<pre>
|
||||
┌─────────────┐ ┌──────────────┐ ┌──────────┐
|
||||
│ Screenshot │────▶│ imagod-web │────▶│ Imago │
|
||||
│ (Windows/ │ │ (Rust) │ │ (Python)│
|
||||
│ Linux) │ │ │ │ │
|
||||
└─────────────┘ │ • Watcher │ │ • Stock │
|
||||
│ • Hash │ │ • OCR │
|
||||
│ • Upload │ │ • IA │
|
||||
│ • Dashboard │ │ • EXIF │
|
||||
└──────────────┘ └──────────┘
|
||||
</pre>
|
||||
|
||||
<h2>Liens</h2>
|
||||
<ul>
|
||||
<li><a href="https://git.dracodev.net/Projets/imagod-web" target="_blank">📦 Code source imagod-web</a></li>
|
||||
<li><a href="https://git.dracodev.net/Projets/Imago" target="_blank">🖼️ Backend Imago</a></li>
|
||||
<li><a href="http://dev.lab.home:3001" target="_blank">⚙️ Admin Imago (clés API)</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tip">
|
||||
<strong>📝 Besoin d'aide ?</strong> Contactez Bruno Charest via le repo Gitea pour toute question ou suggestion.
|
||||
</div>
|
||||
|
||||
</main>
|
||||
<footer>imagod-web v0.2.0 — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Aide & Roadmap — imagod-web v{{VERSION}}</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; }
|
||||
body { font-family:'Segoe UI',system-ui,-apple-system,sans-serif; background:#0d1117; color:#c9d1d9; min-height:100vh; }
|
||||
header { background:#161b22; border-bottom:1px solid #30363d; padding:1rem 1.5rem; display:flex; align-items:center; justify-content:space-between; position:sticky; top:0; z-index:100; }
|
||||
header h1 { font-size:1.2rem; color:#58a6ff; }
|
||||
nav { display:flex; gap:0.5rem; }
|
||||
nav a { color:#8b949e; text-decoration:none; padding:0.5rem 1rem; border-radius:6px; font-size:0.9rem; transition:all .2s; }
|
||||
nav a:hover, nav a.active { background:#21262d; color:#f0f6fc; }
|
||||
.version-badge { background:#23863622; color:#3fb950; padding:0.2rem 0.6rem; border-radius:1rem; font-size:0.75rem; border:1px solid #23863644; }
|
||||
main { max-width:900px; margin:2rem auto; padding:0 1.5rem; }
|
||||
h2 { color:#f0f6fc; margin:1.5rem 0 1rem; padding-bottom:0.5rem; border-bottom:1px solid #30363d; }
|
||||
h3 { color:#58a6ff; margin:1rem 0 0.5rem; }
|
||||
h4 { color:#8b949e; margin:0.5rem 0; }
|
||||
p { margin:0.5rem 0; line-height:1.6; }
|
||||
code { background:#161b22; padding:0.15em 0.4em; border-radius:3px; font-size:0.9em; color:#d2a8ff; }
|
||||
pre { background:#161b22; border:1px solid #30363d; border-radius:6px; padding:1rem; overflow-x:auto; margin:0.75rem 0; }
|
||||
pre code { background:none; padding:0; color:#c9d1d9; }
|
||||
li { margin:0.3rem 0 0.3rem 1.5rem; }
|
||||
li input[type=checkbox] { margin-right:0.5rem; }
|
||||
table { border-collapse:collapse; width:100%; margin:0.75rem 0; }
|
||||
th, td { border:1px solid #30363d; padding:0.5rem 0.75rem; text-align:left; font-size:0.9rem; }
|
||||
th { background:#161b22; color:#f0f6fc; }
|
||||
hr { border:none; border-top:1px solid #30363d; margin:1.5rem 0; }
|
||||
a { color:#58a6ff; }
|
||||
a:hover { text-decoration:underline; }
|
||||
strong { color:#f0f6fc; }
|
||||
em { color:#8b949e; }
|
||||
footer { text-align:center; color:#484f58; font-size:0.8rem; padding:2rem; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>❓ imagod-web <span class="version-badge">v{{VERSION}}</span></h1>
|
||||
<nav>
|
||||
<a href="/">🖼️ Galerie</a>
|
||||
<a href="/changelog">📋 Changelog</a>
|
||||
<a href="/help" class="active">❓ Aide</a>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
{{HELP_CONTENT}}
|
||||
</main>
|
||||
<footer>imagod-web v{{VERSION}} — <a href="https://git.dracodev.net/Projets/imagod-web">Gitea</a></footer>
|
||||
</body>
|
||||
</html>
|
||||
+32
-23
@@ -63,33 +63,42 @@ async fn serve_static(
|
||||
let path = path.trim_start_matches('/');
|
||||
|
||||
// Par défaut, servir index.html
|
||||
let asset_path = if path.is_empty() { "index.html" } else { path };
|
||||
let asset_path = if path.is_empty() { "index.html".to_string() } else { path.to_string() };
|
||||
|
||||
match FrontendAssets::get(asset_path) {
|
||||
Some(content) => {
|
||||
let mime = mime_guess(asset_path);
|
||||
axum::http::Response::builder()
|
||||
.header("Content-Type", mime)
|
||||
.header("Cache-Control", "public, max-age=3600")
|
||||
// Essayer avec le chemin exact
|
||||
if let Some(content) = FrontendAssets::get(&asset_path) {
|
||||
let mime = mime_guess(&asset_path);
|
||||
return axum::http::Response::builder()
|
||||
.header("Content-Type", mime)
|
||||
.header("Cache-Control", "public, max-age=3600")
|
||||
.body(axum::body::Body::from(content.data.to_vec()))
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// Essayer avec .html si pas d'extension
|
||||
if !asset_path.contains('.') {
|
||||
let html_path = format!("{}.html", asset_path);
|
||||
if let Some(content) = FrontendAssets::get(&html_path) {
|
||||
return axum::http::Response::builder()
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(axum::body::Body::from(content.data.to_vec()))
|
||||
.unwrap()
|
||||
}
|
||||
None => {
|
||||
// Fallback SPA: toutes les routes inconnues renvoient index.html
|
||||
if let Some(index) = FrontendAssets::get("index.html") {
|
||||
axum::http::Response::builder()
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(axum::body::Body::from(index.data.to_vec()))
|
||||
.unwrap()
|
||||
} else {
|
||||
axum::http::Response::builder()
|
||||
.status(axum::http::StatusCode::NOT_FOUND)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(axum::body::Body::from("404 — Page non trouvée"))
|
||||
.unwrap()
|
||||
}
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback SPA: toutes les routes inconnues renvoient index.html
|
||||
if let Some(index) = FrontendAssets::get("index.html") {
|
||||
axum::http::Response::builder()
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.body(axum::body::Body::from(index.data.to_vec()))
|
||||
.unwrap()
|
||||
} else {
|
||||
axum::http::Response::builder()
|
||||
.status(axum::http::StatusCode::NOT_FOUND)
|
||||
.header("Content-Type", "text/plain")
|
||||
.body(axum::body::Body::from("404 — Page non trouvée"))
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// Devine le type MIME à partir de l'extension
|
||||
|
||||
Reference in New Issue
Block a user