fix(ai): adaptive max_tokens per backend (reasoning profiles get 1024+, native llama.cpp stays compact; per-profile max_tokens override); include entry texts in ai_status SSE notify so journal updates live without reload; audit log action filter; links pagination; settings validation & UI improvements

This commit is contained in:
dev
2026-09-17 14:12:36 +03:00
parent a93e37a6b1
commit 57e48a9d4f
12 changed files with 676 additions and 211 deletions
+96 -87
View File
@@ -12,120 +12,106 @@
<div class="main">
<div class="page-head">
<h2>Настройки</h2>
<p class="page-sub">Параметры формы и данные системы</p>
<p class="page-sub">Параметры формы, публичных страниц и данные системы</p>
<span class="page-status" id="authStatus"><span class="spinner dark"></span> Загрузка…</span>
<button class="head-btn" id="reloadBtn" type="button" title="Перезагрузить настройки"><i data-lucide="rotate-ccw"></i> Сбросить</button>
</div>
<div class="settings-stack settings-stack-grid">
<div class="settings-card wide">
<div class="settings-layout">
<aside class="settings-nav" id="settingsNav">
<a href="#sec-system" data-nav="sec-system" class="active"><i data-lucide="bar-chart-3"></i> Система</a>
<a href="#sec-antispam" data-nav="sec-antispam"><i data-lucide="shield"></i> Антиспам</a>
<a href="#sec-share" data-nav="sec-share"><i data-lucide="link"></i> Публичные ссылки</a>
<a href="#sec-form" data-nav="sec-form"><i data-lucide="file-text"></i> Форма ответа</a>
<a href="#sec-ai" data-nav="sec-ai"><i data-lucide="bot"></i> ИИ</a>
<a href="#sec-backup" data-nav="sec-backup"><i data-lucide="database"></i> Бэкапы</a>
<a href="#sec-bans" data-nav="sec-bans"><i data-lucide="shield-off"></i> Блокировки</a>
</aside>
<div class="settings-stack settings-stack-grid">
<div class="settings-card wide" id="sec-system">
<div class="card-head">
<div class="ic"><i data-lucide="bar-chart-3"></i></div>
<h3>Системная информация</h3>
<button class="icon-btn" id="systemRefreshBtn" type="button" title="Обновить" aria-label="Обновить"><i data-lucide="refresh-cw"></i></button>
<span class="head-live" id="systemLive" title="Данные кэшируются на сервере"></span>
</div>
<div id="systemInfo" class="sys-grid">
<div class="sys-card loading">Загрузка…</div>
</div>
</div>
<div class="settings-card">
<div class="settings-card" id="sec-antispam">
<div class="card-head">
<div class="ic"><i data-lucide="shield"></i></div>
<h3>Защита от повторов</h3>
</div>
<p class="hint">Если ученик уже отправил ответ, следующая отправка от него будет отклонена в течение заданного времени. Минимальное значение — 1 минута.</p>
<div class="settings-row">
<div class="settings-field">
<label for="spamInterval">Интервал ожидания</label>
<div class="unit-row">
<input type="number" id="spamInterval" class="settings-input" min="1" max="1440" step="1">
<span class="unit">минут</span>
</div>
<div class="settings-field">
<label for="spamInterval">Интервал ожидания</label>
<div class="unit-row">
<input type="number" id="spamInterval" class="settings-input" min="1" max="1440" step="1">
<span class="unit">минут</span>
</div>
<span class="field-error" id="spamIntervalError"></span>
<div class="preset-btns" id="spamPresets">
<button class="preset-btn" type="button" data-min="1">1 мин</button>
<button class="preset-btn" type="button" data-min="15">15 мин</button>
<button class="preset-btn" type="button" data-min="30">30 мин</button>
<button class="preset-btn" type="button" data-min="60">1 час</button>
<button class="preset-btn" type="button" data-min="1440">Сутки</button>
</div>
<span class="hint" id="spamIntervalHint"></span>
</div>
</div>
<div class="settings-card">
<div class="settings-card" id="sec-share">
<div class="card-head">
<div class="ic"><i data-lucide="link"></i></div>
<h3>Страница публичной ссылки /s/&lt;hash&gt;</h3>
</div>
<p class="hint">Управление видимостью элементов на публичных страницах записей.</p>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="shareShowMessage">
Показать сообщение ученика
</label>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="shareShowDate">
Показать дату в карточке записи
</label>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="shareShowNames">
Показывать фамилии и имена учеников
</label>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="shareShowPhotos">
Показывать фото группы
</label>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label for="cookieNoticeText">Уведомление о cookie</label>
<textarea id="cookieNoticeText" class="settings-input" rows="3" style="min-height:80px"></textarea>
</div>
<label class="toggle">
<input type="checkbox" id="shareShowMessage">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text"><b>Сообщение ученика</b><small>Текст «Что делала(а)» на странице по ссылке</small></span>
</label>
<label class="toggle">
<input type="checkbox" id="shareShowDate">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text"><b>Дата записи</b><small>Дата в карточке записи</small></span>
</label>
<label class="toggle">
<input type="checkbox" id="shareShowNames">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text"><b>Фамилии и имена</b><small>Имена учеников в галерее работ</small></span>
</label>
<label class="toggle">
<input type="checkbox" id="shareShowPhotos">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text"><b>Фото группы</b><small>Групповое фото в шапке страницы</small></span>
</label>
<div class="settings-field">
<label for="cookieNoticeText">Уведомление о cookie</label>
<textarea id="cookieNoticeText" class="settings-input" rows="3" style="min-height:80px"></textarea>
<span class="hint">Пусто — баннер не показывается</span>
</div>
</div>
<div class="settings-card">
<div class="card-head">
<div class="ic"><i data-lucide="file-text"></i></div>
<h3>Футер на форме</h3>
</div>
<p class="hint">Тексты показываются внизу формы ответа ученика.</p>
<div class="settings-row">
<div class="settings-field">
<label for="footerLeft">Текст слева</label>
<input type="text" id="footerLeft" class="settings-input" placeholder="Например: Поле, руководитель">
</div>
<div class="settings-field">
<label for="footerRight">Текст справа</label>
<input type="text" id="footerRight" class="settings-input" placeholder="Например: Год, курс">
</div>
</div>
</div>
<div class="settings-card">
<div class="settings-card" id="sec-ai">
<div class="card-head">
<div class="ic"><i data-lucide="bot"></i></div>
<h3>Промпт ИИ для исправления текста</h3>
</div>
<p class="hint">Инструкция для нейросети при исправлении ошибок в записи «Что делала(а)».</p>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label for="aiPrompt">Промпт</label>
<textarea id="aiPrompt" class="settings-input" rows="4" style="min-height:100px;font-family:monospace;font-size:.85rem"></textarea>
</div>
</div>
<div class="settings-row">
<div class="settings-field" style="flex:1">
<label style="display:flex;align-items:center;gap:8px;cursor:pointer">
<input type="checkbox" id="aiAutoCheck">
Автоматически проверять новые записи (без участия человека)
</label>
</div>
<label class="toggle">
<input type="checkbox" id="aiAutoCheck">
<span class="toggle-track"><span class="toggle-thumb"></span></span>
<span class="toggle-text"><b>Автопроверка новых записей</b><small>Без участия человека — воркер сам обрабатывает очередь</small></span>
</label>
<div class="settings-field">
<label for="aiPrompt">Промпт</label>
<textarea id="aiPrompt" class="settings-input mono" rows="5" spellcheck="false"></textarea>
</div>
<div class="ai-queue" id="aiQueueInfo"></div>
</div>
@@ -149,22 +135,41 @@
<div id="aiProfilesList" class="ai-profiles-list"></div>
</div>
<div class="settings-card">
<div class="settings-card" id="sec-backup">
<div class="card-head">
<div class="ic"><i data-lucide="database"></i></div>
<h3>Резервное копирование</h3>
<button class="icon-btn" id="statsRefreshBtn" type="button" title="Обновить статистику" aria-label="Обновить статистику"><i data-lucide="refresh-cw"></i></button>
</div>
<p class="hint">Скачайте архив со всеми данными (ученики, группы, журнал, настройки) и фотографиями. Восстановление полностью заменит текущие данные.</p>
<div class="data-stats" id="dataStats"><span class="chip">Загрузка статистики…</span></div>
<div class="card-foot">
<button class="btn-primary ghost" id="backupBtn" type="button"><i data-lucide="download"></i> Скачать бэкап</button>
<button class="btn-primary ghost" id="restoreBtn" type="button"><i data-lucide="upload"></i> Восстановить из файла</button>
<button class="btn-primary ghost danger" id="restoreBtn" type="button"><i data-lucide="upload"></i> Восстановить из файла</button>
<input type="file" id="restoreFile" accept=".tar.gz,.gz,application/gzip" style="display:none">
</div>
<div class="restore-status" id="restoreStatus"></div>
</div>
<div class="settings-card" id="bansCard">
<div class="settings-card" id="sec-form">
<div class="card-head">
<div class="ic"><i data-lucide="file-text"></i></div>
<h3>Футер на форме</h3>
</div>
<p class="hint">Тексты показываются внизу формы ответа ученика.</p>
<div class="settings-row">
<div class="settings-field">
<label for="footerLeft">Текст слева</label>
<input type="text" id="footerLeft" class="settings-input" placeholder="Например: Поле, руководитель">
</div>
<div class="settings-field">
<label for="footerRight">Текст справа</label>
<input type="text" id="footerRight" class="settings-input" placeholder="Например: Год, курс">
</div>
</div>
</div>
<div class="settings-card" id="sec-bans">
<div class="card-head">
<div class="ic"><i data-lucide="shield-off"></i></div>
<h3>Блокировки IP</h3>
@@ -172,13 +177,17 @@
<p class="hint">IP автоматически блокируется на 24 часа: за заполнение скрытого антиспам-поля, за 10 неудачных попыток входа или 10 неверных паролей на защищённой ссылке.</p>
<div class="data-stats" id="bansList"><span class="chip">Загрузка…</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="save-bar" id="saveBar">
<span class="hint" id="saveHint" style="display:inline-flex;align-items:center;gap:6px"><i data-lucide="triangle-alert"></i> Есть несохранённые изменения</span>
<button class="btn-primary" id="saveBtn" type="button">Сохранить изменения</button>
<span class="hint" id="saveHint"><i data-lucide="triangle-alert"></i> Есть несохранённые изменения</span>
<span class="save-bar-actions">
<button class="btn-primary ghost" id="resetBtn" type="button"><i data-lucide="rotate-ccw"></i> Отменить</button>
<button class="btn-primary" id="saveBtn" type="button"><i data-lucide="check"></i> Сохранить</button>
</span>
</div>
<div class="modal-overlay" id="aiProfileModal">
@@ -200,7 +209,7 @@
<label for="aiProfileApiKey">API-ключ (если требуется)</label>
<input type="password" id="aiProfileApiKey" class="settings-input" placeholder="sk-...">
</div>
<span id="aiProfileTestStatus" style="font-size:.85rem;min-height:1.2em"></span>
<div class="ai-test-status" id="aiProfileTestStatus"></div>
<div class="actions" style="flex-wrap:wrap">
<button class="btn-primary" id="aiProfileSaveBtn" type="button">Сохранить</button>
<button class="btn-primary ghost" id="aiProfileTestBtn" type="button"><i data-lucide="plug-zap"></i> Проверить</button>