feat: multiple delivery targets (FTP/SFTP), per-preset wiring; The Frame -> TheFrame-Backgrounds gallery
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
@@ -10,20 +10,34 @@ export default function AdminApp() {
|
||||
const [toast, setToast] = useState<string | null>(null);
|
||||
const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState(''); const [nasPw, setNasPw] = useState('');
|
||||
const [storage, setStorage] = useState<any>(null);
|
||||
const [targets, setTargets] = useState<any[]>([]);
|
||||
const [nt, setNt] = useState<any>({ protocol: 'sftp' });
|
||||
const notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2600); };
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const [a, b, c, d, e, f] = await Promise.all([
|
||||
const [a, b, c, d, e, f, g] = 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 })),
|
||||
fetch('/api/admin/delivery-targets').then((r) => r.json()).catch(() => ({ targets: [] })),
|
||||
]);
|
||||
setS(a.settings || {}); setStats(b); setModels(c.models || []); setAvail(c.available || []);
|
||||
setUsers(d.users || []); setTgLinks(e.links || []); setStorage(f.stats || null);
|
||||
setUsers(d.users || []); setTgLinks(e.links || []); setStorage(f.stats || null); setTargets(g.targets || []);
|
||||
}, []);
|
||||
const addTarget = async () => {
|
||||
if (!nt.name?.trim()) { notify('Name fehlt.'); return; }
|
||||
await fetch('/api/admin/delivery-targets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(nt) });
|
||||
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 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());
|
||||
notify(r.message || (r.ok ? 'OK' : 'Fehler'));
|
||||
};
|
||||
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`;
|
||||
|
||||
@@ -222,6 +236,42 @@ export default function AdminApp() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="karte">
|
||||
<div className="kopfzeile"><span className="mono-label">Weitere Ausgabeziele (FTP/SFTP)</span></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>
|
||||
<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 className="reihe">
|
||||
<button className="mini" onClick={() => testTarget(t.id)}>Test</button>
|
||||
<button className="mini" onClick={() => delTarget(t.id)}>✕</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{targets.length === 0 && <div className="fein">Noch keine zusätzlichen Ziele.</div>}
|
||||
</div>
|
||||
<div className="zwei">
|
||||
<div className="feld"><label>Name</label><input className="input" placeholder="z. B. The Frame FTP" value={nt.name ?? ''} onChange={(e) => setNt({ ...nt, name: e.target.value })} /></div>
|
||||
<div className="feld"><label>Protokoll</label>
|
||||
<select className="input" value={nt.protocol} onChange={(e) => setNt({ ...nt, protocol: e.target.value })}>
|
||||
<option value="sftp">SFTP</option><option value="ftps">FTPS</option></select></div>
|
||||
</div>
|
||||
<div className="zwei">
|
||||
<div className="feld"><label>Host</label><input className="input" value={nt.host ?? ''} onChange={(e) => setNt({ ...nt, host: e.target.value })} /></div>
|
||||
<div className="feld"><label>Port</label><input className="input" type="number" value={nt.port ?? ''} onChange={(e) => setNt({ ...nt, port: e.target.value })} /></div>
|
||||
</div>
|
||||
<div className="zwei">
|
||||
<div className="feld"><label>Benutzer</label><input className="input" value={nt.username ?? ''} onChange={(e) => setNt({ ...nt, username: e.target.value })} /></div>
|
||||
<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>
|
||||
<button className="mini" onClick={addTarget}>Ziel hinzufügen</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<button className="knopf" onClick={saveSettings}>Einstellungen speichern</button>
|
||||
|
||||
<section className="karte">
|
||||
|
||||
Reference in New Issue
Block a user