fraim 2.0.361 → 2.0.362-next.2

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.
@@ -3,6 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.setAnalyticsDatabase = setAnalyticsDatabase;
6
7
  const license_presentation_1 = require("../fraim/license-presentation");
7
8
  const express_1 = __importDefault(require("express"));
8
9
  const path_1 = __importDefault(require("path"));
@@ -11,12 +12,59 @@ const db_service_js_1 = require("../fraim/db-service.js");
11
12
  const mcp_service_js_1 = require("../services/mcp-service.js");
12
13
  const git_utils_js_1 = require("../core/utils/git-utils.js");
13
14
  const job_phases_1 = require("../core/job-phases");
15
+ const analytics_read_1 = require("../services/analytics-read");
14
16
  const router = express_1.default.Router();
17
+ const readQueue = new analytics_read_1.AnalyticsReadQueue();
18
+ let analyticsDatabase;
19
+ let sharedAnalyticsService;
20
+ function setAnalyticsDatabase(database) {
21
+ analyticsDatabase = database;
22
+ sharedAnalyticsService = undefined;
23
+ }
24
+ function getAnalyticsDatabase() {
25
+ if (!analyticsDatabase)
26
+ throw new Error('Analytics database was not initialized at server startup');
27
+ return analyticsDatabase;
28
+ }
29
+ function getAnalyticsService() {
30
+ return sharedAnalyticsService ??= new usage_analytics_service_js_1.UsageAnalyticsService(getAnalyticsDatabase());
31
+ }
32
+ function analyticsGet(route, handler) {
33
+ router.get(route, async (req, res) => {
34
+ const started = Date.now();
35
+ try {
36
+ await readQueue.run(async () => {
37
+ if (req.destroyed || res.destroyed)
38
+ return;
39
+ try {
40
+ return await handler(req, res);
41
+ }
42
+ catch (error) {
43
+ if (!['MongoNetworkError', 'MongoServerSelectionError', 'MongoNotConnectedError', 'MongoTopologyClosedError'].includes(error.name))
44
+ throw error;
45
+ await getAnalyticsDatabase().recoverConnection();
46
+ return handler(req, res);
47
+ }
48
+ });
49
+ }
50
+ catch (error) {
51
+ if (res.headersSent || res.destroyed)
52
+ return;
53
+ const { status, headers, body } = (0, analytics_read_1.analyticsReadErrorResponse)(error);
54
+ if (status === 500)
55
+ console.error('[analytics] Read failed:', error);
56
+ res.set(headers).status(status).json(body);
57
+ }
58
+ finally {
59
+ console.info(`[analytics] endpoint=${route} aggregation_ms=${Date.now() - started} status=${res.statusCode}`);
60
+ }
61
+ });
62
+ }
15
63
  /**
16
64
  * GET /api/analytics/stats
17
65
  * Get usage statistics for a time period
18
66
  */
