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:
2026-07-23 21:46:47 +00:00
parent 2588a54eb7
commit e6b325eb53
14 changed files with 186 additions and 35 deletions
+19 -3
View File
@@ -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
View File
@@ -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) {