agentacta 1.2.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +69 -1
- package/package.json +1 -1
- package/public/app.js +78 -0
- package/public/style.css +28 -0
package/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const http = require('http');
|
|
3
3
|
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
|
+
const { EventEmitter } = require('events');
|
|
5
6
|
|
|
6
7
|
// --demo flag: use demo session data (must run before config load)
|
|
7
8
|
if (process.argv.includes('--demo')) {
|
|
@@ -129,6 +130,10 @@ const db = open();
|
|
|
129
130
|
// Live re-indexing setup
|
|
130
131
|
const stmts = createStmts(db);
|
|
131
132
|
|
|
133
|
+
// SSE emitter: notifies connected clients when a session is re-indexed
|
|
134
|
+
const sseEmitter = new EventEmitter();
|
|
135
|
+
sseEmitter.setMaxListeners(100);
|
|
136
|
+
|
|
132
137
|
const sessionDirs = discoverSessionDirs(config);
|
|
133
138
|
|
|
134
139
|
// Initial indexing pass
|
|
@@ -163,7 +168,10 @@ for (const dir of sessionDirs) {
|
|
|
163
168
|
_reindexTimers.delete(filePath);
|
|
164
169
|
try {
|
|
165
170
|
const result = indexFile(db, filePath, dir.agent, stmts, ARCHIVE_MODE);
|
|
166
|
-
if (!result.skipped)
|
|
171
|
+
if (!result.skipped) {
|
|
172
|
+
console.log(`Live re-indexed: ${filename} (${dir.agent})`);
|
|
173
|
+
if (result.sessionId) sseEmitter.emit('session-update', result.sessionId);
|
|
174
|
+
}
|
|
167
175
|
} catch (err) {
|
|
168
176
|
console.error(`Error re-indexing ${filename}:`, err.message);
|
|
169
177
|
}
|
|
@@ -252,6 +260,66 @@ const server = http.createServer((req, res) => {
|
|
|
252
260
|
const total = agent ? db.prepare(countSql).get(agent).c : db.prepare(countSql).get().c;
|
|
253
261
|
json(res, { sessions: rows, total, limit, offset });
|
|
254
262
|
}
|
|
263
|
+
|
|
264
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+\/events$/)) {
|
|
265
|
+
const id = pathname.split('/')[3];
|
|
266
|
+
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id);
|
|
267
|
+
if (!session) return json(res, { error: 'Not found' }, 404);
|
|
268
|
+
|
|
269
|
+
const after = query.after || '1970-01-01T00:00:00.000Z';
|
|
270
|
+
const afterId = query.afterId || '';
|
|
271
|
+
const limit = Math.min(parseInt(query.limit || '50', 10) || 50, 200);
|
|
272
|
+
const rows = db.prepare(
|
|
273
|
+
`SELECT * FROM events
|
|
274
|
+
WHERE session_id = ?
|
|
275
|
+
AND (timestamp > ? OR (timestamp = ? AND id > ?))
|
|
276
|
+
ORDER BY timestamp ASC, id ASC
|
|
277
|
+
LIMIT ?`
|
|
278
|
+
).all(id, after, after, afterId, limit);
|
|
279
|
+
json(res, { events: rows, after, afterId, count: rows.length });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
else if (pathname.match(/^\/api\/sessions\/[^/]+\/stream$/)) {
|
|
283
|
+
const id = pathname.split('/')[3];
|
|
284
|
+
const session = db.prepare('SELECT id FROM sessions WHERE id = ?').get(id);
|
|
285
|
+
if (!session) return json(res, { error: 'Not found' }, 404);
|
|
286
|
+
|
|
287
|
+
res.writeHead(200, {
|
|
288
|
+
'Content-Type': 'text/event-stream',
|
|
289
|
+
'Cache-Control': 'no-cache',
|
|
290
|
+
'Connection': 'keep-alive',
|
|
291
|
+
'X-Accel-Buffering': 'no'
|
|
292
|
+
});
|
|
293
|
+
res.write(': connected\n\n');
|
|
294
|
+
|
|
295
|
+
let lastTs = req.headers['last-event-id'] || query.after || new Date().toISOString();
|
|
296
|
+
|
|
297
|
+
const onUpdate = (sessionId) => {
|
|
298
|
+
if (sessionId !== id) return;
|
|
299
|
+
try {
|
|
300
|
+
const rows = db.prepare(
|
|
301
|
+
'SELECT * FROM events WHERE session_id = ? AND timestamp > ? ORDER BY timestamp ASC'
|
|
302
|
+
).all(id, lastTs);
|
|
303
|
+
if (rows.length) {
|
|
304
|
+
lastTs = rows[rows.length - 1].timestamp;
|
|
305
|
+
res.write(`id: ${lastTs}\ndata: ${JSON.stringify(rows)}\n\n`);
|
|
306
|
+
}
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.error('SSE query error:', err.message);
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
sseEmitter.on('session-update', onUpdate);
|
|
313
|
+
|
|
314
|
+
const ping = setInterval(() => {
|
|
315
|
+
try { res.write(': ping\n\n'); } catch {}
|
|
316
|
+
}, 30000);
|
|
317
|
+
|
|
318
|
+
req.on('close', () => {
|
|
319
|
+
sseEmitter.off('session-update', onUpdate);
|
|
320
|
+
clearInterval(ping);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
255
323
|
else if (pathname.match(/^\/api\/sessions\/[^/]+$/) && !pathname.includes('export')) {
|
|
256
324
|
const id = pathname.split('/')[3];
|
|
257
325
|
const session = db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -383,6 +383,7 @@ async function viewSessions() {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
async function viewSession(id) {
|
|
386
|
+
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
386
387
|
window._currentSessionId = id;
|
|
387
388
|
window.scrollTo(0, 0);
|
|
388
389
|
const data = await api(`/sessions/${id}`);
|
|
@@ -469,6 +470,7 @@ async function viewSession(id) {
|
|
|
469
470
|
|
|
470
471
|
$('#backBtn').addEventListener('click', () => {
|
|
471
472
|
if (onScroll) { window.removeEventListener('scroll', onScroll); onScroll = null; }
|
|
473
|
+
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
472
474
|
if (window._lastView === 'timeline') viewTimeline();
|
|
473
475
|
else if (window._lastView === 'files') viewFiles();
|
|
474
476
|
else if (window._lastView === 'search') viewSearch(window._lastSearchQuery || '');
|
|
@@ -531,6 +533,81 @@ async function viewSession(id) {
|
|
|
531
533
|
}
|
|
532
534
|
});
|
|
533
535
|
}
|
|
536
|
+
|
|
537
|
+
// --- Lightweight realtime updates (polling fallback first) ---
|
|
538
|
+
const knownIds = new Set(allEvents.map(e => e.id));
|
|
539
|
+
let pendingNewCount = 0;
|
|
540
|
+
|
|
541
|
+
const applyIncomingEvents = (incoming) => {
|
|
542
|
+
const container = document.getElementById('eventsContainer');
|
|
543
|
+
if (!container || !incoming?.length) return;
|
|
544
|
+
|
|
545
|
+
const fresh = incoming.filter(e => !knownIds.has(e.id));
|
|
546
|
+
if (!fresh.length) return;
|
|
547
|
+
fresh.forEach(e => knownIds.add(e.id));
|
|
548
|
+
|
|
549
|
+
const isAtTop = window.scrollY < 100;
|
|
550
|
+
for (const ev of fresh) {
|
|
551
|
+
const div = document.createElement('div');
|
|
552
|
+
div.innerHTML = renderEvent(ev);
|
|
553
|
+
const el = div.firstElementChild;
|
|
554
|
+
el.classList.add('event-highlight');
|
|
555
|
+
container.insertBefore(el, container.firstChild);
|
|
556
|
+
setTimeout(() => el.classList.remove('event-highlight'), 2000);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (!isAtTop) {
|
|
560
|
+
pendingNewCount += fresh.length;
|
|
561
|
+
let indicator = document.getElementById('newEventsIndicator');
|
|
562
|
+
if (!indicator) {
|
|
563
|
+
indicator = document.createElement('div');
|
|
564
|
+
indicator.id = 'newEventsIndicator';
|
|
565
|
+
indicator.className = 'new-events-indicator';
|
|
566
|
+
document.body.appendChild(indicator);
|
|
567
|
+
indicator.addEventListener('click', () => {
|
|
568
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
569
|
+
indicator.remove();
|
|
570
|
+
pendingNewCount = 0;
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
indicator.textContent = `${pendingNewCount} new event${pendingNewCount !== 1 ? 's' : ''} ↑`;
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
// Poll every 3s for new events using delta endpoint
|
|
578
|
+
let lastSeenTs = allEvents.length ? allEvents[0].timestamp : new Date(0).toISOString();
|
|
579
|
+
let lastSeenId = allEvents.length ? allEvents[0].id : '';
|
|
580
|
+
const pollNewEvents = async () => {
|
|
581
|
+
try {
|
|
582
|
+
const latest = await api(`/sessions/${id}/events?after=${encodeURIComponent(lastSeenTs)}&afterId=${encodeURIComponent(lastSeenId)}&limit=50`);
|
|
583
|
+
const incoming = latest.events || [];
|
|
584
|
+
if (incoming.length) {
|
|
585
|
+
const tail = incoming[incoming.length - 1];
|
|
586
|
+
lastSeenTs = tail.timestamp || lastSeenTs;
|
|
587
|
+
lastSeenId = tail.id || lastSeenId;
|
|
588
|
+
applyIncomingEvents(incoming);
|
|
589
|
+
}
|
|
590
|
+
} catch (err) {
|
|
591
|
+
// silent; next tick will retry
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
const pollInterval = setInterval(pollNewEvents, 3000);
|
|
596
|
+
|
|
597
|
+
const sseScrollHandler = () => {
|
|
598
|
+
if (window.scrollY < 100) {
|
|
599
|
+
const ind = document.getElementById('newEventsIndicator');
|
|
600
|
+
if (ind) { ind.remove(); pendingNewCount = 0; }
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
window.addEventListener('scroll', sseScrollHandler, { passive: true });
|
|
604
|
+
|
|
605
|
+
window._sseCleanup = () => {
|
|
606
|
+
clearInterval(pollInterval);
|
|
607
|
+
window.removeEventListener('scroll', sseScrollHandler);
|
|
608
|
+
const ind = document.getElementById('newEventsIndicator');
|
|
609
|
+
if (ind) ind.remove();
|
|
610
|
+
};
|
|
534
611
|
}
|
|
535
612
|
|
|
536
613
|
async function viewTimeline(date) {
|
|
@@ -828,6 +905,7 @@ window._lastView = 'sessions';
|
|
|
828
905
|
|
|
829
906
|
$$('.nav-item').forEach(item => {
|
|
830
907
|
item.addEventListener('click', () => {
|
|
908
|
+
if (window._sseCleanup) { window._sseCleanup(); window._sseCleanup = null; }
|
|
831
909
|
$$('.nav-item').forEach(i => i.classList.remove('active'));
|
|
832
910
|
item.classList.add('active');
|
|
833
911
|
const view = item.dataset.view;
|
package/public/style.css
CHANGED
|
@@ -1077,6 +1077,34 @@ mark {
|
|
|
1077
1077
|
animation: highlightFade 2s var(--ease-out);
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
+
/* ---- New Events SSE Indicator ---- */
|
|
1081
|
+
.new-events-indicator {
|
|
1082
|
+
position: fixed;
|
|
1083
|
+
top: 20px;
|
|
1084
|
+
left: 50%;
|
|
1085
|
+
transform: translateX(-50%);
|
|
1086
|
+
background: var(--accent);
|
|
1087
|
+
color: #fff;
|
|
1088
|
+
padding: 8px 20px;
|
|
1089
|
+
border-radius: 20px;
|
|
1090
|
+
font-size: 12px;
|
|
1091
|
+
font-weight: 600;
|
|
1092
|
+
cursor: pointer;
|
|
1093
|
+
z-index: 90;
|
|
1094
|
+
box-shadow: 0 4px 16px rgba(99, 144, 240, 0.4);
|
|
1095
|
+
animation: slideDown 0.3s var(--ease-out);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
.new-events-indicator:hover {
|
|
1099
|
+
background: #4a7ae8;
|
|
1100
|
+
transform: translateX(-50%) scale(1.05);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
@keyframes slideDown {
|
|
1104
|
+
from { opacity: 0; transform: translateX(-50%) translateY(-10px); }
|
|
1105
|
+
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1080
1108
|
/* ---- Search Home Stats ---- */
|
|
1081
1109
|
.search-stats {
|
|
1082
1110
|
display: grid;
|