19
- router.get('/stats', async (req, res) => {
67
+ analyticsGet('/stats', async (req, res) => {
20
68
  let dbService;
21
69
  let analyticsService;
22
70
  try {
@@ -26,10 +74,9 @@ router.get('/stats', async (req, res) => {
26
74
  return res.status(401).json({ error: 'Authentication required' });
27
75
  }
28
76
  // Create database connection
29
- dbService = new db_service_js_1.FraimDbService();
30
- await dbService.connect();
77
+ dbService = getAnalyticsDatabase();
31
78
  // Create analytics service
32
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
79
+ analyticsService = getAnalyticsService();
33
80
  const { type, apiKey, repoIdentifier } = req.query;
34
81
  const timeframe = resolveAnalyticsTimeframe(req.query);
35
82
  const types = type ? (Array.isArray(type) ? type : [type]) : undefined;
@@ -59,20 +106,14 @@ router.get('/stats', async (req, res) => {
59
106
  res.json(stats);
60
107
  }
61
108
  catch (error) {
62
- console.error('Error fetching usage stats:', error);
63
- res.status(500).json({ error: 'Internal server error' });
64
- }
65
- finally {
66
- if (dbService) {
67
- await dbService.close();
68
- }
109
+ throw error;
69
110
  }
70
111
  });
71
112
  /**
72
113
  * GET /api/analytics/components/top
73
114
  * Get top components by usage count
74
115
  */
75
- router.get('/components/top', async (req, res) => {
116
+ analyticsGet('/components/top', async (req, res) => {
76
117
  let dbService;
77
118
  let analyticsService;
78
119
  try {
@@ -82,10 +123,9 @@ router.get('/components/top', async (req, res) => {
82
123
  return res.status(401).json({ error: 'Authentication required' });
83
124
  }
84
125
  // Create database connection
85
- dbService = new db_service_js_1.FraimDbService();
86
- await dbService.connect();
126
+ dbService = getAnalyticsDatabase();
87
127
  // Create analytics service
88
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
128
+ analyticsService = getAnalyticsService();
89
129
  const { limit = '10', apiKey, type, repoIdentifier } = req.query;
90
130
  const timeframe = resolveAnalyticsTimeframe(req.query);
91
131
  const types = type ? (Array.isArray(type) ? type : [type]) : undefined;
@@ -108,20 +148,14 @@ router.get('/components/top', async (req, res) => {
108
148
  res.json(components);
109
149
  }
110
150
  catch (error) {
111
- console.error('Error fetching top components:', error);
112
- res.status(500).json({ error: 'Internal server error' });
113
- }
114
- finally {
115
- if (dbService) {
116
- await dbService.close();
117
- }
151
+ throw error;
118
152
  }
119
153
  });
120
154
  /**
121
155
  * GET /api/analytics/trends
122
156
  * Get trend data for a specific component
123
157
  */
124
- router.get('/trends', async (req, res) => {
158
+ analyticsGet('/trends', async (req, res) => {
125
159
  let dbService;
126
160
  let analyticsService;
127
161
  try {
@@ -136,10 +170,9 @@ router.get('/trends', async (req, res) => {
136
170
  return res.status(400).json({ error: 'Component name and type are required' });
137
171
  }
138
172
  // Create database connection
139
- dbService = new db_service_js_1.FraimDbService();
140
- await dbService.connect();
173
+ dbService = getAnalyticsDatabase();
141
174
  // Create analytics service
142
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
175
+ analyticsService = getAnalyticsService();
143
176
  let targetUserId;
144
177
  if (apiKey && typeof apiKey === 'string') {
145
178
  const isAdmin = apiKeyData.userId === 'sid.mathur@gmail.com';
@@ -159,20 +192,14 @@ router.get('/trends', async (req, res) => {
159
192
  res.json(trend);
160
193
  }
161
194
  catch (error) {
162
- console.error('Error fetching component trends:', error);
163
- res.status(500).json({ error: 'Internal server error' });
164
- }
165
- finally {
166
- if (dbService) {
167
- await dbService.close();
168
- }
195
+ throw error;
169
196
  }
170
197
  });
171
198
  /**
172
199
  * GET /api/analytics/jobs/timeline
173
200
  * Get job execution timeline for the bubble chart
174
201
  */
175
- router.get('/jobs/timeline', async (req, res) => {
202
+ analyticsGet('/jobs/timeline', async (req, res) => {
176
203
  let dbService;
177
204
  let analyticsService;
178
205
  try {
@@ -182,9 +209,8 @@ router.get('/jobs/timeline', async (req, res) => {
182
209
  }
183
210
  const { apiKey, dateUnit, repoIdentifier } = req.query;
184
211
  const timeframe = resolveAnalyticsTimeframe(req.query);
185
- dbService = new db_service_js_1.FraimDbService();
186
- await dbService.connect();
187
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
212
+ dbService = getAnalyticsDatabase();
213
+ analyticsService = getAnalyticsService();
188
214
  let targetUserId;
189
215
  if (apiKey && typeof apiKey === 'string') {
190
216
  const isAdmin = apiKeyData.userId === 'sid.mathur@gmail.com';
@@ -204,20 +230,14 @@ router.get('/jobs/timeline', async (req, res) => {
204
230
  res.json(timeline);
205
231
  }
206
232
  catch (error) {
207
- console.error('Error fetching job timeline:', error);
208
- res.status(500).json({ error: 'Internal server error' });
209
- }
210
- finally {
211
- if (dbService) {
212
- await dbService.close();
213
- }
233
+ throw error;
214
234
  }
215
235
  });
216
236
  /**
217
237
  * GET /api/analytics/jobs/completion
218
238
  * Get job completion metrics for analytics dashboard
219
239
  */
220
- router.get('/jobs/completion', async (req, res) => {
240
+ analyticsGet('/jobs/completion', async (req, res) => {
221
241
  let dbService;
222
242
  let analyticsService;
223
243
  try {
@@ -227,9 +247,8 @@ router.get('/jobs/completion', async (req, res) => {
227
247
  }
228
248
  const { jobName, apiKey, repoIdentifier } = req.query;
229
249
  const timeframe = resolveAnalyticsTimeframe(req.query);
230
- dbService = new db_service_js_1.FraimDbService();
231
- await dbService.connect();
232
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
250
+ dbService = getAnalyticsDatabase();
251
+ analyticsService = getAnalyticsService();
233
252
  let targetUserId;
234
253
  if (apiKey && typeof apiKey === 'string') {
235
254
  const isAdmin = apiKeyData.userId === 'sid.mathur@gmail.com';
@@ -250,33 +269,26 @@ router.get('/jobs/completion', async (req, res) => {
250
269
  res.json(metrics);
251
270
  }
252
271
  catch (error) {
253
- console.error('Error fetching job completion metrics:', error);
254
- res.status(500).json({ error: 'Internal server error' });
255
- }
256
- finally {
257
- if (dbService) {
258
- await dbService.close();
259
- }
272
+ throw error;
260
273
  }
261
274
  });
262
275
  /**
263
276
  * GET /api/analytics/jobs/runs
264
277
  * Get recent job runs for a specific job name (for clickable table rows)
265
278
  */
266
- router.get('/jobs/runs', async (req, res) => {
279
+ analyticsGet('/jobs/runs', async (req, res) => {
267
280
  let dbService;
268
281
  try {
269
282
  const apiKeyData = req.apiKeyData;
270
283
  if (!apiKeyData) {
271
284
  return res.status(401).json({ error: 'Authentication required' });
272
285
  }
273
- const { period = '30d', jobName, limit = '50', repoIdentifier } = req.query;
286
+ const { jobName, limit = '50', repoIdentifier } = req.query;
274
287
  const timeframe = resolveAnalyticsTimeframe(req.query);
275
288
  if (!jobName || typeof jobName !== 'string') {
276
289
  return res.status(400).json({ error: 'jobName query parameter is required' });
277
290
  }
278
- dbService = new db_service_js_1.FraimDbService();
279
- await dbService.connect();
291
+ dbService = getAnalyticsDatabase();
280
292
  const targetUserId = await resolveTargetUserIdFromQuery(dbService, apiKeyData, req.query);
281
293
  const timeRangeDays = parsePeriodDays(timeframe.period);
282
294
  const startDate = timeframe.startDate ?? new Date(Date.now() - timeRangeDays * 24 * 60 * 60 * 1000);
@@ -284,24 +296,14 @@ router.get('/jobs/runs', async (req, res) => {
284
296
  res.json(runs);
285
297
  }
286
298
  catch (error) {
287
- if (error.status === 403)
288
- return res.status(403).json({ error: error.message });
289
- if (error.status === 404)
290
- return res.status(404).json({ error: error.message });
291
- console.error('Error fetching job runs:', error);
292
- res.status(500).json({ error: 'Internal server error' });
293
- }
294
- finally {
295
- if (dbService) {
296
- await dbService.close();
297
- }
299
+ throw error;
298
300
  }
299
301
  });
300
302
  /**
301
303
  * GET /api/analytics/jobs/run/:jobId/timeline
302
304
  * Get phase-by-phase timeline for a specific job run
303
305
  */
304
- router.get('/jobs/run/:jobId/timeline', async (req, res) => {
306
+ analyticsGet('/jobs/run/:jobId/timeline', async (req, res) => {
305
307
  let dbService;
306
308
  try {
307
309
  const apiKeyData = req.apiKeyData;
@@ -312,8 +314,7 @@ router.get('/jobs/run/:jobId/timeline', async (req, res) => {
312
314
  if (!jobId) {
313
315
  return res.status(400).json({ error: 'jobId parameter is required' });
314
316
  }
315
- dbService = new db_service_js_1.FraimDbService();
316
- await dbService.connect();
317
+ dbService = getAnalyticsDatabase();
317
318
  const targetUserId = await resolveTargetUserIdFromQuery(dbService, apiKeyData, req.query);
318
319
  const jobName = req.query.jobName;
319
320
  const events = await dbService.getJobRunTimeline(jobId, targetUserId, jobName);
@@ -330,24 +331,14 @@ router.get('/jobs/run/:jobId/timeline', async (req, res) => {
330
331
  res.json(timeline);
331
332
  }
332
333
  catch (error) {
333
- if (error.status === 403)
334
- return res.status(403).json({ error: error.message });
335
- if (error.status === 404)
336
- return res.status(404).json({ error: error.message });
337
- console.error('Error fetching job run timeline:', error);
338
- res.status(500).json({ error: 'Internal server error' });
339
- }
340
- finally {
341
- if (dbService) {
342
- await dbService.close();
343
- }
334
+ throw error;
344
335
  }
345
336
  });
346
337
  /**
347
338
  * GET /api/analytics/jobs/:jobName/phases
348
339
  * Get the declared FRAIM phase sequence for a job in registry order.
349
340
  */
350
- router.get('/jobs/:jobName/phases', async (req, res) => {
341
+ analyticsGet('/jobs/:jobName/phases', async (req, res) => {
351
342
  try {
352
343
  const apiKeyData = req.apiKeyData;
353
344
  if (!apiKeyData) {
@@ -365,8 +356,7 @@ router.get('/jobs/:jobName/phases', async (req, res) => {
365
356
  res.json({ jobName, phases });
366
357
  }
367
358
  catch (error) {
368
- console.error('Error fetching job phases:', error);
369
- res.status(500).json({ error: 'Internal server error' });
359
+ throw error;
370
360
  }
371
361
  });
372
362
  function formatDuration(ms) {
@@ -382,7 +372,7 @@ function formatDuration(ms) {
382
372
  * GET /api/analytics/jobs/:jobId/tokens
383
373
  * Get per-job token usage (delta computed from cumulative snapshots)
384
374
  */
385
- router.get('/jobs/:jobId/tokens', async (req, res) => {
375
+ analyticsGet('/jobs/:jobId/tokens', async (req, res) => {
386
376
  let dbService;
387
377
  try {
388
378
  const apiKeyData = req.apiKeyData;
@@ -393,8 +383,7 @@ router.get('/jobs/:jobId/tokens', async (req, res) => {
393
383
  if (!jobId) {
394
384
  return res.status(400).json({ error: 'jobId parameter is required' });
395
385
  }
396
- dbService = new db_service_js_1.FraimDbService();
397
- await dbService.connect();
386
+ dbService = getAnalyticsDatabase();
398
387
  const targetUserId = await resolveTargetUserIdFromQuery(dbService, apiKeyData, req.query);
399
388
  const result = await dbService.getTokenUsageByJob(jobId, targetUserId);
400
389
  if (!result) {
@@ -413,17 +402,7 @@ router.get('/jobs/:jobId/tokens', async (req, res) => {
413
402
  res.json(result);
414
403
  }
415
404
  catch (error) {
416
- if (error.status === 403)
417
- return res.status(403).json({ error: error.message });
418
- if (error.status === 404)
419
- return res.status(404).json({ error: error.message });
420
- console.error('Error fetching job token usage:', error);
421
- res.status(500).json({ error: 'Internal server error' });
422
- }
423
- finally {
424
- if (dbService) {
425
- await dbService.close();
426
- }
405
+ throw error;
427
406
  }
428
407
  });
429
408
  /**
@@ -431,7 +410,7 @@ router.get('/jobs/:jobId/tokens', async (req, res) => {
431
410
  * Issue #330 / R2.1 — token + cost aggregate for the You / Team accordion.
432
411
  * Honors apiKey admin-gate (C2) and rejects local-path repoIdentifier (C3).
433
412
  */
434
- router.get('/usage/aggregate', async (req, res) => {
413
+ analyticsGet('/usage/aggregate', async (req, res) => {
435
414
  let dbService;
436
415
  try {
437
416
  const apiKeyData = req.apiKeyData;
@@ -444,8 +423,7 @@ router.get('/usage/aggregate', async (req, res) => {
444
423
  if (repoIdentifier && typeof repoIdentifier === 'string' && repoIdentifier.length > 0 && (0, git_utils_js_1.isExplicitLocalRepoPath)(repoIdentifier)) {
445
424
  return res.status(400).json({ error: 'Invalid repoIdentifier — local paths are not permitted' });
446
425
  }
447
- dbService = new db_service_js_1.FraimDbService();
448
- await dbService.connect();
426
+ dbService = getAnalyticsDatabase();
449
427
  // C2 — admin-only when querying another user's data.
450
428
  let targetUserId;
451
429
  if (apiKey && typeof apiKey === 'string') {
@@ -471,20 +449,14 @@ router.get('/usage/aggregate', async (req, res) => {
471
449
  res.json(aggregate);
472
450
  }
473
451
  catch (error) {
474
- console.error('Error fetching usage aggregate:', error);
475
- res.status(500).json({ error: 'Internal server error' });
476
- }
477
- finally {
478
- if (dbService) {
479
- await dbService.close();
480
- }
452
+ throw error;
481
453
  }
482
454
  });
483
455
  /**
484
456
  * GET /api/analytics/jobs/:jobName/agent-summary
485
457
  * Issue #330 / R2.8 — per-job-name agent split for the per-job page.
486
458
  */
487
- router.get('/jobs/:jobName/agent-summary', async (req, res) => {
459
+ analyticsGet('/jobs/:jobName/agent-summary', async (req, res) => {
488
460
  let dbService;
489
461
  try {
490
462
  const apiKeyData = req.apiKeyData;
@@ -500,8 +472,7 @@ router.get('/jobs/:jobName/agent-summary', async (req, res) => {
500
472
  if (repoIdentifier && typeof repoIdentifier === 'string' && repoIdentifier.length > 0 && (0, git_utils_js_1.isExplicitLocalRepoPath)(repoIdentifier)) {
501
473
  return res.status(400).json({ error: 'Invalid repoIdentifier — local paths are not permitted' });
502
474
  }
503
- dbService = new db_service_js_1.FraimDbService();
504
- await dbService.connect();
475
+ dbService = getAnalyticsDatabase();
505
476
  const targetUserId = await resolveTargetUserIdFromQuery(dbService, apiKeyData, req.query);
506
477
  const summary = await dbService.getJobAgentSummary({
507
478
  jobName,
@@ -513,55 +484,35 @@ router.get('/jobs/:jobName/agent-summary', async (req, res) => {
513
484
  res.json(summary);
514
485
  }
515
486
  catch (error) {
516
- if (error.status === 403)
517
- return res.status(403).json({ error: error.message });
518
- if (error.status === 404)
519
- return res.status(404).json({ error: error.message });
520
- console.error('Error fetching job agent summary:', error);
521
- res.status(500).json({ error: 'Internal server error' });
522
- }
523
- finally {
524
- if (dbService) {
525
- await dbService.close();
526
- }
487
+ throw error;
527
488
  }
528
489
  });
529
490
  /**
530
491
  * GET /api/analytics/jobs/token-summary
531
492
  * Aggregated token usage across all jobs for a user
532
493
  */
533
- router.get('/jobs/token-summary', async (req, res) => {
494
+ analyticsGet('/jobs/token-summary', async (req, res) => {
534
495
  let dbService;
535
496
  try {
536
497
  const apiKeyData = req.apiKeyData;
537
498
  if (!apiKeyData) {
538
499
  return res.status(401).json({ error: 'Authentication required' });
539
500
  }
540
- const period = req.query.period || '30d';
541
- const daysMatch = period.match(/^(\d+)d$/);
542
- const days = daysMatch ? parseInt(daysMatch[1], 10) : 30;
543
- const startDate = new Date();
544
- startDate.setDate(startDate.getDate() - days);
545
- dbService = new db_service_js_1.FraimDbService();
546
- await dbService.connect();
501
+ const timeframe = resolveAnalyticsTimeframe(req.query);
502
+ const startDate = timeframe.startDate ?? new Date(Date.now() - parsePeriodDays(timeframe.period) * 86400_000);
503
+ dbService = getAnalyticsDatabase();
547
504
  const result = await dbService.getTokenUsageSummary(apiKeyData.userId, startDate);
548
505
  res.json(result);
549
506
  }
550
507
  catch (error) {
551
- console.error('Error fetching token usage summary:', error);
552
- res.status(500).json({ error: 'Internal server error' });
553
- }
554
- finally {
555
- if (dbService) {
556
- await dbService.close();
557
- }
508
+ throw error;
558
509
  }
559
510
  });
560
511
  /**
561
512
  * GET /api/analytics/export
562
513
  * Export usage data as CSV
563
514
  */
564
- router.get('/export', async (req, res) => {
515
+ analyticsGet('/export', async (req, res) => {
565
516
  let dbService;
566
517
  let analyticsService;
567
518
  try {
@@ -576,10 +527,9 @@ router.get('/export', async (req, res) => {
576
527
  return res.status(400).json({ error: 'Only CSV format is currently supported' });
577
528
  }
578
529
  // Create database connection
579
- dbService = new db_service_js_1.FraimDbService();
580
- await dbService.connect();
530
+ dbService = getAnalyticsDatabase();
581
531
  // Create analytics service
582
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
532
+ analyticsService = getAnalyticsService();
583
533
  let targetUserId;
584
534
  if (apiKey && typeof apiKey === 'string') {
585
535
  const isAdmin = apiKeyData.userId === 'sid.mathur@gmail.com';
@@ -608,20 +558,14 @@ router.get('/export', async (req, res) => {
608
558
  res.send(csvData);
609
559
  }
610
560
  catch (error) {
611
- console.error('Error exporting usage data:', error);
612
- res.status(500).json({ error: 'Internal server error' });
613
- }
614
- finally {
615
- if (dbService) {
616
- await dbService.close();
617
- }
561
+ throw error;
618
562
  }
619
563
  });
620
564
  /**
621
565
  * GET /api/analytics/users
622
566
  * Get list of users with their API keys and event counts (admin only)
623
567
  */
624
- router.get('/users', async (req, res) => {
568
+ analyticsGet('/users', async (req, res) => {
625
569
  let dbService;
626
570
  try {
627
571
  // Get API key data from middleware
@@ -635,8 +579,7 @@ router.get('/users', async (req, res) => {
635
579
  return res.status(403).json({ error: 'Admin access required' });
636
580
  }
637
581
  // Create database connection
638
- dbService = new db_service_js_1.FraimDbService();
639
- await dbService.connect();
582
+ dbService = getAnalyticsDatabase();
640
583
  // Get all API keys
641
584
  const apiKeys = await dbService.listApiKeys();
642
585
  // Get event counts for each API key
@@ -663,20 +606,14 @@ router.get('/users', async (req, res) => {
663
606
  res.json(usersWithCounts);
664
607
  }
665
608
  catch (error) {
666
- console.error('Error fetching users:', error);
667
- res.status(500).json({ error: 'Internal server error' });
668
- }
669
- finally {
670
- if (dbService) {
671
- await dbService.close();
672
- }
609
+ throw error;
673
610
  }
674
611
  });
675
612
  /**
676
613
  * GET /api/analytics/repos
677
614
  * Get unique repositories that have generated telemetry for a user
678
615
  */
679
- router.get('/repos', async (req, res) => {
616
+ analyticsGet('/repos', async (req, res) => {
680
617
  let dbService;
681
618
  let analyticsService;
682
619
  try {
@@ -684,20 +621,13 @@ router.get('/repos', async (req, res) => {
684
621
  if (!apiKeyData) {
685
622
  return res.status(401).json({ error: 'Authentication required' });
686
623
  }
687
- dbService = new db_service_js_1.FraimDbService();
688
- await dbService.connect();
689
- analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
624
+ dbService = getAnalyticsDatabase();
625
+ analyticsService = getAnalyticsService();
690
626
  const repos = await analyticsService.getUniqueRepos(apiKeyData.userId);
691
627
  res.json(repos);
692
628
  }
693
629
  catch (error) {
694
- console.error('Error fetching repositories:', error);
695
- res.status(500).json({ error: 'Internal server error' });
696
- }
697
- finally {
698
- if (dbService) {
699
- await dbService.close();
700
- }
630
+ throw error;
701
631
  }
702
632
  });
703
633
  /**
@@ -875,7 +805,7 @@ router.post('/quality-score', async (req, res) => {
875
805
  * GET /api/analytics/me
876
806
  * Get current user information
877
807
  */
878
- router.get('/me', async (req, res) => {
808
+ analyticsGet('/me', async (req, res) => {
879
809
  try {
880
810
  // Get API key data from middleware
881
811
  const apiKeyData = req.apiKeyData;
@@ -889,8 +819,7 @@ router.get('/me', async (req, res) => {
889
819
  });
890
820
  }
891
821
  catch (error) {
892
- console.error('Error getting user info:', error);
893
- res.status(500).json({ error: 'Failed to get user information' });
822
+ throw error;
894
823
  }
895
824
  });
896
825
  // ===== TEAM ROUTES =====
@@ -913,7 +842,9 @@ function parseDateParam(value, endOfDay = false) {
913
842
  return date;
914
843
  }
915
844
  function resolveAnalyticsTimeframe(query) {
916
- const period = typeof query.period === 'string' && query.period ? query.period : '30d';
845
+ const requestedPeriod = typeof query.period === 'string' && query.period ? query.period : '7d';
846
+ const period = requestedPeriod === '365d' ? '90d'
847
+ : ['7d', '30d', '90d'].includes(requestedPeriod) ? requestedPeriod : '7d';
917
848
  const startDate = parseDateParam(query.startDate);
918
849
  const endDate = parseDateParam(query.endDate, true);
919
850
  if ((query.startDate || query.endDate) && (!startDate || !endDate)) {
@@ -926,7 +857,11 @@ function resolveAnalyticsTimeframe(query) {
926
857
  err.status = 400;
927
858
  throw err;
928
859
  }
929
- return startDate && endDate ? { period: 'custom', startDate, endDate } : { period };
860
+ if (startDate && endDate) {
861
+ const retainedStart = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
862
+ return { period: 'custom', startDate: startDate < retainedStart ? retainedStart : startDate, endDate };
863
+ }
864
+ return { period };
930
865
  }
931
866
  async function resolveManagerMember(dbService, managerId, memberId) {
932
867
  const members = await dbService.getTeamMembers(managerId);
@@ -976,30 +911,46 @@ async function resolveTargetUserIdFromQuery(dbService, apiKeyData, query) {
976
911
  * Returns team member IDs only — fast, no stats queries.
977
912
  * Used by the dashboard to render the member list immediately before lazy-loading stats.
978
913
  */
979
- router.get('/team/members/list', async (req, res) => {
914
+ analyticsGet('/team/members/list', async (req, res) => {
980
915
  let dbService;
981
916
  try {
982
917
  const apiKeyData = req.apiKeyData;
983
918
  if (!apiKeyData)
984
919
  return res.status(401).json({ error: 'Authentication required' });
985
- dbService = new db_service_js_1.FraimDbService();
986
- await dbService.connect();
920
+ dbService = getAnalyticsDatabase();
987
921
  const members = await dbService.getTeamMembers(apiKeyData.userId);
988
922
  res.json(members.map(m => ({ memberId: m.memberId, labels: m.labels || [] })));
989
923
  }
990
924
  catch (error) {
991
- res.status(error.status || 500).json({ error: error.message || 'Failed to load team' });
992
- }
993
- finally {
994
- if (dbService)
995
- await dbService.close();
925
+ throw error;
996
926
  }
997
927
  });
928
+ analyticsGet('/team/members/summaries', async (req, res) => {
929
+ const apiKeyData = req.apiKeyData;
930
+ if (!apiKeyData)
931
+ return res.status(401).json({ error: 'Authentication required' });
932
+ const ids = typeof req.query.ids === 'string' ? req.query.ids.split(',') : [];
933
+ if (!ids.length || ids.some(id => !id.trim() || id !== id.trim() || id.length > 320) || ids.length > 20) {
934
+ return res.status(400).json({ error: 'ids must contain between 1 and 20 member IDs' });
935
+ }
936
+ const memberIds = [...new Set(ids)];
937
+ const timeframe = resolveAnalyticsTimeframe(req.query);
938
+ const database = getAnalyticsDatabase();
939
+ const members = await database.getTeamMembers(apiKeyData.userId);
940
+ const authorized = new Set(members.map(member => member.memberId));
941
+ if (memberIds.some(id => !authorized.has(id)))
942
+ return res.status(403).json({ error: 'Not authorized to view this member' });
943
+ const repoIdentifier = typeof req.query.repoIdentifier === 'string' ? req.query.repoIdentifier : undefined;
944
+ if (repoIdentifier && (0, git_utils_js_1.isExplicitLocalRepoPath)(repoIdentifier))
945
+ return res.status(400).json({ error: 'Invalid repoIdentifier' });
946
+ const summaries = await getAnalyticsService().getTeamMemberSummaries(timeframe.period, memberIds, repoIdentifier, timeframe.startDate, timeframe.endDate);
947
+ return res.json(summaries);
948
+ });
998
949
  /**
999
950
  * GET /api/analytics/team/members/:memberId/summary?period=30d
1000
951
  * Returns stats for a single member. Called lazily per-member after the list renders.
1001
952
  */
1002
- router.get('/team/members/:memberId/summary', async (req, res) => {
953
+ analyticsGet('/team/members/:memberId/summary', async (req, res) => {
1003
954
  let dbService;
1004
955
  try {
1005
956
  const apiKeyData = req.apiKeyData;
@@ -1007,17 +958,20 @@ router.get('/team/members/:memberId/summary', async (req, res) => {
1007
958
  return res.status(401).json({ error: 'Authentication required' });
1008
959
  const timeframe = resolveAnalyticsTimeframe(req.query);
1009
960
  const memberId = req.params.memberId;
1010
- dbService = new db_service_js_1.FraimDbService();
1011
- await dbService.connect();
961
+ dbService = getAnalyticsDatabase();
1012
962
  await resolveManagerMember(dbService, apiKeyData.userId, memberId); // auth check
1013
- const analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
1014
- const [dominant, stats, allTimeLastEventAt, hasFraimSetup, keyLifecycle] = await Promise.allSettled([
963
+ const analyticsService = getAnalyticsService();
964
+ const summaryResults = await Promise.allSettled([
1015
965
  analyticsService.getDominantJobCategory(timeframe.period, memberId, undefined, timeframe.startDate, timeframe.endDate),
1016
966
  analyticsService.getUsageStats({ period: timeframe.period, startDate: timeframe.startDate, endDate: timeframe.endDate, types: ['job'], userId: memberId }),
1017
967
  dbService.getLatestUsageEventAt(memberId, ['job']),
1018
968
  dbService.hasFraimSetup(memberId),
1019
969
  dbService.getApiKeyByUserId(memberId, false),
1020
970
  ]);
971
+ const summaryFailure = summaryResults.find(result => result.status === 'rejected');
972
+ if (summaryFailure?.status === 'rejected')
973
+ throw summaryFailure.reason;
974
+ const [dominant, stats, allTimeLastEventAt, hasFraimSetup, keyLifecycle] = summaryResults;
1021
975
  const lastEventAt = stats.status === 'fulfilled' ? stats.value.lastEventAt ?? null : null;
1022
976
  const historicalLastEventAt = allTimeLastEventAt.status === 'fulfilled' ? allTimeLastEventAt.value ?? null : null;
1023
977
  const lifecycle = keyLifecycle.status === 'fulfilled'
@@ -1036,11 +990,7 @@ router.get('/team/members/:memberId/summary', async (req, res) => {
1036
990
  });
1037
991
  }
1038
992
  catch (error) {
1039
- res.status(error.status || 500).json({ error: error.message || 'Failed to load member summary' });
1040
- }
1041
- finally {
1042
- if (dbService)
1043
- await dbService.close();
993
+ throw error;
1044
994
  }
1045
995
  });
1046
996
  /**
@@ -1048,56 +998,34 @@ router.get('/team/members/:memberId/summary', async (req, res) => {
1048
998
  * Returns team members with dominant category (manager view).
1049
999
  * Kept for backwards compatibility — prefer /list + /summary for non-blocking UI.
1050
1000
  */
1051
- router.get('/team/members', async (req, res) => {
1001
+ analyticsGet('/team/members', async (req, res) => {
1052
1002
  let dbService;
1053
1003
  try {
1054
1004
  const apiKeyData = req.apiKeyData;
1055
1005
  if (!apiKeyData)
1056
1006
  return res.status(401).json({ error: 'Authentication required' });
1057
1007
  const timeframe = resolveAnalyticsTimeframe(req.query);
1058
- dbService = new db_service_js_1.FraimDbService();
1059
- await dbService.connect();
1060
- const analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
1008
+ dbService = getAnalyticsDatabase();
1009
+ const analyticsService = getAnalyticsService();
1061
1010
  const members = await dbService.getTeamMembers(apiKeyData.userId);
1062
1011
  if (members.length === 0)
1063
1012
  return res.json([]);
1064
- const results = await Promise.allSettled(members.map(async (m) => {
1065
- const [dominant, stats, allTimeLastEventAt, hasFraimSetup] = await Promise.allSettled([
1066
- analyticsService.getDominantJobCategory(timeframe.period, m.memberId, undefined, timeframe.startDate, timeframe.endDate),
1067
- analyticsService.getUsageStats({ period: timeframe.period, startDate: timeframe.startDate, endDate: timeframe.endDate, types: ['job'], userId: m.memberId }),
1068
- dbService.getLatestUsageEventAt(m.memberId, ['job']),
1069
- dbService.hasFraimSetup(m.memberId)
1070
- ]);
1071
- const lastEventAt = stats.status === 'fulfilled' ? stats.value.lastEventAt ?? null : null;
1072
- const historicalLastEventAt = allTimeLastEventAt.status === 'fulfilled' ? allTimeLastEventAt.value ?? null : null;
1073
- return {
1074
- memberId: m.memberId,
1075
- labels: m.labels || [],
1076
- dominantCategory: dominant.status === 'fulfilled' ? dominant.value?.category ?? null : null,
1077
- dominantCategoryCount: dominant.status === 'fulfilled' ? dominant.value?.count ?? 0 : 0,
1078
- totalEvents: stats.status === 'fulfilled' ? stats.value.totalEvents : 0,
1079
- hasFraimSetup: hasFraimSetup.status === 'fulfilled' ? hasFraimSetup.value : false,
1080
- lastEventAt,
1081
- allTimeLastEventAt: historicalLastEventAt,
1082
- lastActiveAt: lastEventAt
1083
- };
1084
- }));
1085
- res.json(results
1086
- .filter(r => r.status === 'fulfilled')
1087
- .map(r => r.value));
1013
+ const summaries = await analyticsService.getTeamMemberSummaries(timeframe.period, members.map(member => member.memberId), undefined, timeframe.startDate, timeframe.endDate);
1014
+ // Historical activity remains available for callers of the legacy endpoint,
1015
+ // but is not part of the default overview request.
1016
+ const historical = await dbService.getLatestUsageEventsByUser(members.map(member => member.memberId), ['job']);
1017
+ const labels = new Map(members.map(member => [member.memberId, member.labels || []]));
1018
+ res.json(summaries.map(summary => ({ ...summary, labels: labels.get(summary.memberId),
1019
+ allTimeLastEventAt: historical.get(summary.memberId) ?? null })));
1088
1020
  }
1089
1021
  catch (error) {
1090
- res.status(error.status || 500).json({ error: error.message || 'Failed to load team' });
1091
- }
1092
- finally {
1093
- if (dbService)
1094
- await dbService.close();
1022
+ throw error;
1095
1023
  }
1096
1024
  });
1097
1025
  /**
1098
1026
  * GET /api/analytics/team/member/:memberId/stats
1099
1027
  */
1100
- router.get('/team/member/:memberId/stats', async (req, res) => {
1028
+ analyticsGet('/team/member/:memberId/stats', async (req, res) => {
1101
1029
  let dbService;
1102
1030
  try {
1103
1031
  const apiKeyData = req.apiKeyData;
@@ -1106,10 +1034,9 @@ router.get('/team/member/:memberId/stats', async (req, res) => {
1106
1034
  const { memberId } = req.params;
1107
1035
  const { repoIdentifier } = req.query;
1108
1036
  const timeframe = resolveAnalyticsTimeframe(req.query);
1109
- dbService = new db_service_js_1.FraimDbService();
1110
- await dbService.connect();
1037
+ dbService = getAnalyticsDatabase();
1111
1038
  const memberUserId = await resolveManagerMember(dbService, apiKeyData.userId, memberId);
1112
- const analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
1039
+ const analyticsService = getAnalyticsService();
1113
1040
  const stats = await analyticsService.getUsageStats({
1114
1041
  period: timeframe.period,
1115
1042
  startDate: timeframe.startDate,
@@ -1121,11 +1048,7 @@ router.get('/team/member/:memberId/stats', async (req, res) => {
1121
1048
  res.json(stats);
1122
1049
  }
1123
1050
  catch (error) {
1124
- res.status(error.status || 500).json({ error: error.message || 'Failed to load member stats' });
1125
- }
1126
- finally {
1127
- if (dbService)
1128
- await dbService.close();
1051
+ throw error;
1129
1052
  }
1130
1053
  });
1131
1054
  /**
@@ -1133,7 +1056,7 @@ router.get('/team/member/:memberId/stats', async (req, res) => {
1133
1056
  * Issue #330 / R2.5 — token + cost aggregate for a team member, scoped
1134
1057
  * to the calling manager's permission set via resolveManagerMember.
1135
1058
  */
1136
- router.get('/team/member/:memberId/usage/aggregate', async (req, res) => {
1059
+ analyticsGet('/team/member/:memberId/usage/aggregate', async (req, res) => {
1137
1060
  let dbService;
1138
1061
  try {
1139
1062
  const apiKeyData = req.apiKeyData;
@@ -1145,8 +1068,7 @@ router.get('/team/member/:memberId/usage/aggregate', async (req, res) => {
1145
1068
  if (repoIdentifier && typeof repoIdentifier === 'string' && repoIdentifier.length > 0 && (0, git_utils_js_1.isExplicitLocalRepoPath)(repoIdentifier)) {
1146
1069
  return res.status(400).json({ error: 'Invalid repoIdentifier — local paths are not permitted' });
1147
1070
  }
1148
- dbService = new db_service_js_1.FraimDbService();
1149
- await dbService.connect();
1071
+ dbService = getAnalyticsDatabase();
1150
1072
  const memberUserId = await resolveManagerMember(dbService, apiKeyData.userId, memberId);
1151
1073
  const aggregate = await dbService.getUsageAggregate({
1152
1074
  userId: memberUserId,
@@ -1157,17 +1079,13 @@ router.get('/team/member/:memberId/usage/aggregate', async (req, res) => {
1157
1079
  res.json(aggregate);
1158
1080
  }
1159
1081
  catch (error) {
1160
- res.status(error.status || 500).json({ error: error.message || 'Failed to load member usage aggregate' });
1161
- }
1162
- finally {
1163
- if (dbService)
1164
- await dbService.close();
1082
+ throw error;
1165
1083
  }
1166
1084
  });
1167
1085
  /**
1168
1086
  * GET /api/analytics/team/member/:memberId/components/top
1169
1087
  */
1170
- router.get('/team/member/:memberId/components/top', async (req, res) => {
1088
+ analyticsGet('/team/member/:memberId/components/top', async (req, res) => {
1171
1089
  let dbService;
1172
1090
  try {
1173
1091
  const apiKeyData = req.apiKeyData;
@@ -1176,25 +1094,20 @@ router.get('/team/member/:memberId/components/top', async (req, res) => {
1176
1094
  const { memberId } = req.params;
1177
1095
  const { limit = '10', repoIdentifier } = req.query;
1178
1096
  const timeframe = resolveAnalyticsTimeframe(req.query);
1179
- dbService = new db_service_js_1.FraimDbService();
1180
- await dbService.connect();
1097
+ dbService = getAnalyticsDatabase();
1181
1098
  const memberUserId = await resolveManagerMember(dbService, apiKeyData.userId, memberId);
1182
- const analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
1099
+ const analyticsService = getAnalyticsService();
1183
1100
  const components = await analyticsService.getTopComponents(parseInt(limit, 10), timeframe.period, ['job'], memberUserId, repoIdentifier, timeframe.startDate, timeframe.endDate);
1184
1101
  res.json(components);
1185
1102
  }
1186
1103
  catch (error) {
1187
- res.status(error.status || 500).json({ error: error.message || 'Failed to load member components' });
1188
- }
1189
- finally {
1190
- if (dbService)
1191
- await dbService.close();
1104
+ throw error;
1192
1105
  }
1193
1106
  });
1194
1107
  /**
1195
1108
  * GET /api/analytics/team/member/:memberId/jobs/timeline
1196
1109
  */
1197
- router.get('/team/member/:memberId/jobs/timeline', async (req, res) => {
1110
+ analyticsGet('/team/member/:memberId/jobs/timeline', async (req, res) => {
1198
1111
  let dbService;
1199
1112
  try {
1200
1113
  const apiKeyData = req.apiKeyData;
@@ -1203,64 +1116,49 @@ router.get('/team/member/:memberId/jobs/timeline', async (req, res) => {
1203
1116
  const { memberId } = req.params;
1204
1117
  const { dateUnit, repoIdentifier } = req.query;
1205
1118
  const timeframe = resolveAnalyticsTimeframe(req.query);
1206
- dbService = new db_service_js_1.FraimDbService();
1207
- await dbService.connect();
1119
+ dbService = getAnalyticsDatabase();
1208
1120
  const memberUserId = await resolveManagerMember(dbService, apiKeyData.userId, memberId);
1209
- const analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
1121
+ const analyticsService = getAnalyticsService();
1210
1122
  const timeline = await analyticsService.getJobRunsOverTime(timeframe.period, memberUserId, dateUnit, repoIdentifier, timeframe.startDate, timeframe.endDate);
1211
1123
  res.json(timeline);
1212
1124
  }
1213
1125
  catch (error) {
1214
- res.status(error.status || 500).json({ error: error.message || 'Failed to load member timeline' });
1215
- }
1216
- finally {
1217
- if (dbService)
1218
- await dbService.close();
1126
+ throw error;
1219
1127
  }
1220
1128
  });
1221
- router.get('/team/member/:memberId/repos', async (req, res) => {
1129
+ analyticsGet('/team/member/:memberId/repos', async (req, res) => {
1222
1130
  let dbService;
1223
1131
  try {
1224
1132
  const apiKeyData = req.apiKeyData;
1225
1133
  if (!apiKeyData)
1226
1134
  return res.status(401).json({ error: 'Authentication required' });
1227
1135
  const { memberId } = req.params;
1228
- dbService = new db_service_js_1.FraimDbService();
1229
- await dbService.connect();
1136
+ dbService = getAnalyticsDatabase();
1230
1137
  const memberUserId = await resolveManagerMember(dbService, apiKeyData.userId, memberId);
1231
- const analyticsService = new usage_analytics_service_js_1.UsageAnalyticsService(dbService);
1138
+ const analyticsService = getAnalyticsService();
1232
1139
  const repos = await analyticsService.getUniqueRepos(memberUserId);
1233
1140
  res.json(repos);
1234
1141
  }
1235
1142
  catch (error) {
1236
- res.status(error.status || 500).json({ error: error.message || 'Failed to load member repositories' });
1237
- }
1238
- finally {
1239
- if (dbService)
1240
- await dbService.close();
1143
+ throw error;
1241
1144
  }
1242
1145
  });
1243
1146
  /**
1244
1147
  * GET /api/analytics/team/my-managers
1245
1148
  * Returns list of manager emails for the current user (for transparency notice)
1246
1149
  */
1247
- router.get('/team/my-managers', async (req, res) => {
1150
+ analyticsGet('/team/my-managers', async (req, res) => {
1248
1151
  let dbService;
1249
1152
  try {
1250
1153
  const apiKeyData = req.apiKeyData;
1251
1154
  if (!apiKeyData)
1252
1155
  return res.status(401).json({ error: 'Authentication required' });
1253
- dbService = new db_service_js_1.FraimDbService();
1254
- await dbService.connect();
1156
+ dbService = getAnalyticsDatabase();
1255
1157
  const managers = await dbService.getManagersForMember(apiKeyData.userId);
1256
1158
  res.json(managers);
1257
1159
  }
1258
1160
  catch (error) {
1259
- res.status(500).json({ error: 'Failed to load managers' });
1260
- }
1261
- finally {
1262
- if (dbService)
1263
- await dbService.close();
1161
+ throw error;
1264
1162
  }
1265
1163
  });
1266
1164
  /**
@@ -1268,7 +1166,7 @@ router.get('/team/my-managers', async (req, res) => {
1268
1166
  * Returns per-interview quality scores and latest gate decision for a team member.
1269
1167
  * Used by the manager dashboard Quality tab (Issue #251).
1270
1168
  */
1271
- router.get('/team/member/:memberId/quality/interviews', async (req, res) => {
1169
+ analyticsGet('/team/member/:memberId/quality/interviews', async (req, res) => {
1272
1170
  let dbService;
1273
1171
  try {
1274
1172
  const apiKeyData = req.apiKeyData;
@@ -1276,8 +1174,7 @@ router.get('/team/member/:memberId/quality/interviews', async (req, res) => {
1276
1174
  return res.status(401).json({ error: 'Authentication required' });
1277
1175
  const { memberId } = req.params;
1278
1176
  const { repoIdentifier } = req.query;
1279
- dbService = new db_service_js_1.FraimDbService();
1280
- await dbService.connect();
1177
+ dbService = getAnalyticsDatabase();
1281
1178
  // Privacy check: manager must have active relationship with member
1282
1179
  await resolveManagerMember(dbService, apiKeyData.userId, memberId);
1283
1180
  // Fetch interview quality scores (chronological)
@@ -1330,11 +1227,7 @@ router.get('/team/member/:memberId/quality/interviews', async (req, res) => {
1330
1227
  });
1331
1228
  }
1332
1229
  catch (error) {
1333
- res.status(error.status || 500).json({ error: error.message || 'Failed to load quality data' });
1334
- }
1335
- finally {
1336
- if (dbService)
1337
- await dbService.close();
1230
+ throw error;
1338
1231
  }
1339
1232
  });
1340
1233
  /**
@@ -1377,7 +1270,7 @@ router.post('/quality', async (req, res) => {
1377
1270
  * Returns quality scorecard: one summary per founder journey stage.
1378
1271
  * Used by the quality tile grid in both personal and manager views.
1379
1272
  */
1380
- router.get('/quality/scorecard/:userId', async (req, res) => {
1273
+ analyticsGet('/quality/scorecard/:userId', async (req, res) => {
1381
1274
  let dbService;
1382
1275
  try {
1383
1276
  const apiKeyData = req.apiKeyData;
@@ -1387,8 +1280,7 @@ router.get('/quality/scorecard/:userId', async (req, res) => {
1387
1280
  const { repoIdentifier } = req.query;
1388
1281
  const timeframe = resolveAnalyticsTimeframe(req.query);
1389
1282
  const periodDays = timeframe.startDate ? undefined : parsePeriodDays(timeframe.period);
1390
- dbService = new db_service_js_1.FraimDbService();
1391
- await dbService.connect();
1283
+ dbService = getAnalyticsDatabase();
1392
1284
  // If requesting another user's data, verify manager-member relationship
1393
1285
  if (apiKeyData.userId !== userId) {
1394
1286
  await resolveManagerMember(dbService, apiKeyData.userId, userId);
@@ -1397,18 +1289,14 @@ router.get('/quality/scorecard/:userId', async (req, res) => {
1397
1289
  res.json({ stages });
1398
1290
  }
1399
1291
  catch (error) {
1400
- res.status(error.status || 500).json({ error: error.message || 'Failed to load quality scorecard' });
1401
- }
1402
- finally {
1403
- if (dbService)
1404
- await dbService.close();
1292
+ throw error;
1405
1293
  }
1406
1294
  });
1407
1295
  /**
1408
1296
  * GET /api/analytics/quality/stage/:userId/:stageCategory
1409
1297
  * Returns full assessment history for a single stage (tile click detail view).
1410
1298
  */
1411
- router.get('/quality/stage/:userId/:stageCategory', async (req, res) => {
1299
+ analyticsGet('/quality/stage/:userId/:stageCategory', async (req, res) => {
1412
1300
  let dbService;
1413
1301
  try {
1414
1302
  const apiKeyData = req.apiKeyData;
@@ -1418,8 +1306,7 @@ router.get('/quality/stage/:userId/:stageCategory', async (req, res) => {
1418
1306
  const { repoIdentifier } = req.query;
1419
1307
  const timeframe = resolveAnalyticsTimeframe(req.query);
1420
1308
  const periodDays = timeframe.startDate ? undefined : parsePeriodDays(timeframe.period);
1421
- dbService = new db_service_js_1.FraimDbService();
1422
- await dbService.connect();
1309
+ dbService = getAnalyticsDatabase();
1423
1310
  // If requesting another user's data, verify manager-member relationship
1424
1311
  if (apiKeyData.userId !== userId) {
1425
1312
  await resolveManagerMember(dbService, apiKeyData.userId, userId);
@@ -1428,11 +1315,7 @@ router.get('/quality/stage/:userId/:stageCategory', async (req, res) => {
1428
1315
  res.json(detail);
1429
1316
  }
1430
1317
  catch (error) {
1431
- res.status(error.status || 500).json({ error: error.message || 'Failed to load stage detail' });
1432
- }
1433
- finally {
1434
- if (dbService)
1435
- await dbService.close();
1318
+ throw error;
1436
1319
  }
1437
1320
  });
1438
1321
  /**