@qiaolei81/copilot-session-viewer 0.3.4 → 0.3.6
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 +3 -3
- package/bin/copilot-session-viewer +2 -2
- package/dist/server.min.js +99 -0
- package/package.json +5 -17
- package/public/js/homepage.min.js +9 -9
- package/public/js/session-detail.min.js +36 -7
- package/public/vendor/marked.umd.min.js +8 -0
- package/public/vendor/purify.min.js +3 -0
- package/public/vendor/vue-virtual-scroller.css +1 -0
- package/public/vendor/vue-virtual-scroller.min.js +2 -0
- package/public/vendor/vue.global.prod.min.js +19 -0
- package/views/session-vue.ejs +31 -6
- package/views/time-analyze.ejs +2 -2
- package/lib/parsers/README.md +0 -239
- package/lib/parsers/base-parser.js +0 -53
- package/lib/parsers/claude-parser.js +0 -181
- package/lib/parsers/copilot-parser.js +0 -143
- package/lib/parsers/index.js +0 -15
- package/lib/parsers/parser-factory.js +0 -77
- package/lib/parsers/pi-mono-parser.js +0 -119
- package/lib/parsers/vscode-parser.js +0 -591
- package/server.js +0 -29
- package/src/app.js +0 -129
- package/src/config/index.js +0 -27
- package/src/controllers/insightController.js +0 -136
- package/src/controllers/sessionController.js +0 -449
- package/src/controllers/tagController.js +0 -113
- package/src/controllers/uploadController.js +0 -648
- package/src/middleware/common.js +0 -67
- package/src/middleware/rateLimiting.js +0 -62
- package/src/models/Session.js +0 -146
- package/src/routes/api.js +0 -11
- package/src/routes/insights.js +0 -12
- package/src/routes/pages.js +0 -12
- package/src/routes/uploads.js +0 -14
- package/src/schemas/event.schema.js +0 -73
- package/src/services/eventNormalizer.js +0 -291
- package/src/services/insightService.js +0 -535
- package/src/services/sessionRepository.js +0 -1092
- package/src/services/sessionService.js +0 -1919
- package/src/services/tagService.js +0 -205
- package/src/telemetry.js +0 -152
- package/src/utils/fileUtils.js +0 -305
- package/src/utils/helpers.js +0 -45
- package/src/utils/processManager.js +0 -85
|
@@ -1,449 +0,0 @@
|
|
|
1
|
-
const SessionService = require('../services/sessionService');
|
|
2
|
-
const { isValidSessionId, buildMetadata } = require('../utils/helpers');
|
|
3
|
-
const { trackEvent, trackMetric } = require('../telemetry');
|
|
4
|
-
const AdmZip = require('adm-zip');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
const fs = require('fs');
|
|
7
|
-
|
|
8
|
-
class SessionController {
|
|
9
|
-
constructor(sessionService = null) {
|
|
10
|
-
this.sessionService = sessionService || new SessionService();
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
// Homepage with initial load (first batch)
|
|
14
|
-
async getHomepage(req, res) {
|
|
15
|
-
try {
|
|
16
|
-
// Only load default pill (copilot) first 20 sessions
|
|
17
|
-
const paginationData = await this.sessionService.getPaginatedSessions(1, 20, 'copilot');
|
|
18
|
-
|
|
19
|
-
// Build source path hints from repository config
|
|
20
|
-
const sourceHints = {};
|
|
21
|
-
if (this.sessionService.sessionRepository && this.sessionService.sessionRepository.sources) {
|
|
22
|
-
for (const src of this.sessionService.sessionRepository.sources) {
|
|
23
|
-
// Replace home dir with ~ for display
|
|
24
|
-
const home = require('os').homedir();
|
|
25
|
-
let displayPath = src.dir;
|
|
26
|
-
if (displayPath.startsWith(home)) {
|
|
27
|
-
displayPath = '~' + displayPath.slice(home.length);
|
|
28
|
-
}
|
|
29
|
-
// Normalize path separators for display
|
|
30
|
-
displayPath = displayPath.replace(/\\/g, '/');
|
|
31
|
-
if (!displayPath.endsWith('/')) displayPath += '/';
|
|
32
|
-
sourceHints[src.type] = displayPath;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Pass data for infinite scroll
|
|
37
|
-
const templateData = {
|
|
38
|
-
sessions: paginationData.sessions,
|
|
39
|
-
hasMore: paginationData.hasNextPage,
|
|
40
|
-
totalSessions: paginationData.totalSessions,
|
|
41
|
-
sourceHints: JSON.stringify(sourceHints)
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// Track HomepageViewed event
|
|
45
|
-
trackEvent('HomepageViewed', {
|
|
46
|
-
sessionCount: paginationData.totalSessions.toString(),
|
|
47
|
-
sourceFilter: 'copilot'
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
res.render('index', templateData);
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.error('Error loading sessions:', err);
|
|
53
|
-
res.status(500).send('Error loading sessions');
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Session detail page
|
|
58
|
-
async getSessionDetail(req, res) {
|
|
59
|
-
try {
|
|
60
|
-
const sessionId = req.params.id;
|
|
61
|
-
|
|
62
|
-
if (!isValidSessionId(sessionId)) {
|
|
63
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const session = await this.sessionService.sessionRepository.findById(sessionId);
|
|
67
|
-
if (!session) {
|
|
68
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const metadata = buildMetadata(session);
|
|
72
|
-
|
|
73
|
-
// Track SessionViewed event
|
|
74
|
-
trackEvent('SessionViewed', {
|
|
75
|
-
sessionId,
|
|
76
|
-
source: session.source || 'unknown',
|
|
77
|
-
eventCount: (session.eventCount || metadata.totalEvents || 0).toString(),
|
|
78
|
-
duration: (session.duration || metadata.duration || 0).toString(),
|
|
79
|
-
model: session.model || metadata.model || 'unknown',
|
|
80
|
-
sessionStatus: session.status || metadata.status || 'unknown'
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
// Track SessionEventCount metric
|
|
84
|
-
const eventCount = session.eventCount || metadata.totalEvents || 0;
|
|
85
|
-
if (eventCount > 0) {
|
|
86
|
-
trackMetric('SessionEventCount', eventCount, { sessionId, source: session.source || 'unknown' });
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Track SessionDuration metric
|
|
90
|
-
const duration = session.duration || metadata.duration || 0;
|
|
91
|
-
if (duration > 0) {
|
|
92
|
-
trackMetric('SessionDuration', duration, { sessionId, source: session.source || 'unknown' });
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
res.render('session-vue', { sessionId, events: [], metadata });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.error('Error loading session:', err);
|
|
98
|
-
res.status(500).json({ error: 'Error loading session' });
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Time analysis page
|
|
103
|
-
async getTimeAnalysis(req, res) {
|
|
104
|
-
try {
|
|
105
|
-
const sessionId = req.params.id;
|
|
106
|
-
|
|
107
|
-
if (!isValidSessionId(sessionId)) {
|
|
108
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const session = await this.sessionService.sessionRepository.findById(sessionId);
|
|
112
|
-
if (!session) {
|
|
113
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
const metadata = buildMetadata(session);
|
|
117
|
-
|
|
118
|
-
// Track TimeAnalysisViewed event
|
|
119
|
-
trackEvent('TimeAnalysisViewed', {
|
|
120
|
-
sessionId,
|
|
121
|
-
turnCount: (metadata.totalEvents || 0).toString()
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
res.render('time-analyze', { sessionId, events: [], metadata });
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.error('Error loading time analysis:', err);
|
|
127
|
-
res.status(500).json({ error: 'Error loading analysis' });
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// API: Get sessions with optional pagination
|
|
132
|
-
async getSessions(req, res) {
|
|
133
|
-
try {
|
|
134
|
-
const page = req.query.page ? parseInt(req.query.page) : null;
|
|
135
|
-
const limit = req.query.limit ? parseInt(req.query.limit) : null;
|
|
136
|
-
const sourceFilter = req.query.source || null;
|
|
137
|
-
|
|
138
|
-
if (page && limit) {
|
|
139
|
-
// Return paginated response
|
|
140
|
-
if (page < 1 || limit < 1 || limit > 100) {
|
|
141
|
-
return res.status(400).json({ error: 'Invalid pagination parameters' });
|
|
142
|
-
}
|
|
143
|
-
const paginationData = await this.sessionService.getPaginatedSessions(page, limit, sourceFilter);
|
|
144
|
-
|
|
145
|
-
// Track SessionListLoaded event for API pagination
|
|
146
|
-
trackEvent('SessionListLoaded', {
|
|
147
|
-
page: page.toString(),
|
|
148
|
-
limit: limit.toString(),
|
|
149
|
-
totalSessions: paginationData.totalSessions.toString()
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
res.set({ 'Cache-Control': 'public, max-age=60' });
|
|
153
|
-
res.json(paginationData);
|
|
154
|
-
} else if (sourceFilter && limit) {
|
|
155
|
-
// Source-filtered first page (for pill switching)
|
|
156
|
-
const sessions = await this.sessionService.getAllSessions(sourceFilter);
|
|
157
|
-
const sliced = sessions.slice(0, limit);
|
|
158
|
-
res.set({ 'Cache-Control': 'public, max-age=60' });
|
|
159
|
-
res.json({ sessions: sliced, hasMore: sessions.length > limit, totalSessions: sessions.length });
|
|
160
|
-
} else {
|
|
161
|
-
// Return all sessions for backward compatibility
|
|
162
|
-
const sessions = await this.sessionService.getAllSessions(sourceFilter);
|
|
163
|
-
res.set({ 'Cache-Control': 'public, max-age=300' });
|
|
164
|
-
res.json(sessions);
|
|
165
|
-
}
|
|
166
|
-
} catch (err) {
|
|
167
|
-
console.error('Error loading sessions:', err);
|
|
168
|
-
res.status(500).json({ error: 'Error loading sessions' });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// API: Load more sessions for infinite scroll
|
|
173
|
-
async loadMoreSessions(req, res) {
|
|
174
|
-
try {
|
|
175
|
-
const offset = parseInt(req.query.offset) || 0;
|
|
176
|
-
const limit = parseInt(req.query.limit) || 20;
|
|
177
|
-
const sourceFilter = req.query.source || null;
|
|
178
|
-
|
|
179
|
-
// Validate parameters
|
|
180
|
-
if (offset < 0 || limit < 1 || limit > 50) {
|
|
181
|
-
return res.status(400).json({ error: 'Invalid parameters' });
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Calculate page number from offset
|
|
185
|
-
const page = Math.floor(offset / limit) + 1;
|
|
186
|
-
const paginationData = await this.sessionService.getPaginatedSessions(page, limit, sourceFilter);
|
|
187
|
-
|
|
188
|
-
// Track SessionListLoaded event
|
|
189
|
-
trackEvent('SessionListLoaded', {
|
|
190
|
-
page: page.toString(),
|
|
191
|
-
limit: limit.toString(),
|
|
192
|
-
totalSessions: paginationData.totalSessions.toString()
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
res.json({
|
|
196
|
-
sessions: paginationData.sessions,
|
|
197
|
-
hasMore: paginationData.hasNextPage,
|
|
198
|
-
totalSessions: paginationData.totalSessions
|
|
199
|
-
});
|
|
200
|
-
} catch (err) {
|
|
201
|
-
console.error('Error loading more sessions:', err);
|
|
202
|
-
res.status(500).json({ error: 'Error loading more sessions' });
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// API: Get session events
|
|
207
|
-
async getSessionEvents(req, res) {
|
|
208
|
-
try {
|
|
209
|
-
const sessionId = req.params.id;
|
|
210
|
-
|
|
211
|
-
// Validate session ID format
|
|
212
|
-
if (!isValidSessionId(sessionId)) {
|
|
213
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Check if pagination is requested
|
|
217
|
-
const isPaginationRequested = req.query.limit !== undefined || req.query.offset !== undefined;
|
|
218
|
-
|
|
219
|
-
// Parse pagination parameters (only if requested)
|
|
220
|
-
let limit, offset, result;
|
|
221
|
-
|
|
222
|
-
if (isPaginationRequested) {
|
|
223
|
-
limit = parseInt(req.query.limit) || 100; // Default 100 events per page
|
|
224
|
-
offset = parseInt(req.query.offset) || 0;
|
|
225
|
-
|
|
226
|
-
// Validate pagination parameters
|
|
227
|
-
if (limit < 1 || limit > 1000) {
|
|
228
|
-
return res.status(400).json({ error: 'Limit must be between 1 and 1000' });
|
|
229
|
-
}
|
|
230
|
-
if (offset < 0) {
|
|
231
|
-
return res.status(400).json({ error: 'Offset must be non-negative' });
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Get session (needed for findById, no caching)
|
|
236
|
-
const session = await this.sessionService.sessionRepository.findById(sessionId);
|
|
237
|
-
if (!session) {
|
|
238
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Load events (with or without pagination)
|
|
242
|
-
if (isPaginationRequested) {
|
|
243
|
-
result = await this.sessionService.getSessionEvents(sessionId, { limit, offset });
|
|
244
|
-
} else {
|
|
245
|
-
// Load all events (backward compatibility)
|
|
246
|
-
const events = await this.sessionService.getSessionEvents(sessionId);
|
|
247
|
-
result = events; // Direct array
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// No caching for events - session files are live/active
|
|
251
|
-
res.set({
|
|
252
|
-
'Cache-Control': 'no-store',
|
|
253
|
-
'Vary': 'Accept-Encoding'
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
// Return response (paginated or plain array)
|
|
257
|
-
if (isPaginationRequested) {
|
|
258
|
-
res.json({
|
|
259
|
-
events: result.events,
|
|
260
|
-
pagination: {
|
|
261
|
-
total: result.total,
|
|
262
|
-
limit,
|
|
263
|
-
offset,
|
|
264
|
-
hasMore: offset + limit < result.total
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
} else {
|
|
268
|
-
res.json(result); // Plain array for backward compatibility
|
|
269
|
-
}
|
|
270
|
-
} catch (err) {
|
|
271
|
-
console.error('Error loading events:', err);
|
|
272
|
-
res.status(500).json({ error: 'Error loading events' });
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// API: Get timeline data (source-agnostic)
|
|
277
|
-
async getTimeline(req, res) {
|
|
278
|
-
try {
|
|
279
|
-
const sessionId = req.params.id;
|
|
280
|
-
|
|
281
|
-
// Validate session ID format
|
|
282
|
-
if (!isValidSessionId(sessionId)) {
|
|
283
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
const session = await this.sessionService.getSessionById(sessionId);
|
|
287
|
-
if (!session) {
|
|
288
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Generate unified timeline structure
|
|
292
|
-
const timeline = await this.sessionService.getTimeline(sessionId);
|
|
293
|
-
|
|
294
|
-
// Set caching headers
|
|
295
|
-
const crypto = require('crypto');
|
|
296
|
-
const etagBase = `${sessionId}-timeline-${session.updatedAt || session.createdAt}`;
|
|
297
|
-
const etag = crypto.createHash('md5').update(etagBase).digest('hex');
|
|
298
|
-
|
|
299
|
-
res.set({
|
|
300
|
-
'ETag': etag,
|
|
301
|
-
'Cache-Control': 'private, max-age=300',
|
|
302
|
-
'Vary': 'Accept-Encoding'
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
res.json(timeline);
|
|
306
|
-
} catch (err) {
|
|
307
|
-
console.error('Error loading timeline:', err);
|
|
308
|
-
res.status(500).json({ error: 'Error loading timeline' });
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Export session as zip
|
|
313
|
-
async exportSession(req, res) {
|
|
314
|
-
const sessionId = req.params.id;
|
|
315
|
-
|
|
316
|
-
if (!isValidSessionId(sessionId)) {
|
|
317
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
try {
|
|
321
|
-
// Get session to verify it exists and get its source
|
|
322
|
-
const session = await this.sessionService.sessionRepository.findById(sessionId);
|
|
323
|
-
if (!session) {
|
|
324
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// Find session file/directory path based on source
|
|
328
|
-
let sessionPath;
|
|
329
|
-
let isDirectory = false;
|
|
330
|
-
|
|
331
|
-
if (session.directory) {
|
|
332
|
-
// Try session directory first (copilot dirs, vscode dirs)
|
|
333
|
-
try {
|
|
334
|
-
const stats = await fs.promises.stat(session.directory);
|
|
335
|
-
if (stats.isDirectory()) {
|
|
336
|
-
sessionPath = session.directory;
|
|
337
|
-
isDirectory = true;
|
|
338
|
-
}
|
|
339
|
-
} catch {
|
|
340
|
-
// Fall through to filePath
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (!sessionPath && session.filePath) {
|
|
345
|
-
// File-based sessions (claude .jsonl, pi-mono .jsonl, copilot .jsonl)
|
|
346
|
-
try {
|
|
347
|
-
await fs.promises.access(session.filePath);
|
|
348
|
-
sessionPath = session.filePath;
|
|
349
|
-
} catch {
|
|
350
|
-
// Not accessible
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
// Legacy source-specific lookup as fallback
|
|
355
|
-
if (!sessionPath) {
|
|
356
|
-
if (session.source === 'copilot') {
|
|
357
|
-
const copilotSource = this.sessionService.sessionRepository.sources.find(s => s.type === 'copilot');
|
|
358
|
-
if (copilotSource) {
|
|
359
|
-
const basePath = path.join(copilotSource.dir, sessionId);
|
|
360
|
-
try {
|
|
361
|
-
const stats = await fs.promises.stat(basePath);
|
|
362
|
-
if (stats.isDirectory()) {
|
|
363
|
-
sessionPath = basePath;
|
|
364
|
-
isDirectory = true;
|
|
365
|
-
} else {
|
|
366
|
-
sessionPath = `${basePath}.jsonl`;
|
|
367
|
-
}
|
|
368
|
-
} catch {
|
|
369
|
-
sessionPath = `${basePath}.jsonl`;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
} else if (session.source === 'claude') {
|
|
373
|
-
const claudeSource = this.sessionService.sessionRepository.sources.find(s => s.type === 'claude');
|
|
374
|
-
if (claudeSource) {
|
|
375
|
-
const projectDirs = await fs.promises.readdir(path.join(claudeSource.dir, 'projects'));
|
|
376
|
-
for (const projectDir of projectDirs) {
|
|
377
|
-
const candidatePath = path.join(claudeSource.dir, 'projects', projectDir, `${sessionId}.jsonl`);
|
|
378
|
-
try {
|
|
379
|
-
await fs.promises.access(candidatePath);
|
|
380
|
-
sessionPath = candidatePath;
|
|
381
|
-
break;
|
|
382
|
-
} catch {
|
|
383
|
-
// Try next project
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
} else if (session.source === 'pi-mono') {
|
|
388
|
-
const piMonoSource = this.sessionService.sessionRepository.sources.find(s => s.type === 'pi-mono');
|
|
389
|
-
if (piMonoSource) {
|
|
390
|
-
const files = await fs.promises.readdir(piMonoSource.dir);
|
|
391
|
-
const matchingFile = files.find(f => f.includes(sessionId) && f.endsWith('.jsonl'));
|
|
392
|
-
if (matchingFile) {
|
|
393
|
-
sessionPath = path.join(piMonoSource.dir, matchingFile);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (!sessionPath) {
|
|
400
|
-
return res.status(404).json({ error: 'Session file not found' });
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Verify path exists
|
|
404
|
-
try {
|
|
405
|
-
await fs.promises.access(sessionPath);
|
|
406
|
-
} catch {
|
|
407
|
-
return res.status(404).json({ error: 'Session file not accessible' });
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
// Create zip
|
|
411
|
-
const zip = new AdmZip();
|
|
412
|
-
|
|
413
|
-
if (isDirectory) {
|
|
414
|
-
// Add entire directory (includes tags.json if present)
|
|
415
|
-
zip.addLocalFolder(sessionPath, sessionId);
|
|
416
|
-
} else {
|
|
417
|
-
// Add session file
|
|
418
|
-
const fileName = path.basename(sessionPath);
|
|
419
|
-
zip.addLocalFile(sessionPath, '', fileName);
|
|
420
|
-
|
|
421
|
-
// Also include tags file if it exists
|
|
422
|
-
const TagService = require('../services/tagService');
|
|
423
|
-
const tagService = new TagService();
|
|
424
|
-
const tagsFilePath = tagService.getSessionTagsFilePath(session);
|
|
425
|
-
try {
|
|
426
|
-
await fs.promises.access(tagsFilePath);
|
|
427
|
-
zip.addLocalFile(tagsFilePath, '', path.basename(tagsFilePath));
|
|
428
|
-
} catch {
|
|
429
|
-
// No tags file, skip
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// Send zip file
|
|
434
|
-
const zipBuffer = zip.toBuffer();
|
|
435
|
-
res.setHeader('Content-Type', 'application/zip');
|
|
436
|
-
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.zip"`);
|
|
437
|
-
|
|
438
|
-
// Track SessionExported event
|
|
439
|
-
trackEvent('SessionExported', { sessionId });
|
|
440
|
-
|
|
441
|
-
res.send(zipBuffer);
|
|
442
|
-
} catch (err) {
|
|
443
|
-
console.error('Error exporting session:', err);
|
|
444
|
-
res.status(500).json({ error: 'Error exporting session' });
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
module.exports = SessionController;
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
const TagService = require('../services/tagService');
|
|
2
|
-
const SessionRepository = require('../services/sessionRepository');
|
|
3
|
-
const { isValidSessionId } = require('../utils/helpers');
|
|
4
|
-
const { trackEvent } = require('../telemetry');
|
|
5
|
-
|
|
6
|
-
class TagController {
|
|
7
|
-
constructor(tagService = null, sessionRepository = null) {
|
|
8
|
-
this.tagService = tagService || new TagService();
|
|
9
|
-
this.sessionRepository = sessionRepository || new SessionRepository();
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* GET /api/tags
|
|
14
|
-
* Get all unique tags across all sessions (for autocomplete)
|
|
15
|
-
*/
|
|
16
|
-
async getAllTags(req, res) {
|
|
17
|
-
try {
|
|
18
|
-
const tags = await this.tagService.getAllKnownTags();
|
|
19
|
-
res.json({ tags });
|
|
20
|
-
} catch (err) {
|
|
21
|
-
console.error('Error getting all tags:', err);
|
|
22
|
-
res.status(500).json({ error: 'Error loading tags' });
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* GET /api/sessions/:id/tags
|
|
28
|
-
* Get tags for a specific session
|
|
29
|
-
*/
|
|
30
|
-
async getSessionTags(req, res) {
|
|
31
|
-
try {
|
|
32
|
-
const sessionId = req.params.id;
|
|
33
|
-
|
|
34
|
-
if (!isValidSessionId(sessionId)) {
|
|
35
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Find session by ID
|
|
39
|
-
const session = await this.sessionRepository.findById(sessionId);
|
|
40
|
-
if (!session) {
|
|
41
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const tags = await this.tagService.getSessionTags(session);
|
|
45
|
-
res.json({ tags });
|
|
46
|
-
} catch (err) {
|
|
47
|
-
console.error('Error getting session tags:', err);
|
|
48
|
-
res.status(500).json({ error: 'Error loading session tags' });
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* PUT /api/sessions/:id/tags
|
|
54
|
-
* Set tags for a specific session
|
|
55
|
-
* Body: { tags: ["tag1", "tag2"] }
|
|
56
|
-
*/
|
|
57
|
-
async setSessionTags(req, res) {
|
|
58
|
-
try {
|
|
59
|
-
const sessionId = req.params.id;
|
|
60
|
-
const { tags } = req.body;
|
|
61
|
-
|
|
62
|
-
if (!isValidSessionId(sessionId)) {
|
|
63
|
-
return res.status(400).json({ error: 'Invalid session ID' });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
if (!Array.isArray(tags)) {
|
|
67
|
-
return res.status(400).json({ error: 'Tags must be an array' });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Validate tag count
|
|
71
|
-
if (tags.length > 10) {
|
|
72
|
-
return res.status(400).json({ error: 'Maximum 10 tags per session' });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Validate tag length
|
|
76
|
-
for (const tag of tags) {
|
|
77
|
-
if (typeof tag !== 'string' || tag.trim().length === 0) {
|
|
78
|
-
return res.status(400).json({ error: 'Tags must be non-empty strings' });
|
|
79
|
-
}
|
|
80
|
-
if (tag.length > 30) {
|
|
81
|
-
return res.status(400).json({ error: 'Tag length must not exceed 30 characters' });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Find session by ID
|
|
86
|
-
const session = await this.sessionRepository.findById(sessionId);
|
|
87
|
-
if (!session) {
|
|
88
|
-
return res.status(404).json({ error: 'Session not found' });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const savedTags = await this.tagService.setSessionTags(session, tags);
|
|
92
|
-
|
|
93
|
-
// Track TagUpdated event
|
|
94
|
-
trackEvent('TagUpdated', {
|
|
95
|
-
sessionId,
|
|
96
|
-
tagCount: savedTags.length.toString()
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
res.json({ tags: savedTags });
|
|
100
|
-
} catch (err) {
|
|
101
|
-
console.error('Error setting session tags:', err);
|
|
102
|
-
if (err.message === 'Maximum 10 tags per session') {
|
|
103
|
-
return res.status(400).json({ error: err.message });
|
|
104
|
-
}
|
|
105
|
-
if (err.message === 'Session must have a directory field') {
|
|
106
|
-
return res.status(400).json({ error: 'Session does not support tagging' });
|
|
107
|
-
}
|
|
108
|
-
res.status(500).json({ error: 'Error saving session tags' });
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
module.exports = TagController;
|