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:
+72
-14
@@ -427,6 +427,7 @@ let enhanceEngineLoaded = false;
|
||||
let photoAiEnabled = false;
|
||||
let photoAiPollTimer = null;
|
||||
let enhanceAiResultPath = null;
|
||||
let enhanceAiJobId = null;
|
||||
let enhanceClipPos = 50;
|
||||
|
||||
function setEnhanceClip(p) {
|
||||
@@ -554,6 +555,7 @@ async function runPhotoAi() {
|
||||
throw new Error(err.error || 'Ошибка запуска ИИ');
|
||||
}
|
||||
const { jobId } = await res.json();
|
||||
enhanceAiJobId = jobId;
|
||||
showEnhanceResult(false);
|
||||
setEnhanceLoadingText('ИИ обрабатывает фото... (может занять до минуты)');
|
||||
btn.textContent = '⏳ ИИ...';
|
||||
@@ -627,6 +629,7 @@ function openEnhance(id) {
|
||||
enhanceEntryId = id;
|
||||
enhanceSourceImg = null;
|
||||
enhanceAiResultPath = null;
|
||||
enhanceAiJobId = null;
|
||||
['Brightness', 'Contrast', 'Saturate'].forEach(k => {
|
||||
const el = document.getElementById('enhance' + k);
|
||||
el.value = 100;
|
||||
@@ -658,16 +661,40 @@ function openEnhance(id) {
|
||||
document.getElementById('enhanceModal').classList.add('open');
|
||||
}
|
||||
|
||||
function cleanupAiPreview() {
|
||||
if (!enhanceAiResultPath || !enhanceEntryId) return;
|
||||
const toClean = enhanceAiResultPath;
|
||||
const entryId = enhanceEntryId;
|
||||
fetch(`${API}/api/entries/${entryId}/photo/enhance-ai/preview`, {
|
||||
method: 'DELETE',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ path: toClean }),
|
||||
}).catch(() => {});
|
||||
function isDefaultEnhanceParams() {
|
||||
const p = enhanceParams();
|
||||
return p.brightness === 100 && p.contrast === 100 && p.saturate === 100 && p.denoise === 0 && p.sharp === 0;
|
||||
}
|
||||
|
||||
async function applyAiResult() {
|
||||
if (!enhanceAiJobId || !enhanceEntryId) return false;
|
||||
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/jobs/${enhanceAiJobId}/apply`, {
|
||||
method: 'POST',
|
||||
headers: hdr(),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || 'Ошибка применения ИИ-результата');
|
||||
enhanceAiResultPath = null;
|
||||
enhanceAiJobId = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
function cleanupAiPreview() {
|
||||
if (!enhanceEntryId) return;
|
||||
if (enhanceAiJobId) {
|
||||
fetch(`${API}/api/entries/${enhanceEntryId}/photo/jobs/${enhanceAiJobId}/reject`, {
|
||||
method: 'POST',
|
||||
headers: hdr(),
|
||||
}).catch(() => {});
|
||||
} else if (enhanceAiResultPath) {
|
||||
fetch(`${API}/api/entries/${enhanceEntryId}/photo/enhance-ai/preview`, {
|
||||
method: 'DELETE',
|
||||
headers: hdrJson(),
|
||||
body: JSON.stringify({ path: enhanceAiResultPath }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
enhanceAiResultPath = null;
|
||||
enhanceAiJobId = null;
|
||||
}
|
||||
|
||||
function closeEnhance() {
|
||||
@@ -686,7 +713,7 @@ function photoJobThumb(p) {
|
||||
}
|
||||
|
||||
const PHOTO_JOB_ACTION_LABELS = { ai: 'ИИ-обработка', enhance: 'Улучшение', restore: 'Возврат оригинала', rollback: 'Откат' };
|
||||
const PHOTO_JOB_STATUS_LABELS = { pending: 'В очереди', processing: 'Обрабатывается', done: 'Готово', error: 'Ошибка' };
|
||||
const PHOTO_JOB_STATUS_LABELS = { pending: 'В очереди', processing: 'Обрабатывается', done: 'Готово', error: 'Ошибка', rejected: 'Отклонено' };
|
||||
|
||||
async function loadPhotoHistory() {
|
||||
if (!enhanceEntryId) return;
|
||||
@@ -703,7 +730,8 @@ async function loadPhotoHistory() {
|
||||
list.innerHTML = rows.map(r => {
|
||||
const before = r.before_path ? `<img class="photo-thumb" loading="lazy" src="${esc(photoJobThumb(r.before_path))}" alt="было">` : '<span style="color:var(--muted)">—</span>';
|
||||
const after = r.after_path ? `<img class="photo-thumb" loading="lazy" src="${esc(photoJobThumb(r.after_path))}" alt="стало">` : '<span style="color:var(--muted)">—</span>';
|
||||
const canRollback = r.status === 'done' && r.before_path && r.action !== 'rollback';
|
||||
const canApply = r.status === 'done' && r.applied === false && r.action === 'ai' && !!r.after_path;
|
||||
const canRollback = r.status === 'done' && r.before_path && r.action !== 'rollback' && !canApply;
|
||||
const err = r.error ? `<div style="color:#ef4444;font-size:.75rem;margin-top:4px">${esc(r.error)}</div>` : '';
|
||||
const time = r.finished_at || r.created_at;
|
||||
return `<div style="display:flex;gap:10px;align-items:center;border:1px solid var(--border);border-radius:10px;padding:10px">
|
||||
@@ -723,6 +751,7 @@ async function loadPhotoHistory() {
|
||||
${err}
|
||||
</div>
|
||||
${canRollback ? `<button type="button" class="btn-primary ghost" style="white-space:nowrap" data-job-rollback="${r.id}">↩ Откатить</button>` : ''}
|
||||
${canApply ? `<button type="button" class="btn-primary" style="white-space:nowrap" data-job-apply="${r.id}">✓ Применить</button>` : ''}
|
||||
</div>`;
|
||||
}).join('');
|
||||
} catch (e) {
|
||||
@@ -944,7 +973,11 @@ async function saveEnhance() {
|
||||
try {
|
||||
let done = false;
|
||||
let engineLabel = 'клиент';
|
||||
if (enhanceEngine !== 'client' && !enhanceAiResultPath) {
|
||||
if (enhanceAiResultPath && isDefaultEnhanceParams()) {
|
||||
await applyAiResult();
|
||||
engineLabel = 'ИИ';
|
||||
done = true;
|
||||
} else if (enhanceEngine !== 'client' && !enhanceAiResultPath) {
|
||||
try {
|
||||
const ctrl = new AbortController();
|
||||
const timer = setTimeout(() => ctrl.abort(), 8000);
|
||||
@@ -1351,9 +1384,34 @@ document.getElementById('photoHistoryModal').addEventListener('click', e => {
|
||||
});
|
||||
document.getElementById('photoHistoryList').addEventListener('click', e => {
|
||||
const btn = e.target.closest('[data-job-rollback]');
|
||||
if (!btn) return;
|
||||
rollbackPhotoJob(parseInt(btn.dataset.jobRollback, 10));
|
||||
if (btn) {
|
||||
rollbackPhotoJob(parseInt(btn.dataset.jobRollback, 10));
|
||||
return;
|
||||
}
|
||||
const applyBtn = e.target.closest('[data-job-apply]');
|
||||
if (applyBtn) applyPhotoJob(parseInt(applyBtn.dataset.jobApply, 10));
|
||||
});
|
||||
|
||||
async function applyPhotoJob(jobId) {
|
||||
if (!enhanceEntryId || enhanceSaving) return;
|
||||
if (!confirm('Применить обработанную фотографию к записи? Текущая версия сохранится в истории.')) return;
|
||||
enhanceSaving = true;
|
||||
try {
|
||||
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/jobs/${jobId}/apply`, { method: 'POST', headers: hdr() });
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) throw new Error(data.error || 'Ошибка применения');
|
||||
showToast('Фотография применена');
|
||||
const mp = data.photo_path;
|
||||
const origImg = document.getElementById('enhanceOrig');
|
||||
if (origImg && mp) origImg.src = `${API}${mp}`;
|
||||
loadPhotoHistory();
|
||||
refreshEntryCard(enhanceEntryId);
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
} finally {
|
||||
enhanceSaving = false;
|
||||
}
|
||||
}
|
||||
document.getElementById('enhanceModal').addEventListener('click', e => { if (e.target === e.currentTarget) closeEnhance(); });
|
||||
document.getElementById('enhanceBrightness').addEventListener('input', scheduleEnhancePreview);
|
||||
document.getElementById('enhanceContrast').addEventListener('input', scheduleEnhancePreview);
|
||||
|
||||
@@ -35,6 +35,7 @@ const PHOTO_STATUS_LABELS = {
|
||||
processing: 'Обрабатывается',
|
||||
done: 'Готово',
|
||||
error: 'Ошибка',
|
||||
rejected: 'Отклонено',
|
||||
};
|
||||
const PHOTO_STATUS_BADGE = {
|
||||
done: 'ai-badge-done',
|
||||
|
||||
Reference in New Issue
Block a user