feat(journal): live updates on new entry via SSE (/api/events) — admin journal auto-refreshes when a student submits
This commit is contained in:
@@ -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(() => {});
|
||||
|
||||
Reference in New Issue
Block a user