feat: single-image transform, NAS/FTP in delivery, backup-all any target, metadata sidecar, prompt view, preset manager, API token + MCP server
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -33,6 +33,21 @@ export default function AdminApp() {
|
||||
setNt({ protocol: 'sftp' }); notify('Ziel angelegt.'); load();
|
||||
};
|
||||
const delTarget = async (id: string) => { if (!confirm('Ziel löschen?')) return; await fetch(`/api/admin/delivery-targets?id=${id}`, { method: 'DELETE' }); load(); };
|
||||
const toggleBackup = async (t: any) => {
|
||||
await fetch('/api/admin/delivery-targets', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ id: t.id, is_backup: !t.is_backup }) });
|
||||
load();
|
||||
};
|
||||
const backupAllTargets = async () => {
|
||||
if (!confirm('Alle fertigen Bilder auf alle Backup-Ziele (NAS + markierte Ziele) sichern?')) return;
|
||||
notify('Sichere … (kann dauern)');
|
||||
const r = await fetch('/api/admin/delivery-targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'backup_all' }) }).then((x) => x.json());
|
||||
notify(r.items != null ? `${r.mirrored} Kopien auf Backup-Ziele${r.failed ? `, ${r.failed} Fehler` : ''}.` : (r.error || 'OK'));
|
||||
load();
|
||||
};
|
||||
const genToken = async () => {
|
||||
await fetch('/api/admin/settings', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ generate_api_token: true }) });
|
||||
notify('API-Token erzeugt.'); load();
|
||||
};
|
||||
const testTarget = async (id: string) => {
|
||||
notify('Teste Ziel …');
|
||||
const r = await fetch('/api/admin/delivery-targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'test', id }) }).then((x) => x.json());
|
||||
@@ -59,6 +74,7 @@ export default function AdminApp() {
|
||||
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,
|
||||
metadata_sidecar: !!s.metadata_sidecar,
|
||||
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,
|
||||
};
|
||||
@@ -201,6 +217,8 @@ export default function AdminApp() {
|
||||
<span><b>Vorschaubilder erzeugen</b><em>Kleine, sparsame Bilder für die Bibliotheksvorschau.</em></span></label>
|
||||
<label className="schalt"><input type="checkbox" checked={!s.keep_sources} onChange={(e) => field('keep_sources', !e.target.checked)} />
|
||||
<span><b>Quellbilder nach Bearbeitung löschen</b><em>Spart Platz — Originale werden nach dem Ergebnis entfernt.</em></span></label>
|
||||
<label className="schalt"><input type="checkbox" checked={!!s.metadata_sidecar} onChange={(e) => field('metadata_sidecar', e.target.checked)} />
|
||||
<span><b>Metadaten als .md mitschicken</b><em>Zu jedem Bild eine begleitende Textdatei (Prompt, Modell, Format …) bei Picdrop/NAS/FTP.</em></span></label>
|
||||
<div className="feld"><label>Aufbewahrung (Tage)</label>
|
||||
<input className="input" type="number" min={0} placeholder="leer = unbegrenzt" value={s.retention_days ?? ''} onChange={(e) => field('retention_days', e.target.value)} />
|
||||
<div className="fein">Ältere Bilder werden automatisch (täglich) entfernt. Leer = nichts löschen.</div></div>
|
||||
@@ -237,15 +255,18 @@ export default function AdminApp() {
|
||||
</section>
|
||||
|
||||
<section className="karte">
|
||||
<div className="kopfzeile"><span className="mono-label">Weitere Ausgabeziele (FTP/SFTP)</span></div>
|
||||
<div className="kopfzeile"><span className="mono-label">Weitere Ausgabeziele (FTP/SFTP)</span>
|
||||
<button className="mini" onClick={backupAllTargets}>Alle Bilder sichern</button></div>
|
||||
<div className="steuer">
|
||||
<div className="fein">Zusätzliche FTP/SFTP-Ziele — z. B. eine zweite Picdrop-Galerie oder ein anderer Server.
|
||||
Ein Preset (Rezept) oder Ordner kann fest auf ein Ziel zeigen; sonst gilt das Standard-Picdrop oben.</div>
|
||||
Ein Preset/Ordner kann fest auf ein Ziel zeigen; im Studio unter „Wohin?" wählbar (auch NAS).
|
||||
„Backup"-Ziele erhalten automatisch <b>alle</b> Ergebnisse (wie das NAS).</div>
|
||||
<div className="liste">
|
||||
{targets.map((t) => (
|
||||
<div key={t.id} className="zeile">
|
||||
<div><b>{t.name}</b> <span className="fein">{t.protocol}://{t.username}@{t.host}:{t.port} · {t.base_path || '/'}{t.password_set ? '' : ' · ⚠︎ kein Passwort'}</span></div>
|
||||
<div><b>{t.name}</b>{t.is_backup && <span className="pille">Backup</span>} <span className="fein">{t.protocol}://{t.username}@{t.host}:{t.port} · {t.base_path || '/'}{t.password_set ? '' : ' · ⚠︎ kein Passwort'}</span></div>
|
||||
<div className="reihe">
|
||||
<button className="mini" onClick={() => toggleBackup(t)}>{t.is_backup ? 'Backup aus' : 'Als Backup'}</button>
|
||||
<button className="mini" onClick={() => testTarget(t.id)}>Test</button>
|
||||
<button className="mini" onClick={() => delTarget(t.id)}>✕</button>
|
||||
</div>
|
||||
@@ -268,10 +289,23 @@ export default function AdminApp() {
|
||||
<div className="feld"><label>Passwort</label><input className="input" type="password" value={nt.password ?? ''} onChange={(e) => setNt({ ...nt, password: e.target.value })} /></div>
|
||||
</div>
|
||||
<div className="feld"><label>Basisordner</label><input className="input" placeholder="/" value={nt.base_path ?? ''} onChange={(e) => setNt({ ...nt, base_path: e.target.value })} /></div>
|
||||
<label className="schalt"><input type="checkbox" checked={!!nt.is_backup} onChange={(e) => setNt({ ...nt, is_backup: e.target.checked })} />
|
||||
<span><b>Als Backup-Ziel</b><em>Erhält automatisch alle Ergebnisse.</em></span></label>
|
||||
<button className="mini" onClick={addTarget}>Ziel hinzufügen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="karte">
|
||||
<div className="kopfzeile"><span className="mono-label">Automatisierung / MCP-Zugriff</span></div>
|
||||
<div className="steuer">
|
||||
<div className="fein">API-Token für externen/programmatischen Zugriff (z. B. Klarbild-MCP: „nimm diese Bilder und verarbeite sie"). Als <code>Authorization: Bearer <Token></code> an <code>/api/uploads</code> und <code>/api/jobs</code>.</div>
|
||||
{s.api_token
|
||||
? <div className="feld"><label>API-Token</label><input className="input" readOnly value={s.api_token} onFocus={(e) => e.target.select()} /></div>
|
||||
: <div className="fein">Noch kein Token erzeugt.</div>}
|
||||
<button className="mini" onClick={genToken}>{s.api_token ? 'Neuen Token erzeugen' : 'Token erzeugen'}</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button className="knopf" onClick={saveSettings}>Einstellungen speichern</button>
|
||||
|
||||
<section className="karte">
|
||||
|
||||
Reference in New Issue
Block a user