feat(notifications): центр уведомлений о системных событиях
Добавлена система уведомлений о системных и фоновых событиях (новые записи журнала, обработка фото, авто-проверка текста, блокировки IP, бэкапы). - backend (server.js, worker.js): - каталог NOTIFY_TYPES с метаданными и уровнями - таблицы notifications и notification_reads в db/init.sql и db/migration.sql - SSE-стрим GET /api/notifications/stream через Redis pub/sub с in-memory fallback - REST API: список, счётчик непрочитанных, отметка о прочтении, удаление, очистка - настройки уведомлений в settings (notify_enabled, notify_retention_days, notify_<тип>) - автоматическая очистка старых уведомлений по расписанию - frontend: - колокольчик со счётчиком непрочитанных в шапке (admin.js) - страница списка уведомлений public/notifications.html и public/js/notifications.js - секция настроек уведомлений в public/settings.html и public/js/settings.js - стили для уведомлений в public/admin.css - тесты и документация: - добавлены проверки в api.smoketest.js - обновлены README.md и AGENTS.md
This commit is contained in:
@@ -10,11 +10,20 @@ const { textDiff, FIELD_LABELS } = require('./diff');
|
||||
const PHOTO_MAX_ATTEMPTS = 3;
|
||||
const PHOTO_AI_TIMEOUT_MS = 300000;
|
||||
|
||||
function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntries, sharp, photoAiUrl, uploadsDir, storage, bus }) {
|
||||
function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntries, sharp, photoAiUrl, uploadsDir, storage, bus, notifyEvent }) {
|
||||
const AI_URL = photoAiUrl || process.env.PHOTO_AI_URL || '';
|
||||
const IDLE_MIN = 2000;
|
||||
const IDLE_MAX = 30000;
|
||||
|
||||
async function sendNotification(entryId, payload) {
|
||||
if (!notifyEvent) return;
|
||||
try {
|
||||
await notifyEvent(entryId, payload);
|
||||
} catch (e) {
|
||||
console.error('Photo worker notification failed:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
let started = false;
|
||||
let stopped = false;
|
||||
let idleMs = IDLE_MIN;
|
||||
@@ -136,6 +145,12 @@ function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntrie
|
||||
[newPath, job.id]
|
||||
);
|
||||
if (logAudit) await logAudit(null, 'photo.job.preview', { entry_id: job.entry_id, job_id: job.id, action: job.action, after_path: newPath });
|
||||
await sendNotification(job.entry_id, {
|
||||
type: 'photo.job.done',
|
||||
title: job.action === 'ai' ? 'Фото обработано нейросетью: {student}' : 'Фото улучшено на сервере: {student}',
|
||||
body: `Группа {group} · запись #${job.entry_id} · результат ждёт применения`,
|
||||
target: { job_id: job.id, action: job.action, after_path: newPath },
|
||||
});
|
||||
}
|
||||
async function processOne(job) {
|
||||
stats.jobs++;
|
||||
@@ -148,6 +163,12 @@ function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntrie
|
||||
stats.errors++;
|
||||
stats.last_at = new Date().toISOString();
|
||||
stats.last_error = 'У записи нет фото';
|
||||
await sendNotification(job.entry_id, {
|
||||
type: 'photo.job.error',
|
||||
title: 'Ошибка обработки фото: {student}',
|
||||
body: `Группа {group} · запись #${job.entry_id} · у записи нет фото для обработки`,
|
||||
target: { job_id: job.id, action: job.action },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
const photoPath = rows[0].photo_path;
|
||||
@@ -175,6 +196,12 @@ function createPhotoEnhanceWorker({ pool, getSetting, logAudit, invalidateEntrie
|
||||
);
|
||||
stats.errors++;
|
||||
if (logAudit) await logAudit(null, 'photo.job.error', { entry_id: job.entry_id, job_id: job.id, error: message });
|
||||
await sendNotification(job.entry_id, {
|
||||
type: 'photo.job.error',
|
||||
title: 'Ошибка обработки фото: {student}',
|
||||
body: `Группа {group} · запись #${job.entry_id} · ${message}`,
|
||||
target: { job_id: job.id, action: job.action, error: message },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user