let currentEntries = [];
let seenEntryIds = new Set();
let highlightNewIds = new Set();
let highlightClearTimer = null;
let editId = null;
let editGroups = [];
let editModules = [];
let moduleNames = new Map();
let moduleFilterId = '';
let moduleSearchTimer = null;
let editModuleSearchTimer = null;
let modulesLoaded = false;
let groupsLoaded = false;
let studentsLoaded = false;
const PAGE_SIZE = 20;
let page = 1;
let totalEntries = 0;
let searchTimer = null;
let view = localStorage.getItem('journalView') === 'cards' ? 'cards' : 'list';
let aiCorrecting = false;
let editDeletePhoto = false;
let editAiSuggestion = null;
let linkSettings = null;
async function loadLinkSettings() {
if (!linkSettings) {
const res = await fetch(`${API}/api/settings`, { headers: hdr() });
linkSettings = await res.json().catch(() => ({}));
}
return linkSettings;
}
function isImageFile(name) {
return /\.(jpe?g|jfif|png|gif|webp|bmp|avif|svg|ico)$/i.test(name || '');
}
function isPlayableVideoFile(name) {
return /\.(mp4|m4v|webm|ogv)$/i.test(name || '');
}
function isVideoFile(name) {
return /\.(mp4|m4v|webm|ogv|mov|mkv|avi|mpeg|mpg|3gp|ts)$/i.test(name || '');
}
function mainPhotoPath(e) {
if (e.photo_path) return e.photo_path;
if (e.photos && e.photos.length) return e.photos[0].photo_path;
return null;
}
function filesHTML(files) {
if (!files || !files.length) return '';
return `
${files.map(f => {
const url = `${API}/api/files/${f.token}`;
let link;
if (isImageFile(f.name)) {
link = `${esc(f.name)}`;
} else if (isPlayableVideoFile(f.name)) {
link = `${esc(f.name)}`;
} else {
const note = isVideoFile(f.name) ? 'Скачать (браузер не умеет играть этот формат)' : 'Скачать';
link = `${esc(f.name)}`;
}
return `${link}`;
}).join('')}
`;
}
const AI_STATUS_LABELS = {
pending: 'ИИ: в очереди',
processing: 'ИИ: проверяет…',
done: 'ИИ: проверено',
skipped: 'ИИ: пропущено',
error: 'ИИ: ошибка',
reverted: 'ИИ: возвращён оригинал',
};
const AI_STATUS_ICONS = {
pending: 'clock',
processing: 'loader',
done: 'circle-check',
skipped: 'skip-forward',
error: 'circle-alert',
reverted: 'undo',
};
function aiBadge(e) {
if (!e || !e.ai_status) return '';
const st = e.ai_status;
const label = AI_STATUS_LABELS[st] || `ИИ: ${st}`;
const icon = AI_STATUS_ICONS[st] || 'sparkles';
return ``;
}
function findFile(id) {
for (const e of currentEntries) {
const f = (e.files || []).find(x => x.id === id);
if (f) return f;
}
return null;
}
async function detachFile(id) {
const f = findFile(id);
if (!f) return;
if (!confirm(`Открепить файл «${f.name}» от записи? Он переместится в «Файлы → Откреплённые».`)) return;
const res = await fetch(`${API}/api/files/${id}/detach`, { method: 'POST', headers: hdr() });
if (res.ok) {
showToast('Файл откреплён');
if (f.entry_id) refreshEntryCard(f.entry_id);
else loadEntries();
} else {
const err = await res.json().catch(() => ({}));
alert(err.error || 'Ошибка');
}
}
function gridEntryHTML(e) {
const mp = mainPhotoPath(e);
return `
`;
}
function renderGrid() {
const grid = document.getElementById('grid');
if (!currentEntries.length) {
const fmt = d => d ? d.split('-').reverse().join('.') : '';
const from = fmt(document.getElementById('dateFrom').value);
const to = fmt(document.getElementById('dateTo').value);
let range = '';
if (from && to) range = ` за ${from} – ${to}`;
else if (from) range = ` с ${from}`;
else if (to) range = ` по ${to}`;
grid.className = 'grid';
grid.innerHTML = `
Ничего не найдено${range}
`;
return;
}
if (view === 'cards') {
grid.className = 'journal-cards';
grid.innerHTML = currentEntries.map(cardEntryHTML).join('');
} else {
grid.className = 'grid';
grid.innerHTML = currentEntries.map(gridEntryHTML).join('');
}
const items = Array.from(grid.children);
items.forEach((el, i) => {
el.style.animationDelay = `${Math.min(i * 45, 500)}ms`;
});
renderIcons();
}
function refreshEntryCardDom(e) {
const btn = document.querySelector(`[data-edit="${e.id}"]`);
const card = btn ? btn.closest('.card, .journal-card') : null;
if (!card) return;
const tmp = document.createElement('div');
tmp.innerHTML = (view === 'cards' ? cardEntryHTML(e) : gridEntryHTML(e)).trim();
const fresh = tmp.firstElementChild;
fresh.classList.remove('entry-enter', 'entry-appear');
fresh.style.animationDelay = '';
card.replaceWith(fresh);
renderIcons();
}
async function refreshEntryCard(id) {
const res = await fetch(`${API}/api/entries/${id}`, { headers: hdr() });
if (!res.ok) return false;
const e = await res.json();
const idx = currentEntries.findIndex(x => x.id === e.id);
if (idx !== -1) currentEntries[idx] = e;
refreshEntryCardDom(e);
return true;
}
function setView(v) {
view = v === 'cards' ? 'cards' : 'list';
localStorage.setItem('journalView', view);
document.querySelectorAll('#viewToggle button').forEach(b => {
b.classList.toggle('active', b.dataset.view === view);
});
renderGrid();
}
function setDefaultDates() {
const today = todayIso();
document.getElementById('dateFrom').value = today;
document.getElementById('dateTo').value = today;
}
async function loadGroupOptions() {
if (groupsLoaded) return;
const sel = document.getElementById('groupFilter');
while (sel.options.length > 1) sel.remove(1);
const res = await fetch(`${API}/api/groups`, { headers: hdr() });
const groups = await res.json();
groups.forEach(g => {
const o = document.createElement('option');
o.value = g.id; o.textContent = g.name;
sel.appendChild(o);
});
groupsLoaded = true;
}
function renderModuleOptions(datalistId, list) {
list.forEach(m => moduleNames.set(m.name, m.id));
const dl = document.getElementById(datalistId);
if (!dl) return;
dl.innerHTML = '';
list.forEach(m => {
const opt = document.createElement('option');
opt.value = m.name;
dl.appendChild(opt);
});
}
async function fetchModules(q, limit) {
const p = new URLSearchParams();
if (q) p.set('search', q);
p.set('limit', String(limit));
p.set('active', '1');
const res = await fetch(`${API}/api/modules?${p}`, { headers: hdr() });
if (!res.ok) return [];
const data = await res.json().catch(() => ({}));
return data.modules || [];
}
async function resolveModuleId(name) {
const n = (name || '').trim();
if (!n) return '';
if (moduleNames.has(n)) return String(moduleNames.get(n));
const list = await fetchModules(n, 10);
const lower = n.toLowerCase();
const exact = list.find(m => m.name.toLowerCase() === lower);
return exact ? String(exact.id) : null;
}
async function loadModuleOptions() {
editModules = await fetchModules('', 50);
modulesLoaded = true;
renderModuleOptions('moduleFilterList', editModules);
}
async function applyModuleFilter() {
const inp = document.getElementById('moduleFilter');
const resolved = await resolveModuleId(inp.value);
if (resolved === null) {
inp.value = '';
moduleFilterId = '';
showToast('Модуль не найден — фильтр сброшен');
} else {
moduleFilterId = resolved;
}
page = 1;
loadEntries();
}
function fillEditModuleInput(entry) {
const inp = document.getElementById('editModule');
if (!inp) return;
if (entry.module_name && entry.module_id) moduleNames.set(entry.module_name, entry.module_id);
inp.value = entry.module_name || '';
renderModuleOptions('editModuleList', editModules);
}
async function loadStudentOptions() {
if (studentsLoaded) return;
const dl = document.getElementById('studentFilterList');
const res = await fetch(`${API}/api/students/names`, { headers: hdr() });
const names = await res.json();
dl.innerHTML = names.map(n => ``).join('');
}
sel.value = e.group_id;
if (!modulesLoaded) await loadModuleOptions();
fillEditModuleInput(e);
renderEditPhotos(e.photos || [], e.photo_path);
document.getElementById('editModal').classList.add('open');
}
function renderEditPhotos(photos, mainPath) {
const preview = document.getElementById('editPhotoPreview');
preview.innerHTML = '';
if (!photos.length) return;
photos.forEach((p) => {
const isMain = p.photo_path === mainPath;
const div = document.createElement('div');
div.className = 'file-preview-item';
div.style.cssText = 'display:flex;align-items:center;gap:8px;padding:6px 8px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:.8rem';
div.dataset.photoId = p.id;
const applyStyle = isMain
? 'background:#16a34a;border:none;color:#fff;border-radius:6px;padding:4px 10px;font-size:.75rem;cursor:default;white-space:nowrap'
: 'background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:6px;padding:4px 10px;font-size:.75rem;cursor:pointer;white-space:nowrap';
div.innerHTML = `${esc(p.photo_path.split('/').pop())} `;
preview.appendChild(div);
const thumb = div.querySelector('img');
const showMissing = () => {
const ph = document.createElement('span');
ph.textContent = 'нет файла';
ph.style.cssText = 'width:48px;height:48px;display:flex;align-items:center;justify-content:center;background:var(--bg);border:1px dashed var(--border);border-radius:4px;font-size:.6rem;color:var(--muted);text-align:center;line-height:1.1;flex:0 0 auto';
thumb.replaceWith(ph);
};
thumb.addEventListener('error', showMissing);
if (thumb.complete && thumb.naturalWidth === 0) showMissing();
div.querySelector('.delete-entry-photo').addEventListener('click', async () => {
if (!confirm('Удалить это фото?')) return;
const res = await fetch(`${API}/api/entries/${editId}/photos/${p.id}`, { method: 'DELETE', headers: hdr() });
if (res.ok) {
div.remove();
showToast('Фото удалено');
refreshEntryCard(editId);
} else {
alert('Ошибка удаления');
}
});
});
}
async function applyMainPhoto(btn) {
const div = btn.closest('.file-preview-item');
const photoId = div?.dataset.photoId;
if (!photoId) return;
btn.disabled = true;
const res = await fetch(`${API}/api/entries/${editId}/photos/${photoId}/main`, { method: 'PUT', headers: hdr() });
if (!res.ok) {
btn.disabled = false;
const err = await res.json().catch(() => ({}));
alert(err.error || 'Ошибка');
return;
}
document.querySelectorAll('#editPhotoPreview .apply-photo-btn').forEach(b => {
b.disabled = false;
b.textContent = 'Применить';
b.title = 'Сделать главным фото записи';
b.style.background = 'var(--card)';
b.style.color = 'var(--text)';
});
btn.disabled = true;
btn.textContent = 'Применено ✓';
btn.title = 'Главное фото записи';
btn.style.background = '#16a34a';
btn.style.color = '#fff';
showToast('Главное фото обновлено');
refreshEntryCard(editId);
}
function closeEdit() {
document.getElementById('editModal').classList.remove('open');
document.getElementById('editFiles').value = '';
document.getElementById('editFilesPreview').innerHTML = '';
document.getElementById('editPhoto').value = '';
document.getElementById('editPhotoPreview').innerHTML = '';
document.getElementById('editAiStatus').style.display = 'none';
editId = null;
editDeletePhoto = false;
}
let enhanceEntryId = null;
let enhanceSourceImg = null;
let enhanceTimer = null;
let enhanceSaving = false;
let enhanceEngine = 'auto';
let enhanceEngineLoaded = false;
let photoAiEnabled = false;
let photoAiFaceMode = 'off';
let photoAiFaceModel = 'gfpgan';
let photoAiDevice = '';
let photoAiPollTimer = null;
let enhanceAiResultPath = null;
let enhanceAiJobId = null;
let enhanceClipPos = 50;
function setEnhanceClip(p) {
enhanceClipPos = Math.min(100, Math.max(0, p));
document.getElementById('enhanceTop').style.clipPath = `inset(0 0 0 ${enhanceClipPos}%)`;
document.getElementById('enhanceDivider').style.left = enhanceClipPos + '%';
}
function showEnhanceResult(show) {
document.getElementById('enhanceTop').style.display = show ? '' : 'none';
document.getElementById('enhanceLoading').style.display = show ? 'none' : '';
}
function setEnhanceLoadingText(text) {
const el = document.getElementById('enhanceLoading');
el.style.display = '';
el.textContent = text;
}
let enhanceZoom = 1;
let enhancePanX = 0;
let enhancePanY = 0;
function applyEnhanceTransform() {
const t = `translate(${enhancePanX}px, ${enhancePanY}px) scale(${enhanceZoom})`;
document.getElementById('enhanceStage').style.transform = t;
document.getElementById('enhanceInner').style.transform = t;
const area = document.getElementById('enhanceCompare');
area.classList.toggle('zoomed', enhanceZoom > 1);
if (enhanceZoom <= 1) area.classList.remove('panning');
}
function clampEnhancePan() {
const rect = document.getElementById('enhanceCompare').getBoundingClientRect();
const minX = rect.width * (1 - enhanceZoom);
const minY = rect.height * (1 - enhanceZoom);
enhancePanX = Math.min(0, Math.max(minX, enhancePanX));
enhancePanY = Math.min(0, Math.max(minY, enhancePanY));
}
function resetEnhanceZoom() {
enhanceZoom = 1;
enhancePanX = 0;
enhancePanY = 0;
applyEnhanceTransform();
}
(function initEnhanceDrag() {
const area = document.getElementById('enhanceCompare');
let dragging = false;
let dividerDrag = false;
let lastX = 0;
let lastY = 0;
const move = ev => {
const rect = area.getBoundingClientRect();
setEnhanceClip(((ev.clientX - rect.left) / rect.width) * 100);
};
area.addEventListener('pointerdown', ev => {
dividerDrag = !!ev.target.closest('#enhanceDivider');
dragging = true;
lastX = ev.clientX;
lastY = ev.clientY;
area.setPointerCapture(ev.pointerId);
if (!dividerDrag && enhanceZoom === 1) move(ev);
});
area.addEventListener('pointermove', ev => {
if (!dragging) return;
if (dividerDrag || enhanceZoom === 1) {
move(ev);
} else {
area.classList.add('panning');
enhancePanX += ev.clientX - lastX;
enhancePanY += ev.clientY - lastY;
lastX = ev.clientX;
lastY = ev.clientY;
clampEnhancePan();
applyEnhanceTransform();
}
});
const end = () => {
dragging = false;
area.classList.remove('panning');
};
area.addEventListener('pointerup', end);
area.addEventListener('pointercancel', end);
area.addEventListener('wheel', ev => {
ev.preventDefault();
const rect = area.getBoundingClientRect();
const cx = ev.clientX - rect.left;
const cy = ev.clientY - rect.top;
const prev = enhanceZoom;
const next = Math.min(5, Math.max(1, enhanceZoom * (ev.deltaY < 0 ? 1.15 : 1 / 1.15)));
if (next === prev) return;
enhanceZoom = next;
enhancePanX = cx - (cx - enhancePanX) * (next / prev);
enhancePanY = cy - (cy - enhancePanY) * (next / prev);
clampEnhancePan();
applyEnhanceTransform();
}, { passive: false });
area.addEventListener('dblclick', resetEnhanceZoom);
})();
const PHOTO_AI_MODES = {
x2: { model: 'x2plus', face: 'off', face_model: null, title: 'Универсально (x2)' },
x4: { model: 'general-x4v3', face: 'off', face_model: null, title: 'Быстро (x4)' },
faces: { model: 'x2plus', face: 'face', face_model: null, title: 'Лица (восстановление)' },
facesbg: { model: 'x2plus', face: 'all', face_model: null, title: 'Лица + фон' },
};
function photoAiMode() {
const sel = document.getElementById('enhanceAiMode');
const key = sel && PHOTO_AI_MODES[sel.value] ? sel.value : 'x2';
const m = { ...PHOTO_AI_MODES[key] };
m.face_model = m.face === 'off' ? null : photoAiFaceModel;
return m;
}
function defaultPhotoAiModeKey() {
if (photoAiFaceMode === 'all') return 'facesbg';
if (photoAiFaceMode === 'face') return 'faces';
return 'x2';
}
function updateAiHint() {
const hint = document.getElementById('enhanceAiHint');
if (!hint) return;
const cpu = photoAiDevice.startsWith('cpu');
hint.textContent = cpu ? 'Фото-ИИ считает на CPU — лицевые режимы могут занять несколько минут' : '';
hint.style.display = photoAiEnabled && cpu ? '' : 'none';
}
async function loadPhotoAiDevice() {
if (!isAdmin()) return;
try {
const r = await fetch(`${API}/api/photo-ai/health`, { headers: hdr() });
if (!r.ok) return;
const h = await r.json();
if (h && h.device) photoAiDevice = String(h.device);
updateAiHint();
} catch (e) {}
}
async function loadEnhanceEngine() {
if (enhanceEngineLoaded) return;
enhanceEngineLoaded = true;
try {
const r = await fetch(`${API}/api/public-settings`);
const f = await r.json();
if (['auto', 'server', 'client'].includes(f.photo_enhance_engine)) enhanceEngine = f.photo_enhance_engine;
photoAiEnabled = f.photo_ai_enabled === 'true';
if (['off', 'face', 'all'].includes(f.photo_ai_face_mode)) photoAiFaceMode = f.photo_ai_face_mode;
if (['gfpgan', 'codeformer'].includes(f.photo_ai_face_model)) photoAiFaceModel = f.photo_ai_face_model;
const sel = document.getElementById('enhanceAiMode');
if (sel) sel.value = defaultPhotoAiModeKey();
loadPhotoAiDevice();
} catch (e) {}
}
async function runPhotoAi() {
if (!enhanceEntryId || enhanceSaving) return;
const entryId = enhanceEntryId;
const mode = photoAiMode();
const faceNote = mode.face === 'off'
? 'Может занять до минуты на CPU.'
: `Восстановление лиц (${mode.face_model}), на CPU может занять несколько минут.`;
if (!confirm(`ИИ-обработка фото: ${mode.title}. ${faceNote} Продолжить?`)) return;
enhanceSaving = true;
const btn = document.getElementById('enhanceAiBtn');
btn.disabled = true;
try {
const res = await fetch(`${API}/api/entries/${entryId}/photo/enhance-ai`, {
method: 'POST',
headers: hdrJson(),
body: JSON.stringify({ model: mode.model, face: mode.face, face_model: mode.face_model }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Ошибка запуска ИИ');
}
const { jobId } = await res.json();
enhanceAiJobId = jobId;
showEnhanceResult(false);
setEnhanceLoadingText(mode.face === 'off'
? 'ИИ обрабатывает фото... (может занять до минуты)'
: 'ИИ обрабатывает фото и восстанавливает лица...');
btn.textContent = '⏳ ИИ...';
const result = await new Promise((resolve, reject) => {
photoAiPollTimer = setInterval(async () => {
try {
const r = await fetch(`${API}/api/entries/${entryId}/photo/enhance-ai/${jobId}`, { headers: hdr() });
if (!r.ok) throw new Error('Ошибка опроса статуса');
const s = await r.json();
if (s.status === 'done') { clearInterval(photoAiPollTimer); resolve(s); }
else if (s.status === 'error') { clearInterval(photoAiPollTimer); reject(new Error(s.error || 'Ошибка ИИ')); }
} catch (e) { clearInterval(photoAiPollTimer); reject(e); }
}, 2000);
});
showToast('ИИ результат готов');
enhanceAiResultPath = result.photo_path;
const aiImg = new Image();
aiImg.onload = () => {
enhanceSourceImg = aiImg;
['Brightness', 'Contrast', 'Saturate'].forEach(k => {
document.getElementById('enhance' + k).value = 100;
document.getElementById('enhance' + k + 'Val').textContent = '100%';
});
document.getElementById('enhanceSharp').value = 0;
document.getElementById('enhanceSharpVal').textContent = '0%';
document.getElementById('enhanceDenoise').value = 0;
document.getElementById('enhanceDenoiseVal').textContent = '0%';
renderEnhancePreview();
};
aiImg.onerror = () => { showToast('Не удалось загрузить результат ИИ'); };
aiImg.src = `${API}${result.photo_path}`;
} catch (e) {
showEnhanceResult(true);
alert(e.message);
} finally {
enhanceSaving = false;
btn.disabled = false;
btn.textContent = '🤖 ИИ';
}
}
async function restoreOriginal() {
if (!enhanceEntryId || enhanceSaving) return;
if (!confirm('Вернуть исходную фотографию? Текущая улучшенная версия будет удалена.')) return;
enhanceSaving = true;
const btn = document.getElementById('enhanceRestoreBtn');
btn.disabled = true;
try {
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/restore-original`, { method: 'POST', headers: hdr() });
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.error || 'Ошибка восстановления');
}
const savedId = enhanceEntryId;
showToast('Оригинал возвращён');
closeEnhance();
refreshEntryCard(savedId);
} catch (e) {
alert(e.message);
} finally {
enhanceSaving = false;
btn.disabled = false;
}
}
function openEnhance(id) {
const e = currentEntries.find(x => x.id === id);
if (!e) return;
const mp = mainPhotoPath(e);
if (!mp) { showToast('У записи нет фото'); return; }
enhanceEntryId = id;
enhanceSourceImg = null;
enhanceAiResultPath = null;
enhanceAiJobId = null;
['Brightness', 'Contrast', 'Saturate'].forEach(k => {
const el = document.getElementById('enhance' + k);
el.value = 100;
document.getElementById('enhance' + k + 'Val').textContent = '100%';
});
const sharp = document.getElementById('enhanceSharp');
sharp.value = 0;
document.getElementById('enhanceSharpVal').textContent = '0%';
const orig = document.getElementById('enhanceOrig');
orig.src = `${API}${mp}`;
document.getElementById('enhanceRestoreBtn').style.display = e.photo_original_path ? '' : 'none';
loadEnhanceEngine().then(() => {
const show = photoAiEnabled ? '' : 'none';
document.getElementById('enhanceAiBtn').style.display = show;
document.getElementById('enhanceAiMode').style.display = show;
updateAiHint();
});
const canvas = document.getElementById('enhanceCanvas');
setEnhanceClip(50);
resetEnhanceZoom();
showEnhanceResult(false);
setEnhanceLoadingText('Загрузка...');
const img = new Image();
img.onload = () => {
enhanceSourceImg = img;
renderEnhancePreview();
};
img.onerror = () => {
setEnhanceLoadingText('Не удалось загрузить фото');
};
img.src = `${API}${mp}`;
document.getElementById('enhanceModal').classList.add('open');
}
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() {
cleanupAiPreview();
document.getElementById('enhanceModal').classList.remove('open');
document.getElementById('photoHistoryModal').classList.remove('open');
enhanceEntryId = null;
enhanceSourceImg = null;
enhanceAiResultPath = null;
}
function photoJobThumb(p) {
if (!p) return '';
if (p.startsWith('/uploads/.originals/')) return `${API}${p}`;
return `${API}/uploads/thumb/${p.split('/').pop()}`;
}
const PHOTO_JOB_ACTION_LABELS = { ai: 'ИИ-обработка', enhance: 'Улучшение', restore: 'Возврат оригинала', rollback: 'Откат' };
const PHOTO_JOB_STATUS_LABELS = { pending: 'В очереди', processing: 'Обрабатывается', done: 'Готово', error: 'Ошибка', rejected: 'Отклонено' };
async function loadPhotoHistory() {
if (!enhanceEntryId) return;
const list = document.getElementById('photoHistoryList');
list.innerHTML = '
Загрузка...
';
try {
const res = await fetch(`${API}/api/entries/${enhanceEntryId}/photo/jobs`, { headers: hdr() });
if (!res.ok) throw new Error('Ошибка загрузки истории');
const rows = await res.json();
if (!rows.length) {
list.innerHTML = '