Files
dev 45033bffa8 feat(photo-ai): Stage 5 — фронтенд фото-ИИ (режимы, статус, метаданные)
Только фронтенд: server.js, worker.js, photo-ai/, compose и схема БД не тронуты.
Контракт /enhance-ai, /api/photo-ai/health и service в /api/photo-jobs/status
взят из Stage 4 как есть (I6 — новых колонок нет, метаданные из photo_jobs.params
и audit_log.target).

journal.html + journal.js:
- селект режима рядом с «🤖 ИИ»: Универсально (x2) / Быстро (x4) / Лица /
  Лица + фон; PHOTO_AI_MODES — единственная таблица режим→{model, face}
- значение уходит в POST .../enhance-ai телом {model, face, face_model};
  face_model подставляется только когда face !== 'off'
- дефолт селекта из photo_ai_face_mode (face=all → facesbg, face=face → faces)
- подсказка про медленный CPU: loadPhotoAiDevice() читает
  GET /api/photo-ai/health (только для админа)
- текст подтверждения и надпись «ИИ обрабатывает…» различают апскейл и face-режим
- кнопка и селект скрываются вместе при photo_ai_enabled === 'false' (I3)

settings.html + settings.js:
- новый раздел «Фото-ИИ»: режим по умолчанию, модель лиц, желаемое устройство
- read-only статус из /api/photo-jobs/status: фактическое устройство,
  device_name, VRAM, модели/модели лиц/загруженные; жёлтым — расхождение с
  желаемым устройством, красным — недоступность и незаданный PHOTO_AI_URL
- renderStackInfo() рисует карточку «Фото-ИИ» из stack.photo_ai
- новые id в DIRTY_FIELDS и в payload PUT /api/settings
- photo_ai_device_pref документирующий: фактическое устройство задаёт
  PHOTO_AI_DEVICE в контейнере

worker.html + worker.js:
- PHOTO_ACTION_LABELS: ai_face «ИИ + лица», ai_upscale «ИИ-апскейл»
- в модалке сравнения — чипы с моделью, режимом лиц, найденными лицами,
  устройством, временем обработки и предупреждениями
- строка состояния учитывает service.reachable === false («задания ждут»)
  и дописывает «· расчёт на <device>»

audit.js: 10 меток для кодов, которые раньше показывались сырыми (профиль и
фото ученика, главное фото, фото модуля, удаление/очистка уведомлений, логотип).

Проверено: node --check для всех четырёх js. Приёмка на живом стеке в этом
изменении не гонялась — чекбокс приёмки Stage 5 в TODO_PHOTO_FACE_AI.md
оставлен пустым, там же журнал раздела 5 с замечаниями для Stage 6.
2026-09-30 11:15:54 +03:00

194 lines
9.9 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<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">
<div class="sidebar" id="sidebar"></div>
<div class="main">
<div class="page-head">
<h2>Воркер ИИ</h2>
<p class="page-sub">Отслеживание автоматической проверки сообщений записей</p>
</div>
<div class="actions-row">
<span class="worker-state" id="stateLine"><span class="worker-dot"></span> Загрузка…</span>
<span style="flex:1"></span>
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:.85rem">
<input type="checkbox" id="autoRefresh"> Автообновление
</label>
<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>
<div class="sys-grid" id="workerCards" style="margin-top:16px">
<div class="sys-card loading">Загрузка…</div>
</div>
<div class="worker-counts" id="workerCounts"></div>
<div class="settings-stack settings-stack-grid" style="margin-top:20px">
<div class="settings-card wide">
<div class="card-head">
<div class="ic"><i data-lucide="hourglass"></i></div>
<h3>Очередь (<span id="pendingCount">0</span>)</h3>
</div>
<p class="hint">Записи, ожидающие проверки моделью. Обновляются автоматически.</p>
<div class="audit-wrap">
<table class="audit-table" id="pendingTable">
<thead>
<tr><th>ID</th><th>Ученик</th><th>Группа</th><th>Создана</th></tr>
</thead>
<tbody></tbody>
</table>
<div class="empty" id="pendingEmpty" style="display:none">Очередь пуста.</div>
</div>
</div>
<div class="settings-card wide">
<div class="card-head">
<div class="ic"><i data-lucide="history"></i></div>
<h3>Последние проверки</h3>
</div>
<div class="actions-row" style="margin-bottom:10px">
<select id="recentFilter" class="settings-input" style="max-width:220px">
<option value="">Все статусы</option>
<option value="done">Проверено</option>
<option value="skipped">Пропущено</option>
<option value="error">Ошибка</option>
<option value="reverted">Возвращён оригинал</option>
</select>
<span class="hint" id="recentCount"></span>
</div>
<div class="audit-wrap">
<table class="audit-table" id="recentTable">
<thead>
<tr><th>Проверено</th><th>Ученик</th><th>Группа</th><th>Статус</th><th>Изменения</th><th>Детали</th></tr>
</thead>
<tbody></tbody>
</table>
<div class="empty" id="recentEmpty" style="display:none">Проверок пока нет.</div>
</div>
<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>
<p class="hint">В очереди (<span id="photoPendingCount">0</span>)</p>
<div class="audit-wrap" style="margin-bottom:14px">
<table class="audit-table" id="photoPendingTable">
<thead>
<tr><th>Задание</th><th>Запись</th><th>Действие</th><th>Поставлено</th></tr>
</thead>
<tbody></tbody>
</table>
<div class="empty" id="photoPendingEmpty" style="display:none">Очередь пуста.</div>
</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 class="pager" id="photoRecentPager"></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>
<h3>Ошибки (<span id="errorsCount">0</span>)</h3>
</div>
<p class="hint">Записи, которые не удалось обработать после всех попыток. Нажмите «Перепроверить ошибки», чтобы запустить их снова.</p>
<div class="audit-wrap">
<table class="audit-table" id="errorsTable">
<thead>
<tr><th>Время</th><th>ID</th><th>Ученик</th><th>Ошибка</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<div class="modal-overlay" id="aiDetailModal">
<div class="edit-modal" style="max-width:680px">
<h3 id="aiDetailTitle">Детали ИИ-проверки</h3>
<div class="ai-detail-block">
<div class="ai-detail-label">Отправлено в модель</div>
<pre id="aiDetailInput"></pre>
</div>
<div class="ai-detail-block">
<div class="ai-detail-label">Ответ модели</div>
<pre id="aiDetailOutput"></pre>
</div>
<div class="card-foot" style="justify-content:flex-end;gap:8px">
<button type="button" class="cancel" id="aiDetailCloseBtn">Закрыть</button>
</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="data-stats" id="photoJobMeta" style="margin-top:12px"></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>
<script src="js/worker.js"></script>
</body>
</html>