feat: per-user visibility (own/shared), anonymous generations, private sessions (generate+forget), NSFW model gating
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNQ8ghPfzAfsyVYd6HgFb6
This commit is contained in:
+19
-3
@@ -76,8 +76,22 @@ export async function mirrorAllToNas(): Promise<{ mirrored: number; failed: numb
|
||||
return { mirrored: r.mirrored, failed: r.failed, total: r.items };
|
||||
}
|
||||
|
||||
/** Private Aufträge „vergessen": Ergebnisse + Aufträge nach kurzer Zeit löschen. */
|
||||
export async function purgePrivate(olderThanMin = 60): Promise<{ deleted: number }> {
|
||||
const rows = await query<any>(
|
||||
`SELECT i.id, i.result_path, i.thumb_path FROM items i JOIN jobs j ON j.id=i.job_id
|
||||
WHERE j.private=true AND i.created_at < now() - ($1 || ' minutes')::interval`, [String(olderThanMin)]);
|
||||
for (const it of rows) {
|
||||
for (const k of [it.result_path, it.thumb_path].filter(Boolean)) await deleteObject(k).catch(() => {});
|
||||
await query('DELETE FROM items WHERE id=$1', [it.id]);
|
||||
}
|
||||
// leere private Aufträge entfernen
|
||||
await query(`DELETE FROM jobs WHERE private=true AND id NOT IN (SELECT DISTINCT job_id FROM items WHERE job_id IS NOT NULL)`);
|
||||
return { deleted: rows.length };
|
||||
}
|
||||
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
/** Täglicher Retention-Lauf (falls in den Einstellungen aktiviert). */
|
||||
/** Wartungs-Lauf: Retention + private Sessions vergessen. */
|
||||
export function startMaintenance(): void {
|
||||
if (timer) return;
|
||||
const tick = async () => {
|
||||
@@ -87,8 +101,10 @@ export function startMaintenance(): void {
|
||||
const r = await runRetention(s.retention_days);
|
||||
if (r.deleted) console.log(`[maintenance] Retention: ${r.deleted} alte Positionen entfernt.`);
|
||||
}
|
||||
const p = await purgePrivate(60);
|
||||
if (p.deleted) console.log(`[maintenance] Privat: ${p.deleted} Ergebnisse vergessen.`);
|
||||
} catch (e) { console.error('[maintenance]', e); }
|
||||
};
|
||||
timer = setInterval(tick, 6 * 60 * 60 * 1000); // alle 6 h
|
||||
setTimeout(tick, 60 * 1000); // erster Lauf nach 1 min
|
||||
timer = setInterval(tick, 30 * 60 * 1000); // alle 30 min (auch für private Purge)
|
||||
setTimeout(tick, 60 * 1000); // erster Lauf nach 1 min
|
||||
}
|
||||
|
||||
+18
-13
@@ -61,10 +61,11 @@ export async function processItem(itemId: string): Promise<ProcessResult> {
|
||||
'SELECT id, job_id, source_path, source_paths, filename FROM items WHERE id=$1', [itemId]);
|
||||
if (!item) return { ok: false, cost: 0, error: 'Position nicht gefunden' };
|
||||
|
||||
const job = await one<{ recipe_snapshot: RecipeSnapshot; mode: Mode }>(
|
||||
'SELECT recipe_snapshot, mode FROM jobs WHERE id=$1', [item.job_id]);
|
||||
const job = await one<{ recipe_snapshot: RecipeSnapshot; mode: Mode; private: boolean }>(
|
||||
'SELECT recipe_snapshot, mode, private FROM jobs WHERE id=$1', [item.job_id]);
|
||||
const r = (job?.recipe_snapshot || {}) as RecipeSnapshot;
|
||||
const mode: Mode = job?.mode || 'each';
|
||||
const isPrivate = !!job?.private;
|
||||
const tasks = r.tasks || [];
|
||||
const dpi = r.dpi ?? 300;
|
||||
const cropMode: CropMode = r.crop_mode === 'extend' ? 'extend' : 'crop';
|
||||
@@ -146,26 +147,30 @@ export async function processItem(itemId: string): Promise<ProcessResult> {
|
||||
|
||||
const filename = buildResultFilename(item.filename, formatToken(r.output_format), 'png');
|
||||
const outputPx = `${fin.width}x${fin.height}`;
|
||||
const deliveryStatus = (r.delivery === 'picdrop' || r.delivery === 'both') ? 'pending' : 'none';
|
||||
const nasStatus = cfg?.nas_enabled ? 'pending' : 'none';
|
||||
// Private Aufträge: kein Delivery, kein Backup, kein gespeicherter Prompt.
|
||||
const deliveryStatus = (!isPrivate && (r.delivery === 'picdrop' || r.delivery === 'both')) ? 'pending' : 'none';
|
||||
const nasStatus = (!isPrivate && cfg?.nas_enabled) ? 'pending' : 'none';
|
||||
|
||||
await query(
|
||||
`UPDATE items SET status='done', result_path=$2, thumb_path=$3, filename=$4, output_px=$5, dpi=$6,
|
||||
has_alpha=$7, model_used=$8, prompt_used=$9, cost=$10, error_message=NULL,
|
||||
delivery_status=$11, nas_status=$12 WHERE id=$1`,
|
||||
[itemId, key, thumbPath, filename, outputPx, dpi, hasAlpha, modelUsed, prompt.slice(0, 1000),
|
||||
cost, deliveryStatus, nasStatus]);
|
||||
[itemId, key, thumbPath, filename, outputPx, dpi, hasAlpha, modelUsed,
|
||||
isPrivate ? null : prompt.slice(0, 1000), cost, deliveryStatus, nasStatus]);
|
||||
|
||||
// Quellbilder löschen, wenn nicht behalten (Speicher sparen).
|
||||
if (cfg?.keep_sources === false) {
|
||||
// Quellbilder löschen, wenn nicht behalten — bei privaten Aufträgen immer.
|
||||
if (isPrivate || cfg?.keep_sources === false) {
|
||||
for (const k of sourceKeys) await deleteObject(k).catch(() => {});
|
||||
if (isPrivate) await query(`UPDATE items SET source_path=NULL, source_paths=NULL WHERE id=$1`, [itemId]);
|
||||
}
|
||||
|
||||
// Zweite Sicherung auf alle Backup-Ziele (NAS + is_backup-Ziele), best effort.
|
||||
try {
|
||||
const { backupItem } = await import('./backup');
|
||||
await backupItem(itemId);
|
||||
} catch (e) { console.error('[process] Backup fehlgeschlagen', e); }
|
||||
// Zweite Sicherung auf alle Backup-Ziele — nicht bei privaten Aufträgen.
|
||||
if (!isPrivate) {
|
||||
try {
|
||||
const { backupItem } = await import('./backup');
|
||||
await backupItem(itemId);
|
||||
} catch (e) { console.error('[process] Backup fehlgeschlagen', e); }
|
||||
}
|
||||
|
||||
return { ok: true, cost };
|
||||
} catch (e: any) {
|
||||
|
||||
Reference in New Issue
Block a user