@qiaolei81/copilot-session-viewer 0.1.9 → 0.2.1
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/.nycrc +29 -0
- package/CHANGELOG.md +48 -0
- package/README.md +154 -15
- package/examples/parser-usage.js +114 -0
- package/lib/parsers/README.md +239 -0
- package/lib/parsers/base-parser.js +53 -0
- package/lib/parsers/claude-parser.js +181 -0
- package/lib/parsers/copilot-parser.js +143 -0
- package/lib/parsers/index.js +13 -0
- package/lib/parsers/parser-factory.js +77 -0
- package/lib/parsers/pi-mono-parser.js +119 -0
- package/package.json +12 -4
- package/server.js +17 -2
- package/src/app.js +45 -20
- package/src/controllers/insightController.js +44 -8
- package/src/controllers/sessionController.js +217 -3
- package/src/controllers/uploadController.js +447 -7
- package/src/middleware/rateLimiting.js +7 -1
- package/src/models/Session.js +26 -0
- package/src/schemas/event.schema.js +73 -0
- package/src/services/eventNormalizer.js +291 -0
- package/src/services/insightService.js +140 -48
- package/src/services/sessionRepository.js +584 -49
- package/src/services/sessionService.js +1588 -36
- package/src/utils/helpers.js +6 -1
- package/views/index.ejs +111 -4
- package/views/session-vue.ejs +272 -65
- package/views/time-analyze.ejs +127 -55
package/src/app.js
CHANGED
|
@@ -7,7 +7,8 @@ const helmet = require('helmet');
|
|
|
7
7
|
const config = require('./config');
|
|
8
8
|
|
|
9
9
|
// Middleware
|
|
10
|
-
|
|
10
|
+
// Rate limiting disabled for local development
|
|
11
|
+
// const { globalLimiter, insightGenerationLimiter, insightAccessLimiter, uploadLimiter } = require('./middleware/rateLimiting');
|
|
11
12
|
const { requestTimeout, developmentCors, errorHandler, notFoundHandler } = require('./middleware/common');
|
|
12
13
|
|
|
13
14
|
// Controllers
|
|
@@ -20,23 +21,45 @@ function createApp(options = {}) {
|
|
|
20
21
|
|
|
21
22
|
// Create controller instances (with optional dependency injection)
|
|
22
23
|
const sessionController = new SessionController(options.sessionService);
|
|
23
|
-
const insightController = new InsightController(options.insightService);
|
|
24
|
+
const insightController = new InsightController(options.insightService, options.sessionService);
|
|
24
25
|
const uploadController = new UploadController();
|
|
25
26
|
|
|
26
|
-
//
|
|
27
|
+
// Minimal security headers for local development tool
|
|
28
|
+
// Custom CSP without upgrade-insecure-requests
|
|
29
|
+
app.use((req, res, next) => {
|
|
30
|
+
res.setHeader(
|
|
31
|
+
'Content-Security-Policy',
|
|
32
|
+
"default-src 'self'; " +
|
|
33
|
+
"style-src 'self' 'unsafe-inline' https: http:; " +
|
|
34
|
+
"font-src 'self' https: http:; " +
|
|
35
|
+
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https: http:; " +
|
|
36
|
+
"img-src 'self' data: https: http:; " +
|
|
37
|
+
"connect-src 'self' https: http:"
|
|
38
|
+
);
|
|
39
|
+
next();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Other helmet protections (without CSP and HSTS)
|
|
27
43
|
app.use(helmet({
|
|
28
|
-
contentSecurityPolicy:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
44
|
+
contentSecurityPolicy: false,
|
|
45
|
+
hsts: false,
|
|
46
|
+
referrerPolicy: false,
|
|
47
|
+
crossOriginEmbedderPolicy: false,
|
|
48
|
+
crossOriginOpenerPolicy: false,
|
|
49
|
+
crossOriginResourcePolicy: false
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
app.use(compression({
|
|
53
|
+
level: 9, // Maximum compression for local use (CPU is not a bottleneck)
|
|
54
|
+
threshold: 1024, // Compress responses > 1KB
|
|
55
|
+
filter: (req, res) => {
|
|
56
|
+
// Always compress JSON responses
|
|
57
|
+
if (res.getHeader('Content-Type')?.includes('application/json')) {
|
|
58
|
+
return true;
|
|
35
59
|
}
|
|
60
|
+
return compression.filter(req, res);
|
|
36
61
|
}
|
|
37
62
|
}));
|
|
38
|
-
|
|
39
|
-
app.use(compression());
|
|
40
63
|
app.use(express.json({ limit: '1mb' }));
|
|
41
64
|
app.use(express.urlencoded({ extended: true }));
|
|
42
65
|
app.use(requestTimeout);
|
|
@@ -46,8 +69,8 @@ function createApp(options = {}) {
|
|
|
46
69
|
app.use(developmentCors);
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
// Rate limiting
|
|
50
|
-
app.use(globalLimiter);
|
|
72
|
+
// Rate limiting - DISABLED for local development
|
|
73
|
+
// app.use(globalLimiter);
|
|
51
74
|
|
|
52
75
|
// Static files
|
|
53
76
|
app.use('/public', express.static(path.join(__dirname, '../public')));
|
|
@@ -62,11 +85,13 @@ function createApp(options = {}) {
|
|
|
62
85
|
app.get('/', sessionController.getHomepage.bind(sessionController));
|
|
63
86
|
app.get('/session/:id', sessionController.getSessionDetail.bind(sessionController));
|
|
64
87
|
app.get('/session/:id/time-analyze', sessionController.getTimeAnalysis.bind(sessionController));
|
|
88
|
+
app.get('/session/:id/export', sessionController.exportSession.bind(sessionController));
|
|
65
89
|
|
|
66
90
|
// API routes (more specific routes first)
|
|
67
91
|
app.get('/api/sessions/load-more', sessionController.loadMoreSessions.bind(sessionController));
|
|
68
92
|
app.get('/api/sessions', sessionController.getSessions.bind(sessionController));
|
|
69
93
|
app.get('/api/sessions/:id/events', sessionController.getSessionEvents.bind(sessionController));
|
|
94
|
+
app.get('/api/sessions/:id/timeline', sessionController.getTimeline.bind(sessionController));
|
|
70
95
|
|
|
71
96
|
// Upload routes
|
|
72
97
|
app.get('/session/:id/share', uploadController.shareSession.bind(uploadController));
|
|
@@ -75,13 +100,13 @@ function createApp(options = {}) {
|
|
|
75
100
|
uploadController.importSession.bind(uploadController)
|
|
76
101
|
);
|
|
77
102
|
|
|
78
|
-
// Insight routes
|
|
79
|
-
app.post('/session/:id/insight',
|
|
80
|
-
app.get('/session/:id/insight', insightController.getInsightStatus.bind(insightController));
|
|
81
|
-
app.delete('/session/:id/insight',
|
|
103
|
+
// Insight routes (rate limiting disabled)
|
|
104
|
+
app.post('/session/:id/insight', insightController.generateInsight.bind(insightController));
|
|
105
|
+
app.get('/session/:id/insight', insightController.getInsightStatus.bind(insightController));
|
|
106
|
+
app.delete('/session/:id/insight', insightController.deleteInsight.bind(insightController));
|
|
82
107
|
|
|
83
|
-
// Upload rate limiting
|
|
84
|
-
app.use('/session/import', uploadLimiter);
|
|
108
|
+
// Upload rate limiting - DISABLED
|
|
109
|
+
// app.use('/session/import', uploadLimiter);
|
|
85
110
|
|
|
86
111
|
// Error handling
|
|
87
112
|
app.use(notFoundHandler);
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
const InsightService = require('../services/insightService');
|
|
2
2
|
const { isValidSessionId } = require('../utils/helpers');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const os = require('os');
|
|
5
3
|
|
|
6
4
|
class InsightController {
|
|
7
|
-
constructor(insightService = null) {
|
|
5
|
+
constructor(insightService = null, sessionService = null) {
|
|
8
6
|
if (insightService) {
|
|
9
7
|
this.insightService = insightService;
|
|
10
8
|
} else {
|
|
11
|
-
|
|
12
|
-
this.insightService = new InsightService(
|
|
9
|
+
// Use default multi-source configuration
|
|
10
|
+
this.insightService = new InsightService();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// SessionService for getting session metadata (source)
|
|
14
|
+
if (sessionService) {
|
|
15
|
+
this.sessionService = sessionService;
|
|
16
|
+
} else {
|
|
17
|
+
const SessionService = require('../services/sessionService');
|
|
18
|
+
this.sessionService = new SessionService();
|
|
13
19
|
}
|
|
14
20
|
}
|
|
15
21
|
|
|
@@ -23,7 +29,17 @@ class InsightController {
|
|
|
23
29
|
return res.status(400).json({ error: 'Invalid session ID' });
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
|
|
32
|
+
// Get session to determine source and directory
|
|
33
|
+
const session = await this.sessionService.getSessionById(sessionId);
|
|
34
|
+
if (!session) {
|
|
35
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!session.directory) {
|
|
39
|
+
return res.status(400).json({ error: 'Session directory not available' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const result = await this.insightService.generateInsight(session.id, session.directory, session.source, forceRegenerate);
|
|
27
43
|
res.json(result);
|
|
28
44
|
} catch (err) {
|
|
29
45
|
console.error('Error generating insight:', err);
|
|
@@ -40,7 +56,17 @@ class InsightController {
|
|
|
40
56
|
return res.status(400).json({ error: 'Invalid session ID' });
|
|
41
57
|
}
|
|
42
58
|
|
|
43
|
-
|
|
59
|
+
// Get session to determine directory
|
|
60
|
+
const session = await this.sessionService.getSessionById(sessionId);
|
|
61
|
+
if (!session) {
|
|
62
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (!session.directory) {
|
|
66
|
+
return res.status(400).json({ error: 'Session directory not available' });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const result = await this.insightService.getInsightStatus(session.id, session.directory, session.source);
|
|
44
70
|
res.json(result);
|
|
45
71
|
} catch (err) {
|
|
46
72
|
console.error('Error getting insight status:', err);
|
|
@@ -57,7 +83,17 @@ class InsightController {
|
|
|
57
83
|
return res.status(400).json({ error: 'Invalid session ID' });
|
|
58
84
|
}
|
|
59
85
|
|
|
60
|
-
|
|
86
|
+
// Get session to determine directory
|
|
87
|
+
const session = await this.sessionService.getSessionById(sessionId);
|
|
88
|
+
if (!session) {
|
|
89
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!session.directory) {
|
|
93
|
+
return res.status(400).json({ error: 'Session directory not available' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result = await this.insightService.deleteInsight(session.id, session.directory, session.source);
|
|
61
97
|
res.json(result);
|
|
62
98
|
} catch (err) {
|
|
63
99
|
console.error('Error deleting insight:', err);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
const SessionService = require('../services/sessionService');
|
|
2
2
|
const { isValidSessionId } = require('../utils/helpers');
|
|
3
|
+
const AdmZip = require('adm-zip');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
3
6
|
|
|
4
7
|
class SessionController {
|
|
5
8
|
constructor(sessionService = null) {
|
|
@@ -9,7 +12,7 @@ class SessionController {
|
|
|
9
12
|
// Homepage with initial load (first batch)
|
|
10
13
|
async getHomepage(req, res) {
|
|
11
14
|
try {
|
|
12
|
-
const initialLimit =
|
|
15
|
+
const initialLimit = 100; // Load first 100 sessions to ensure Pi-Mono sessions are included
|
|
13
16
|
const paginationData = await this.sessionService.getPaginatedSessions(1, initialLimit);
|
|
14
17
|
|
|
15
18
|
// Pass data for infinite scroll
|
|
@@ -67,6 +70,7 @@ class SessionController {
|
|
|
67
70
|
}
|
|
68
71
|
|
|
69
72
|
const { events, metadata } = sessionData;
|
|
73
|
+
// Use original time-analyze view (supports all sources via normalized events)
|
|
70
74
|
res.render('time-analyze', { sessionId, events, metadata });
|
|
71
75
|
} catch (err) {
|
|
72
76
|
console.error('Error loading time analysis:', err);
|
|
@@ -150,13 +154,223 @@ class SessionController {
|
|
|
150
154
|
return res.status(400).json({ error: 'Invalid session ID' });
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
|
|
154
|
-
|
|
157
|
+
// Check if pagination is requested
|
|
158
|
+
const isPaginationRequested = req.query.limit !== undefined || req.query.offset !== undefined;
|
|
159
|
+
|
|
160
|
+
// Parse pagination parameters (only if requested)
|
|
161
|
+
let limit, offset, result;
|
|
162
|
+
|
|
163
|
+
if (isPaginationRequested) {
|
|
164
|
+
limit = parseInt(req.query.limit) || 100; // Default 100 events per page
|
|
165
|
+
offset = parseInt(req.query.offset) || 0;
|
|
166
|
+
|
|
167
|
+
// Validate pagination parameters
|
|
168
|
+
if (limit < 1 || limit > 1000) {
|
|
169
|
+
return res.status(400).json({ error: 'Limit must be between 1 and 1000' });
|
|
170
|
+
}
|
|
171
|
+
if (offset < 0) {
|
|
172
|
+
return res.status(400).json({ error: 'Offset must be non-negative' });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Get session metadata for ETag generation
|
|
177
|
+
const session = await this.sessionService.getSessionById(sessionId);
|
|
178
|
+
if (!session) {
|
|
179
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Generate ETag from session ID + timestamp + pagination params (if used)
|
|
183
|
+
const crypto = require('crypto');
|
|
184
|
+
const etagBase = isPaginationRequested
|
|
185
|
+
? `${sessionId}-${session.updated || session.created}-${limit}-${offset}`
|
|
186
|
+
: `${sessionId}-${session.updated || session.created}`;
|
|
187
|
+
const etag = crypto.createHash('md5').update(etagBase).digest('hex');
|
|
188
|
+
|
|
189
|
+
// Check If-None-Match header (client cache)
|
|
190
|
+
const clientEtag = req.headers['if-none-match'];
|
|
191
|
+
if (clientEtag === etag) {
|
|
192
|
+
return res.status(304).end(); // Not Modified - use cached version
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Load events (with or without pagination)
|
|
196
|
+
if (isPaginationRequested) {
|
|
197
|
+
result = await this.sessionService.getSessionEvents(sessionId, { limit, offset });
|
|
198
|
+
} else {
|
|
199
|
+
// Load all events (backward compatibility)
|
|
200
|
+
const events = await this.sessionService.getSessionEvents(sessionId);
|
|
201
|
+
result = events; // Direct array
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Set caching headers
|
|
205
|
+
res.set({
|
|
206
|
+
'ETag': etag,
|
|
207
|
+
'Cache-Control': 'private, max-age=0, no-cache', // Disable cache during development
|
|
208
|
+
'Vary': 'Accept-Encoding'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Return response (paginated or plain array)
|
|
212
|
+
if (isPaginationRequested) {
|
|
213
|
+
res.json({
|
|
214
|
+
events: result.events,
|
|
215
|
+
pagination: {
|
|
216
|
+
total: result.total,
|
|
217
|
+
limit,
|
|
218
|
+
offset,
|
|
219
|
+
hasMore: offset + limit < result.total
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
} else {
|
|
223
|
+
res.json(result); // Plain array for backward compatibility
|
|
224
|
+
}
|
|
155
225
|
} catch (err) {
|
|
156
226
|
console.error('Error loading events:', err);
|
|
157
227
|
res.status(500).json({ error: 'Error loading events' });
|
|
158
228
|
}
|
|
159
229
|
}
|
|
230
|
+
|
|
231
|
+
// API: Get timeline data (source-agnostic)
|
|
232
|
+
async getTimeline(req, res) {
|
|
233
|
+
try {
|
|
234
|
+
const sessionId = req.params.id;
|
|
235
|
+
|
|
236
|
+
// Validate session ID format
|
|
237
|
+
if (!isValidSessionId(sessionId)) {
|
|
238
|
+
return res.status(400).json({ error: 'Invalid session ID' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const session = await this.sessionService.getSessionById(sessionId);
|
|
242
|
+
if (!session) {
|
|
243
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Generate unified timeline structure
|
|
247
|
+
const timeline = await this.sessionService.getTimeline(sessionId);
|
|
248
|
+
|
|
249
|
+
// Set caching headers
|
|
250
|
+
const crypto = require('crypto');
|
|
251
|
+
const etagBase = `${sessionId}-timeline-${session.updated || session.created}`;
|
|
252
|
+
const etag = crypto.createHash('md5').update(etagBase).digest('hex');
|
|
253
|
+
|
|
254
|
+
res.set({
|
|
255
|
+
'ETag': etag,
|
|
256
|
+
'Cache-Control': 'private, max-age=300',
|
|
257
|
+
'Vary': 'Accept-Encoding'
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
res.json(timeline);
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error('Error loading timeline:', err);
|
|
263
|
+
res.status(500).json({ error: 'Error loading timeline' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Export session as zip
|
|
268
|
+
async exportSession(req, res) {
|
|
269
|
+
const sessionId = req.params.id;
|
|
270
|
+
|
|
271
|
+
if (!isValidSessionId(sessionId)) {
|
|
272
|
+
return res.status(400).json({ error: 'Invalid session ID' });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
// Get session to verify it exists and get its source
|
|
277
|
+
const session = await this.sessionService.sessionRepository.findById(sessionId);
|
|
278
|
+
if (!session) {
|
|
279
|
+
return res.status(404).json({ error: 'Session not found' });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Find session file/directory path based on source
|
|
283
|
+
let sessionPath;
|
|
284
|
+
let isDirectory = false;
|
|
285
|
+
|
|
286
|
+
if (session.source === 'copilot') {
|
|
287
|
+
const copilotSource = this.sessionService.sessionRepository.sources.find(s => s.type === 'copilot');
|
|
288
|
+
if (!copilotSource) {
|
|
289
|
+
return res.status(404).json({ error: 'Copilot source not found' });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const basePath = path.join(copilotSource.dir, sessionId);
|
|
293
|
+
try {
|
|
294
|
+
const stats = await fs.promises.stat(basePath);
|
|
295
|
+
if (stats.isDirectory()) {
|
|
296
|
+
sessionPath = basePath;
|
|
297
|
+
isDirectory = true;
|
|
298
|
+
} else {
|
|
299
|
+
sessionPath = `${basePath}.jsonl`;
|
|
300
|
+
}
|
|
301
|
+
} catch {
|
|
302
|
+
sessionPath = `${basePath}.jsonl`;
|
|
303
|
+
}
|
|
304
|
+
} else if (session.source === 'claude') {
|
|
305
|
+
const claudeSource = this.sessionService.sessionRepository.sources.find(s => s.type === 'claude');
|
|
306
|
+
if (!claudeSource) {
|
|
307
|
+
return res.status(404).json({ error: 'Claude source not found' });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Claude sessions are in projects/*/sessionId.jsonl
|
|
311
|
+
const projectDirs = await fs.promises.readdir(path.join(claudeSource.dir, 'projects'));
|
|
312
|
+
for (const projectDir of projectDirs) {
|
|
313
|
+
const candidatePath = path.join(claudeSource.dir, 'projects', projectDir, `${sessionId}.jsonl`);
|
|
314
|
+
try {
|
|
315
|
+
await fs.promises.access(candidatePath);
|
|
316
|
+
sessionPath = candidatePath;
|
|
317
|
+
break;
|
|
318
|
+
} catch {
|
|
319
|
+
// Try next project
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!sessionPath) {
|
|
324
|
+
return res.status(404).json({ error: 'Session file not found' });
|
|
325
|
+
}
|
|
326
|
+
} else if (session.source === 'pi-mono') {
|
|
327
|
+
const piMonoSource = this.sessionService.sessionRepository.sources.find(s => s.type === 'pi-mono');
|
|
328
|
+
if (!piMonoSource) {
|
|
329
|
+
return res.status(404).json({ error: 'Pi-Mono source not found' });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Pi-Mono sessions are timestamp-based JSONL files
|
|
333
|
+
const files = await fs.promises.readdir(piMonoSource.dir);
|
|
334
|
+
const matchingFile = files.find(f => f.includes(sessionId) && f.endsWith('.jsonl'));
|
|
335
|
+
if (!matchingFile) {
|
|
336
|
+
return res.status(404).json({ error: 'Session file not found' });
|
|
337
|
+
}
|
|
338
|
+
sessionPath = path.join(piMonoSource.dir, matchingFile);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!sessionPath) {
|
|
342
|
+
return res.status(404).json({ error: 'Session file not found' });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Verify path exists
|
|
346
|
+
try {
|
|
347
|
+
await fs.promises.access(sessionPath);
|
|
348
|
+
} catch {
|
|
349
|
+
return res.status(404).json({ error: 'Session file not accessible' });
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Create zip
|
|
353
|
+
const zip = new AdmZip();
|
|
354
|
+
|
|
355
|
+
if (isDirectory) {
|
|
356
|
+
// Add entire directory
|
|
357
|
+
zip.addLocalFolder(sessionPath, sessionId);
|
|
358
|
+
} else {
|
|
359
|
+
// Add single file
|
|
360
|
+
const fileName = path.basename(sessionPath);
|
|
361
|
+
zip.addLocalFile(sessionPath, '', fileName);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Send zip file
|
|
365
|
+
const zipBuffer = zip.toBuffer();
|
|
366
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
367
|
+
res.setHeader('Content-Disposition', `attachment; filename="session-${sessionId}.zip"`);
|
|
368
|
+
res.send(zipBuffer);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
console.error('Error exporting session:', err);
|
|
371
|
+
res.status(500).json({ error: 'Error exporting session' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
160
374
|
}
|
|
161
375
|
|
|
162
376
|
module.exports = SessionController;
|