feat: upload/recipe/job API routes + Klarbild visual identity
- api: /uploads (multipart, HEIC, 25MB guard, S3), /recipes (GET/POST), /jobs (create+snapshot+enqueue, list), /jobs/:id (status+items), /jobs/:id/:action (pause/resume/cancel/retry-failed), /items/:id/retry - design: adopt Till mockup identity — Space Grotesk + Space Mono (self-hosted, no google/US), blue accent, paper/card, registration-mark motif; OKLCH tokens - verified: astro build passes
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { one, query } from '../../../lib/db';
|
||||
import { enqueue } from '../../../lib/queue';
|
||||
|
||||
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 ({ locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const rows = await query(
|
||||
`SELECT j.*, u.display_name AS by_name FROM jobs j
|
||||
LEFT JOIN users u ON u.id=j.created_by ORDER BY j.created_at DESC LIMIT 50`);
|
||||
return json({ jobs: rows });
|
||||
};
|
||||
|
||||
// Body: { recipeId?, recipe?, sources:[{source_path, filename, source_quality?}], origin? }
|
||||
export const POST: APIRoute = async ({ request, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const b = await request.json();
|
||||
const sources: any[] = b.sources || [];
|
||||
if (!sources.length) return json({ error: 'Keine Bilder.' }, 400);
|
||||
|
||||
let snapshot: any = b.recipe;
|
||||
if (b.recipeId) {
|
||||
const r = await one('SELECT * FROM recipes WHERE id=$1', [b.recipeId]);
|
||||
if (!r) return json({ error: 'Rezept nicht gefunden.' }, 404);
|
||||
snapshot = r;
|
||||
}
|
||||
if (!snapshot) return json({ error: 'Kein Rezept.' }, 400);
|
||||
|
||||
// Rezept-Snapshot einfrieren
|
||||
const snap = {
|
||||
tasks: snapshot.tasks || [],
|
||||
output_format: snapshot.output_format,
|
||||
orientation: snapshot.orientation,
|
||||
crop_mode: snapshot.crop_mode || 'crop',
|
||||
dpi: snapshot.dpi || 300,
|
||||
contour_mm: snapshot.contour_mm ?? null,
|
||||
model_key: snapshot.model_key ?? null,
|
||||
custom_instruction: snapshot.custom_instruction ?? null,
|
||||
delivery: snapshot.delivery || 'library',
|
||||
picdrop_gallery: snapshot.picdrop_gallery ?? null,
|
||||
};
|
||||
|
||||
const job = await one<{ id: string }>(
|
||||
`INSERT INTO jobs (created_by, origin, recipe_snapshot, status, total)
|
||||
VALUES ($1,$2,$3,'queued',$4) RETURNING id`,
|
||||
[locals.user.uid, b.origin || 'web', JSON.stringify(snap), sources.length]);
|
||||
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
const s = sources[i];
|
||||
const item = await one<{ id: string }>(
|
||||
`INSERT INTO items (job_id, position, status, source_path, filename, source_quality)
|
||||
VALUES ($1,$2,'queued',$3,$4,$5) RETURNING id`,
|
||||
[job!.id, i, s.source_path, s.filename || null, s.source_quality || 'original']);
|
||||
await enqueue({ itemId: item!.id, jobId: job!.id });
|
||||
}
|
||||
|
||||
return json({ jobId: job!.id });
|
||||
};
|
||||
Reference in New Issue
Block a user