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:
@@ -8,12 +8,12 @@ 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,
|
||||
const rows = await query<any>(`SELECT id, name, protocol, host, port, username, base_path, is_backup,
|
||||
(password_enc IS NOT NULL) AS password_set FROM delivery_targets ORDER BY name`);
|
||||
return json({ targets: rows });
|
||||
};
|
||||
|
||||
// POST: anlegen | { action:'test', id } testen
|
||||
// POST: anlegen | { action:'test', id } | { action:'backup_all' }
|
||||
export const POST: APIRoute = async ({ request }) => {
|
||||
const b = await request.json();
|
||||
if (b.action === 'test') {
|
||||
@@ -21,12 +21,16 @@ export const POST: APIRoute = async ({ request }) => {
|
||||
if (!cfg) return json({ ok: false, message: 'Ziel unvollständig konfiguriert.' });
|
||||
return json(await testConnection(cfg));
|
||||
}
|
||||
if (b.action === 'backup_all') {
|
||||
const { backupAll } = await import('../../../lib/backup');
|
||||
return json(await backupAll());
|
||||
}
|
||||
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`,
|
||||
`INSERT INTO delivery_targets (name, protocol, host, port, username, password_enc, base_path, is_backup)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8) 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]);
|
||||
b.password ? encrypt(String(b.password)) : null, b.base_path || null, !!b.is_backup]);
|
||||
return json({ id: row!.id });
|
||||
};
|
||||
|
||||
@@ -39,6 +43,7 @@ export const PATCH: APIRoute = async ({ request }) => {
|
||||
for (const c of ['name', 'protocol', 'host', 'port', 'username', 'base_path']) {
|
||||
if (c in b) set(c, b[c] === '' ? null : b[c]);
|
||||
}
|
||||
if ('is_backup' in b) set('is_backup', !!b.is_backup);
|
||||
if (b.password) set('password_enc', encrypt(String(b.password)));
|
||||
if (!sets.length) return json({ ok: true });
|
||||
args.push(b.id);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { one, query } from '../../../lib/db';
|
||||
import { encrypt, maskSecret, decrypt } from '../../../lib/crypto';
|
||||
|
||||
export const prerender = false;
|
||||
const randomToken = () => 'klb_' + randomBytes(24).toString('hex');
|
||||
const json = (b: unknown, s = 200) =>
|
||||
new Response(JSON.stringify(b), { status: s, headers: { 'Content-Type': 'application/json' } });
|
||||
|
||||
@@ -21,6 +23,7 @@ export const GET: APIRoute = async () => {
|
||||
cricut_sheet_cm: s.cricut_sheet_cm, monthly_budget: s.monthly_budget, n8n_webhook_url: s.n8n_webhook_url,
|
||||
// Speicherverwaltung
|
||||
keep_sources: s.keep_sources, make_thumbnails: s.make_thumbnails, retention_days: s.retention_days,
|
||||
metadata_sidecar: s.metadata_sidecar, api_token: s.api_token,
|
||||
// NAS-Sicherung
|
||||
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, nas_password_set: !!s.nas_password_enc,
|
||||
@@ -35,10 +38,11 @@ export const PATCH: APIRoute = async ({ request }) => {
|
||||
if (b.openrouter_key) set('openrouter_key_enc', encrypt(String(b.openrouter_key)));
|
||||
if (b.picdrop_password) set('picdrop_password_enc', encrypt(String(b.picdrop_password)));
|
||||
if (b.nas_password) set('nas_password_enc', encrypt(String(b.nas_password)));
|
||||
const boolCols = ['keep_sources', 'make_thumbnails', 'nas_enabled'];
|
||||
if (b.generate_api_token) set('api_token', randomToken());
|
||||
const boolCols = ['keep_sources', 'make_thumbnails', 'nas_enabled', 'metadata_sidecar'];
|
||||
for (const col of ['picdrop_host', 'picdrop_protocol', 'picdrop_port', 'picdrop_user', 'picdrop_base_path',
|
||||
'picdrop_default_gallery', 'default_dpi', 'default_crop_mode', 'concurrency', 'cricut_sheet_cm', 'monthly_budget', 'n8n_webhook_url',
|
||||
'keep_sources', 'make_thumbnails', 'retention_days',
|
||||
'keep_sources', 'make_thumbnails', 'retention_days', 'metadata_sidecar', 'api_token',
|
||||
'nas_enabled', 'nas_host', 'nas_protocol', 'nas_port', 'nas_user', 'nas_base_path']) {
|
||||
if (col in b) {
|
||||
const raw = b[col];
|
||||
|
||||
@@ -24,7 +24,7 @@ export const GET: APIRoute = async ({ url, locals }) => {
|
||||
|
||||
const rows = await query(
|
||||
`SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.thumb_path, i.folder_id,
|
||||
i.delivery_status, i.nas_status, i.created_at, i.model_used, i.variant_of, j.mode, u.display_name AS by_name,
|
||||
i.delivery_status, i.nas_status, i.prompt_used, i.created_at, i.model_used, i.variant_of, j.mode, u.display_name AS by_name,
|
||||
j.recipe_snapshot->'tasks' AS tasks
|
||||
FROM items i
|
||||
JOIN jobs j ON j.id = i.job_id
|
||||
|
||||
@@ -24,7 +24,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const promptText: string = (b.prompt_text || '').trim();
|
||||
|
||||
if (mode === 'each' && !sources.length) return json({ error: 'Keine Bilder.' }, 400);
|
||||
if (mode === 'compose' && sources.length < 2) return json({ error: 'Zum Kombinieren mindestens 2 Bilder.' }, 400);
|
||||
if (mode === 'compose' && sources.length < 1) return json({ error: 'Mindestens 1 Bild nötig.' }, 400);
|
||||
if (mode === 'compose' && !promptText) return json({ error: 'Bitte beschreiben, was entstehen soll.' }, 400);
|
||||
if (mode === 'generate' && !promptText) return json({ error: 'Bitte einen Text eingeben.' }, 400);
|
||||
|
||||
|
||||
@@ -16,10 +16,11 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
const b = await request.json();
|
||||
const row = await one(
|
||||
`INSERT INTO recipes (name, tasks, output_format, orientation, crop_mode, dpi, contour_mm,
|
||||
model_key, delivery, picdrop_gallery, custom_instruction, is_default, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,false,$12) RETURNING *`,
|
||||
model_key, delivery, picdrop_gallery, delivery_target_id, custom_instruction, mode, is_default, created_by)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,false,$14) RETURNING *`,
|
||||
[b.name, JSON.stringify(b.tasks || []), b.output_format, b.orientation, b.crop_mode || 'crop',
|
||||
b.dpi || 300, b.contour_mm ?? null, b.model_key ?? null, b.delivery || 'library',
|
||||
b.picdrop_gallery ?? null, b.custom_instruction ?? null, locals.user.uid]);
|
||||
b.picdrop_gallery ?? null, b.delivery_target_id ?? null, b.custom_instruction ?? null,
|
||||
['each', 'compose', 'generate'].includes(b.mode) ? b.mode : 'each', locals.user.uid]);
|
||||
return json({ recipe: row });
|
||||
};
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { one, 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' } });
|
||||
|
||||
const COLS = ['name', 'tasks', 'output_format', 'orientation', 'crop_mode', 'dpi', 'contour_mm',
|
||||
'model_key', 'delivery', 'picdrop_gallery', 'delivery_target_id', 'custom_instruction', 'mode'];
|
||||
|
||||
export const PATCH: APIRoute = async ({ params, request, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const b = await request.json();
|
||||
const sets: string[] = []; const args: any[] = [];
|
||||
const set = (c: string, v: any) => { args.push(v); sets.push(`${c}=$${args.length}`); };
|
||||
for (const c of COLS) {
|
||||
if (c in b) set(c, c === 'tasks' ? JSON.stringify(b[c] || []) : (b[c] === '' ? null : b[c]));
|
||||
}
|
||||
if (!sets.length) return json({ error: 'Nichts zu ändern.' }, 400);
|
||||
args.push(params.id);
|
||||
const row = await one(`UPDATE recipes SET ${sets.join(',')} WHERE id=$${args.length} RETURNING *`, args);
|
||||
return json({ recipe: row });
|
||||
};
|
||||
|
||||
export const DELETE: APIRoute = async ({ params, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
await query('DELETE FROM recipes WHERE id=$1', [params.id]);
|
||||
return json({ ok: true });
|
||||
};
|
||||
Reference in New Issue
Block a user