feat: Studio UI (upload/recipe/submit island) + queue view + library/file API
- StudioApp.tsx: upload (file/DnD/paste, HEIC, to /api/uploads), combinable task toggles with validation (contour needs cutout, freistellen+theframe blocked), format+orientation+live px+crop, contour mm, custom instruction, delivery, recipe prefill - QueueApp.tsx + warteschlange.astro: live polling, progress+ETA, per-item status, retry/pause/resume/cancel, result thumbnails+download - api/items (library filters), api/items/:id/file (stream result/source from S3) - index.astro renders Studio island
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import type { APIRoute } from 'astro';
|
||||
import { one } from '../../../../lib/db';
|
||||
import { getObject } from '../../../../lib/storage';
|
||||
|
||||
export const prerender = false;
|
||||
|
||||
// Streamt das Ergebnis- (oder Quell-)Bild aus dem Objektspeicher.
|
||||
export const GET: APIRoute = async ({ params, url, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const which = url.searchParams.get('src') === '1' ? 'source_path' : 'result_path';
|
||||
const item = await one<any>(`SELECT ${which} AS path, filename, has_alpha FROM items WHERE id=$1`, [params.id]);
|
||||
if (!item?.path) return new Response('Nicht gefunden', { status: 404 });
|
||||
try {
|
||||
const buf = await getObject(item.path);
|
||||
const download = url.searchParams.get('download') === '1';
|
||||
return new Response(buf, {
|
||||
headers: {
|
||||
'Content-Type': item.has_alpha ? 'image/png' : 'image/png',
|
||||
'Cache-Control': 'private, max-age=300',
|
||||
...(download ? { 'Content-Disposition': `attachment; filename="${item.filename || 'klarbild.png'}"` } : {}),
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return new Response('Datei nicht verfügbar', { status: 404 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
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' } });
|
||||
|
||||
// Bibliothek mit Filtern: ?task=&format=&folder=&delivery=&by=
|
||||
export const GET: APIRoute = async ({ url, locals }) => {
|
||||
if (!locals.user) return new Response('Unauthorized', { status: 401 });
|
||||
const q = url.searchParams;
|
||||
const where: string[] = [`i.status='done'`];
|
||||
const args: any[] = [];
|
||||
const add = (cond: string, val: any) => { args.push(val); where.push(cond.replace('?', `$${args.length}`)); };
|
||||
|
||||
if (q.get('folder')) add('i.folder_id = ?', q.get('folder'));
|
||||
if (q.get('delivery')) add('i.delivery_status = ?', q.get('delivery'));
|
||||
if (q.get('by')) add('j.created_by = ?', q.get('by'));
|
||||
if (q.get('format')) add('i.output_px = ?', q.get('format'));
|
||||
if (q.get('task')) {
|
||||
args.push(JSON.stringify([q.get('task')]));
|
||||
where.push(`j.recipe_snapshot->'tasks' @> $${args.length}::jsonb`);
|
||||
}
|
||||
|
||||
const rows = await query(
|
||||
`SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.folder_id,
|
||||
i.delivery_status, i.created_at, u.display_name AS by_name,
|
||||
j.recipe_snapshot->'tasks' AS tasks
|
||||
FROM items i
|
||||
JOIN jobs j ON j.id = i.job_id
|
||||
LEFT JOIN users u ON u.id = j.created_by
|
||||
WHERE ${where.join(' AND ')}
|
||||
ORDER BY i.created_at DESC LIMIT 300`, args);
|
||||
return json({ items: rows });
|
||||
};
|
||||
Reference in New Issue
Block a user