@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,421 @@
1
+ /**
2
+ * Usage Admin Handlers
3
+ *
4
+ * Administrative handlers for circuit breaker management and data backfill.
5
+ * Extracted from platform-usage.ts as part of handler modularisation.
6
+ */
7
+
8
+ import type { Env, SamplingMode, DailyUsageMetrics } from '../shared';
9
+ import { SamplingMode as SamplingModeEnum, CB_KEYS, jsonResponse } from '../shared';
10
+ import { createLoggerFromEnv } from '@littlebearapps/platform-consumer-sdk';
11
+ import { CloudflareGraphQL, calculateDailyCosts } from '../../shared/cloudflare';
12
+
13
+ // =============================================================================
14
+ // RESET CIRCUIT BREAKER HANDLER
15
+ // =============================================================================
16
+
17
+ /**
18
+ * Handle POST /usage/reset-circuit-breaker
19
+ *
20
+ * Manually resets circuit breaker state for any or all projects.
21
+ * This allows immediate recovery after fixing issues, without waiting
22
+ * for the 24-hour KV expiration.
23
+ *
24
+ * Request body:
25
+ * - service: string - project ID to reset, or 'all' (default: 'all')
26
+ * - resetSampling: boolean (default: true) - also reset platform-usage sampling to FULL
27
+ */
28
+ export async function handleResetCircuitBreaker(request: Request, env: Env): Promise<Response> {
29
+ const startTime = Date.now();
30
+
31
+ try {
32
+ const body = (await request.json().catch(() => ({}))) as {
33
+ service?: string;
34
+ resetSampling?: boolean;
35
+ };
36
+
37
+ const service = body.service ?? 'all';
38
+ const resetSampling = body.resetSampling !== false; // Default true
39
+
40
+ const resetActions: string[] = [];
41
+
42
+ // Get registered projects from D1 to reset their circuit breakers
43
+ // TODO: Populate project_registry with your projects, or hardcode project IDs here.
44
+ const projectRows = await env.PLATFORM_DB.prepare(
45
+ `SELECT project_id FROM project_registry WHERE project_id != 'all'`
46
+ ).all<{ project_id: string }>();
47
+
48
+ const registeredProjects = projectRows.results?.map((r) => r.project_id) ?? ['platform'];
49
+
50
+ for (const projectId of registeredProjects) {
51
+ if (service === projectId || service === 'all') {
52
+ const cbKey = `PROJECT:${projectId.toUpperCase().replace(/-/g, '-')}:STATUS`;
53
+ await env.PLATFORM_CACHE.delete(cbKey);
54
+ resetActions.push(projectId);
55
+ }
56
+ }
57
+
58
+ // Optionally reset platform-usage sampling mode to FULL
59
+ if (resetSampling) {
60
+ await env.PLATFORM_CACHE.put(CB_KEYS.USAGE_SAMPLING_MODE, SamplingModeEnum.FULL.toString());
61
+ resetActions.push('sampling-mode');
62
+ }
63
+
64
+ // Log the reset event to D1 for audit trail
65
+ try {
66
+ const resetId = `cb-reset-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
67
+ await env.PLATFORM_DB.prepare(
68
+ `
69
+ INSERT INTO circuit_breaker_logs (id, event_type, service, reason, sampling_mode, created_at)
70
+ VALUES (?, 'reset', ?, ?, ?, unixepoch())
71
+ `
72
+ )
73
+ .bind(
74
+ resetId,
75
+ service,
76
+ `Manual reset via API (services: ${resetActions.join(', ')})`,
77
+ resetSampling ? 'FULL' : null
78
+ )
79
+ .run();
80
+ } catch {
81
+ // Don't fail the reset if logging fails
82
+ }
83
+
84
+ // Circuit breaker reset completed
85
+
86
+ return jsonResponse({
87
+ success: true,
88
+ message: 'Circuit breaker(s) reset successfully',
89
+ reset: resetActions,
90
+ timestamp: new Date().toISOString(),
91
+ responseTimeMs: Date.now() - startTime,
92
+ });
93
+ } catch (error) {
94
+ const errorMessage = error instanceof Error ? error.message : String(error);
95
+ // Error resetting circuit breaker
96
+
97
+ return jsonResponse(
98
+ {
99
+ success: false,
100
+ error: 'Failed to reset circuit breaker',
101
+ message: errorMessage,
102
+ },
103
+ 500
104
+ );
105
+ }
106
+ }
107
+
108
+ // =============================================================================
109
+ // BACKFILL HANDLER
110
+ // =============================================================================
111
+
112
+ /**
113
+ * Handle POST /usage/backfill
114
+ *
115
+ * Backfills daily_usage_rollups from Cloudflare GraphQL for historical data.
116
+ * Required because the worker was just deployed and D1 tables are empty.
117
+ *
118
+ * Query params:
119
+ * - startDate: YYYY-MM-DD (required)
120
+ * - endDate: YYYY-MM-DD (required)
121
+ *
122
+ * Note: Cloudflare GraphQL supports up to 90 days lookback.
123
+ */
124
+ export async function handleBackfill(request: Request, env: Env): Promise<Response> {
125
+ const url = new URL(request.url);
126
+ const startDateStr = url.searchParams.get('startDate');
127
+ const endDateStr = url.searchParams.get('endDate');
128
+
129
+ // Validate required params
130
+ if (!startDateStr || !endDateStr) {
131
+ return jsonResponse(
132
+ { error: 'Missing required params: startDate and endDate (YYYY-MM-DD)' },
133
+ 400
134
+ );
135
+ }
136
+
137
+ // Validate date format
138
+ const dateRegex = /^\d{4}-\d{2}-\d{2}$/;
139
+ if (!dateRegex.test(startDateStr) || !dateRegex.test(endDateStr)) {
140
+ return jsonResponse({ error: 'Invalid date format. Use YYYY-MM-DD.' }, 400);
141
+ }
142
+
143
+ const startDate = new Date(startDateStr);
144
+ const endDate = new Date(endDateStr);
145
+
146
+ if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
147
+ return jsonResponse({ error: 'Invalid date values' }, 400);
148
+ }
149
+
150
+ if (startDate > endDate) {
151
+ return jsonResponse({ error: 'startDate must be before endDate' }, 400);
152
+ }
153
+
154
+ // Check max range (90 days - Cloudflare API limit)
155
+ const daysDiff = Math.ceil((endDate.getTime() - startDate.getTime()) / (1000 * 60 * 60 * 24));
156
+ if (daysDiff > 90) {
157
+ return jsonResponse(
158
+ { error: `Date range too large: ${daysDiff} days. Maximum is 90 days.` },
159
+ 400
160
+ );
161
+ }
162
+
163
+ const log = createLoggerFromEnv(env, 'platform-usage', 'platform:usage:backfill');
164
+ log.info('Starting backfill', {
165
+ startDate: startDateStr,
166
+ endDate: endDateStr,
167
+ days: daysDiff + 1,
168
+ });
169
+
170
+ // Create GraphQL client
171
+ const client = new CloudflareGraphQL({
172
+ CLOUDFLARE_ACCOUNT_ID: env.CLOUDFLARE_ACCOUNT_ID,
173
+ CLOUDFLARE_API_TOKEN: env.CLOUDFLARE_API_TOKEN,
174
+ });
175
+
176
+ const results: Array<{ date: string; status: string; error?: string }> = [];
177
+ let successCount = 0;
178
+ let errorCount = 0;
179
+
180
+ // Process each day
181
+ const currentDate = new Date(startDate);
182
+ while (currentDate <= endDate) {
183
+ const dateStr = currentDate.toISOString().split('T')[0];
184
+
185
+ try {
186
+ // For single-day queries, Cloudflare GraphQL requires endDate > startDate
187
+ // (date_leq appears to be exclusive). Use next day as endDate.
188
+ const nextDay = new Date(currentDate);
189
+ nextDay.setDate(nextDay.getDate() + 1);
190
+ const nextDayStr = nextDay.toISOString().split('T')[0];
191
+
192
+ log.info('Processing', { date: dateStr, queryRange: `${dateStr} to ${nextDayStr}` });
193
+
194
+ // Fetch metrics from Cloudflare GraphQL for this single day
195
+ const metrics = await client.getMetricsForDateRange({
196
+ startDate: dateStr,
197
+ endDate: nextDayStr,
198
+ });
199
+
200
+ // DEBUG: Log raw metrics structure to diagnose empty results
201
+ log.info('Raw metrics', {
202
+ date: dateStr,
203
+ workersCount: metrics.workers?.length ?? 'undefined',
204
+ d1Count: metrics.d1?.length ?? 'undefined',
205
+ kvCount: metrics.kv?.length ?? 'undefined',
206
+ r2Count: metrics.r2?.length ?? 'undefined',
207
+ doExists: metrics.durableObjects ? 'yes' : 'no',
208
+ vectorizeCount: metrics.vectorize?.length ?? 'undefined',
209
+ aiGatewayCount: metrics.aiGateway?.length ?? 'undefined',
210
+ pagesCount: metrics.pages?.length ?? 'undefined',
211
+ firstWorker: metrics.workers?.[0]
212
+ ? { name: metrics.workers[0].scriptName, requests: metrics.workers[0].requests }
213
+ : 'none',
214
+ doData: metrics.durableObjects
215
+ ? {
216
+ requests: metrics.durableObjects.requests,
217
+ gbSeconds: metrics.durableObjects.gbSeconds,
218
+ }
219
+ : 'none',
220
+ });
221
+
222
+ // Aggregate Workers metrics
223
+ const workersRequests = metrics.workers.reduce((sum, w) => sum + w.requests, 0);
224
+ const workersErrors = metrics.workers.reduce((sum, w) => sum + w.errors, 0);
225
+ const workersCpuTimeMs = metrics.workers.reduce((sum, w) => sum + w.cpuTimeMs, 0);
226
+ const workersDurationP50 =
227
+ metrics.workers.length > 0
228
+ ? metrics.workers.reduce((sum, w) => sum + w.duration50thMs, 0) / metrics.workers.length
229
+ : 0;
230
+ const workersDurationP99 =
231
+ metrics.workers.length > 0 ? Math.max(...metrics.workers.map((w) => w.duration99thMs)) : 0;
232
+
233
+ // Aggregate D1 metrics
234
+ const d1RowsRead = metrics.d1.reduce((sum, d) => sum + d.rowsRead, 0);
235
+ const d1RowsWritten = metrics.d1.reduce((sum, d) => sum + d.rowsWritten, 0);
236
+
237
+ // Aggregate KV metrics
238
+ const kvReads = metrics.kv.reduce((sum, k) => sum + k.reads, 0);
239
+ const kvWrites = metrics.kv.reduce((sum, k) => sum + k.writes, 0);
240
+ const kvDeletes = metrics.kv.reduce((sum, k) => sum + k.deletes, 0);
241
+ const kvLists = metrics.kv.reduce((sum, k) => sum + k.lists, 0);
242
+
243
+ // Aggregate R2 metrics
244
+ const r2ClassA = metrics.r2.reduce((sum, r) => sum + r.classAOperations, 0);
245
+ const r2ClassB = metrics.r2.reduce((sum, r) => sum + r.classBOperations, 0);
246
+ const r2StorageBytes = metrics.r2.reduce((sum, r) => sum + r.storageBytes, 0);
247
+ const r2EgressBytes = metrics.r2.reduce((sum, r) => sum + r.egressBytes, 0);
248
+
249
+ // Durable Objects metrics (single object, not array)
250
+ const doRequests = metrics.durableObjects.requests ?? 0;
251
+ const doGbSeconds = metrics.durableObjects.gbSeconds ?? 0;
252
+
253
+ // Vectorize metrics
254
+ // Note: VectorizeInfo from REST API doesn't include query counts
255
+ // Query counts are collected via GraphQL (vectorizeV2QueriesAdaptiveGroups) in the scheduled cron
256
+ const vectorizeQueries = 0; // Query data comes from hourly collection, not available in daily endpoint
257
+
258
+ // AI Gateway metrics (array - aggregate all gateways)
259
+ const aiGatewayRequests = metrics.aiGateway.reduce(
260
+ (sum, g) => sum + (g.totalRequests ?? 0),
261
+ 0
262
+ );
263
+ const aiGatewayTokensIn = metrics.aiGateway.reduce((sum, g) => sum + (g.totalTokens ?? 0), 0);
264
+ const aiGatewayTokensOut = 0; // Not available in AIGatewayMetrics interface
265
+ const aiGatewayCachedRequests = metrics.aiGateway.reduce(
266
+ (sum, g) => sum + (g.cachedRequests ?? 0),
267
+ 0
268
+ );
269
+
270
+ // Pages metrics (array - aggregate all projects)
271
+ const pagesDeployments = metrics.pages.reduce((sum, p) => sum + (p.totalBuilds ?? 0), 0);
272
+ const pagesBandwidth = 0; // Bandwidth not in PagesMetrics interface
273
+
274
+ // Calculate costs using the shared cost calculation function
275
+ const usage: DailyUsageMetrics = {
276
+ workersRequests,
277
+ workersCpuMs: workersCpuTimeMs,
278
+ d1Reads: d1RowsRead,
279
+ d1Writes: d1RowsWritten,
280
+ kvReads,
281
+ kvWrites,
282
+ kvDeletes,
283
+ kvLists,
284
+ r2ClassA,
285
+ r2ClassB,
286
+ vectorizeQueries,
287
+ aiGatewayRequests,
288
+ durableObjectsRequests: doRequests,
289
+ durableObjectsGbSeconds: doGbSeconds,
290
+ };
291
+
292
+ const costs = calculateDailyCosts(usage);
293
+
294
+ // DEBUG: Log aggregated values before D1 insert
295
+ log.info('Aggregated values', {
296
+ date: dateStr,
297
+ workersRequests,
298
+ workersErrors,
299
+ d1RowsRead,
300
+ d1RowsWritten,
301
+ kvReads,
302
+ doRequests,
303
+ doGbSeconds,
304
+ totalCost: costs.total,
305
+ });
306
+
307
+ // Insert into daily_usage_rollups
308
+ await env.PLATFORM_DB.prepare(
309
+ `
310
+ INSERT OR REPLACE INTO daily_usage_rollups (
311
+ snapshot_date, project,
312
+ workers_requests, workers_errors, workers_cpu_time_ms,
313
+ workers_duration_p50_ms_avg, workers_duration_p99_ms_max, workers_cost_usd,
314
+ d1_rows_read, d1_rows_written, d1_storage_bytes_max, d1_cost_usd,
315
+ kv_reads, kv_writes, kv_deletes, kv_list_ops, kv_storage_bytes_max, kv_cost_usd,
316
+ r2_class_a_ops, r2_class_b_ops, r2_storage_bytes_max, r2_egress_bytes, r2_cost_usd,
317
+ do_requests, do_gb_seconds, do_websocket_connections, do_storage_reads,
318
+ do_storage_writes, do_storage_deletes, do_cost_usd,
319
+ vectorize_queries, vectorize_vectors_stored_max, vectorize_cost_usd,
320
+ aigateway_requests, aigateway_tokens_in, aigateway_tokens_out,
321
+ aigateway_cached_requests, aigateway_cost_usd,
322
+ pages_deployments, pages_bandwidth_bytes, pages_cost_usd,
323
+ queues_messages_produced, queues_messages_consumed, queues_cost_usd,
324
+ workersai_requests, workersai_neurons, workersai_cost_usd,
325
+ workflows_executions, workflows_successes, workflows_failures,
326
+ workflows_wall_time_ms, workflows_cpu_time_ms, workflows_cost_usd,
327
+ total_cost_usd, samples_count, rollup_version
328
+ ) VALUES (
329
+ ?, 'all',
330
+ ?, ?, ?,
331
+ ?, ?, ?,
332
+ ?, ?, 0, ?,
333
+ ?, ?, ?, ?, 0, ?,
334
+ ?, ?, ?, ?, ?,
335
+ ?, ?, 0, 0, 0, 0, ?,
336
+ ?, 0, ?,
337
+ ?, ?, ?, ?, ?,
338
+ ?, ?, ?,
339
+ 0, 0, ?,
340
+ 0, 0, ?,
341
+ 0, 0, 0, 0, 0, 0,
342
+ ?, 24, 2
343
+ )
344
+ `
345
+ )
346
+ .bind(
347
+ dateStr,
348
+ // Workers
349
+ workersRequests,
350
+ workersErrors,
351
+ workersCpuTimeMs,
352
+ workersDurationP50,
353
+ workersDurationP99,
354
+ costs.workers,
355
+ // D1
356
+ d1RowsRead,
357
+ d1RowsWritten,
358
+ costs.d1,
359
+ // KV
360
+ kvReads,
361
+ kvWrites,
362
+ kvDeletes,
363
+ kvLists,
364
+ costs.kv,
365
+ // R2
366
+ r2ClassA,
367
+ r2ClassB,
368
+ r2StorageBytes,
369
+ r2EgressBytes,
370
+ costs.r2,
371
+ // DO
372
+ doRequests,
373
+ doGbSeconds,
374
+ costs.durableObjects,
375
+ // Vectorize
376
+ vectorizeQueries,
377
+ costs.vectorize,
378
+ // AI Gateway
379
+ aiGatewayRequests,
380
+ aiGatewayTokensIn,
381
+ aiGatewayTokensOut,
382
+ aiGatewayCachedRequests,
383
+ costs.aiGateway,
384
+ // Pages
385
+ pagesDeployments,
386
+ pagesBandwidth,
387
+ 0, // Pages cost is always 0 on paid plan
388
+ // Queues
389
+ costs.queues,
390
+ // Workers AI
391
+ costs.workersAI,
392
+ // Total
393
+ costs.total
394
+ )
395
+ .run();
396
+
397
+ results.push({ date: dateStr, status: 'ok' });
398
+ successCount++;
399
+ log.info('Backfill success', { date: dateStr, totalCost: costs.total });
400
+ } catch (error) {
401
+ const errorMsg = error instanceof Error ? error.message : String(error);
402
+ results.push({ date: dateStr, status: 'error', error: errorMsg });
403
+ errorCount++;
404
+ log.error(`Backfill error for ${dateStr}: ${errorMsg}`);
405
+ }
406
+
407
+ // Move to next day
408
+ currentDate.setDate(currentDate.getDate() + 1);
409
+ }
410
+
411
+ log.info('Backfill complete', { success: successCount, errors: errorCount });
412
+
413
+ return jsonResponse({
414
+ success: errorCount === 0,
415
+ message: `Backfilled ${successCount} days (${errorCount} errors)`,
416
+ backfilled: successCount,
417
+ errors: errorCount,
418
+ dateRange: { start: startDateStr, end: endDateStr },
419
+ results,
420
+ });
421
+ }