diff --git a/src/components/AdminApp.tsx b/src/components/AdminApp.tsx index 4898be4..214e328 100644 --- a/src/components/AdminApp.tsx +++ b/src/components/AdminApp.tsx @@ -8,20 +8,24 @@ export default function AdminApp() { const [users, setUsers] = useState([]); const [tgLinks, setTgLinks] = useState([]); const [toast, setToast] = useState(null); - const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState(''); + const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState(''); const [nasPw, setNasPw] = useState(''); + const [storage, setStorage] = useState(null); const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2600); }; const load = useCallback(async () => { - const [a, b, c, d, e] = await Promise.all([ + const [a, b, c, d, e, f] = await Promise.all([ fetch('/api/admin/settings').then((r) => r.json()), fetch('/api/admin/stats').then((r) => r.json()), fetch('/api/admin/models?available=1').then((r) => r.json()), fetch('/api/admin/users').then((r) => r.json()), fetch('/api/admin/telegram/links').then((r) => r.json()).catch(() => ({ links: [] })), + fetch('/api/admin/storage').then((r) => r.json()).catch(() => ({ stats: null })), ]); setS(a.settings || {}); setStats(b); setModels(c.models || []); setAvail(c.available || []); - setUsers(d.users || []); setTgLinks(e.links || []); + setUsers(d.users || []); setTgLinks(e.links || []); setStorage(f.stats || null); }, []); + const fmtBytes = (n: number | null) => n == null ? '—' + : n > 1e9 ? `${(n / 1e9).toFixed(2)} GB` : n > 1e6 ? `${(n / 1e6).toFixed(1)} MB` : `${Math.round(n / 1e3)} KB`; const pairCode = async (userId: string) => { const r = await fetch('/api/admin/telegram/pairing-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ userId }) }).then((x) => x.json()); @@ -40,17 +44,33 @@ export default function AdminApp() { picdrop_user: s.picdrop_user, picdrop_base_path: s.picdrop_base_path, picdrop_default_gallery: s.picdrop_default_gallery, default_dpi: s.default_dpi, default_crop_mode: s.default_crop_mode, concurrency: s.concurrency, cricut_sheet_cm: s.cricut_sheet_cm, monthly_budget: s.monthly_budget, n8n_webhook_url: s.n8n_webhook_url, + keep_sources: !!s.keep_sources, make_thumbnails: s.make_thumbnails !== false, retention_days: s.retention_days, + nas_enabled: !!s.nas_enabled, nas_host: s.nas_host, nas_protocol: s.nas_protocol, nas_port: s.nas_port, + nas_user: s.nas_user, nas_base_path: s.nas_base_path, }; if (orKey.trim()) body.openrouter_key = orKey.trim(); if (pdPw.trim()) body.picdrop_password = pdPw.trim(); + if (nasPw.trim()) body.nas_password = nasPw.trim(); await fetch('/api/admin/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); - setOrKey(''); setPdPw(''); notify('Gespeichert.'); load(); + setOrKey(''); setPdPw(''); setNasPw(''); notify('Gespeichert.'); load(); }; const testPicdrop = async () => { notify('Teste …'); const r = await fetch('/api/admin/test-picdrop', { method: 'POST' }).then((x) => x.json()); notify(r.message || (r.ok ? 'OK' : 'Fehler')); }; + const testNas = async () => { + notify('Teste NAS …'); + const r = await fetch('/api/admin/test-nas', { method: 'POST' }).then((x) => x.json()); + notify(r.message || (r.ok ? 'OK' : 'Fehler')); + }; + const storageAction = async (action: string, confirmMsg?: string) => { + if (confirmMsg && !confirm(confirmMsg)) return; + notify('Räume auf …'); + const r = await fetch('/api/admin/storage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }) }).then((x) => x.json()); + notify(r.deleted != null ? `${r.deleted} alte Positionen entfernt.` : r.purged != null ? `${r.purged} Quelldateien gelöscht.` : (r.error || 'OK')); + load(); + }; const addModel = async (m: any) => { await fetch('/api/admin/models', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ model_id: m.model_id, label: m.name, supports_alpha: m.supports_alpha, active: true }) }); @@ -134,6 +154,47 @@ export default function AdminApp() { +
+
Speicher & Aufräumen + {storage && {fmtBytes(storage.bytes)} · {storage.results} Ergebnisse · {storage.sources} Quellen · {storage.thumbs} Vorschau}
+
+ + +
+ field('retention_days', e.target.value)} /> +
Ältere Bilder werden automatisch (täglich) entfernt. Leer = nichts löschen.
+
+ + {s.retention_days ? : null} +
+
+
+ +
+
Synology-NAS (zweite Sicherung) +
+
+ +
+
field('nas_host', e.target.value)} />
+
field('nas_port', e.target.value)} />
+
+
+
+
+
field('nas_user', e.target.value)} />
+
+
+ setNasPw(e.target.value)} />
+
field('nas_base_path', e.target.value)} />
+
Bei Synology am einfachsten SFTP aktivieren (Systemsteuerung → Terminal & SNMP → SFTP). Zugangsdaten werden verschlüsselt gespeichert.
+
+
+
@@ -213,6 +274,11 @@ function AdminStyles() { .zeile b{font-size:14px;} .pille{font-family:var(--font-mono);font-size:9px;letter-spacing:.1em;text-transform:uppercase;background:var(--accent-bg);color:var(--accent);padding:2px 7px;border-radius:20px;margin-left:6px;} .pille.alpha{background:var(--paper);color:var(--soft);} + .schalt{display:flex;gap:10px;align-items:flex-start;cursor:pointer;} + .schalt input{margin-top:3px;width:16px;height:16px;accent-color:var(--accent);flex:0 0 auto;} + .schalt b{display:block;font-size:14px;} + .schalt em{display:block;font-style:normal;font-size:11.5px;color:var(--soft);margin-top:1px;line-height:1.4;} + .schalt code{font-family:var(--font-mono);font-size:11px;} .avail{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;} .reihe{display:flex;gap:6px;} .mini{background:var(--card);border:1px solid var(--line);border-radius:3px;padding:6px 10px;cursor:pointer;font-family:inherit;font-size:12px;color:var(--ink);}