feat: watcher (phase 3+6) + navigation dashboard (version, changelog, aide)
Phase 3 — Clipboard Watcher (Windows): - clipboard.rs: détection d'images via arboard, conversion BGRA→PNG - Gère les doublons consécutifs (même image dans le presse-papier) Phase 6 — Watcher Loop: - Windows: polling clipboard → hash → dédoublonnage → upload - Linux/macOS: polling de dossier configurable (watch_dir) - Ajout champ watch_dir dans WatcherConfig - Client Imago cloné avant await (évite problème Send/Sync) Dashboard: - Badge version v0.1.0 dans le header - Barre de navigation: Galerie / Changelog / Aide - Pages changelog.html et help.html (chargement dynamique) - Endpoint GET /api/version avec infos build - Correction import delete inutilisé dans routes.rs
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<!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">
|
||||
<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 }
|
||||
</style>
|
||||
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!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">
|
||||
<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 }
|
||||
</style>
|
||||
|
||||
<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>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -47,6 +47,59 @@
|
||||
.ws-dot.off { background: var(--red); }
|
||||
.ws-status { font-size: 12px; color: var(--fg-dim); display: flex; align-items: center; gap: 6px; white-space: nowrap; }
|
||||
|
||||
/* ─── Version ─── */
|
||||
.version-badge {
|
||||
font-size: 10px; padding: 2px 8px; border-radius: 10px;
|
||||
background: rgba(88,166,255,0.1); color: var(--accent);
|
||||
font-weight: 500; white-space: nowrap;
|
||||
border: 1px solid rgba(88,166,255,0.2);
|
||||
}
|
||||
|
||||
/* ─── Nav ─── */
|
||||
.nav-bar {
|
||||
display: flex; gap: 0; border-bottom: 1px solid var(--border);
|
||||
background: var(--bg); padding: 0 24px;
|
||||
position: sticky; top: 56px; z-index: 99;
|
||||
}
|
||||
.nav-tab {
|
||||
padding: 10px 20px; font-size: 13px; font-weight: 500;
|
||||
color: var(--fg-dim); cursor: pointer; border: none;
|
||||
background: none; border-bottom: 2px solid transparent;
|
||||
transition: all 0.15s; white-space: nowrap;
|
||||
font-family: inherit;
|
||||
}
|
||||
.nav-tab:hover { color: var(--fg); }
|
||||
.nav-tab.active { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
|
||||
/* ─── Pages ─── */
|
||||
.page { display: none; }
|
||||
.page.active { display: block; }
|
||||
.page-content {
|
||||
max-width: 900px; margin: 0 auto; padding: 24px;
|
||||
line-height: 1.7;
|
||||
}
|
||||
.page-content h1 { border-bottom: 1px solid var(--border); padding-bottom: 8px; margin-bottom: 16px; }
|
||||
.page-content h2 { margin-top: 24px; color: var(--accent); }
|
||||
.page-content h3 { margin-top: 16px; color: var(--accent-hover); }
|
||||
.page-content ul, .page-content ol { padding-left: 24px; }
|
||||
.page-content li { margin: 4px 0; }
|
||||
.page-content pre {
|
||||
background: var(--bg-card); padding: 12px 16px; border-radius: var(--radius);
|
||||
overflow-x: auto; font-size: 13px; border: 1px solid var(--border);
|
||||
}
|
||||
.page-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
|
||||
.page-content td, .page-content th { border: 1px solid var(--border); padding: 8px 12px; font-size: 13px; }
|
||||
.page-content th { background: var(--bg-card); }
|
||||
.page-content a { color: var(--accent); }
|
||||
.page-content .callout {
|
||||
border-left: 3px solid var(--accent); padding: 8px 16px;
|
||||
background: rgba(88,166,255,0.05); border-radius: 0 var(--radius) var(--radius) 0;
|
||||
margin: 12px 0;
|
||||
}
|
||||
.page-content .callout.tip {
|
||||
border-left-color: var(--green); background: rgba(63,185,80,0.05);
|
||||
}
|
||||
|
||||
/* ─── Search ─── */
|
||||
.search-bar { display: flex; gap: 8px; flex: 1; min-width: 180px; }
|
||||
.search-bar input {
|
||||
@@ -361,13 +414,24 @@
|
||||
<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.1.0</span>
|
||||
</div>
|
||||
|
||||
<!-- ─── Navigation ─── -->
|
||||
<nav class="nav-bar" id="nav-bar">
|
||||
<button class="nav-tab active" data-page="gallery">🖼️ Galerie</button>
|
||||
<button class="nav-tab" data-page="changelog">📋 Changelog</button>
|
||||
<button class="nav-tab" data-page="help">❓ Aide</button>
|
||||
</nav>
|
||||
|
||||
<!-- ─── Toast Container ─── -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<!-- ─── Main Content ─── -->
|
||||
<div class="main">
|
||||
|
||||
<!-- Page: Galerie -->
|
||||
<div class="page active" id="page-gallery">
|
||||
<!-- Stats -->
|
||||
<div class="stats-row" id="stats-row">
|
||||
<div class="stat-card"><div class="stat-label">Images</div><div class="stat-value" id="stat-images">…</div></div>
|
||||
@@ -392,6 +456,27 @@
|
||||
<div style="font-size:48px;opacity:0.35">🔍</div>
|
||||
<p>Aucun résultat trouvé</p>
|
||||
</div>
|
||||
</div><!-- /#page-gallery -->
|
||||
|
||||
<!-- Page: Changelog -->
|
||||
<div class="page" id="page-changelog">
|
||||
<div class="page-content" id="changelog-content">
|
||||
<div style="text-align:center;padding:40px;color:var(--fg-dim)">
|
||||
<div style="font-size:32px;margin-bottom:12px">📋</div>
|
||||
Chargement du changelog…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Page: Aide -->
|
||||
<div class="page" id="page-help">
|
||||
<div class="page-content" id="help-content">
|
||||
<div style="text-align:center;padding:40px;color:var(--fg-dim)">
|
||||
<div style="font-size:32px;margin-bottom:12px">❓</div>
|
||||
Chargement de l'aide…
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ─── Modal ─── -->
|
||||
@@ -409,6 +494,66 @@ let currentQuery = '';
|
||||
let ws = null;
|
||||
let apiKey = null;
|
||||
let userName = null;
|
||||
let currentNavPage = 'gallery';
|
||||
|
||||
// ─── Navigation ───
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.querySelectorAll('.nav-tab').forEach(tab => {
|
||||
tab.addEventListener('click', () => switchPage(tab.dataset.page));
|
||||
});
|
||||
});
|
||||
|
||||
function switchPage(page) {
|
||||
currentNavPage = page;
|
||||
document.querySelectorAll('.nav-tab').forEach(t => t.classList.toggle('active', t.dataset.page === page));
|
||||
document.querySelectorAll('.page').forEach(p => {
|
||||
p.classList.toggle('active', p.id === 'page-' + page);
|
||||
});
|
||||
|
||||
// Cacher/montrer les éléments spécifiques à la galerie
|
||||
const fabUpload = document.getElementById('fab-upload');
|
||||
if (fabUpload) fabUpload.style.display = page === 'gallery' ? '' : 'none';
|
||||
|
||||
// Charger le contenu si première fois
|
||||
if (page === 'changelog') loadChangelog();
|
||||
if (page === 'help') loadHelp();
|
||||
}
|
||||
|
||||
let changelogLoaded = false;
|
||||
let helpLoaded = false;
|
||||
|
||||
async function loadChangelog() {
|
||||
if (changelogLoaded) return;
|
||||
const container = document.getElementById('changelog-content');
|
||||
try {
|
||||
const r = await fetch('/changelog.html');
|
||||
if (r.ok) {
|
||||
let html = await r.text();
|
||||
// Extraire le contenu du body (les pages sont des HTML complets)
|
||||
const m = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
container.innerHTML = m ? m[1] : html;
|
||||
changelogLoaded = true;
|
||||
}
|
||||
} catch(e) {
|
||||
container.innerHTML = '<div style="color:var(--red)">Erreur de chargement du changelog.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHelp() {
|
||||
if (helpLoaded) return;
|
||||
const container = document.getElementById('help-content');
|
||||
try {
|
||||
const r = await fetch('/help.html');
|
||||
if (r.ok) {
|
||||
let html = await r.text();
|
||||
const m = html.match(/<body[^>]*>([\s\S]*)<\/body>/i);
|
||||
container.innerHTML = m ? m[1] : html;
|
||||
helpLoaded = true;
|
||||
}
|
||||
} catch(e) {
|
||||
container.innerHTML = '<div style="color:var(--red)">Erreur de chargement de l\'aide.</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Auth Helpers ───
|
||||
function getAuthHeaders() {
|
||||
|
||||
Reference in New Issue
Block a user