@littlebearapps/platform-admin-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +112 -0
  2. package/dist/index.d.ts +16 -0
  3. package/dist/index.js +89 -0
  4. package/dist/prompts.d.ts +27 -0
  5. package/dist/prompts.js +80 -0
  6. package/dist/scaffold.d.ts +5 -0
  7. package/dist/scaffold.js +65 -0
  8. package/dist/templates.d.ts +16 -0
  9. package/dist/templates.js +131 -0
  10. package/package.json +46 -0
  11. package/templates/full/migrations/006_pattern_discovery.sql +199 -0
  12. package/templates/full/migrations/007_notifications_search.sql +127 -0
  13. package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
  14. package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
  15. package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
  16. package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
  17. package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
  18. package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
  19. package/templates/full/workers/pattern-discovery.ts +661 -0
  20. package/templates/full/workers/platform-alert-router.ts +1809 -0
  21. package/templates/full/workers/platform-notifications.ts +424 -0
  22. package/templates/full/workers/platform-search.ts +480 -0
  23. package/templates/full/workers/platform-settings.ts +436 -0
  24. package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
  25. package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
  26. package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
  27. package/templates/full/wrangler.search.jsonc.hbs +16 -0
  28. package/templates/full/wrangler.settings.jsonc.hbs +23 -0
  29. package/templates/shared/README.md.hbs +69 -0
  30. package/templates/shared/config/budgets.yaml.hbs +72 -0
  31. package/templates/shared/config/services.yaml.hbs +45 -0
  32. package/templates/shared/migrations/001_core_tables.sql +117 -0
  33. package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
  34. package/templates/shared/migrations/003_feature_tracking.sql +250 -0
  35. package/templates/shared/migrations/004_settings_alerts.sql +452 -0
  36. package/templates/shared/migrations/seed.sql.hbs +4 -0
  37. package/templates/shared/package.json.hbs +21 -0
  38. package/templates/shared/scripts/sync-config.ts +242 -0
  39. package/templates/shared/tsconfig.json +12 -0
  40. package/templates/shared/workers/lib/analytics-engine.ts +357 -0
  41. package/templates/shared/workers/lib/billing.ts +293 -0
  42. package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
  43. package/templates/shared/workers/lib/control.ts +292 -0
  44. package/templates/shared/workers/lib/economics.ts +368 -0
  45. package/templates/shared/workers/lib/metrics.ts +103 -0
  46. package/templates/shared/workers/lib/platform-settings.ts +407 -0
  47. package/templates/shared/workers/lib/shared/allowances.ts +333 -0
  48. package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
  49. package/templates/shared/workers/lib/shared/types.ts +58 -0
  50. package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
  51. package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
  52. package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
  53. package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
  54. package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
  55. package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
  56. package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
  57. package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
  58. package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
  59. package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
  60. package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
  61. package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
  62. package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
  63. package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
  64. package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
  65. package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
  66. package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
  67. package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
  68. package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
  69. package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
  70. package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
  71. package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
  72. package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
  73. package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
  74. package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
  75. package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
  76. package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
  77. package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
  78. package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
  79. package/templates/shared/workers/platform-usage.ts +1915 -0
  80. package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
  81. package/templates/standard/migrations/005_error_collection.sql +162 -0
  82. package/templates/standard/workers/error-collector.ts +2670 -0
  83. package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
  84. package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
  85. package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
  86. package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
  87. package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
  88. package/templates/standard/workers/lib/error-collector/github.ts +329 -0
  89. package/templates/standard/workers/lib/error-collector/types.ts +262 -0
  90. package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
  91. package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
  92. package/templates/standard/workers/platform-sentinel.ts +1744 -0
  93. package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
  94. package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
