ai-agent-session-center 1.0.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.
Files changed (41) hide show
  1. package/README.md +618 -0
  2. package/bin/cli.js +20 -0
  3. package/hooks/dashboard-hook-codex.sh +67 -0
  4. package/hooks/dashboard-hook-gemini.sh +102 -0
  5. package/hooks/dashboard-hook.ps1 +147 -0
  6. package/hooks/dashboard-hook.sh +142 -0
  7. package/hooks/dashboard-hooks-backup.json +103 -0
  8. package/hooks/install-hooks.js +543 -0
  9. package/hooks/reset.js +357 -0
  10. package/hooks/setup-wizard.js +156 -0
  11. package/package.json +52 -0
  12. package/public/css/dashboard.css +10200 -0
  13. package/public/index.html +915 -0
  14. package/public/js/analyticsPanel.js +467 -0
  15. package/public/js/app.js +1148 -0
  16. package/public/js/browserDb.js +806 -0
  17. package/public/js/chartUtils.js +383 -0
  18. package/public/js/historyPanel.js +298 -0
  19. package/public/js/movementManager.js +155 -0
  20. package/public/js/navController.js +32 -0
  21. package/public/js/robotManager.js +526 -0
  22. package/public/js/sceneManager.js +7 -0
  23. package/public/js/sessionPanel.js +2477 -0
  24. package/public/js/settingsManager.js +924 -0
  25. package/public/js/soundManager.js +249 -0
  26. package/public/js/statsPanel.js +118 -0
  27. package/public/js/terminalManager.js +391 -0
  28. package/public/js/timelinePanel.js +278 -0
  29. package/public/js/wsClient.js +88 -0
  30. package/server/apiRouter.js +321 -0
  31. package/server/config.js +120 -0
  32. package/server/hookProcessor.js +55 -0
  33. package/server/hookRouter.js +18 -0
  34. package/server/hookStats.js +107 -0
  35. package/server/index.js +314 -0
  36. package/server/logger.js +67 -0
  37. package/server/mqReader.js +218 -0
  38. package/server/serverConfig.js +27 -0
  39. package/server/sessionStore.js +1049 -0
  40. package/server/sshManager.js +339 -0
  41. package/server/wsManager.js +83 -0
