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.
- package/README.md +618 -0
- package/bin/cli.js +20 -0
- package/hooks/dashboard-hook-codex.sh +67 -0
- package/hooks/dashboard-hook-gemini.sh +102 -0
- package/hooks/dashboard-hook.ps1 +147 -0
- package/hooks/dashboard-hook.sh +142 -0
- package/hooks/dashboard-hooks-backup.json +103 -0
- package/hooks/install-hooks.js +543 -0
- package/hooks/reset.js +357 -0
- package/hooks/setup-wizard.js +156 -0
- package/package.json +52 -0
- package/public/css/dashboard.css +10200 -0
- package/public/index.html +915 -0
- package/public/js/analyticsPanel.js +467 -0
- package/public/js/app.js +1148 -0
- package/public/js/browserDb.js +806 -0
- package/public/js/chartUtils.js +383 -0
- package/public/js/historyPanel.js +298 -0
- package/public/js/movementManager.js +155 -0
- package/public/js/navController.js +32 -0
- package/public/js/robotManager.js +526 -0
- package/public/js/sceneManager.js +7 -0
- package/public/js/sessionPanel.js +2477 -0
- package/public/js/settingsManager.js +924 -0
- package/public/js/soundManager.js +249 -0
- package/public/js/statsPanel.js +118 -0
- package/public/js/terminalManager.js +391 -0
- package/public/js/timelinePanel.js +278 -0
- package/public/js/wsClient.js +88 -0
- package/server/apiRouter.js +321 -0
- package/server/config.js +120 -0
- package/server/hookProcessor.js +55 -0
- package/server/hookRouter.js +18 -0
- package/server/hookStats.js +107 -0
- package/server/index.js +314 -0
- package/server/logger.js +67 -0
- package/server/mqReader.js +218 -0
- package/server/serverConfig.js +27 -0
- package/server/sessionStore.js +1049 -0
- package/server/sshManager.js +339 -0
- package/server/wsManager.js +83 -0
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Draw a bar chart as SVG inside the given container.
|
|
5
|
+
* data = [{label, value, color?}]
|
|
6
|
+
*/
|
|
7
|
+
export function drawBarChart(container, data, options = {}) {
|
|
8
|
+
const {
|
|
9
|
+
horizontal = true,
|
|
10
|
+
maxBars = 15,
|
|
11
|
+
barHeight = 20,
|
|
12
|
+
gap = 4,
|
|
13
|
+
showLabels = true,
|
|
14
|
+
showValues = true,
|
|
15
|
+
} = options;
|
|
16
|
+
|
|
17
|
+
container.innerHTML = '';
|
|
18
|
+
const items = data.slice(0, maxBars);
|
|
19
|
+
if (items.length === 0) return;
|
|
20
|
+
|
|
21
|
+
const maxVal = Math.max(...items.map(d => d.value), 1);
|
|
22
|
+
const labelWidth = showLabels ? 100 : 0;
|
|
23
|
+
const valueWidth = showValues ? 50 : 0;
|
|
24
|
+
|
|
25
|
+
if (horizontal) {
|
|
26
|
+
const totalHeight = items.length * (barHeight + gap) - gap;
|
|
27
|
+
const svgWidth = container.clientWidth || 400;
|
|
28
|
+
const barAreaWidth = svgWidth - labelWidth - valueWidth - 10;
|
|
29
|
+
|
|
30
|
+
const svg = createSvg(svgWidth, totalHeight);
|
|
31
|
+
|
|
32
|
+
items.forEach((d, i) => {
|
|
33
|
+
const y = i * (barHeight + gap);
|
|
34
|
+
const barW = Math.max(1, (d.value / maxVal) * barAreaWidth);
|
|
35
|
+
const color = d.color || 'var(--accent-cyan, #00e5ff)';
|
|
36
|
+
|
|
37
|
+
if (showLabels) {
|
|
38
|
+
const text = createSvgEl('text', {
|
|
39
|
+
x: labelWidth - 6,
|
|
40
|
+
y: y + barHeight / 2 + 4,
|
|
41
|
+
fill: '#8892b0',
|
|
42
|
+
'font-size': '11',
|
|
43
|
+
'text-anchor': 'end',
|
|
44
|
+
});
|
|
45
|
+
text.textContent = d.label;
|
|
46
|
+
svg.appendChild(text);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rect = createSvgEl('rect', {
|
|
50
|
+
x: labelWidth,
|
|
51
|
+
y,
|
|
52
|
+
width: barW,
|
|
53
|
+
height: barHeight,
|
|
54
|
+
rx: 3,
|
|
55
|
+
fill: color,
|
|
56
|
+
opacity: 0.85,
|
|
57
|
+
});
|
|
58
|
+
svg.appendChild(rect);
|
|
59
|
+
|
|
60
|
+
if (showValues) {
|
|
61
|
+
const valText = createSvgEl('text', {
|
|
62
|
+
x: labelWidth + barW + 6,
|
|
63
|
+
y: y + barHeight / 2 + 4,
|
|
64
|
+
fill: '#ccd6f6',
|
|
65
|
+
'font-size': '11',
|
|
66
|
+
});
|
|
67
|
+
valText.textContent = formatNumber(d.value);
|
|
68
|
+
svg.appendChild(valText);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
container.appendChild(svg);
|
|
73
|
+
} else {
|
|
74
|
+
// Vertical bars
|
|
75
|
+
const svgHeight = 200;
|
|
76
|
+
const svgWidth = container.clientWidth || 400;
|
|
77
|
+
const barWidth = Math.max(4, (svgWidth - 40) / items.length - gap);
|
|
78
|
+
const chartHeight = svgHeight - 30;
|
|
79
|
+
|
|
80
|
+
const svg = createSvg(svgWidth, svgHeight);
|
|
81
|
+
|
|
82
|
+
items.forEach((d, i) => {
|
|
83
|
+
const x = 20 + i * (barWidth + gap);
|
|
84
|
+
const barH = Math.max(1, (d.value / maxVal) * chartHeight);
|
|
85
|
+
const y = chartHeight - barH;
|
|
86
|
+
const color = d.color || 'var(--accent-cyan, #00e5ff)';
|
|
87
|
+
|
|
88
|
+
const rect = createSvgEl('rect', {
|
|
89
|
+
x,
|
|
90
|
+
y,
|
|
91
|
+
width: barWidth,
|
|
92
|
+
height: barH,
|
|
93
|
+
rx: 2,
|
|
94
|
+
fill: color,
|
|
95
|
+
opacity: 0.85,
|
|
96
|
+
});
|
|
97
|
+
svg.appendChild(rect);
|
|
98
|
+
|
|
99
|
+
if (showLabels) {
|
|
100
|
+
const text = createSvgEl('text', {
|
|
101
|
+
x: x + barWidth / 2,
|
|
102
|
+
y: svgHeight - 4,
|
|
103
|
+
fill: '#8892b0',
|
|
104
|
+
'font-size': '9',
|
|
105
|
+
'text-anchor': 'middle',
|
|
106
|
+
});
|
|
107
|
+
text.textContent = d.label;
|
|
108
|
+
svg.appendChild(text);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (showValues) {
|
|
112
|
+
const valText = createSvgEl('text', {
|
|
113
|
+
x: x + barWidth / 2,
|
|
114
|
+
y: y - 4,
|
|
115
|
+
fill: '#ccd6f6',
|
|
116
|
+
'font-size': '9',
|
|
117
|
+
'text-anchor': 'middle',
|
|
118
|
+
});
|
|
119
|
+
valText.textContent = formatNumber(d.value);
|
|
120
|
+
svg.appendChild(valText);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
container.appendChild(svg);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Draw a line chart as SVG inside the given container.
|
|
130
|
+
* data = [{label, value}]
|
|
131
|
+
*/
|
|
132
|
+
export function drawLineChart(container, data, options = {}) {
|
|
133
|
+
const {
|
|
134
|
+
color = '#00e5ff',
|
|
135
|
+
areaFill = false,
|
|
136
|
+
showDots = true,
|
|
137
|
+
height = 250,
|
|
138
|
+
} = options;
|
|
139
|
+
|
|
140
|
+
container.innerHTML = '';
|
|
141
|
+
if (data.length === 0) return;
|
|
142
|
+
|
|
143
|
+
const svgWidth = container.clientWidth || 500;
|
|
144
|
+
const paddingLeft = 45;
|
|
145
|
+
const paddingRight = 15;
|
|
146
|
+
const paddingTop = 15;
|
|
147
|
+
const paddingBottom = 30;
|
|
148
|
+
const chartW = svgWidth - paddingLeft - paddingRight;
|
|
149
|
+
const chartH = height - paddingTop - paddingBottom;
|
|
150
|
+
|
|
151
|
+
const maxVal = Math.max(...data.map(d => d.value), 1);
|
|
152
|
+
const svg = createSvg(svgWidth, height);
|
|
153
|
+
|
|
154
|
+
// Y-axis labels (5 ticks)
|
|
155
|
+
for (let i = 0; i <= 4; i++) {
|
|
156
|
+
const val = (maxVal / 4) * i;
|
|
157
|
+
const y = paddingTop + chartH - (i / 4) * chartH;
|
|
158
|
+
const text = createSvgEl('text', {
|
|
159
|
+
x: paddingLeft - 6,
|
|
160
|
+
y: y + 4,
|
|
161
|
+
fill: '#8892b0',
|
|
162
|
+
'font-size': '10',
|
|
163
|
+
'text-anchor': 'end',
|
|
164
|
+
});
|
|
165
|
+
text.textContent = formatNumber(val);
|
|
166
|
+
svg.appendChild(text);
|
|
167
|
+
|
|
168
|
+
// Grid line
|
|
169
|
+
const line = createSvgEl('line', {
|
|
170
|
+
x1: paddingLeft,
|
|
171
|
+
y1: y,
|
|
172
|
+
x2: svgWidth - paddingRight,
|
|
173
|
+
y2: y,
|
|
174
|
+
stroke: '#1e2a4a',
|
|
175
|
+
'stroke-width': 1,
|
|
176
|
+
});
|
|
177
|
+
svg.appendChild(line);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Build points
|
|
181
|
+
const points = data.map((d, i) => {
|
|
182
|
+
const x = paddingLeft + (i / Math.max(data.length - 1, 1)) * chartW;
|
|
183
|
+
const y = paddingTop + chartH - (d.value / maxVal) * chartH;
|
|
184
|
+
return { x, y, label: d.label, value: d.value };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Area fill
|
|
188
|
+
if (areaFill && points.length > 1) {
|
|
189
|
+
const areaPoints = [
|
|
190
|
+
`${points[0].x},${paddingTop + chartH}`,
|
|
191
|
+
...points.map(p => `${p.x},${p.y}`),
|
|
192
|
+
`${points[points.length - 1].x},${paddingTop + chartH}`,
|
|
193
|
+
].join(' ');
|
|
194
|
+
const polygon = createSvgEl('polygon', {
|
|
195
|
+
points: areaPoints,
|
|
196
|
+
fill: color,
|
|
197
|
+
opacity: 0.1,
|
|
198
|
+
});
|
|
199
|
+
svg.appendChild(polygon);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Line
|
|
203
|
+
if (points.length > 1) {
|
|
204
|
+
const polyline = createSvgEl('polyline', {
|
|
205
|
+
points: points.map(p => `${p.x},${p.y}`).join(' '),
|
|
206
|
+
fill: 'none',
|
|
207
|
+
stroke: color,
|
|
208
|
+
'stroke-width': 2,
|
|
209
|
+
'stroke-linejoin': 'round',
|
|
210
|
+
});
|
|
211
|
+
svg.appendChild(polyline);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Dots
|
|
215
|
+
if (showDots) {
|
|
216
|
+
points.forEach(p => {
|
|
217
|
+
const circle = createSvgEl('circle', {
|
|
218
|
+
cx: p.x,
|
|
219
|
+
cy: p.y,
|
|
220
|
+
r: 3,
|
|
221
|
+
fill: color,
|
|
222
|
+
});
|
|
223
|
+
circle.addEventListener('mouseenter', (e) => showTooltip(`${p.label}: ${formatNumber(p.value)}`, e.pageX, e.pageY));
|
|
224
|
+
circle.addEventListener('mouseleave', hideTooltip);
|
|
225
|
+
svg.appendChild(circle);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// X-axis labels (show up to ~10 evenly spaced)
|
|
230
|
+
const labelStep = Math.max(1, Math.floor(data.length / 10));
|
|
231
|
+
points.forEach((p, i) => {
|
|
232
|
+
if (i % labelStep !== 0 && i !== points.length - 1) return;
|
|
233
|
+
const text = createSvgEl('text', {
|
|
234
|
+
x: p.x,
|
|
235
|
+
y: height - 6,
|
|
236
|
+
fill: '#8892b0',
|
|
237
|
+
'font-size': '9',
|
|
238
|
+
'text-anchor': 'middle',
|
|
239
|
+
});
|
|
240
|
+
text.textContent = p.label;
|
|
241
|
+
svg.appendChild(text);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
container.appendChild(svg);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Draw a heatmap grid using CSS grid.
|
|
249
|
+
* data = [{row, col, value}] where row=0-6 (Mon-Sun), col=0-23 (hours)
|
|
250
|
+
*/
|
|
251
|
+
export function drawHeatmapGrid(container, data, options = {}) {
|
|
252
|
+
const {
|
|
253
|
+
cellSize = 14,
|
|
254
|
+
gap = 2,
|
|
255
|
+
colorMin = '#12122a',
|
|
256
|
+
colorMax = '#00ff88',
|
|
257
|
+
} = options;
|
|
258
|
+
|
|
259
|
+
container.innerHTML = '';
|
|
260
|
+
|
|
261
|
+
const dayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
|
262
|
+
const maxVal = Math.max(...data.map(d => d.value), 1);
|
|
263
|
+
|
|
264
|
+
// Build value lookup
|
|
265
|
+
const valueMap = new Map();
|
|
266
|
+
data.forEach(d => valueMap.set(`${d.row}-${d.col}`, d.value));
|
|
267
|
+
|
|
268
|
+
const grid = document.createElement('div');
|
|
269
|
+
grid.style.display = 'grid';
|
|
270
|
+
grid.style.gridTemplateColumns = `40px repeat(24, ${cellSize}px)`;
|
|
271
|
+
grid.style.gridTemplateRows = `${cellSize}px repeat(7, ${cellSize}px)`;
|
|
272
|
+
grid.style.gap = `${gap}px`;
|
|
273
|
+
grid.style.alignItems = 'center';
|
|
274
|
+
|
|
275
|
+
// Top-left empty corner
|
|
276
|
+
const corner = document.createElement('div');
|
|
277
|
+
grid.appendChild(corner);
|
|
278
|
+
|
|
279
|
+
// Hour labels (top row)
|
|
280
|
+
for (let h = 0; h < 24; h++) {
|
|
281
|
+
const lbl = document.createElement('div');
|
|
282
|
+
lbl.textContent = h;
|
|
283
|
+
lbl.style.fontSize = '9px';
|
|
284
|
+
lbl.style.color = '#8892b0';
|
|
285
|
+
lbl.style.textAlign = 'center';
|
|
286
|
+
grid.appendChild(lbl);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Rows
|
|
290
|
+
for (let r = 0; r < 7; r++) {
|
|
291
|
+
// Day label
|
|
292
|
+
const dayLbl = document.createElement('div');
|
|
293
|
+
dayLbl.textContent = dayLabels[r];
|
|
294
|
+
dayLbl.style.fontSize = '10px';
|
|
295
|
+
dayLbl.style.color = '#8892b0';
|
|
296
|
+
dayLbl.style.textAlign = 'right';
|
|
297
|
+
dayLbl.style.paddingRight = '4px';
|
|
298
|
+
grid.appendChild(dayLbl);
|
|
299
|
+
|
|
300
|
+
for (let c = 0; c < 24; c++) {
|
|
301
|
+
const val = valueMap.get(`${r}-${c}`) || 0;
|
|
302
|
+
const cell = document.createElement('div');
|
|
303
|
+
cell.style.width = `${cellSize}px`;
|
|
304
|
+
cell.style.height = `${cellSize}px`;
|
|
305
|
+
cell.style.borderRadius = '2px';
|
|
306
|
+
cell.style.backgroundColor = interpolateColor(val, 0, maxVal, colorMin, colorMax);
|
|
307
|
+
cell.style.cursor = 'pointer';
|
|
308
|
+
cell.addEventListener('mouseenter', (e) => showTooltip(`${dayLabels[r]} ${c}:00 - ${val}`, e.pageX, e.pageY));
|
|
309
|
+
cell.addEventListener('mouseleave', hideTooltip);
|
|
310
|
+
grid.appendChild(cell);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
container.appendChild(grid);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Format a number for display: 0->"0", 999->"999", 1000->"1.0k", 1500->"1.5k", 1000000->"1.0M"
|
|
319
|
+
*/
|
|
320
|
+
export function formatNumber(n) {
|
|
321
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
322
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
323
|
+
return String(Math.round(n));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Show a tooltip div at the given page coordinates.
|
|
328
|
+
*/
|
|
329
|
+
export function showTooltip(text, x, y) {
|
|
330
|
+
let tip = document.querySelector('.chart-tooltip');
|
|
331
|
+
if (!tip) {
|
|
332
|
+
tip = document.createElement('div');
|
|
333
|
+
tip.className = 'chart-tooltip';
|
|
334
|
+
document.body.appendChild(tip);
|
|
335
|
+
}
|
|
336
|
+
tip.textContent = text;
|
|
337
|
+
tip.style.left = `${x + 10}px`;
|
|
338
|
+
tip.style.top = `${y - 28}px`;
|
|
339
|
+
tip.style.display = 'block';
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Hide the tooltip.
|
|
344
|
+
*/
|
|
345
|
+
export function hideTooltip() {
|
|
346
|
+
const tip = document.querySelector('.chart-tooltip');
|
|
347
|
+
if (tip) tip.style.display = 'none';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Interpolate a hex color between colorStart and colorEnd based on value's position in [min, max].
|
|
352
|
+
*/
|
|
353
|
+
export function interpolateColor(value, min, max, colorStart = '#12122a', colorEnd = '#00ff88') {
|
|
354
|
+
const t = max === min ? 0 : Math.max(0, Math.min(1, (value - min) / (max - min)));
|
|
355
|
+
const r1 = parseInt(colorStart.slice(1, 3), 16);
|
|
356
|
+
const g1 = parseInt(colorStart.slice(3, 5), 16);
|
|
357
|
+
const b1 = parseInt(colorStart.slice(5, 7), 16);
|
|
358
|
+
const r2 = parseInt(colorEnd.slice(1, 3), 16);
|
|
359
|
+
const g2 = parseInt(colorEnd.slice(3, 5), 16);
|
|
360
|
+
const b2 = parseInt(colorEnd.slice(5, 7), 16);
|
|
361
|
+
const r = Math.round(r1 + (r2 - r1) * t);
|
|
362
|
+
const g = Math.round(g1 + (g2 - g1) * t);
|
|
363
|
+
const b = Math.round(b1 + (b2 - b1) * t);
|
|
364
|
+
return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// -- Internal helpers --
|
|
368
|
+
|
|
369
|
+
function createSvg(width, height) {
|
|
370
|
+
const svg = document.createElementNS(SVG_NS, 'svg');
|
|
371
|
+
svg.setAttribute('width', width);
|
|
372
|
+
svg.setAttribute('height', height);
|
|
373
|
+
svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
|
|
374
|
+
return svg;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function createSvgEl(tag, attrs) {
|
|
378
|
+
const el = document.createElementNS(SVG_NS, tag);
|
|
379
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
380
|
+
el.setAttribute(k, v);
|
|
381
|
+
}
|
|
382
|
+
return el;
|
|
383
|
+
}
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import * as db from './browserDb.js';
|
|
2
|
+
|
|
3
|
+
let currentPage = 1;
|
|
4
|
+
let debounceTimer = null;
|
|
5
|
+
|
|
6
|
+
export async function init() {
|
|
7
|
+
// Fetch projects for dropdown
|
|
8
|
+
const projects = await db.getDistinctProjects();
|
|
9
|
+
const select = document.getElementById('history-project-filter');
|
|
10
|
+
projects.forEach(p => {
|
|
11
|
+
const opt = document.createElement('option');
|
|
12
|
+
opt.value = p.project_path;
|
|
13
|
+
opt.textContent = p.project_name;
|
|
14
|
+
select.appendChild(opt);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Wire up filter change events
|
|
18
|
+
document.getElementById('search-input').addEventListener('input', () => {
|
|
19
|
+
clearTimeout(debounceTimer);
|
|
20
|
+
debounceTimer = setTimeout(() => { currentPage = 1; loadSessions(); }, 300);
|
|
21
|
+
});
|
|
22
|
+
['history-project-filter', 'history-status-filter', 'history-date-from', 'history-date-to', 'history-sort-by'].forEach(id => {
|
|
23
|
+
document.getElementById(id).addEventListener('change', () => { currentPage = 1; loadSessions(); });
|
|
24
|
+
});
|
|
25
|
+
document.getElementById('history-sort-dir').addEventListener('click', (e) => {
|
|
26
|
+
e.target.textContent = e.target.textContent === 'DESC' ? 'ASC' : 'DESC';
|
|
27
|
+
currentPage = 1;
|
|
28
|
+
loadSessions();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function refresh() {
|
|
33
|
+
await loadSessions();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function loadSessions() {
|
|
37
|
+
const query = document.getElementById('search-input').value || undefined;
|
|
38
|
+
const project = document.getElementById('history-project-filter').value || undefined;
|
|
39
|
+
const statusVal = document.getElementById('history-status-filter').value;
|
|
40
|
+
let status, archived;
|
|
41
|
+
if (statusVal === 'archived') {
|
|
42
|
+
archived = 'true';
|
|
43
|
+
} else if (statusVal) {
|
|
44
|
+
status = statusVal;
|
|
45
|
+
}
|
|
46
|
+
const dateFromRaw = document.getElementById('history-date-from').value;
|
|
47
|
+
const dateFrom = dateFromRaw ? new Date(dateFromRaw).getTime() : undefined;
|
|
48
|
+
const dateToRaw = document.getElementById('history-date-to').value;
|
|
49
|
+
const dateTo = dateToRaw ? new Date(dateToRaw + 'T23:59:59').getTime() : undefined;
|
|
50
|
+
const sortByMap = { date: 'startedAt', duration: 'endedAt', prompts: 'totalPrompts', tools: 'totalToolCalls' };
|
|
51
|
+
const rawSort = document.getElementById('history-sort-by').value;
|
|
52
|
+
const sortBy = sortByMap[rawSort] || 'startedAt';
|
|
53
|
+
const sortDir = document.getElementById('history-sort-dir').textContent.toLowerCase();
|
|
54
|
+
const page = currentPage;
|
|
55
|
+
const pageSize = 50;
|
|
56
|
+
|
|
57
|
+
const result = await db.searchSessions({ query, project, status, dateFrom, dateTo, archived, sortBy, sortDir, page, pageSize });
|
|
58
|
+
|
|
59
|
+
// Map camelCase IndexedDB fields to snake_case expected by renderResults
|
|
60
|
+
const mapped = result.sessions.map(s => ({
|
|
61
|
+
id: s.id,
|
|
62
|
+
title: s.title || '',
|
|
63
|
+
project_name: s.projectName || '',
|
|
64
|
+
started_at: s.startedAt,
|
|
65
|
+
ended_at: s.endedAt,
|
|
66
|
+
status: s.status,
|
|
67
|
+
total_prompts: s.totalPrompts || 0,
|
|
68
|
+
total_tool_calls: s.totalToolCalls || 0,
|
|
69
|
+
git_branch: s.gitBranch || '',
|
|
70
|
+
}));
|
|
71
|
+
renderResults(mapped, result.total, result.page, result.pageSize);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function renderResults(sessions, total, page, pageSize) {
|
|
75
|
+
const container = document.getElementById('history-results');
|
|
76
|
+
if (sessions.length === 0) {
|
|
77
|
+
container.innerHTML = '<div class="tab-empty">No sessions found</div>';
|
|
78
|
+
document.getElementById('history-pagination').innerHTML = '';
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
container.innerHTML = sessions.map(s => {
|
|
83
|
+
const duration = s.ended_at
|
|
84
|
+
? formatDuration(s.ended_at - s.started_at)
|
|
85
|
+
: formatDuration(Date.now() - s.started_at);
|
|
86
|
+
const date = new Date(s.started_at).toLocaleString('en-US', {
|
|
87
|
+
year: 'numeric', month: 'short', day: 'numeric',
|
|
88
|
+
hour: '2-digit', minute: '2-digit', hour12: false,
|
|
89
|
+
});
|
|
90
|
+
return `<div class="history-row" data-session-id="${s.id}">
|
|
91
|
+
<span class="history-title">${escapeHtml(s.title)}</span>
|
|
92
|
+
<span class="history-project">${escapeHtml(s.project_name)}</span>
|
|
93
|
+
<span class="history-date">${date}</span>
|
|
94
|
+
<span class="history-duration">${duration}</span>
|
|
95
|
+
<span class="history-status ${s.status}">${s.status.toUpperCase()}</span>
|
|
96
|
+
<span class="history-prompts">${s.total_prompts} prompts</span>
|
|
97
|
+
<span class="history-tools">${s.total_tool_calls} tools</span>
|
|
98
|
+
<span class="history-branch">${escapeHtml(s.git_branch || '')}</span>
|
|
99
|
+
<button class="history-delete" title="Delete session">×</button>
|
|
100
|
+
</div>`;
|
|
101
|
+
}).join('');
|
|
102
|
+
|
|
103
|
+
// Click handler for rows
|
|
104
|
+
container.querySelectorAll('.history-row').forEach(row => {
|
|
105
|
+
row.addEventListener('click', (e) => {
|
|
106
|
+
if (e.target.closest('.history-delete')) return;
|
|
107
|
+
openHistoryDetail(row.dataset.sessionId);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Delete button handler
|
|
112
|
+
container.querySelectorAll('.history-delete').forEach(btn => {
|
|
113
|
+
btn.addEventListener('click', async (e) => {
|
|
114
|
+
e.stopPropagation();
|
|
115
|
+
const row = btn.closest('.history-row');
|
|
116
|
+
const sid = row.dataset.sessionId;
|
|
117
|
+
if (!confirm('Delete this session from history? This cannot be undone.')) return;
|
|
118
|
+
await db.deleteSession(sid);
|
|
119
|
+
row.style.transition = 'opacity 0.3s';
|
|
120
|
+
row.style.opacity = '0';
|
|
121
|
+
setTimeout(() => {
|
|
122
|
+
row.remove();
|
|
123
|
+
// Reload if the page is now empty
|
|
124
|
+
if (container.querySelectorAll('.history-row').length === 0) loadSessions();
|
|
125
|
+
}, 300);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Pagination
|
|
130
|
+
renderPagination(total, page, pageSize);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function renderPagination(total, page, pageSize) {
|
|
134
|
+
const container = document.getElementById('history-pagination');
|
|
135
|
+
const totalPages = Math.ceil(total / pageSize);
|
|
136
|
+
if (totalPages <= 1) {
|
|
137
|
+
container.innerHTML = '';
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const buttons = [];
|
|
142
|
+
|
|
143
|
+
// Previous button
|
|
144
|
+
buttons.push(
|
|
145
|
+
`<button class="page-btn${page <= 1 ? ' disabled' : ''}" data-page="${page - 1}"${page <= 1 ? ' disabled' : ''}>« Prev</button>`
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// Page number buttons: show first, last, current +/- 2, with ellipses
|
|
149
|
+
const range = [];
|
|
150
|
+
for (let i = 1; i <= totalPages; i++) {
|
|
151
|
+
if (i === 1 || i === totalPages || (i >= page - 2 && i <= page + 2)) {
|
|
152
|
+
range.push(i);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let lastShown = 0;
|
|
157
|
+
range.forEach(i => {
|
|
158
|
+
if (lastShown && i - lastShown > 1) {
|
|
159
|
+
buttons.push('<span class="page-ellipsis">...</span>');
|
|
160
|
+
}
|
|
161
|
+
buttons.push(
|
|
162
|
+
`<button class="page-btn${i === page ? ' active' : ''}" data-page="${i}">${i}</button>`
|
|
163
|
+
);
|
|
164
|
+
lastShown = i;
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// Next button
|
|
168
|
+
buttons.push(
|
|
169
|
+
`<button class="page-btn${page >= totalPages ? ' disabled' : ''}" data-page="${page + 1}"${page >= totalPages ? ' disabled' : ''}>Next »</button>`
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
container.innerHTML = buttons.join('');
|
|
173
|
+
|
|
174
|
+
// Wire click handlers
|
|
175
|
+
container.querySelectorAll('.page-btn:not(.disabled)').forEach(btn => {
|
|
176
|
+
btn.addEventListener('click', () => {
|
|
177
|
+
currentPage = parseInt(btn.dataset.page, 10);
|
|
178
|
+
loadSessions();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function openHistoryDetail(sessionId) {
|
|
184
|
+
const data = await db.getSessionDetail(sessionId);
|
|
185
|
+
if (!data) return;
|
|
186
|
+
const s = {
|
|
187
|
+
session: data.session,
|
|
188
|
+
prompts: data.prompts,
|
|
189
|
+
responses: (data.responses || []).map(r => ({ ...r, text: r.textExcerpt || r.text || '' })),
|
|
190
|
+
tools: (data.tool_calls || []).map(t => ({ tool: t.toolName, input: t.toolInputSummary || '', timestamp: t.timestamp })),
|
|
191
|
+
events: data.events,
|
|
192
|
+
notes: data.notes,
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
// Populate header
|
|
196
|
+
const sess = s.session || s;
|
|
197
|
+
document.getElementById('detail-project-name').textContent = sess.projectName || sess.project_name || '';
|
|
198
|
+
const badge = document.getElementById('detail-status-badge');
|
|
199
|
+
badge.textContent = (sess.status || '').toUpperCase();
|
|
200
|
+
badge.className = `status-badge ${sess.status}`;
|
|
201
|
+
document.getElementById('detail-model').textContent = sess.model || '';
|
|
202
|
+
const startedAt = sess.startedAt || sess.started_at;
|
|
203
|
+
const endedAt = sess.endedAt || sess.ended_at;
|
|
204
|
+
document.getElementById('detail-duration').textContent = endedAt
|
|
205
|
+
? formatDuration(endedAt - startedAt)
|
|
206
|
+
: formatDuration(Date.now() - startedAt);
|
|
207
|
+
|
|
208
|
+
// Character model selector + preview
|
|
209
|
+
const charSelect = document.getElementById('detail-char-model');
|
|
210
|
+
if (charSelect) {
|
|
211
|
+
charSelect.value = sess.characterModel || sess.character_model || '';
|
|
212
|
+
charSelect.dataset.sessionId = sessionId;
|
|
213
|
+
}
|
|
214
|
+
// Mini preview with session's accent color
|
|
215
|
+
const previewEl = document.getElementById('detail-char-preview');
|
|
216
|
+
if (previewEl) {
|
|
217
|
+
const model = sess.characterModel || sess.character_model || 'robot';
|
|
218
|
+
const accentColor = sess.accentColor || sess.accent_color || 'var(--accent-cyan)';
|
|
219
|
+
import('./robotManager.js').then(rm => {
|
|
220
|
+
previewEl.innerHTML = '';
|
|
221
|
+
const mini = document.createElement('div');
|
|
222
|
+
mini.className = `css-robot char-${model}`;
|
|
223
|
+
mini.dataset.status = sess.status || 'ended';
|
|
224
|
+
mini.style.setProperty('--robot-color', accentColor);
|
|
225
|
+
if (rm._getTemplates) {
|
|
226
|
+
const templates = rm._getTemplates();
|
|
227
|
+
if (templates[model]) mini.innerHTML = templates[model](accentColor);
|
|
228
|
+
}
|
|
229
|
+
previewEl.appendChild(mini);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Conversation tab (interleaved prompts + responses, newest first)
|
|
234
|
+
const convoEl = document.getElementById('detail-conversation');
|
|
235
|
+
const allEntries = [
|
|
236
|
+
...(s.prompts || []).map(p => ({ type: 'prompt', timestamp: p.timestamp, text: p.text })),
|
|
237
|
+
...(s.responses || []).map(r => ({ type: 'response', timestamp: r.timestamp, text: r.text })),
|
|
238
|
+
].sort((a, b) => a.timestamp - b.timestamp);
|
|
239
|
+
convoEl.innerHTML = allEntries.map(e => {
|
|
240
|
+
const cls = e.type === 'prompt' ? 'prompt-entry' : 'response-entry';
|
|
241
|
+
return `<div class="${cls}">
|
|
242
|
+
<span class="${e.type}-time">${formatTime(e.timestamp)}</span>
|
|
243
|
+
<div class="${e.type}-text">${escapeHtml(e.text)}</div>
|
|
244
|
+
</div>`;
|
|
245
|
+
}).join('');
|
|
246
|
+
|
|
247
|
+
// Activity tab (merged tool calls + events)
|
|
248
|
+
const histItems = [];
|
|
249
|
+
for (const t of (s.tools || [])) {
|
|
250
|
+
histItems.push({ kind: 'tool', tool: t.tool, input: t.input, timestamp: t.timestamp });
|
|
251
|
+
}
|
|
252
|
+
for (const e of (s.events || [])) {
|
|
253
|
+
histItems.push({ kind: 'event', type: e.type, detail: e.detail, timestamp: e.timestamp });
|
|
254
|
+
}
|
|
255
|
+
histItems.sort((a, b) => b.timestamp - a.timestamp);
|
|
256
|
+
const actEl = document.getElementById('detail-activity-log');
|
|
257
|
+
if (actEl) {
|
|
258
|
+
actEl.innerHTML = histItems.length > 0
|
|
259
|
+
? histItems.map(item => {
|
|
260
|
+
if (item.kind === 'tool') {
|
|
261
|
+
return `<div class="activity-entry activity-tool">
|
|
262
|
+
<span class="activity-time">${formatTime(item.timestamp)}</span>
|
|
263
|
+
<span class="activity-badge activity-badge-tool">${escapeHtml(item.tool)}</span>
|
|
264
|
+
<span class="activity-detail">${escapeHtml(item.input)}</span>
|
|
265
|
+
</div>`;
|
|
266
|
+
} else {
|
|
267
|
+
return `<div class="activity-entry activity-event">
|
|
268
|
+
<span class="activity-time">${formatTime(item.timestamp)}</span>
|
|
269
|
+
<span class="activity-badge activity-badge-event">${escapeHtml(item.type)}</span>
|
|
270
|
+
<span class="activity-detail">${escapeHtml(item.detail)}</span>
|
|
271
|
+
</div>`;
|
|
272
|
+
}
|
|
273
|
+
}).join('')
|
|
274
|
+
: '<div class="tab-empty">No activity recorded</div>';
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
document.getElementById('session-detail-overlay').classList.remove('hidden');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// -- Helpers (same as sessionPanel.js) --
|
|
281
|
+
|
|
282
|
+
function formatDuration(ms) {
|
|
283
|
+
const s = Math.floor(ms / 1000);
|
|
284
|
+
const m = Math.floor(s / 60);
|
|
285
|
+
const h = Math.floor(m / 60);
|
|
286
|
+
if (h > 0) return `${h}h ${m % 60}m`;
|
|
287
|
+
if (m > 0) return `${m}m ${s % 60}s`;
|
|
288
|
+
return `${s}s`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function formatTime(ts) {
|
|
292
|
+
return new Date(ts).toLocaleTimeString('en-US', { hour12: false });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function escapeHtml(str) {
|
|
296
|
+
if (!str) return '';
|
|
297
|
+
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
298
|
+
}
|