feat(journal): live updates on new entry via SSE (/api/events) — admin journal auto-refreshes when a student submits

This commit is contained in:
dev
2026-09-16 20:09:06 +03:00
parent e8337c2845
commit 2784084c68
2 changed files with 55 additions and 0 deletions
+31
View File
@@ -63,6 +63,36 @@ function invalidateSettings() { cacheDrop('setting:'); cacheDrop('share:payload:
function invalidateStudents() { cacheDrop('students:'); }
function invalidateGroups() { cacheDrop('groups:'); cacheDrop('students:'); cacheDrop('share:payload:'); }
function invalidateEntries() { cacheDrop('entries:'); cacheDrop('students:'); cacheDrop('share:payload:'); }
const sseClients = new Set();
function broadcastEntryChanged() {
const frame = `event: entries_changed\ndata: ${JSON.stringify({ ts: Date.now() })}\n\n`;
for (const client of sseClients) {
try { client.write(frame); } catch (e) { sseClients.delete(client); }
}
}
app.get('/api/events', async (req, res) => {
try {
const token = req.headers['x-auth-token'] || req.query.token;
const user = await loadUserByToken(token);
if (!user || !user.is_active) return res.status(401).end();
} catch (e) {
return res.status(500).end();
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
Connection: 'keep-alive',
'X-Accel-Buffering': 'no'
});
res.write(':ok\n\n');
sseClients.add(res);
const ping = setInterval(() => {
try { res.write(':ping\n\n'); } catch (e) { clearInterval(ping); sseClients.delete(res); }
}, 25000);
req.on('close', () => { clearInterval(ping); sseClients.delete(res); });
});
function invalidateShare() { cacheDrop('share:payload:'); }
function invalidateStats() { cacheDrop('stats:'); cacheDrop('dashboard:'); cacheDrop('system-info'); }
function invalidateAll() { cacheStore.clear(); }
@@ -3325,6 +3355,7 @@ app.post('/api/entries', entryLimiter, (req, res, next) => {
if (entryAutoChecker) entryAutoChecker.notify();
invalidateEntries();
invalidateStats();
broadcastEntryChanged();
res.status(201).json({ ...rows[0], files: projectFiles.length, photos: photos.length });
} catch (e) {
await client.query('ROLLBACK').catch(() => {});