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:
@@ -0,0 +1,21 @@
|
|||||||
|
-- Mehrere Auslieferungsziele (FTP/SFTP-Quellen), z. B. eine zweite Picdrop-Galerie
|
||||||
|
-- oder ein anderer FTP-Server. Rezepte/Presets können ein Ziel fest verdrahten.
|
||||||
|
CREATE TABLE IF NOT EXISTS delivery_targets (
|
||||||
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
name text NOT NULL,
|
||||||
|
protocol text CHECK (protocol IN ('ftps','sftp')),
|
||||||
|
host text,
|
||||||
|
port int,
|
||||||
|
username text,
|
||||||
|
password_enc text,
|
||||||
|
base_path text,
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Preset → festes Ziel (NULL = Standard-Picdrop aus den Einstellungen).
|
||||||
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS delivery_target_id uuid
|
||||||
|
REFERENCES delivery_targets(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- Ordner → festes Ziel (optional).
|
||||||
|
ALTER TABLE folders ADD COLUMN IF NOT EXISTS delivery_target_id uuid
|
||||||
|
REFERENCES delivery_targets(id) ON DELETE SET NULL;
|
||||||
@@ -10,20 +10,34 @@ export default function AdminApp() {
|
|||||||
const [toast, setToast] = useState<string | null>(null);
|
const [toast, setToast] = useState<string | null>(null);
|
||||||
const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState(''); const [nasPw, setNasPw] = useState('');
|
const [orKey, setOrKey] = useState(''); const [pdPw, setPdPw] = useState(''); const [nasPw, setNasPw] = useState('');
|
||||||
const [storage, setStorage] = useState<any>(null);
|
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 notify = (t: string) => { setToast(t); setTimeout(() => setToast(null), 2600); };
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
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/settings').then((r) => r.json()),
|
||||||
fetch('/api/admin/stats').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/models?available=1').then((r) => r.json()),
|
||||||
fetch('/api/admin/users').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/telegram/links').then((r) => r.json()).catch(() => ({ links: [] })),
|
||||||
fetch('/api/admin/storage').then((r) => r.json()).catch(() => ({ stats: null })),
|
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 || []);
|
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 ? '—'
|
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`;
|
: 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>
|
</div>
|
||||||
</section>
|
</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>
|
<button className="knopf" onClick={saveSettings}>Einstellungen speichern</button>
|
||||||
|
|
||||||
<section className="karte">
|
<section className="karte">
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ export default function StudioApp({ recipes }: { recipes: any[] }) {
|
|||||||
const [custom, setCustom] = useState('');
|
const [custom, setCustom] = useState('');
|
||||||
const [desc, setDesc] = useState('');
|
const [desc, setDesc] = useState('');
|
||||||
const [delivery, setDelivery] = useState<'library' | 'picdrop' | 'both'>('library');
|
const [delivery, setDelivery] = useState<'library' | 'picdrop' | 'both'>('library');
|
||||||
|
const [gallery, setGallery] = useState('');
|
||||||
|
const [targetId, setTargetId] = useState('');
|
||||||
|
const [targets, setTargets] = useState<any[]>([]);
|
||||||
const [models, setModels] = useState<any[]>([]);
|
const [models, setModels] = useState<any[]>([]);
|
||||||
const [modelKey, setModelKey] = useState<string>('');
|
const [modelKey, setModelKey] = useState<string>('');
|
||||||
const [dragging, setDragging] = useState(false);
|
const [dragging, setDragging] = useState(false);
|
||||||
@@ -113,6 +116,7 @@ export default function StudioApp({ recipes }: { recipes: any[] }) {
|
|||||||
const def = ms.find((m: any) => m.is_default) || ms[0];
|
const def = ms.find((m: any) => m.is_default) || ms[0];
|
||||||
if (def) setModelKey(def.model_id);
|
if (def) setModelKey(def.model_id);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
|
fetch('/api/delivery-targets').then((r) => r.json()).then((j) => setTargets(j.targets || [])).catch(() => {});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Ergebnis aus der Bibliothek übernehmen: ?reuse=<itemId> lädt es als Vorlage.
|
// Ergebnis aus der Bibliothek übernehmen: ?reuse=<itemId> lädt es als Vorlage.
|
||||||
@@ -151,6 +155,8 @@ export default function StudioApp({ recipes }: { recipes: any[] }) {
|
|||||||
setContourMm(Number(r.contour_mm) || 3);
|
setContourMm(Number(r.contour_mm) || 3);
|
||||||
setCustom(r.custom_instruction || '');
|
setCustom(r.custom_instruction || '');
|
||||||
setDelivery(r.delivery || 'library');
|
setDelivery(r.delivery || 'library');
|
||||||
|
setGallery(r.picdrop_gallery || '');
|
||||||
|
setTargetId(r.delivery_target_id || '');
|
||||||
if (r.model_key) setModelKey(r.model_key);
|
if (r.model_key) setModelKey(r.model_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,6 +182,8 @@ export default function StudioApp({ recipes }: { recipes: any[] }) {
|
|||||||
contour_mm: tasks.includes('contour') ? contourMm : null,
|
contour_mm: tasks.includes('contour') ? contourMm : null,
|
||||||
custom_instruction: mode === 'each' ? (custom || null) : null,
|
custom_instruction: mode === 'each' ? (custom || null) : null,
|
||||||
delivery, model_key: modelKey || null,
|
delivery, model_key: modelKey || null,
|
||||||
|
picdrop_gallery: (delivery !== 'library' && gallery.trim()) ? gallery.trim() : null,
|
||||||
|
delivery_target_id: (delivery !== 'library' && targetId) ? targetId : null,
|
||||||
};
|
};
|
||||||
const body: any = { recipe, mode, delivery };
|
const body: any = { recipe, mode, delivery };
|
||||||
if (mode !== 'each') body.prompt_text = desc.trim();
|
if (mode !== 'each') body.prompt_text = desc.trim();
|
||||||
@@ -348,6 +356,18 @@ export default function StudioApp({ recipes }: { recipes: any[] }) {
|
|||||||
<button className={delivery === 'picdrop' ? 'an' : ''} onClick={() => setDelivery('picdrop')}>Picdrop</button>
|
<button className={delivery === 'picdrop' ? 'an' : ''} onClick={() => setDelivery('picdrop')}>Picdrop</button>
|
||||||
<button className={delivery === 'both' ? 'an' : ''} onClick={() => setDelivery('both')}>Beides</button>
|
<button className={delivery === 'both' ? 'an' : ''} onClick={() => setDelivery('both')}>Beides</button>
|
||||||
</div>
|
</div>
|
||||||
|
{delivery !== 'library' && (
|
||||||
|
<div className="ziel">
|
||||||
|
{targets.length > 0 && (
|
||||||
|
<select className="select" value={targetId} onChange={(e) => setTargetId(e.target.value)}>
|
||||||
|
<option value="">Standard-Picdrop</option>
|
||||||
|
{targets.map((t) => <option key={t.id} value={t.id}>{t.name}</option>)}
|
||||||
|
</select>
|
||||||
|
)}
|
||||||
|
<input className="input" value={gallery} onChange={(e) => setGallery(e.target.value)}
|
||||||
|
placeholder="Galerie/Ordner (leer = Standard)" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button className="knopf" disabled={!canRun} onClick={run}>
|
<button className="knopf" disabled={!canRun} onClick={run}>
|
||||||
@@ -412,6 +432,7 @@ function StudioStyles() {
|
|||||||
.schalter button{flex:1;background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px;cursor:pointer;font-family:inherit;font-size:13px;color:var(--soft);}
|
.schalter button{flex:1;background:#fff;border:1px solid var(--line);border-radius:3px;padding:8px;cursor:pointer;font-family:inherit;font-size:13px;color:var(--soft);}
|
||||||
.schalter button.an{border-color:var(--accent);background:var(--accent-bg);color:var(--ink);font-weight:600;}
|
.schalter button.an{border-color:var(--accent);background:var(--accent-bg);color:var(--ink);font-weight:600;}
|
||||||
.schalter.zart button{font-size:12.5px;}
|
.schalter.zart button{font-size:12.5px;}
|
||||||
|
.ziel{display:flex;flex-direction:column;gap:7px;margin-top:8px;}
|
||||||
.schalter.wrap{flex-wrap:wrap;}
|
.schalter.wrap{flex-wrap:wrap;}
|
||||||
.schalter.wrap button{flex:1 1 auto;min-width:110px;}
|
.schalter.wrap button{flex:1 1 auto;min-width:110px;}
|
||||||
.knopf{width:100%;border:none;border-radius:3px;padding:13px;cursor:pointer;background:var(--accent);color:#fff;font-family:inherit;font-weight:600;font-size:15px;}
|
.knopf{width:100%;border:none;border-radius:3px;padding:13px;cursor:pointer;background:var(--accent);color:#fff;font-family:inherit;font-weight:600;font-size:15px;}
|
||||||
|
|||||||
+15
-3
@@ -1,6 +1,6 @@
|
|||||||
import { one, query } from './db';
|
import { one, query } from './db';
|
||||||
import { getObject } from './storage';
|
import { getObject } from './storage';
|
||||||
import { loadConfig, uploadBuffer } from './picdrop';
|
import { loadConfig, loadTargetConfig, uploadBuffer, type PicdropCfg } from './picdrop';
|
||||||
|
|
||||||
// Standard-Galerie wie initial gebrieft.
|
// Standard-Galerie wie initial gebrieft.
|
||||||
const DEFAULT_GALLERY = process.env.PICDROP_DEFAULT_GALLERY || 'POSTER LEA';
|
const DEFAULT_GALLERY = process.env.PICDROP_DEFAULT_GALLERY || 'POSTER LEA';
|
||||||
@@ -17,12 +17,24 @@ async function galleryFor(item: { folder_id: string | null; job_id: string }): P
|
|||||||
return s?.picdrop_default_gallery || DEFAULT_GALLERY;
|
return s?.picdrop_default_gallery || DEFAULT_GALLERY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ziel-Zugang: 1) Ordner-Ziel 2) Rezept-Ziel 3) Standard-Picdrop (Einstellungen). */
|
||||||
|
async function targetFor(item: { folder_id: string | null; job_id: string }): Promise<PicdropCfg | null> {
|
||||||
|
if (item.folder_id) {
|
||||||
|
const f = await one<{ delivery_target_id: string | null }>('SELECT delivery_target_id FROM folders WHERE id=$1', [item.folder_id]).catch(() => null);
|
||||||
|
if (f?.delivery_target_id) { const c = await loadTargetConfig(f.delivery_target_id); if (c) return c; }
|
||||||
|
}
|
||||||
|
const j = await one<{ recipe_snapshot: any }>('SELECT recipe_snapshot FROM jobs WHERE id=$1', [item.job_id]);
|
||||||
|
const tid = j?.recipe_snapshot?.delivery_target_id;
|
||||||
|
if (tid) { const c = await loadTargetConfig(tid); if (c) return c; }
|
||||||
|
return loadConfig(); // Standard-Picdrop
|
||||||
|
}
|
||||||
|
|
||||||
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus. */
|
/** Liefert ein fertiges Item an die passende Picdrop-Galerie aus. */
|
||||||
export async function deliverItem(itemId: string): Promise<{ ok: boolean; message: string }> {
|
export async function deliverItem(itemId: string): Promise<{ ok: boolean; message: string }> {
|
||||||
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id FROM items WHERE id=$1', [itemId]);
|
const it = await one<any>('SELECT id, result_path, filename, folder_id, job_id FROM items WHERE id=$1', [itemId]);
|
||||||
if (!it?.result_path) return { ok: false, message: 'Kein Ergebnis vorhanden.' };
|
if (!it?.result_path) return { ok: false, message: 'Kein Ergebnis vorhanden.' };
|
||||||
const cfg = await loadConfig();
|
const cfg = await targetFor(it);
|
||||||
if (!cfg) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Picdrop nicht konfiguriert.' }; }
|
if (!cfg) { await query(`UPDATE items SET delivery_status='failed' WHERE id=$1`, [itemId]); return { ok: false, message: 'Kein Auslieferungsziel konfiguriert.' }; }
|
||||||
|
|
||||||
await query(`UPDATE items SET delivery_status='pending' WHERE id=$1`, [itemId]);
|
await query(`UPDATE items SET delivery_status='pending' WHERE id=$1`, [itemId]);
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -22,6 +22,20 @@ export async function loadConfig(): Promise<PicdropCfg | null> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Zusätzliches Auslieferungsziel aus delivery_targets. */
|
||||||
|
export async function loadTargetConfig(id: string): Promise<PicdropCfg | null> {
|
||||||
|
const s = await one<any>(`SELECT protocol, host, port, username, password_enc, base_path
|
||||||
|
FROM delivery_targets WHERE id=$1`, [id]);
|
||||||
|
if (!s?.host || !s?.username || !s?.password_enc) return null;
|
||||||
|
let password = '';
|
||||||
|
try { password = decrypt(s.password_enc); } catch { return null; }
|
||||||
|
return {
|
||||||
|
host: s.host, protocol: (s.protocol || 'sftp'),
|
||||||
|
port: s.port || (s.protocol === 'ftps' ? 21 : 22),
|
||||||
|
user: s.username, password, basePath: s.base_path || '/',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Verbindung testen: verbinden + Basisordner listen. */
|
/** Verbindung testen: verbinden + Basisordner listen. */
|
||||||
export async function testConnection(cfg: PicdropCfg): Promise<{ ok: boolean; message: string }> {
|
export async function testConnection(cfg: PicdropCfg): Promise<{ ok: boolean; message: string }> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
+7
-1
@@ -48,8 +48,14 @@ export async function seed(): Promise<void> {
|
|||||||
|
|
||||||
await R('Nur bereinigen', ['clean'], 'keep', 'landscape', { is_default: true });
|
await R('Nur bereinigen', ['clean'], 'keep', 'landscape', { is_default: true });
|
||||||
await R('Poster 30×40', ['clean', 'format'], '30x40', 'portrait');
|
await R('Poster 30×40', ['clean', 'format'], '30x40', 'portrait');
|
||||||
await R('The Frame', ['clean', 'format'], 'theframe', 'landscape');
|
await R('The Frame', ['clean', 'format'], 'theframe', 'landscape',
|
||||||
|
{ delivery: 'both', picdrop_gallery: 'TheFrame-Backgrounds' });
|
||||||
await R('Sticker 5 cm', ['clean', 'cutout', 'format', 'contour'], 'sticker5', 'landscape',
|
await R('Sticker 5 cm', ['clean', 'cutout', 'format', 'contour'], 'sticker5', 'landscape',
|
||||||
{ contour_mm: 3 });
|
{ contour_mm: 3 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The-Frame-Rezept auf die eigene Galerie verdrahten (auch für bestehende Installationen).
|
||||||
|
await query(
|
||||||
|
`UPDATE recipes SET picdrop_gallery='TheFrame-Backgrounds', delivery='both'
|
||||||
|
WHERE output_format='theframe' AND (picdrop_gallery IS NULL OR picdrop_gallery='')`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { one, query } from '../../../lib/db';
|
||||||
|
import { encrypt } from '../../../lib/crypto';
|
||||||
|
import { loadTargetConfig, testConnection } from '../../../lib/picdrop';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
const json = (b: unknown, s = 200) =>
|
||||||
|
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
const rows = await query<any>(`SELECT id, name, protocol, host, port, username, base_path,
|
||||||
|
(password_enc IS NOT NULL) AS password_set FROM delivery_targets ORDER BY name`);
|
||||||
|
return json({ targets: rows });
|
||||||
|
};
|
||||||
|
|
||||||
|
// POST: anlegen | { action:'test', id } testen
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
const b = await request.json();
|
||||||
|
if (b.action === 'test') {
|
||||||
|
const cfg = await loadTargetConfig(b.id);
|
||||||
|
if (!cfg) return json({ ok: false, message: 'Ziel unvollständig konfiguriert.' });
|
||||||
|
return json(await testConnection(cfg));
|
||||||
|
}
|
||||||
|
if (!b.name?.trim()) return json({ error: 'Name fehlt.' }, 400);
|
||||||
|
const row = await one<any>(
|
||||||
|
`INSERT INTO delivery_targets (name, protocol, host, port, username, password_enc, base_path)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7) RETURNING id`,
|
||||||
|
[b.name.trim(), b.protocol || 'sftp', b.host || null, b.port || null, b.username || null,
|
||||||
|
b.password ? encrypt(String(b.password)) : null, b.base_path || null]);
|
||||||
|
return json({ id: row!.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
// PATCH: bearbeiten
|
||||||
|
export const PATCH: APIRoute = async ({ request }) => {
|
||||||
|
const b = await request.json();
|
||||||
|
if (!b.id) return json({ error: 'Keine ID.' }, 400);
|
||||||
|
const sets: string[] = []; const args: any[] = [];
|
||||||
|
const set = (c: string, v: any) => { args.push(v); sets.push(`${c}=$${args.length}`); };
|
||||||
|
for (const c of ['name', 'protocol', 'host', 'port', 'username', 'base_path']) {
|
||||||
|
if (c in b) set(c, b[c] === '' ? null : b[c]);
|
||||||
|
}
|
||||||
|
if (b.password) set('password_enc', encrypt(String(b.password)));
|
||||||
|
if (!sets.length) return json({ ok: true });
|
||||||
|
args.push(b.id);
|
||||||
|
await query(`UPDATE delivery_targets SET ${sets.join(',')} WHERE id=$${args.length}`, args);
|
||||||
|
return json({ ok: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DELETE: APIRoute = async ({ request, url }) => {
|
||||||
|
let id = url.searchParams.get('id');
|
||||||
|
if (!id) { try { id = (await request.json())?.id; } catch { /* egal */ } }
|
||||||
|
if (!id) return json({ error: 'Keine ID.' }, 400);
|
||||||
|
await query('DELETE FROM delivery_targets WHERE id=$1', [id]);
|
||||||
|
return json({ ok: true });
|
||||||
|
};
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { query } from '../../lib/db';
|
||||||
|
|
||||||
|
export const prerender = false;
|
||||||
|
const json = (b: unknown, s = 200) =>
|
||||||
|
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||||
|
|
||||||
|
// Namen der Zusatzziele fürs Studio-Dropdown (kein Zugang).
|
||||||
|
export const GET: APIRoute = async ({ locals }) => {
|
||||||
|
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||||
|
const rows = await query('SELECT id, name FROM delivery_targets ORDER BY name');
|
||||||
|
return json({ targets: rows });
|
||||||
|
};
|
||||||
@@ -49,6 +49,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
prompt_text: promptText || null,
|
prompt_text: promptText || null,
|
||||||
delivery: snapshot.delivery || 'library',
|
delivery: snapshot.delivery || 'library',
|
||||||
picdrop_gallery: snapshot.picdrop_gallery ?? null,
|
picdrop_gallery: snapshot.picdrop_gallery ?? null,
|
||||||
|
delivery_target_id: snapshot.delivery_target_id ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
const total = (mode === 'each') ? sources.length : 1;
|
const total = (mode === 'each') ? sources.length : 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user