Только фронтенд: 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.
259 lines
16 KiB
HTML
259 lines
16 KiB
HTML
<!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">
|
||
<link rel="stylesheet" href="admin.css">
|
||
<style>
|
||
.enhance-modal { max-width: 1100px; width: 94vw; }
|
||
.enhance-body { display: flex; gap: 16px; align-items: stretch; margin-top: 4px; }
|
||
.enhance-compare { position: relative; flex: 1 1 auto; min-width: 0; border-radius: 10px; overflow: hidden; background: var(--bg); border: 1px solid var(--border); user-select: none; touch-action: none; cursor: ew-resize; }
|
||
.enhance-compare.zoomed { cursor: grab; }
|
||
.enhance-compare.panning { cursor: grabbing; }
|
||
.enhance-stage { transform-origin: 0 0; will-change: transform; }
|
||
.enhance-stage > img, .enhance-stage > canvas { display: block; width: 100%; height: 100%; }
|
||
.enhance-stage > img { height: auto; }
|
||
.enhance-top { position: absolute; inset: 0; overflow: hidden; }
|
||
.enhance-top .enhance-stage { position: absolute; inset: 0; }
|
||
.enhance-divider { position: absolute; top: 0; bottom: 0; left: 50%; width: 32px; transform: translateX(-50%); pointer-events: auto; 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; }
|
||
.enhance-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: var(--muted); font-size: .85rem; background: var(--bg); }
|
||
.enhance-sliders { flex: 0 0 240px; display: flex; flex-direction: column; gap: 14px; }
|
||
.enhance-sliders .settings-field input[type=range] { width: 100%; }
|
||
.enhance-foot { justify-content: flex-end; gap: 8px; margin-top: 14px; }
|
||
.enhance-ai-mode { width: auto; min-width: 200px; padding: 9px 12px; font-size: .85rem; }
|
||
.enhance-ai-hint { font-size: .75rem; color: var(--muted); max-width: 340px; }
|
||
.photo-thumb { width: 56px; height: 56px; object-fit: cover; border-radius: 6px; border: 1px solid var(--border); display: block; }
|
||
@media (max-width: 800px) {
|
||
.enhance-body { flex-direction: column; }
|
||
.enhance-sliders { flex: 1 1 auto; display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body data-page="journal">
|
||
<div class="layout">
|
||
<div class="sidebar" id="sidebar"></div>
|
||
<div class="main">
|
||
<h2>Журнал</h2>
|
||
<div class="filters">
|
||
<div><label>Поиск</label><input type="text" id="searchInput" class="search-input" placeholder="Имя или описание"></div>
|
||
<div><label>Студент</label><input type="text" id="studentFilter" list="studentFilterList" placeholder="Все / введите имя..."><datalist id="studentFilterList"></datalist></div>
|
||
<div><label>Группа</label><select id="groupFilter"><option value="">Все группы</option></select></div>
|
||
<div><label>Модуль</label><input type="text" id="moduleFilter" list="moduleFilterList" placeholder="Все модули" autocomplete="off"><datalist id="moduleFilterList"></datalist></div>
|
||
<div><label>С</label><input type="date" id="dateFrom"></div>
|
||
<div><label>По</label><input type="date" id="dateTo"></div>
|
||
<div><label> </label><button type="button" id="exportCsvBtn" style="background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 16px;font-size:.85rem;cursor:pointer;white-space:nowrap">Экспорт CSV</button></div>
|
||
<div><label> </label><button type="button" id="createLinkBtn" style="background:var(--card);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 16px;font-size:.85rem;cursor:pointer;white-space:nowrap">Ссылка</button></div>
|
||
<div><label> </label>
|
||
<div class="view-toggle" id="viewToggle">
|
||
<button data-view="list" type="button">Список</button>
|
||
<button data-view="cards" type="button">Карточки</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="grid" id="grid"><div class="empty">Загрузка...</div></div>
|
||
<div class="pager" id="pager"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="imgModal">
|
||
<img id="imgModalSrc">
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="editModal">
|
||
<div class="edit-modal" style="max-width:720px">
|
||
<h3>Редактирование записи</h3>
|
||
<form id="editForm" class="link-fields" style="gap:12px">
|
||
<div class="settings-field">
|
||
<label>Фамилия и имя <span style="color:#ef4444">*</span></label>
|
||
<input type="text" id="editName" class="settings-input" list="studentEditList" required autocomplete="off">
|
||
<datalist id="studentEditList"></datalist>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Группа <span style="color:#ef4444">*</span></label>
|
||
<select id="editGroup" class="settings-input" required></select>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Тема модуля</label>
|
||
<input type="text" id="editModule" class="settings-input" list="editModuleList" placeholder="Начните вводить название..." autocomplete="off">
|
||
<datalist id="editModuleList"></datalist>
|
||
</div>
|
||
<div class="settings-field full">
|
||
<label>Что делала(а) <span style="color:#ef4444">*</span>
|
||
<button type="button" id="aiCorrectBtn" class="ai-btn" title="Исправить ошибки ИИ">✨</button>
|
||
</label>
|
||
<textarea id="editDesc" class="settings-input" required></textarea>
|
||
<div id="editAiStatus" class="ai-status" style="display:none"></div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Фотография</label>
|
||
<input type="file" id="editPhoto" accept=".jpg,.jpeg,.png,.gif,.webp,.bmp,.avif,.ico,.heic,.heif,.jfif">
|
||
<div class="files-preview" id="editPhotoPreview"></div>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Прикрепить файлы</label>
|
||
<input type="file" id="editFiles" multiple accept=".pdf,.doc,.docx,.txt,.md,.html,.htm,.zip,.rar,.7z,.jpg,.jpeg,.png,.gif,.webp,.bmp,.avif,.ico,.heic,.heif,.jfif">
|
||
<div class="files-preview" id="editFilesPreview"></div>
|
||
</div>
|
||
<div class="card-foot full" style="justify-content:flex-end;gap:8px">
|
||
<button type="button" class="btn-primary ghost" id="editCancelBtn">Отмена</button>
|
||
<button type="submit" class="btn-primary">Сохранить</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="aiCorrectModal">
|
||
<div class="edit-modal" style="max-width:560px">
|
||
<h3>✨ Исправление текста</h3>
|
||
<div style="font-size:.85rem;color:var(--muted);margin-bottom:12px">ИИ предлагает вариант:</div>
|
||
<div id="aiSuggestion" style="background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:12px;min-height:80px;max-height:300px;overflow:auto;white-space:pre-wrap;font-family:inherit"></div>
|
||
<div class="card-foot" style="justify-content:flex-end;gap:8px;margin-top:12px">
|
||
<button type="button" class="btn-primary ghost" id="aiRejectBtn">Отклонить</button>
|
||
<button type="button" class="btn-primary" id="aiAcceptBtn">Применить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="linkModal">
|
||
<div class="edit-modal" style="max-width:640px">
|
||
<div id="linkCreateView">
|
||
<h3>Создать ссылку</h3>
|
||
<div style="font-size:.85rem;color:var(--muted);background:var(--bg);border:1px solid var(--border);border-radius:8px;padding:10px 12px;line-height:1.5" id="linkPreview"></div>
|
||
<div class="link-fields">
|
||
<div class="settings-field">
|
||
<label>Название ссылки <span style="color:#ef4444">*</span></label>
|
||
<input type="text" id="linkNameInput" class="settings-input" required placeholder="Например: Апрельский журнал">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Группа (если выбрана)</label>
|
||
<input type="text" id="linkGroupPreview" class="settings-input" readonly style="background:var(--bg);color:var(--muted);cursor:not-allowed">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Ученик (необязательно)</label>
|
||
<input type="text" id="linkStudent" class="settings-input" list="linkStudentList" placeholder="Все / введите имя..."><datalist id="linkStudentList"></datalist>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Ссылка</label>
|
||
<input type="url" id="linkUrl" class="settings-input" placeholder="https://...">
|
||
</div>
|
||
<div class="settings-field full">
|
||
<label>Сообщение</label>
|
||
<textarea id="linkMessage" class="settings-input" rows="3" placeholder="Текст, который увидят на публичной странице"></textarea>
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Истекает через (дни)</label>
|
||
<input type="number" id="linkExpiresDays" class="settings-input" min="1" max="365" value="7" placeholder="7">
|
||
</div>
|
||
<div class="settings-field">
|
||
<label>Пароль для доступа (необязательно)</label>
|
||
<input type="password" id="linkPassword" class="settings-input" placeholder="Оставьте пустым для открытого доступа">
|
||
</div>
|
||
<div class="settings-field full">
|
||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||
<input type="checkbox" id="linkShowNames">
|
||
Показывать фамилии и имена учеников
|
||
</label>
|
||
</div>
|
||
<div class="settings-field full">
|
||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||
<input type="checkbox" id="linkShowMessage">
|
||
Показать сообщение ученика
|
||
</label>
|
||
</div>
|
||
<div class="settings-field full">
|
||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||
<input type="checkbox" id="linkShowDate">
|
||
Показать дату в карточке записи
|
||
</label>
|
||
</div>
|
||
<div class="settings-field full">
|
||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
|
||
<input type="checkbox" id="linkShowPhotos">
|
||
Показывать фото группы
|
||
</label>
|
||
</div>
|
||
<div class="card-foot full" style="justify-content:flex-end;gap:8px">
|
||
<button type="button" class="btn-primary ghost" id="linkCreateCloseBtn">Отмена</button>
|
||
<button type="button" class="btn-primary" id="linkCreateSaveBtn">Создать</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div id="linkResultView" style="display:none">
|
||
<h3>Ссылка готова</h3>
|
||
<div style="font-size:.85rem;color:var(--muted);margin-bottom:8px">Открывается публичная страница с карточками — без входа и выбора параметров. Ссылку можно изменить или удалить в разделе «Ссылки».</div>
|
||
<input type="text" id="linkUrlResult" class="settings-input" readonly style="font-size:.8rem;color:var(--accent)">
|
||
<div class="card-foot" style="justify-content:flex-end;gap:8px">
|
||
<a class="btn-primary ghost" href="links.html" style="text-decoration:none">К списку ссылок</a>
|
||
<button class="btn-primary" type="button" id="linkCopyBtn">Копировать</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="enhanceModal">
|
||
<div class="edit-modal enhance-modal">
|
||
<h3>Улучшение фотографии</h3>
|
||
<div class="enhance-body">
|
||
<div class="enhance-compare" id="enhanceCompare">
|
||
<div class="enhance-stage" id="enhanceStage">
|
||
<img id="enhanceOrig" alt="Оригинал">
|
||
</div>
|
||
<div class="enhance-top" id="enhanceTop" style="clip-path:inset(0 0 0 50%)">
|
||
<div class="enhance-stage" id="enhanceInner">
|
||
<canvas id="enhanceCanvas"></canvas>
|
||
</div>
|
||
</div>
|
||
<div class="enhance-divider" id="enhanceDivider"><div class="enhance-handle">⇔</div></div>
|
||
<div class="enhance-badge left">Оригинал</div>
|
||
<div class="enhance-badge right">Результат</div>
|
||
<div class="enhance-loading" id="enhanceLoading">Загрузка...</div>
|
||
</div>
|
||
<div class="enhance-sliders">
|
||
<div class="settings-field"><label>Яркость <span id="enhanceBrightnessVal">100%</span></label><input type="range" id="enhanceBrightness" min="50" max="150" value="100"></div>
|
||
<div class="settings-field"><label>Контраст <span id="enhanceContrastVal">100%</span></label><input type="range" id="enhanceContrast" min="50" max="150" value="100"></div>
|
||
<div class="settings-field"><label>Насыщенность <span id="enhanceSaturateVal">100%</span></label><input type="range" id="enhanceSaturate" min="0" max="200" value="100"></div>
|
||
<div class="settings-field"><label>Шумодав <span id="enhanceDenoiseVal">0%</span></label><input type="range" id="enhanceDenoise" min="0" max="100" value="0"></div>
|
||
<div class="settings-field"><label>Резкость <span id="enhanceSharpVal">0%</span></label><input type="range" id="enhanceSharp" min="0" max="100" value="0"></div>
|
||
</div>
|
||
</div>
|
||
<div class="card-foot enhance-foot">
|
||
<button type="button" class="btn-primary ghost" id="enhanceHistoryBtn" title="История изменений фотографии">🕘 История</button>
|
||
<button type="button" class="btn-primary ghost" id="enhanceRestoreBtn" style="display:none" title="Вернуть исходную фотографию">↩ Вернуть оригинал</button>
|
||
<select id="enhanceAiMode" class="settings-input enhance-ai-mode" style="display:none" title="Режим ИИ-обработки фото">
|
||
<option value="x2">Универсально (x2)</option>
|
||
<option value="x4">Быстро (x4)</option>
|
||
<option value="faces">Лица (GFPGAN)</option>
|
||
<option value="facesbg">Лица + фон</option>
|
||
</select>
|
||
<span class="enhance-ai-hint" id="enhanceAiHint" style="display:none"></span>
|
||
<button type="button" class="btn-primary ghost" id="enhanceAiBtn" style="display:none" title="ИИ-восстановление (Real-ESRGAN)">🤖 ИИ</button>
|
||
<button type="button" class="btn-primary ghost" id="enhanceAutoBtn" title="Подобрать настройки автоматически">✨ Авто</button>
|
||
<button type="button" class="btn-primary ghost" id="enhanceCancelBtn">Оставить как есть</button>
|
||
<button type="button" class="btn-primary" id="enhanceSaveBtn">Применить</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="photoHistoryModal">
|
||
<div class="edit-modal enhance-modal">
|
||
<h3 id="photoHistoryTitle">История изменений фотографии</h3>
|
||
<div id="photoHistoryList" style="display:flex;flex-direction:column;gap:12px;max-height:60vh;overflow:auto"></div>
|
||
<div class="card-foot enhance-foot">
|
||
<button type="button" class="btn-primary ghost" id="photoHistoryCloseBtn">Закрыть</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/journal.js"></script>
|
||
</body>
|
||
</html> |