feat: photo jobs apply/reject flow with applied flag in history

- DB: add photo_jobs.applied column (init + migration + ensure)
- Worker: generate preview only (after_path), no longer mutates entry
- New POST /api/entries/:id/photo/jobs/:jobId/apply — apply done job
  result to entry (backs up current photo, marks applied)
- New POST /api/entries/:id/photo/jobs/:jobId/reject — discard result,
  delete temp file, mark rejected
- saveEnhance: apply AI result directly when sliders are at defaults
- Photo history: '✓ Применить' action for unapplied done AI jobs;
  'rejected' status label
- sweepOrphanedUploads keeps done-not-applied preview files
This commit is contained in:
dev
2026-09-17 18:51:26 +03:00
parent 881b5b4c67
commit 5874e2b619
6 changed files with 192 additions and 30 deletions
+5 -11
View File
@@ -138,18 +138,12 @@ function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntrie
fs.writeFileSync(path.join(uploadsDir, newName), out);
return `/uploads/${newName}`;
}
async function applyResult(job, photoPath, newPath) {
const beforePath = backupOldFile(photoPath);
await pool.query('UPDATE entries SET photo_path = $1 WHERE id = $2', [newPath, job.entry_id]);
await pool.query('UPDATE entry_photos SET photo_path = $1 WHERE entry_id = $2 AND photo_path = $3', [newPath, job.entry_id, photoPath]);
const oldThumb = path.join(uploadsDir, '.thumbs', path.basename(photoPath).replace(/\.[^.]+$/, '') + '.webp');
safeUnlinkPath(oldThumb);
async function applyResult(job, newPath) {
await pool.query(
`UPDATE photo_jobs SET status = 'done', before_path = $1, after_path = $2, error = NULL, finished_at = now() WHERE id = $3`,
[beforePath, newPath, job.id]
`UPDATE photo_jobs SET status = 'done', after_path = $1, error = NULL, finished_at = now() WHERE id = $2`,
[newPath, job.id]
);
if (logAudit) await logAudit(null, 'photo.job.done', { entry_id: job.entry_id, job_id: job.id, action: job.action, before_path: beforePath, after_path: newPath });
if (invalidateEntries) invalidateEntries();
if (logAudit) await logAudit(null, 'photo.job.preview', { entry_id: job.entry_id, job_id: job.id, action: job.action, after_path: newPath });
}
async function processOne(job) {
stats.jobs++;
@@ -168,7 +162,7 @@ function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntrie
const srcAbs = path.join(uploadsDir, String(photoPath).replace(/^\/+/, '').replace(/^uploads\//, ''));
try {
const newPath = job.action === 'ai' ? await runAiEnhance(srcAbs) : await enhanceWithSharp(srcAbs, job.params);
await applyResult(job, photoPath, newPath);
await applyResult(job, newPath);
stats.done++;
stats.last_at = new Date().toISOString();
stats.last_error = null;