@@ -0,0 +1,364 @@
1
+ /**
2
+ * DLQ Admin Handlers
3
+ *
4
+ * Admin endpoints for managing Dead Letter Queue messages.
5
+ * Provides visibility into failed messages and replay capability.
6
+ */
7
+
8
+ import type { Env, TelemetryMessage } from '../shared';
9
+ import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
10
+ import { jsonResponse, generateId } from '../shared';
11
+
12
+ // =============================================================================
13
+ // TYPES
14
+ // =============================================================================
15
+
16
+ interface DLQMessage {
17
+ id: string;
18
+ feature_key: string;
19
+ project: string;
20
+ category: string | null;
21
+ feature: string | null;
22
+ error_message: string | null;
23
+ error_category: string | null;
24
+ error_fingerprint: string | null;
25
+ retry_count: number;
26
+ correlation_id: string | null;
27
+ status: string;
28
+ created_at: number;
29
+ replayed_at: number | null;
30
+ }
31
+
32
+ interface DLQListResponse {
33
+ success: boolean;
34
+ messages: DLQMessage[];
35
+ total: number;
36
+ pending: number;
37
+ replayed: number;
38
+ discarded: number;
39
+ timestamp: string;
40
+ }
41
+
42
+ interface DLQStatsResponse {
43
+ success: boolean;
44
+ stats: {
45
+ total: number;
46
+ pending: number;
47
+ replayed: number;
48
+ discarded: number;
49
+ byProject: Record<string, number>;
50
+ byErrorCategory: Record<string, number>;
51
+ oldestPending: string | null;
52
+ };
53
+ timestamp: string;
54
+ }
55
+
56
+ // =============================================================================
57
+ // LIST DLQ MESSAGES
58
+ // =============================================================================
59
+
60
+ /**
61
+ * List DLQ messages with optional filtering.
62
+ * GET /admin/dlq?status=pending&project=my-app&limit=50
63
+ */
64
+ export async function handleListDLQ(url: URL, env: Env): Promise<Response> {
65
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:dlq-admin');
66
+
67
+ const status = url.searchParams.get('status') || 'pending';
68
+ const project = url.searchParams.get('project');
69
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '50', 10), 100);
70
+ const offset = parseInt(url.searchParams.get('offset') || '0', 10);
71
+
72
+ try {
73
+ // Build query with filters
74
+ let query = `SELECT id, feature_key, project, category, feature, error_message,
75
+ error_category, error_fingerprint, retry_count, correlation_id,
76
+ status, created_at, replayed_at
77
+ FROM dead_letter_queue WHERE 1=1`;
78
+ const params: (string | number)[] = [];
79
+
80
+ if (status !== 'all') {
81
+ query += ` AND status = ?`;
82
+ params.push(status);
83
+ }
84
+
85
+ if (project) {
86
+ query += ` AND project = ?`;
87
+ params.push(project);
88
+ }
89
+
90
+ query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`;
91
+ params.push(limit, offset);
92
+
93
+ const result = await env.PLATFORM_DB.prepare(query)
94
+ .bind(...params)
95
+ .all<DLQMessage>();
96
+
97
+ // Get counts
98
+ const counts = await env.PLATFORM_DB.prepare(
99
+ `SELECT
100
+ COUNT(*) as total,
101
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
102
+ SUM(CASE WHEN status = 'replayed' THEN 1 ELSE 0 END) as replayed,
103
+ SUM(CASE WHEN status = 'discarded' THEN 1 ELSE 0 END) as discarded
104
+ FROM dead_letter_queue`
105
+ ).first<{ total: number; pending: number; replayed: number; discarded: number }>();
106
+
107
+ const response: DLQListResponse = {
108
+ success: true,
109
+ messages: result.results || [],
110
+ total: counts?.total || 0,
111
+ pending: counts?.pending || 0,
112
+ replayed: counts?.replayed || 0,
113
+ discarded: counts?.discarded || 0,
114
+ timestamp: new Date().toISOString(),
115
+ };
116
+
117
+ log.info('DLQ list retrieved', { count: response.messages.length, status });
118
+ return jsonResponse(response);
119
+ } catch (error) {
120
+ log.error('Failed to list DLQ messages', error);
121
+ return jsonResponse({ success: false, error: 'Failed to list DLQ messages' }, 500);
122
+ }
123
+ }
124
+
125
+ // =============================================================================
126
+ // GET DLQ STATS
127
+ // =============================================================================
128
+
129
+ /**
130
+ * Get DLQ statistics for monitoring.
131
+ * GET /admin/dlq/stats
132
+ */
133
+ export async function handleDLQStats(env: Env): Promise<Response> {
134
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:dlq-admin');
135
+
136
+ try {
137
+ // Get overall counts
138
+ const counts = await env.PLATFORM_DB.prepare(
139
+ `SELECT
140
+ COUNT(*) as total,
141
+ SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
142
+ SUM(CASE WHEN status = 'replayed' THEN 1 ELSE 0 END) as replayed,
143
+ SUM(CASE WHEN status = 'discarded' THEN 1 ELSE 0 END) as discarded
144
+ FROM dead_letter_queue`
145
+ ).first<{ total: number; pending: number; replayed: number; discarded: number }>();
146
+
147
+ // Get counts by project
148
+ const byProject = await env.PLATFORM_DB.prepare(
149
+ `SELECT project, COUNT(*) as count FROM dead_letter_queue
150
+ WHERE status = 'pending' GROUP BY project`
151
+ ).all<{ project: string; count: number }>();
152
+
153
+ // Get counts by error category
154
+ const byCategory = await env.PLATFORM_DB.prepare(
155
+ `SELECT error_category, COUNT(*) as count FROM dead_letter_queue
156
+ WHERE status = 'pending' GROUP BY error_category`
157
+ ).all<{ error_category: string | null; count: number }>();
158
+
159
+ // Get oldest pending message
160
+ const oldest = await env.PLATFORM_DB.prepare(
161
+ `SELECT created_at FROM dead_letter_queue
162
+ WHERE status = 'pending' ORDER BY created_at ASC LIMIT 1`
163
+ ).first<{ created_at: number }>();
164
+
165
+ const projectMap: Record<string, number> = {};
166
+ for (const row of byProject.results || []) {
167
+ projectMap[row.project] = row.count;
168
+ }
169
+
170
+ const categoryMap: Record<string, number> = {};
171
+ for (const row of byCategory.results || []) {
172
+ categoryMap[row.error_category || 'unknown'] = row.count;
173
+ }
174
+
175
+ const response: DLQStatsResponse = {
176
+ success: true,
177
+ stats: {
178
+ total: counts?.total || 0,
179
+ pending: counts?.pending || 0,
180
+ replayed: counts?.replayed || 0,
181
+ discarded: counts?.discarded || 0,
182
+ byProject: projectMap,
183
+ byErrorCategory: categoryMap,
184
+ oldestPending: oldest ? new Date(oldest.created_at * 1000).toISOString() : null,
185
+ },
186
+ timestamp: new Date().toISOString(),
187
+ };
188
+
189
+ return jsonResponse(response);
190
+ } catch (error) {
191
+ log.error('Failed to get DLQ stats', error);
192
+ return jsonResponse({ success: false, error: 'Failed to get DLQ stats' }, 500);
193
+ }
194
+ }
195
+
196
+ // =============================================================================
197
+ // REPLAY DLQ MESSAGE
198
+ // =============================================================================
199
+
200
+ /**
201
+ * Replay a DLQ message by re-queuing it.
202
+ * POST /admin/dlq/:id/replay
203
+ */
204
+ export async function handleReplayDLQ(messageId: string, env: Env): Promise<Response> {
205
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:dlq-admin');
206
+
207
+ try {
208
+ // Get the message
209
+ const message = await env.PLATFORM_DB.prepare(
210
+ `SELECT id, message_payload, status FROM dead_letter_queue WHERE id = ?`
211
+ )
212
+ .bind(messageId)
213
+ .first<{ id: string; message_payload: string; status: string }>();
214
+
215
+ if (!message) {
216
+ return jsonResponse({ success: false, error: 'Message not found' }, 404);
217
+ }
218
+
219
+ if (message.status !== 'pending') {
220
+ return jsonResponse(
221
+ { success: false, error: `Message is not pending (status: ${message.status})` },
222
+ 400
223
+ );
224
+ }
225
+
226
+ // Parse the payload
227
+ const telemetry: TelemetryMessage = JSON.parse(message.message_payload);
228
+
229
+ // Re-queue the message
230
+ await env.PLATFORM_TELEMETRY.send(telemetry);
231
+
232
+ // Update the DLQ record
233
+ await env.PLATFORM_DB.prepare(
234
+ `UPDATE dead_letter_queue
235
+ SET status = 'replayed', replayed_at = unixepoch(), replayed_by = 'admin', updated_at = unixepoch()
236
+ WHERE id = ?`
237
+ )
238
+ .bind(messageId)
239
+ .run();
240
+
241
+ log.info('DLQ message replayed', {
242
+ messageId,
243
+ feature_key: telemetry.feature_key,
244
+ });
245
+
246
+ return jsonResponse({
247
+ success: true,
248
+ message: 'Message replayed successfully',
249
+ messageId,
250
+ feature_key: telemetry.feature_key,
251
+ });
252
+ } catch (error) {
253
+ log.error('Failed to replay DLQ message', error, { messageId });
254
+ return jsonResponse({ success: false, error: 'Failed to replay message' }, 500);
255
+ }
256
+ }
257
+
258
+ // =============================================================================
259
+ // DISCARD DLQ MESSAGE
260
+ // =============================================================================
261
+
262
+ /**
263
+ * Discard a DLQ message (mark as not needing replay).
264
+ * POST /admin/dlq/:id/discard
265
+ */
266
+ export async function handleDiscardDLQ(
267
+ messageId: string,
268
+ reason: string,
269
+ env: Env
270
+ ): Promise<Response> {
271
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:dlq-admin');
272
+
273
+ try {
274
+ // Update the DLQ record
275
+ const result = await env.PLATFORM_DB.prepare(
276
+ `UPDATE dead_letter_queue
277
+ SET status = 'discarded', discard_reason = ?, updated_at = unixepoch()
278
+ WHERE id = ? AND status = 'pending'`
279
+ )
280
+ .bind(reason || 'Manually discarded by admin', messageId)
281
+ .run();
282
+
283
+ if (result.meta.changes === 0) {
284
+ return jsonResponse({ success: false, error: 'Message not found or not pending' }, 404);
285
+ }
286
+
287
+ log.info('DLQ message discarded', { messageId, reason });
288
+
289
+ return jsonResponse({
290
+ success: true,
291
+ message: 'Message discarded successfully',
292
+ messageId,
293
+ });
294
+ } catch (error) {
295
+ log.error('Failed to discard DLQ message', error, { messageId });
296
+ return jsonResponse({ success: false, error: 'Failed to discard message' }, 500);
297
+ }
298
+ }
299
+
300
+ // =============================================================================
301
+ // BULK OPERATIONS
302
+ // =============================================================================
303
+
304
+ /**
305
+ * Replay all pending DLQ messages (with optional filter).
306
+ * POST /admin/dlq/replay-all?project=my-app
307
+ */
308
+ export async function handleReplayAllDLQ(url: URL, env: Env): Promise<Response> {
309
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:dlq-admin');
310
+ const project = url.searchParams.get('project');
311
+ const limit = Math.min(parseInt(url.searchParams.get('limit') || '100', 10), 500);
312
+
313
+ try {
314
+ // Get pending messages
315
+ let query = `SELECT id, message_payload FROM dead_letter_queue WHERE status = 'pending'`;
316
+ const params: (string | number)[] = [];
317
+
318
+ if (project) {
319
+ query += ` AND project = ?`;
320
+ params.push(project);
321
+ }
322
+
323
+ query += ` LIMIT ?`;
324
+ params.push(limit);
325
+
326
+ const messages = await env.PLATFORM_DB.prepare(query)
327
+ .bind(...params)
328
+ .all<{ id: string; message_payload: string }>();
329
+
330
+ let replayed = 0;
331
+ let failed = 0;
332
+
333
+ for (const msg of messages.results || []) {
334
+ try {
335
+ const telemetry: TelemetryMessage = JSON.parse(msg.message_payload);
336
+ await env.PLATFORM_TELEMETRY.send(telemetry);
337
+
338
+ await env.PLATFORM_DB.prepare(
339
+ `UPDATE dead_letter_queue
340
+ SET status = 'replayed', replayed_at = unixepoch(), replayed_by = 'admin-bulk', updated_at = unixepoch()
341
+ WHERE id = ?`
342
+ )
343
+ .bind(msg.id)
344
+ .run();
345
+
346
+ replayed++;
347
+ } catch {
348
+ failed++;
349
+ }
350
+ }
351
+
352
+ log.info('Bulk DLQ replay complete', { replayed, failed, project });
353
+
354
+ return jsonResponse({
355
+ success: true,
356
+ replayed,
357
+ failed,
358
+ total: (messages.results || []).length,
359
+ });
360
+ } catch (error) {
361
+ log.error('Failed to replay all DLQ messages', error);
362
+ return jsonResponse({ success: false, error: 'Failed to replay messages' }, 500);
363
+ }
364
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Health Trends Handler
3
+ *
4
+ * API endpoints for querying project health trends from the health_trends table.
5
+ * Part of Phase 2 AI Judge enhancements - Dashboard Trends.
6
+ *
7
+ * @see docs/plans/ai-judge-enhancements.md
8
+ */
9
+
10
+ import type { Env } from '../shared';
11
+ import { jsonResponse } from '../shared';
12
+
13
+ /**
14
+ * Health trend record from D1
15
+ */
16
+ interface HealthTrendRecord {
17
+ id: number;
18
+ project: string;
19
+ audit_id: string;
20
+ audit_date: string;
21
+ composite_score: number;
22
+ sdk_score: number | null;
23
+ observability_score: number | null;
24
+ cost_score: number | null;
25
+ security_score: number | null;
26
+ trend: 'improving' | 'stable' | 'declining';
27
+ score_delta: number;
28
+ created_at: string;
29
+ }
30
+
31
+ /**
32
+ * API response for health trends
33
+ */
34
+ interface HealthTrendsResponse {
35
+ success: boolean;
36
+ data: {
37
+ project: string;
38
+ trends: {
39
+ date: string;
40
+ compositeScore: number;
41
+ rubricScores: {
42
+ sdk: number | null;
43
+ observability: number | null;
44
+ cost: number | null;
45
+ security: number | null;
46
+ };
47
+ trend: 'improving' | 'stable' | 'declining';
48
+ delta: number;
49
+ }[];
50
+ latestScore: number | null;
51
+ latestTrend: 'improving' | 'stable' | 'declining' | null;
52
+ }[];
53
+ period: {
54
+ days: number;
55
+ from: string;
56
+ to: string;
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Handle GET /usage/health-trends
62
+ *
63
+ * Query params:
64
+ * - project: 'all' | <your-project-ids> (default: 'all')
65
+ * - days: number (default: 90)
66
+ *
67
+ * Returns health trends for the specified project(s) over the given period.
68
+ */
69
+ export async function handleGetHealthTrends(url: URL, env: Env): Promise<Response> {
70
+ const project = url.searchParams.get('project') || 'all';
71
+ const days = parseInt(url.searchParams.get('days') || '90', 10);
72
+
73
+ // Calculate date range
74
+ const now = new Date();
75
+ const fromDate = new Date(now);
76
+ fromDate.setDate(fromDate.getDate() - days);
77
+ const fromDateStr = fromDate.toISOString().split('T')[0];
78
+ const toDateStr = now.toISOString().split('T')[0];
79
+
80
+ try {
81
+ // Build query based on project filter
82
+ let query: string;
83
+ let params: unknown[];
84
+
85
+ if (project === 'all') {
86
+ query = `
87
+ SELECT * FROM health_trends
88
+ WHERE audit_date >= ?
89
+ ORDER BY project, audit_date DESC
90
+ `;
91
+ params = [fromDateStr];
92
+ } else {
93
+ query = `
94
+ SELECT * FROM health_trends
95
+ WHERE project = ? AND audit_date >= ?
96
+ ORDER BY audit_date DESC
97
+ `;
98
+ params = [project, fromDateStr];
99
+ }
100
+
101
+ const results = await env.PLATFORM_DB.prepare(query)
102
+ .bind(...params)
103
+ .all<HealthTrendRecord>();
104
+
105
+ if (!results.success) {
106
+ return jsonResponse({ success: false, error: 'Database query failed' }, 500);
107
+ }
108
+
109
+ // Group results by project
110
+ const projectMap = new Map<string, HealthTrendRecord[]>();
111
+ for (const record of results.results) {
112
+ const existing = projectMap.get(record.project) || [];
113
+ existing.push(record);
114
+ projectMap.set(record.project, existing);
115
+ }
116
+
117
+ // Format response
118
+ const data: HealthTrendsResponse['data'] = [];
119
+ for (const [projectName, records] of projectMap) {
120
+ const trends = records.map((r) => ({
121
+ date: r.audit_date,
122
+ compositeScore: r.composite_score,
123
+ rubricScores: {
124
+ sdk: r.sdk_score,
125
+ observability: r.observability_score,
126
+ cost: r.cost_score,
127
+ security: r.security_score,
128
+ },
129
+ trend: r.trend,
130
+ delta: r.score_delta,
131
+ }));
132
+
133
+ const latest = records[0]; // Already sorted DESC
134
+ data.push({
135
+ project: projectName,
136
+ trends,
137
+ latestScore: latest?.composite_score ?? null,
138
+ latestTrend: latest?.trend ?? null,
139
+ });
140
+ }
141
+
142
+ const response: HealthTrendsResponse = {
143
+ success: true,
144
+ data,
145
+ period: {
146
+ days,
147
+ from: fromDateStr,
148
+ to: toDateStr,
149
+ },
150
+ };
151
+
152
+ return jsonResponse(response, 200);
153
+ } catch (error) {
154
+ console.error('Failed to query health trends:', error);
155
+ return jsonResponse(
156
+ {
157
+ success: false,
158
+ error: 'Failed to query health trends',
159
+ details: error instanceof Error ? error.message : String(error),
160
+ },
161
+ 500
162
+ );
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Handle GET /usage/health-trends/latest
168
+ *
169
+ * Returns only the most recent health score for each project.
170
+ * Useful for dashboard summary view.
171
+ */
172
+ export async function handleGetLatestHealthTrends(env: Env): Promise<Response> {
173
+ try {
174
+ // Use the view we created for latest health per project
175
+ const results = await env.PLATFORM_DB.prepare(
176
+ `
177
+ SELECT * FROM v_project_health_latest
178
+ ORDER BY project
179
+ `
180
+ ).all<HealthTrendRecord & { previous_score: number | null }>();
181
+
182
+ if (!results.success) {
183
+ return jsonResponse({ success: false, error: 'Database query failed' }, 500);
184
+ }
185
+
186
+ const data = results.results.map(
187
+ (r: HealthTrendRecord & { previous_score: number | null }) => ({
188
+ project: r.project,
189
+ compositeScore: r.composite_score,
190
+ previousScore: r.previous_score,
191
+ rubricScores: {
192
+ sdk: r.sdk_score,
193
+ observability: r.observability_score,
194
+ cost: r.cost_score,
195
+ security: r.security_score,
196
+ },
197
+ trend: r.trend,
198
+ delta: r.score_delta,
199
+ auditDate: r.audit_date,
200
+ })
201
+ );
202
+
203
+ return jsonResponse(
204
+ {
205
+ success: true,
206
+ data,
207
+ timestamp: new Date().toISOString(),
208
+ },
209
+ 200
210
+ );
211
+ } catch (error) {
212
+ console.error('Failed to query latest health trends:', error);
213
+ return jsonResponse(
214
+ {
215
+ success: false,
216
+ error: 'Failed to query latest health trends',
217
+ details: error instanceof Error ? error.message : String(error),
218
+ },
219
+ 500
220
+ );
221
+ }
222
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Handler Module Exports
3
+ *
4
+ * Barrel export for all usage handler modules.
5
+ */
6
+
7
+ // Data query functions (used by handlers)
8
+ export * from './data-queries';
9
+
10
+ // Usage metrics handlers (handleUsage, handleCosts, etc.)
11
+ export * from './usage-metrics';
12
+
13
+ // Feature-related handlers (handleFeatures, handleWorkersAI, etc.)
14
+ export * from './usage-features';
15
+
16
+ // Settings handlers (handleGetSettings, handlePutSettings, etc.)
17
+ export * from './usage-settings';
18
+
19
+ // Admin handlers (handleResetCircuitBreaker, handleBackfill)
20
+ export * from './usage-admin';
21
+
22
+ // DLQ admin handlers (handleListDLQ, handleReplayDLQ, etc.)
23
+ export * from './dlq-admin';
24
+
25
+ // Health trends handlers (handleGetHealthTrends, handleGetLatestHealthTrends)
26
+ export * from './health-trends';
27
+
28
+ // Gap detection and backfill handlers
29
+ export * from './backfill';
30
+
31
+ // Audit handlers (handleGetAudit, handleGetAuditHistory, handleGetAttribution, handleGetFeatureCoverage)
32
+ export * from './audit';
33
+
34
+ // Behavioral analysis handlers (handleGetBehavioral, handleGetHotspots, handleGetRegressions, handleAcknowledgeRegression)
35
+ export * from './behavioral';