@@ -0,0 +1,467 @@
1
+ import { drawBarChart, drawLineChart, drawHeatmapGrid, formatNumber, showTooltip, hideTooltip } from './chartUtils.js';
2
+ import * as db from './browserDb.js';
3
+
4
+ const SVG_NS = 'http://www.w3.org/2000/svg';
5
+
6
+ let initialized = false;
7
+
8
+ export async function init() {
9
+ if (initialized) return;
10
+ initialized = true;
11
+ await loadAll();
12
+ }
13
+
14
+ export async function refresh() {
15
+ await init();
16
+ await loadAll();
17
+ }
18
+
19
+ async function loadAll() {
20
+ const [summary, tools, trends, projects, heatmap] = await Promise.all([
21
+ db.getSummaryStats(),
22
+ db.getToolBreakdown(),
23
+ db.getDurationTrends({ period: 'day' }),
24
+ db.getActiveProjects(),
25
+ db.getHeatmap(),
26
+ ]);
27
+
28
+ renderSummary(summary);
29
+ renderToolUsage(tools);
30
+ renderDurationTrends(trends);
31
+ renderActiveProjects(projects);
32
+ renderHeatmap(heatmap);
33
+ }
34
+
35
+ // -- 1. Summary Stats --
36
+
37
+ function renderSummary(data) {
38
+ const container = document.getElementById('analytics-summary');
39
+ container.innerHTML = '';
40
+
41
+ const mostTool = data.most_used_tool;
42
+ const busiestProj = data.busiest_project;
43
+
44
+ const stats = [
45
+ { label: 'Total Sessions', value: formatNumber(data.total_sessions || 0), detail: 'all time' },
46
+ { label: 'Total Prompts', value: formatNumber(data.total_prompts || 0), detail: 'all time' },
47
+ { label: 'Total Tool Calls', value: formatNumber(data.total_tool_calls || 0), detail: 'all time' },
48
+ { label: 'Avg Duration', value: formatDuration(data.avg_duration || data.avg_session_duration_ms || 0), detail: 'per session' },
49
+ {
50
+ label: 'Most Used Tool',
51
+ value: mostTool ? (mostTool.tool_name || mostTool.name) : 'N/A',
52
+ detail: mostTool ? formatNumber(mostTool.count) + ' calls' : '',
53
+ },
54
+ {
55
+ label: 'Busiest Project',
56
+ value: busiestProj ? (busiestProj.name || busiestProj.project_path) : 'N/A',
57
+ detail: busiestProj ? formatNumber(busiestProj.count || busiestProj.sessions) + ' sessions' : '',
58
+ },
59
+ ];
60
+
61
+ stats.forEach(s => {
62
+ const card = document.createElement('div');
63
+ card.className = 'summary-stat';
64
+ card.innerHTML =
65
+ '<div class="stat-label">' + escapeHtml(s.label) + '</div>' +
66
+ '<div class="stat-value">' + escapeHtml(s.value) + '</div>' +
67
+ '<div class="stat-detail">' + escapeHtml(s.detail) + '</div>';
68
+ container.appendChild(card);
69
+ });
70
+ }
71
+
72
+ // -- 2. Tool Usage Breakdown --
73
+
74
+ function renderToolUsage(data) {
75
+ const container = document.getElementById('tool-usage-chart');
76
+ container.innerHTML = '';
77
+
78
+ const tools = (Array.isArray(data) ? data : data.tools || []).slice(0, 15);
79
+ if (tools.length === 0) {
80
+ container.insertAdjacentHTML('beforeend', '<div class="tab-empty">No tool data</div>');
81
+ return;
82
+ }
83
+
84
+ const totalCalls = tools.reduce((sum, t) => sum + (t.count || 0), 0) || 1;
85
+
86
+ const chartDiv = document.createElement('div');
87
+ container.appendChild(chartDiv);
88
+
89
+ const barHeight = 20;
90
+ const gap = 4;
91
+ const labelWidth = 120;
92
+ const valueWidth = 90;
93
+ const totalHeight = tools.length * (barHeight + gap) - gap;
94
+ const svgWidth = chartDiv.clientWidth || container.clientWidth || 500;
95
+ const barAreaWidth = Math.max(50, svgWidth - labelWidth - valueWidth - 10);
96
+
97
+ const svg = document.createElementNS(SVG_NS, 'svg');
98
+ svg.setAttribute('width', '100%');
99
+ svg.setAttribute('height', totalHeight);
100
+ svg.setAttribute('viewBox', '0 0 ' + svgWidth + ' ' + totalHeight);
101
+
102
+ const maxVal = Math.max(...tools.map(t => t.count || 0), 1);
103
+
104
+ tools.forEach((t, i) => {
105
+ const y = i * (barHeight + gap);
106
+ const count = t.count || 0;
107
+ const barW = Math.max(1, (count / maxVal) * barAreaWidth);
108
+ const pct = ((count / totalCalls) * 100).toFixed(1);
109
+ const name = t.tool_name || t.name || '';
110
+
111
+ // Label
112
+ const text = createSvgText(labelWidth - 6, y + barHeight / 2 + 4, name, {
113
+ fill: '#8892b0', 'font-size': '11', 'text-anchor': 'end',
114
+ });
115
+ svg.appendChild(text);
116
+
117
+ // Bar
118
+ const rect = document.createElementNS(SVG_NS, 'rect');
119
+ setAttrs(rect, {
120
+ x: labelWidth, y: y,
121
+ width: barW, height: barHeight,
122
+ rx: 3, fill: '#00e5ff', opacity: 0.85,
123
+ });
124
+ rect.addEventListener('mouseenter', (e) => {
125
+ rect.setAttribute('opacity', '1');
126
+ showTooltip(name + ': ' + formatNumber(count) + ' (' + pct + '%)', e.pageX, e.pageY);
127
+ });
128
+ rect.addEventListener('mouseleave', () => {
129
+ rect.setAttribute('opacity', '0.85');
130
+ hideTooltip();
131
+ });
132
+ svg.appendChild(rect);
133
+
134
+ // Value + percentage
135
+ const valText = createSvgText(labelWidth + barW + 6, y + barHeight / 2 + 4,
136
+ formatNumber(count) + ' (' + pct + '%)', {
137
+ fill: '#ccd6f6', 'font-size': '11',
138
+ });
139
+ svg.appendChild(valText);
140
+ });
141
+
142
+ chartDiv.appendChild(svg);
143
+ }
144
+
145
+ // -- 3. Duration Trends --
146
+
147
+ function renderDurationTrends(data) {
148
+ const container = document.getElementById('duration-trends-chart');
149
+ container.innerHTML = '';
150
+
151
+ const points = Array.isArray(data) ? data : (data.buckets || data.trends || []);
152
+ if (points.length === 0) {
153
+ container.insertAdjacentHTML('beforeend', '<div class="tab-empty">No duration data</div>');
154
+ return;
155
+ }
156
+
157
+ const chartDiv = document.createElement('div');
158
+ container.appendChild(chartDiv);
159
+
160
+ const svgWidth = chartDiv.clientWidth || container.clientWidth || 500;
161
+ const height = 250;
162
+ const paddingLeft = 55;
163
+ const paddingRight = 15;
164
+ const paddingTop = 15;
165
+ const paddingBottom = 30;
166
+ const chartW = svgWidth - paddingLeft - paddingRight;
167
+ const chartH = height - paddingTop - paddingBottom;
168
+
169
+ const lineData = points.map(p => {
170
+ const raw = p.period || p.timestamp || p.date || p.label;
171
+ let label = String(raw);
172
+ // Try to parse date strings like "2024-01-15" into "Jan 15"
173
+ if (typeof raw === 'string' && /^\d{4}-\d{2}-\d{2}/.test(raw)) {
174
+ const date = new Date(raw + 'T00:00:00');
175
+ if (!isNaN(date.getTime())) {
176
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
177
+ label = months[date.getMonth()] + ' ' + date.getDate();
178
+ }
179
+ }
180
+ return { label, value: p.avg_duration || p.avg_duration_ms || 0 };
181
+ });
182
+
183
+ const maxVal = Math.max(...lineData.map(d => d.value), 1);
184
+ const color = '#00e5ff';
185
+
186
+ const svg = document.createElementNS(SVG_NS, 'svg');
187
+ svg.setAttribute('width', '100%');
188
+ svg.setAttribute('height', height);
189
+ svg.setAttribute('viewBox', '0 0 ' + svgWidth + ' ' + height);
190
+
191
+ // Y-axis with duration formatting
192
+ for (let i = 0; i <= 4; i++) {
193
+ const val = (maxVal / 4) * i;
194
+ const y = paddingTop + chartH - (i / 4) * chartH;
195
+
196
+ const text = createSvgText(paddingLeft - 6, y + 4, formatDuration(val), {
197
+ fill: '#8892b0', 'font-size': '10', 'text-anchor': 'end',
198
+ });
199
+ svg.appendChild(text);
200
+
201
+ const line = document.createElementNS(SVG_NS, 'line');
202
+ setAttrs(line, {
203
+ x1: paddingLeft, y1: y,
204
+ x2: svgWidth - paddingRight, y2: y,
205
+ stroke: '#1e2a4a', 'stroke-width': 1,
206
+ });
207
+ svg.appendChild(line);
208
+ }
209
+
210
+ // Build coordinate points
211
+ const pts = lineData.map((d, i) => {
212
+ const x = paddingLeft + (i / Math.max(lineData.length - 1, 1)) * chartW;
213
+ const y = paddingTop + chartH - (d.value / maxVal) * chartH;
214
+ return { x: x, y: y, label: d.label, value: d.value };
215
+ });
216
+
217
+ // Area fill
218
+ if (pts.length > 1) {
219
+ const areaPoints = [
220
+ pts[0].x + ',' + (paddingTop + chartH),
221
+ ...pts.map(p => p.x + ',' + p.y),
222
+ pts[pts.length - 1].x + ',' + (paddingTop + chartH),
223
+ ].join(' ');
224
+ const polygon = document.createElementNS(SVG_NS, 'polygon');
225
+ setAttrs(polygon, { points: areaPoints, fill: color, opacity: 0.1 });
226
+ svg.appendChild(polygon);
227
+ }
228
+
229
+ // Line
230
+ if (pts.length > 1) {
231
+ const polyline = document.createElementNS(SVG_NS, 'polyline');
232
+ setAttrs(polyline, {
233
+ points: pts.map(p => p.x + ',' + p.y).join(' '),
234
+ fill: 'none', stroke: color,
235
+ 'stroke-width': 2, 'stroke-linejoin': 'round',
236
+ });
237
+ svg.appendChild(polyline);
238
+ }
239
+
240
+ // Dots with duration tooltip
241
+ pts.forEach(p => {
242
+ const circle = document.createElementNS(SVG_NS, 'circle');
243
+ setAttrs(circle, { cx: p.x, cy: p.y, r: 3, fill: color });
244
+ circle.addEventListener('mouseenter', (e) => showTooltip(p.label + ': ' + formatDuration(p.value), e.pageX, e.pageY));
245
+ circle.addEventListener('mouseleave', hideTooltip);
246
+ svg.appendChild(circle);
247
+ });
248
+
249
+ // X-axis labels
250
+ const labelStep = Math.max(1, Math.floor(lineData.length / 10));
251
+ pts.forEach((p, i) => {
252
+ if (i % labelStep !== 0 && i !== pts.length - 1) return;
253
+ const text = createSvgText(p.x, height - 6, p.label, {
254
+ fill: '#8892b0', 'font-size': '9', 'text-anchor': 'middle',
255
+ });
256
+ svg.appendChild(text);
257
+ });
258
+
259
+ chartDiv.appendChild(svg);
260
+ }
261
+
262
+ // -- 4. Active Projects --
263
+
264
+ function renderActiveProjects(data) {
265
+ const container = document.getElementById('active-projects-chart');
266
+ container.innerHTML = '';
267
+
268
+ const projects = (Array.isArray(data) ? data : data.projects || [])
269
+ .sort((a, b) => (b.session_count || 0) - (a.session_count || 0));
270
+
271
+ if (projects.length === 0) {
272
+ container.insertAdjacentHTML('beforeend', '<div class="tab-empty">No project data</div>');
273
+ return;
274
+ }
275
+
276
+ const chartDiv = document.createElement('div');
277
+ container.appendChild(chartDiv);
278
+
279
+ const barHeight = 22;
280
+ const gap = 4;
281
+ const labelWidth = 130;
282
+ const valueWidth = 160;
283
+ const totalHeight = projects.length * (barHeight + gap) - gap;
284
+ const svgWidth = chartDiv.clientWidth || container.clientWidth || 500;
285
+ const barAreaWidth = Math.max(50, svgWidth - labelWidth - valueWidth - 10);
286
+ const maxVal = Math.max(...projects.map(p => p.session_count || 0), 1);
287
+
288
+ const svg = document.createElementNS(SVG_NS, 'svg');
289
+ svg.setAttribute('width', '100%');
290
+ svg.setAttribute('height', totalHeight);
291
+ svg.setAttribute('viewBox', '0 0 ' + svgWidth + ' ' + totalHeight);
292
+
293
+ projects.forEach((p, i) => {
294
+ const y = i * (barHeight + gap);
295
+ const count = p.session_count || 0;
296
+ const barW = Math.max(1, (count / maxVal) * barAreaWidth);
297
+ const name = p.project_name || p.name || p.project_path || '';
298
+ const lastActive = (p.last_activity || p.last_active_at) ? formatDate(p.last_activity || p.last_active_at) : '';
299
+
300
+ // Project name
301
+ const text = createSvgText(labelWidth - 6, y + barHeight / 2 + 4, name, {
302
+ fill: '#8892b0', 'font-size': '11', 'text-anchor': 'end',
303
+ });
304
+ svg.appendChild(text);
305
+
306
+ // Bar
307
+ const rect = document.createElementNS(SVG_NS, 'rect');
308
+ setAttrs(rect, {
309
+ x: labelWidth, y: y,
310
+ width: barW, height: barHeight,
311
+ rx: 3, fill: '#00e5ff', opacity: 0.85,
312
+ });
313
+ rect.addEventListener('mouseenter', (e) => {
314
+ rect.setAttribute('opacity', '1');
315
+ showTooltip(name + ': ' + formatNumber(count) + ' sessions, ' + formatNumber(p.total_prompts || 0) + ' prompts, ' + formatNumber(p.total_tools || 0) + ' tools', e.pageX, e.pageY);
316
+ });
317
+ rect.addEventListener('mouseleave', () => {
318
+ rect.setAttribute('opacity', '0.85');
319
+ hideTooltip();
320
+ });
321
+ svg.appendChild(rect);
322
+
323
+ // Session count and last active
324
+ const detail = formatNumber(count) + ' sessions' + (lastActive ? ' | ' + lastActive : '');
325
+ const valText = createSvgText(labelWidth + barW + 6, y + barHeight / 2 + 4, detail, {
326
+ fill: '#ccd6f6', 'font-size': '11',
327
+ });
328
+ svg.appendChild(valText);
329
+ });
330
+
331
+ chartDiv.appendChild(svg);
332
+ }
333
+
334
+ // -- 5. Daily Heatmap --
335
+
336
+ function renderHeatmap(data) {
337
+ const container = document.getElementById('daily-heatmap-chart');
338
+ container.innerHTML = '';
339
+
340
+ const rawData = Array.isArray(data) ? data : (data.cells || data.heatmap || []);
341
+ if (rawData.length === 0) {
342
+ container.insertAdjacentHTML('beforeend', '<div class="tab-empty">No heatmap data</div>');
343
+ return;
344
+ }
345
+
346
+ const dayLabelsFull = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
347
+ const dayLabelsShort = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
348
+
349
+ const gridData = rawData.map(d => ({
350
+ row: d.day_of_week != null ? d.day_of_week : (d.day != null ? d.day : d.row),
351
+ col: d.hour != null ? d.hour : d.col,
352
+ value: d.count || d.value || 0,
353
+ }));
354
+
355
+ const cellSize = 14;
356
+ const gapSize = 2;
357
+ const colorMin = '#12122a';
358
+ const colorMax = '#00ff88';
359
+
360
+ const maxVal = Math.max(...gridData.map(d => d.value), 1);
361
+ const valueMap = new Map();
362
+ gridData.forEach(d => valueMap.set(d.row + '-' + d.col, d.value));
363
+
364
+ const chartDiv = document.createElement('div');
365
+ container.appendChild(chartDiv);
366
+
367
+ const grid = document.createElement('div');
368
+ grid.style.display = 'grid';
369
+ grid.style.gridTemplateColumns = '40px repeat(24, ' + cellSize + 'px)';
370
+ grid.style.gridTemplateRows = cellSize + 'px repeat(7, ' + cellSize + 'px)';
371
+ grid.style.gap = gapSize + 'px';
372
+ grid.style.alignItems = 'center';
373
+
374
+ // Top-left empty corner
375
+ grid.appendChild(document.createElement('div'));
376
+
377
+ // Hour labels (top row)
378
+ for (let h = 0; h < 24; h++) {
379
+ const lbl = document.createElement('div');
380
+ lbl.textContent = h;
381
+ lbl.style.fontSize = '9px';
382
+ lbl.style.color = '#8892b0';
383
+ lbl.style.textAlign = 'center';
384
+ grid.appendChild(lbl);
385
+ }
386
+
387
+ // Rows
388
+ for (let r = 0; r < 7; r++) {
389
+ // Day label
390
+ const dayLbl = document.createElement('div');
391
+ dayLbl.textContent = dayLabelsShort[r];
392
+ dayLbl.style.fontSize = '10px';
393
+ dayLbl.style.color = '#8892b0';
394
+ dayLbl.style.textAlign = 'right';
395
+ dayLbl.style.paddingRight = '4px';
396
+ grid.appendChild(dayLbl);
397
+
398
+ for (let c = 0; c < 24; c++) {
399
+ const val = valueMap.get(r + '-' + c) || 0;
400
+ const cell = document.createElement('div');
401
+ cell.style.width = cellSize + 'px';
402
+ cell.style.height = cellSize + 'px';
403
+ cell.style.borderRadius = '2px';
404
+ cell.style.backgroundColor = interpolateColor(val, 0, maxVal, colorMin, colorMax);
405
+ cell.style.cursor = 'pointer';
406
+ const tipText = dayLabelsFull[r] + ' ' + c.toString().padStart(2, '0') + ':00 - ' + val + ' events';
407
+ cell.addEventListener('mouseenter', (e) => showTooltip(tipText, e.pageX, e.pageY));
408
+ cell.addEventListener('mouseleave', hideTooltip);
409
+ grid.appendChild(cell);
410
+ }
411
+ }
412
+
413
+ chartDiv.appendChild(grid);
414
+ }
415
+
416
+ // -- Helpers --
417
+
418
+ function formatDuration(ms) {
419
+ const s = Math.floor(ms / 1000);
420
+ const m = Math.floor(s / 60);
421
+ const h = Math.floor(m / 60);
422
+ if (h > 0) return h + 'h ' + (m % 60) + 'm';
423
+ if (m > 0) return m + 'm ' + (s % 60) + 's';
424
+ return s + 's';
425
+ }
426
+
427
+ function formatDate(ts) {
428
+ const d = new Date(ts);
429
+ const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
430
+ return months[d.getMonth()] + ' ' + d.getDate();
431
+ }
432
+
433
+ function interpolateColor(value, min, max, colorStart, colorEnd) {
434
+ const t = max === min ? 0 : Math.max(0, Math.min(1, (value - min) / (max - min)));
435
+ const r1 = parseInt(colorStart.slice(1, 3), 16);
436
+ const g1 = parseInt(colorStart.slice(3, 5), 16);
437
+ const b1 = parseInt(colorStart.slice(5, 7), 16);
438
+ const r2 = parseInt(colorEnd.slice(1, 3), 16);
439
+ const g2 = parseInt(colorEnd.slice(3, 5), 16);
440
+ const b2 = parseInt(colorEnd.slice(5, 7), 16);
441
+ const r = Math.round(r1 + (r2 - r1) * t);
442
+ const g = Math.round(g1 + (g2 - g1) * t);
443
+ const b = Math.round(b1 + (b2 - b1) * t);
444
+ return '#' + r.toString(16).padStart(2, '0') + g.toString(16).padStart(2, '0') + b.toString(16).padStart(2, '0');
445
+ }
446
+
447
+ function escapeHtml(str) {
448
+ if (!str) return '';
449
+ return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
450
+ }
451
+
452
+ function createSvgText(x, y, content, attrs) {
453
+ const text = document.createElementNS(SVG_NS, 'text');
454
+ text.setAttribute('x', x);
455
+ text.setAttribute('y', y);
456
+ for (const [k, v] of Object.entries(attrs)) {
457
+ text.setAttribute(k, String(v));
458
+ }
459
+ text.textContent = content;
460
+ return text;
461
+ }
462
+
463
+ function setAttrs(el, attrs) {
464
+ for (const [k, v] of Object.entries(attrs)) {
465
+ el.setAttribute(k, String(v));
466
+ }
467
+ }