feat(audit): показывать изменения текста записи по шагам
При сохранении записи журнала (PUT /api/entries/:id) сравнивается состояние до и после, и в audit_log пишется не только факт правки, но и сами изменения: пословный дифф текста, статистика добавленных и удалённых слов, а также смена ФИО, группы и темы модуля. - diff.js: пословный LCS-дифф без зависимостей, обрезка больших текстов, сборка изменений по полям записи, облегчённый target для списка аудита - source правки: manual / ai / ai_manual / ai_revert; журнал шлёт edit_source, сервер доверяет явному значению и определяет источник по description_ai как запасной вариант - те же диффы пишутся для автопроверки ИИ (entry.ai.auto-check) и отката к оригиналу (entry.ai.revert) - GET /api/audit отдаёт список без diff, GET /api/audit/:id — полный target, чтобы не грузить килобайты текста на каждую строку - Аудит: колонка «Кто», сводка в таблице, модалка с подсветкой удалённого и добавленного текста, «было/стало» для полей - auth.login теперь пишет user_id, иначе колонка «Кто» показывала «система» - diff.selftest.js: 16 тестов диффа; README и AGENTS обновлены
This commit is contained in:
@@ -19,6 +19,7 @@ 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() {
|
||||
@@ -395,6 +396,7 @@ async function openEdit(id) {
|
||||
if (!e) return;
|
||||
editId = id;
|
||||
editDeletePhoto = false;
|
||||
editAiSuggestion = null;
|
||||
fillAutocomplete();
|
||||
document.getElementById('editName').value = e.student_name;
|
||||
document.getElementById('editDesc').value = e.description;
|
||||
@@ -1140,6 +1142,7 @@ function acceptAiCorrect() {
|
||||
const suggestion = document.getElementById('aiSuggestion').textContent;
|
||||
if (suggestion && suggestion !== 'Обработка...' && !suggestion.startsWith('Ошибка:')) {
|
||||
document.getElementById('editDesc').value = suggestion;
|
||||
editAiSuggestion = suggestion.trim();
|
||||
}
|
||||
closeAiCorrect();
|
||||
}
|
||||
@@ -1190,6 +1193,11 @@ async function recheckAi(id) {
|
||||
refreshEntryCard(id);
|
||||
}
|
||||
|
||||
function editSourceFor(description) {
|
||||
if (!editAiSuggestion) return 'manual';
|
||||
return description === editAiSuggestion ? 'ai' : 'ai_manual';
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editId) return;
|
||||
const name = document.getElementById('editName').value.trim();
|
||||
@@ -1205,6 +1213,7 @@ async function saveEdit() {
|
||||
formData.append('group_id', group_id);
|
||||
formData.append('description', description);
|
||||
formData.append('module_id', moduleId);
|
||||
formData.append('edit_source', editSourceFor(description));
|
||||
|
||||
const photoFiles = document.getElementById('editPhoto').files;
|
||||
for (const f of photoFiles) formData.append('photo', f);
|
||||
|
||||
Reference in New Issue
Block a user