feat: regenerate/alternatives grouped by source (variant_of)

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-23 19:39:04 +00:00
parent 34aa45b292
commit e59d26fadd
3 changed files with 36 additions and 3 deletions
+4
View File
@@ -0,0 +1,4 @@
-- Alternativen/Regenerierung: mehrere Ergebnisse zum selben Ursprung gruppieren.
-- variant_of zeigt auf das „Wurzel"-Item (die erste Fassung); Alternativen teilen es.
ALTER TABLE items ADD COLUMN IF NOT EXISTS variant_of uuid REFERENCES items(id) ON DELETE SET NULL;
CREATE INDEX IF NOT EXISTS items_variant_idx ON items(variant_of);
+29
View File
@@ -0,0 +1,29 @@
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' } });
/** Erzeugt eine weitere Fassung (Alternative) zum selben Ursprung, gleiches Rezept. */
export const POST: APIRoute = async ({ params, locals }) => {
if (!locals.user) return new Response('Unauthorized', { status: 401 });
const item = await one<any>(
`SELECT id, job_id, source_path, source_paths, filename, variant_of FROM items WHERE id=$1`, [params.id]);
if (!item) return json({ error: 'Nicht gefunden.' }, 404);
// Ohne Quelle & ohne Modellaufruf gibt es nichts zu variieren.
const root = item.variant_of || item.id;
const it = await one<{ id: string }>(
`INSERT INTO items (job_id, position, status, source_path, source_paths, filename, source_quality, variant_of)
VALUES ($1, (SELECT COALESCE(max(position),0)+1 FROM items WHERE job_id=$1), 'queued', $2, $3, $4, 'original', $5)
RETURNING id`,
[item.job_id, item.source_path, item.source_paths ? JSON.stringify(item.source_paths) : null,
item.filename, root]);
// Auftrag zählt eine Position mehr und läuft erneut an.
await query(`UPDATE jobs SET total=total+1, status='queued', finished_at=NULL
WHERE id=$1 AND status IN ('done','paused')`, [item.job_id]);
await enqueue({ itemId: it!.id, jobId: item.job_id });
return json({ ok: true, itemId: it!.id });
};
+3 -3
View File
@@ -23,13 +23,13 @@ export const GET: APIRoute = async ({ url, locals }) => {
} }
const rows = await query( const rows = await query(
`SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.folder_id, `SELECT i.id, i.filename, i.output_px, i.has_alpha, i.result_path, i.thumb_path, i.folder_id,
i.delivery_status, i.created_at, i.model_used, u.display_name AS by_name, i.delivery_status, i.created_at, i.model_used, i.variant_of, j.mode, u.display_name AS by_name,
j.recipe_snapshot->'tasks' AS tasks j.recipe_snapshot->'tasks' AS tasks
FROM items i FROM items i
JOIN jobs j ON j.id = i.job_id JOIN jobs j ON j.id = i.job_id
LEFT JOIN users u ON u.id = j.created_by LEFT JOIN users u ON u.id = j.created_by
WHERE ${where.join(' AND ')} WHERE ${where.join(' AND ')}
ORDER BY i.created_at DESC LIMIT 300`, args); ORDER BY i.created_at DESC LIMIT 400`, args);
return json({ items: rows }); return json({ items: rows });
}; };