diff --git a/public/js/shorts.js b/public/js/shorts.js index cbc974a..0b06d7c 100644 --- a/public/js/shorts.js +++ b/public/js/shorts.js @@ -25,9 +25,35 @@ async function init() { document.getElementById('shortsSearch').addEventListener('keydown', e => { if (e.key === 'Enter') { shortsPage = 1; loadShorts(); } }); + connectShortsLive(); await loadShorts(); } +let shortsLiveSource = null; +let shortsRefreshTimer = null; + +function connectShortsLive() { + if (shortsLiveSource) { shortsLiveSource.close(); shortsLiveSource = null; } + const connect = () => { + const t = sessionStorage.getItem('authToken'); + if (!t) return; + shortsLiveSource = new EventSource(`${API}/api/events?token=${encodeURIComponent(t)}`); + shortsLiveSource.addEventListener('shorts_changed', () => { + if (shortsRefreshTimer) return; + shortsRefreshTimer = setTimeout(() => { + shortsRefreshTimer = null; + loadShorts(); + }, 300); + }); + shortsLiveSource.onerror = () => { + if (shortsLiveSource) { shortsLiveSource.close(); shortsLiveSource = null; } + setTimeout(connect, 5000); + }; + }; + connect(); + window.addEventListener('beforeunload', () => { if (shortsLiveSource) shortsLiveSource.close(); }); +} + let shortsPage = 1; const SHORTS_PAGE_SIZE = 50; let shortsTotal = 0; diff --git a/server.js b/server.js index 51a1a39..e0b74b4 100644 --- a/server.js +++ b/server.js @@ -159,6 +159,10 @@ function dispatchEvent(payload) { }); return; } + if (payload && (payload.type === 'entries_changed' || payload.type === 'lessons_changed' || payload.type === 'shorts_changed')) { + writeFrame(payload.type, { ts: Date.now() }); + return; + } if (payload && payload.type === 'ai_status') { writeFrame('ai_status', { id: payload.id, @@ -1991,6 +1995,15 @@ const chatLimiter = rateLimit({ message: { error: 'Слишком много сообщений. Подождите минуту.' }, }); +const aiLimiter = rateLimit({ + windowMs: 60 * 1000, + max: 30, + standardHeaders: true, + legacyHeaders: false, + store: cache.rateLimitStore('ai', 60 * 1000), + message: { error: 'Слишком много запросов к ИИ. Подождите минуту.' }, +}); + async function chatThreadFor(user, threadId) { const id = parseInt(threadId, 10); if (!Number.isInteger(id) || id < 1) return { error: 400, message: 'Invalid thread' }; @@ -2747,7 +2760,7 @@ app.delete('/api/shorts/:id/permanent', requireAuth, requireAdmin, async (req, r } }); -app.post('/api/shorts/ai-correct', requireAuth, async (req, res) => { +app.post('/api/shorts/ai-correct', requireAuth, aiLimiter, async (req, res) => { try { const text = reqStr(req.body?.text, 5000); if (!text) return res.status(400).json({ error: 'Текст не указан' }); @@ -7486,7 +7499,7 @@ app.put('/api/entries/:id/unschedule', requireAuth, async (req, res) => { res.json({ ok: true }); }); -app.post('/api/ai/correct', requireAuth, async (req, res) => { +app.post('/api/ai/correct', requireAuth, aiLimiter, async (req, res) => { const text = reqStr(req.body?.text, 5000); if (!text) return res.status(400).json({ error: 'Текст не указан' }); try {