feat: custom formats (WxH/stickerN), JPG output option, failed-job status

- format.ts parses free formats from the key: sticker<N> -> N×N cm,
  <W>x<H> -> W×H cm. Fixes 'Unbekanntes Format: sticker5' (the seeded
  'Sticker 5 cm' recipe was unusable) and lets admins define ANY format
  without a migration. Guarded to <=300 cm.
- Output file format PNG/JPG: global default (settings.output_ext) plus
  per-recipe/per-conversion override. JPG only for non-alpha; cutouts
  stay PNG. file.ts sniffs content-type from magic bytes.
- Jobs can now be 'failed' (all items failed) -> red in the queue; add
  finished time to queue list + detail. migration 012.
- Studio: custom-measure input + Dateiformat selector; Admin: global
  Standard-Dateiformat.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
2026-07-24 08:32:03 +00:00
parent efcb889c96
commit 3a061aa612
12 changed files with 142 additions and 21 deletions
+18 -2
View File
@@ -22,6 +22,7 @@ interface RecipeSnapshot {
prompt_text?: string | null; // Beschreibung für compose/generate
delivery?: 'library' | 'picdrop' | 'both';
picdrop_gallery?: string | null;
output_ext?: 'png' | 'jpg' | null; // Dateiformat-Override; sonst globale Einstellung
}
/** Welches Modell-ID + Alpha-Fähigkeit? Aus models-Tabelle, sonst Default. */
@@ -130,8 +131,23 @@ export async function processItem(itemId: string): Promise<ProcessResult> {
hasAlpha = true;
}
// Ausgabeformat: Rezept-Override, sonst globale Einstellung. JPG nur ohne Transparenz.
const gset = await one<{ output_ext: string | null }>('SELECT output_ext FROM settings WHERE id=1');
const wantExt = (r.output_ext || gset?.output_ext || 'png').toLowerCase();
let ext = 'png';
let contentType = 'image/png';
if (wantExt === 'jpg' && !hasAlpha) {
outBuf = await sharp(outBuf, { failOn: 'none' })
.flatten({ background: '#ffffff' })
.withMetadata({ density: dpi })
.jpeg({ quality: 92, mozjpeg: true, chromaSubsampling: '4:4:4' })
.toBuffer();
ext = 'jpg';
contentType = 'image/jpeg';
}
const key = resultKey(item.id);
await putObject(key, outBuf, 'image/png');
await putObject(key, outBuf, contentType);
// Vorschaubild (optional, laut Einstellung)
const cfg = await one<{ make_thumbnails: boolean; keep_sources: boolean; nas_enabled: boolean }>(
@@ -145,7 +161,7 @@ export async function processItem(itemId: string): Promise<ProcessResult> {
} catch (e) { console.error('[process] Thumbnail fehlgeschlagen', e); }
}
const filename = buildResultFilename(item.filename, formatToken(r.output_format), 'png');
const filename = buildResultFilename(item.filename, formatToken(r.output_format), ext);
const outputPx = `${fin.width}x${fin.height}`;
// Private Aufträge: kein Delivery, kein Backup, kein gespeicherter Prompt.
const deliveryStatus = (!isPrivate && (r.delivery === 'picdrop' || r.delivery === 'both')) ? 'pending' : 'none';