feat: persistent photo job queue, worker, history with rollback
- Replace in-memory photoAiJobs Map with DB-backed photo_jobs table - Background photo worker (worker.js) with retry, backoff, stale reset - Handles both AI (Real-ESRGAN) and server-side (sharp) enhancement - Controlled via photo_worker_enabled setting - Photo job history in enhance modal with before/after thumbnails + rollback - Worker dashboard: photo jobs section with status counts, recent table, compare slider for before/after, rollback from worker UI - New endpoints: /api/photo-jobs/status|wake|enabled|requeue-failed, /api/entries/:id/photo/jobs (history), .../rollback - swapEntryPhotoFiles logs every mutation to photo_jobs table
This commit is contained in:
@@ -5,6 +5,21 @@
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Воркер ИИ — Админ-панель</title>
|
||||
<link rel="stylesheet" href="admin.css">
|
||||
<style>
|
||||
.enhance-compare { position: relative; border-radius: 10px; overflow: hidden; background: var(--bg); border: 1px solid var(--border); user-select: none; touch-action: none; cursor: ew-resize; }
|
||||
.enhance-stage { transform-origin: 0 0; }
|
||||
.enhance-stage > img { display: block; width: 100%; height: auto; }
|
||||
.enhance-top { position: absolute; inset: 0; overflow: hidden; }
|
||||
.enhance-top .enhance-stage { position: absolute; inset: 0; }
|
||||
.enhance-top .enhance-stage > img { height: 100%; object-fit: fill; }
|
||||
.enhance-divider { position: absolute; top: 0; bottom: 0; left: 50%; width: 32px; transform: translateX(-50%); cursor: ew-resize; }
|
||||
.enhance-divider::before { content: ''; position: absolute; top: 0; bottom: 0; left: 50%; width: 2px; transform: translateX(-50%); background: #fff; box-shadow: 0 0 6px rgba(0,0,0,.6); pointer-events: none; }
|
||||
.enhance-handle { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 34px; height: 34px; border-radius: 50%; background: #fff; color: #333; display: flex; align-items: center; justify-content: center; font-size: 1rem; box-shadow: 0 2px 8px rgba(0,0,0,.4); pointer-events: none; }
|
||||
.enhance-badge { position: absolute; top: 10px; padding: 3px 10px; border-radius: 999px; background: rgba(0,0,0,.55); color: #fff; font-size: .72rem; pointer-events: none; }
|
||||
.enhance-badge.left { left: 10px; }
|
||||
.enhance-badge.right { right: 10px; }
|
||||
.photo-thumb { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); display: block; }
|
||||
</style>
|
||||
</head>
|
||||
<body data-page="worker">
|
||||
<div class="layout">
|
||||
@@ -24,6 +39,8 @@
|
||||
<button class="btn-link" id="refreshBtn" type="button">Обновить</button>
|
||||
<button class="btn-link" id="wakeBtn" type="button" title="Запустить обработку очереди немедленно">Разбудить</button>
|
||||
<button class="btn-link" id="requeueBtn" type="button" title="Вернуть все записи с ошибкой в очередь">Перепроверить ошибки</button>
|
||||
<button class="btn-link" id="photoWakeBtn" type="button" title="Разбудить фото-воркер">📷 Разбудить фото</button>
|
||||
<button class="btn-link" id="photoToggleBtn" type="button">📷 …</button>
|
||||
<button class="btn-link" id="toggleBtn" type="button">…</button>
|
||||
</div>
|
||||
|
||||
@@ -78,6 +95,28 @@
|
||||
<div class="pager" id="recentPager"></div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card wide">
|
||||
<div class="card-head">
|
||||
<div class="ic"><i data-lucide="image"></i></div>
|
||||
<h3>Обработка фотографий</h3>
|
||||
</div>
|
||||
<div class="actions-row" style="margin-bottom:10px">
|
||||
<span class="hint" id="photoStateHint"></span>
|
||||
<span style="flex:1"></span>
|
||||
<button class="btn-link" id="photoRequeueBtn" type="button" title="Вернуть задания с ошибкой в очередь">Повторить ошибки фото</button>
|
||||
</div>
|
||||
<div class="worker-counts" id="photoCounts" style="margin-bottom:10px"></div>
|
||||
<div class="audit-wrap">
|
||||
<table class="audit-table" id="photoTable">
|
||||
<thead>
|
||||
<tr><th>Время</th><th>Запись</th><th>Действие</th><th>Статус</th><th>Было</th><th>Стало</th><th>Ошибка</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
<div class="empty" id="photoEmpty" style="display:none">Обработок фотографий пока нет.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-card wide" id="errorsCard" style="display:none">
|
||||
<div class="card-head">
|
||||
<div class="ic"><i data-lucide="triangle-alert"></i></div>
|
||||
@@ -112,6 +151,28 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-overlay" id="photoJobModal">
|
||||
<div class="edit-modal" style="max-width:900px">
|
||||
<h3 id="photoJobTitle">Обработка фотографии</h3>
|
||||
<div class="enhance-compare" id="photoJobCompare">
|
||||
<div class="enhance-stage">
|
||||
<img id="photoJobBefore" alt="Было">
|
||||
</div>
|
||||
<div class="enhance-top" id="photoJobTop" style="clip-path:inset(0 0 0 50%)">
|
||||
<div class="enhance-stage enhance-stage-fill">
|
||||
<img id="photoJobAfter" alt="Стало">
|
||||
</div>
|
||||
</div>
|
||||
<div class="enhance-divider" id="photoJobDivider"><div class="enhance-handle">⇔</div></div>
|
||||
<div class="enhance-badge left">Было</div>
|
||||
<div class="enhance-badge right">Стало</div>
|
||||
</div>
|
||||
<div class="card-foot" style="justify-content:flex-end;gap:8px;margin-top:14px">
|
||||
<button type="button" class="btn-primary ghost" id="photoJobCloseBtn">Закрыть</button>
|
||||
<button type="button" class="btn-primary" id="photoJobRollbackBtn">↩ Откатить к версии «Было»</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="toast" id="toast"></div>
|
||||
<script src="vendor/lucide.min.js"></script>
|
||||
<script src="admin.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user