@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.
- package/README.md +112 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +89 -0
- package/dist/prompts.d.ts +27 -0
- package/dist/prompts.js +80 -0
- package/dist/scaffold.d.ts +5 -0
- package/dist/scaffold.js +65 -0
- package/dist/templates.d.ts +16 -0
- package/dist/templates.js +131 -0
- package/package.json +46 -0
- package/templates/full/migrations/006_pattern_discovery.sql +199 -0
- package/templates/full/migrations/007_notifications_search.sql +127 -0
- package/templates/full/workers/lib/pattern-discovery/ai-prompt.ts +644 -0
- package/templates/full/workers/lib/pattern-discovery/clustering.ts +278 -0
- package/templates/full/workers/lib/pattern-discovery/shadow-evaluation.ts +603 -0
- package/templates/full/workers/lib/pattern-discovery/storage.ts +806 -0
- package/templates/full/workers/lib/pattern-discovery/types.ts +159 -0
- package/templates/full/workers/lib/pattern-discovery/validation.ts +278 -0
- package/templates/full/workers/pattern-discovery.ts +661 -0
- package/templates/full/workers/platform-alert-router.ts +1809 -0
- package/templates/full/workers/platform-notifications.ts +424 -0
- package/templates/full/workers/platform-search.ts +480 -0
- package/templates/full/workers/platform-settings.ts +436 -0
- package/templates/full/wrangler.alert-router.jsonc.hbs +34 -0
- package/templates/full/wrangler.notifications.jsonc.hbs +23 -0
- package/templates/full/wrangler.pattern-discovery.jsonc.hbs +33 -0
- package/templates/full/wrangler.search.jsonc.hbs +16 -0
- package/templates/full/wrangler.settings.jsonc.hbs +23 -0
- package/templates/shared/README.md.hbs +69 -0
- package/templates/shared/config/budgets.yaml.hbs +72 -0
- package/templates/shared/config/services.yaml.hbs +45 -0
- package/templates/shared/migrations/001_core_tables.sql +117 -0
- package/templates/shared/migrations/002_usage_warehouse.sql +830 -0
- package/templates/shared/migrations/003_feature_tracking.sql +250 -0
- package/templates/shared/migrations/004_settings_alerts.sql +452 -0
- package/templates/shared/migrations/seed.sql.hbs +4 -0
- package/templates/shared/package.json.hbs +21 -0
- package/templates/shared/scripts/sync-config.ts +242 -0
- package/templates/shared/tsconfig.json +12 -0
- package/templates/shared/workers/lib/analytics-engine.ts +357 -0
- package/templates/shared/workers/lib/billing.ts +293 -0
- package/templates/shared/workers/lib/circuit-breaker-middleware.ts +25 -0
- package/templates/shared/workers/lib/control.ts +292 -0
- package/templates/shared/workers/lib/economics.ts +368 -0
- package/templates/shared/workers/lib/metrics.ts +103 -0
- package/templates/shared/workers/lib/platform-settings.ts +407 -0
- package/templates/shared/workers/lib/shared/allowances.ts +333 -0
- package/templates/shared/workers/lib/shared/cloudflare.ts +1362 -0
- package/templates/shared/workers/lib/shared/types.ts +58 -0
- package/templates/shared/workers/lib/telemetry-sampling.ts +360 -0
- package/templates/shared/workers/lib/usage/collectors/example.ts +96 -0
- package/templates/shared/workers/lib/usage/collectors/index.ts +128 -0
- package/templates/shared/workers/lib/usage/handlers/audit.ts +306 -0
- package/templates/shared/workers/lib/usage/handlers/backfill.ts +845 -0
- package/templates/shared/workers/lib/usage/handlers/behavioral.ts +429 -0
- package/templates/shared/workers/lib/usage/handlers/data-queries.ts +507 -0
- package/templates/shared/workers/lib/usage/handlers/dlq-admin.ts +364 -0
- package/templates/shared/workers/lib/usage/handlers/health-trends.ts +222 -0
- package/templates/shared/workers/lib/usage/handlers/index.ts +35 -0
- package/templates/shared/workers/lib/usage/handlers/usage-admin.ts +421 -0
- package/templates/shared/workers/lib/usage/handlers/usage-features.ts +1262 -0
- package/templates/shared/workers/lib/usage/handlers/usage-metrics.ts +2420 -0
- package/templates/shared/workers/lib/usage/handlers/usage-settings.ts +610 -0
- package/templates/shared/workers/lib/usage/queue/budget-enforcement.ts +1032 -0
- package/templates/shared/workers/lib/usage/queue/cost-budget-enforcement.ts +128 -0
- package/templates/shared/workers/lib/usage/queue/cost-calculator.ts +77 -0
- package/templates/shared/workers/lib/usage/queue/dlq-handler.ts +161 -0
- package/templates/shared/workers/lib/usage/queue/index.ts +19 -0
- package/templates/shared/workers/lib/usage/queue/telemetry-processor.ts +790 -0
- package/templates/shared/workers/lib/usage/scheduled/anomaly-detection.ts +732 -0
- package/templates/shared/workers/lib/usage/scheduled/data-collection.ts +956 -0
- package/templates/shared/workers/lib/usage/scheduled/error-digest.ts +343 -0
- package/templates/shared/workers/lib/usage/scheduled/index.ts +18 -0
- package/templates/shared/workers/lib/usage/scheduled/rollups.ts +1561 -0
- package/templates/shared/workers/lib/usage/shared/constants.ts +362 -0
- package/templates/shared/workers/lib/usage/shared/index.ts +14 -0
- package/templates/shared/workers/lib/usage/shared/types.ts +1066 -0
- package/templates/shared/workers/lib/usage/shared/utils.ts +795 -0
- package/templates/shared/workers/platform-usage.ts +1915 -0
- package/templates/shared/wrangler.usage.jsonc.hbs +58 -0
- package/templates/standard/migrations/005_error_collection.sql +162 -0
- package/templates/standard/workers/error-collector.ts +2670 -0
- package/templates/standard/workers/lib/error-collector/capture.ts +213 -0
- package/templates/standard/workers/lib/error-collector/digest.ts +448 -0
- package/templates/standard/workers/lib/error-collector/email-health-alerts.ts +262 -0
- package/templates/standard/workers/lib/error-collector/fingerprint.ts +258 -0
- package/templates/standard/workers/lib/error-collector/gap-alerts.ts +293 -0
- package/templates/standard/workers/lib/error-collector/github.ts +329 -0
- package/templates/standard/workers/lib/error-collector/types.ts +262 -0
- package/templates/standard/workers/lib/sentinel/gap-detection.ts +734 -0
- package/templates/standard/workers/lib/shared/slack-alerts.ts +585 -0
- package/templates/standard/workers/platform-sentinel.ts +1744 -0
- package/templates/standard/wrangler.error-collector.jsonc.hbs +44 -0
- package/templates/standard/wrangler.sentinel.jsonc.hbs +45 -0
|
@@ -0,0 +1,1362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Observability Library
|
|
3
|
+
*
|
|
4
|
+
* Provides unified access to Cloudflare metrics, costs, and analytics.
|
|
5
|
+
* This module consolidates GraphQL client, cost calculator, project registry,
|
|
6
|
+
* and alerting functionality used by the platform-usage worker.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: This is a self-contained version extracted from the dashboard library.
|
|
9
|
+
* In production deployments, the dashboard may have additional UI-specific exports.
|
|
10
|
+
*
|
|
11
|
+
* @module cloudflare
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// GRAPHQL TYPES
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/** Time period for metrics queries */
|
|
21
|
+
export type TimePeriod = '24h' | '7d' | '30d';
|
|
22
|
+
|
|
23
|
+
/** Date range for GraphQL queries */
|
|
24
|
+
export interface DateRange {
|
|
25
|
+
startDate: string;
|
|
26
|
+
endDate: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Custom date range query parameters */
|
|
30
|
+
export interface CustomDateRangeParams {
|
|
31
|
+
startDate: string;
|
|
32
|
+
endDate: string;
|
|
33
|
+
priorStartDate?: string;
|
|
34
|
+
priorEndDate?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Comparison mode */
|
|
38
|
+
export type CompareMode = 'none' | 'lastMonth' | 'custom';
|
|
39
|
+
|
|
40
|
+
/** Workers usage metrics */
|
|
41
|
+
export interface WorkersMetrics {
|
|
42
|
+
scriptName: string;
|
|
43
|
+
requests: number;
|
|
44
|
+
errors: number;
|
|
45
|
+
cpuTimeMs: number;
|
|
46
|
+
duration50thMs: number;
|
|
47
|
+
duration99thMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** D1 database usage metrics */
|
|
51
|
+
export interface D1Metrics {
|
|
52
|
+
databaseId: string;
|
|
53
|
+
databaseName: string;
|
|
54
|
+
rowsRead: number;
|
|
55
|
+
rowsWritten: number;
|
|
56
|
+
readQueries: number;
|
|
57
|
+
writeQueries: number;
|
|
58
|
+
storageBytes: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** KV namespace usage metrics */
|
|
62
|
+
export interface KVMetrics {
|
|
63
|
+
namespaceId: string;
|
|
64
|
+
namespaceName: string;
|
|
65
|
+
reads: number;
|
|
66
|
+
writes: number;
|
|
67
|
+
deletes: number;
|
|
68
|
+
lists: number;
|
|
69
|
+
storageBytes: number;
|
|
70
|
+
keyCount: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** R2 bucket usage metrics */
|
|
74
|
+
export interface R2Metrics {
|
|
75
|
+
bucketName: string;
|
|
76
|
+
classAOperations: number;
|
|
77
|
+
classBOperations: number;
|
|
78
|
+
storageBytes: number;
|
|
79
|
+
egressBytes: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Per-script Durable Objects metrics */
|
|
83
|
+
export interface DOScriptMetrics {
|
|
84
|
+
scriptName: string;
|
|
85
|
+
requests: number;
|
|
86
|
+
gbSeconds: number;
|
|
87
|
+
storageBytes?: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Durable Objects usage metrics */
|
|
91
|
+
export interface DOMetrics {
|
|
92
|
+
requests: number;
|
|
93
|
+
responseBodySize: number;
|
|
94
|
+
gbSeconds: number;
|
|
95
|
+
storageBytes: number;
|
|
96
|
+
storageReadUnits: number;
|
|
97
|
+
storageWriteUnits: number;
|
|
98
|
+
storageDeleteUnits: number;
|
|
99
|
+
byScript?: DOScriptMetrics[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Vectorize index info */
|
|
103
|
+
export interface VectorizeInfo {
|
|
104
|
+
name: string;
|
|
105
|
+
vectorCount: number;
|
|
106
|
+
dimensions: number;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** AI Gateway model breakdown metrics */
|
|
110
|
+
export interface AIGatewayModelBreakdown {
|
|
111
|
+
provider: string;
|
|
112
|
+
model: string;
|
|
113
|
+
requests: number;
|
|
114
|
+
cachedRequests: number;
|
|
115
|
+
tokensIn: number;
|
|
116
|
+
tokensOut: number;
|
|
117
|
+
costUsd: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** AI Gateway usage metrics */
|
|
121
|
+
export interface AIGatewayMetrics {
|
|
122
|
+
gatewayId: string;
|
|
123
|
+
totalRequests: number;
|
|
124
|
+
cachedRequests: number;
|
|
125
|
+
totalTokens: number;
|
|
126
|
+
estimatedCostUsd: number;
|
|
127
|
+
byModel?: AIGatewayModelBreakdown[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Pages usage metrics */
|
|
131
|
+
export interface PagesMetrics {
|
|
132
|
+
projectName: string;
|
|
133
|
+
totalBuilds: number;
|
|
134
|
+
totalDeployments: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Sparkline point */
|
|
138
|
+
export interface SparklinePoint {
|
|
139
|
+
date: string;
|
|
140
|
+
value: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Sparkline data */
|
|
144
|
+
export interface SparklineData {
|
|
145
|
+
points: SparklinePoint[];
|
|
146
|
+
min: number;
|
|
147
|
+
max: number;
|
|
148
|
+
current: number;
|
|
149
|
+
trend: 'up' | 'down' | 'stable';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Workers error breakdown */
|
|
153
|
+
export interface WorkersErrorBreakdown {
|
|
154
|
+
scriptName: string;
|
|
155
|
+
errorCount: number;
|
|
156
|
+
totalRequests: number;
|
|
157
|
+
errorRate: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Queues metrics */
|
|
161
|
+
export interface QueuesMetrics {
|
|
162
|
+
queueName: string;
|
|
163
|
+
messagesProduced: number;
|
|
164
|
+
messagesConsumed: number;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Cache analytics */
|
|
168
|
+
export interface CacheAnalytics {
|
|
169
|
+
totalCacheHits: number;
|
|
170
|
+
totalCacheMisses: number;
|
|
171
|
+
hitRate: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Period comparison */
|
|
175
|
+
export interface PeriodComparison {
|
|
176
|
+
current: number;
|
|
177
|
+
prior: number;
|
|
178
|
+
delta: number;
|
|
179
|
+
percentChange: number;
|
|
180
|
+
trend: 'up' | 'down' | 'stable';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Account-wide usage data */
|
|
184
|
+
export interface AccountUsage {
|
|
185
|
+
workers: WorkersMetrics[];
|
|
186
|
+
d1: D1Metrics[];
|
|
187
|
+
kv: KVMetrics[];
|
|
188
|
+
r2: R2Metrics[];
|
|
189
|
+
durableObjects: DOMetrics;
|
|
190
|
+
vectorize: VectorizeInfo[];
|
|
191
|
+
aiGateway: AIGatewayMetrics[];
|
|
192
|
+
pages: PagesMetrics[];
|
|
193
|
+
period: TimePeriod;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Enhanced usage with additional analytics */
|
|
197
|
+
export interface EnhancedAccountUsage extends AccountUsage {
|
|
198
|
+
sparklines: Record<string, SparklineData>;
|
|
199
|
+
errorBreakdown: WorkersErrorBreakdown[];
|
|
200
|
+
queues: QueuesMetrics[];
|
|
201
|
+
cache: CacheAnalytics;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Workers AI metrics from Analytics Engine */
|
|
205
|
+
export interface WorkersAIMetrics {
|
|
206
|
+
project: string;
|
|
207
|
+
model: string;
|
|
208
|
+
requests: number;
|
|
209
|
+
inputTokens: number;
|
|
210
|
+
outputTokens: number;
|
|
211
|
+
costUsd: number;
|
|
212
|
+
isEstimated: boolean;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Workers AI summary */
|
|
216
|
+
export interface WorkersAISummary {
|
|
217
|
+
totalRequests: number;
|
|
218
|
+
totalTokens: number;
|
|
219
|
+
totalCostUsd: number;
|
|
220
|
+
models: Array<{
|
|
221
|
+
model: string;
|
|
222
|
+
requests: number;
|
|
223
|
+
inputTokens: number;
|
|
224
|
+
outputTokens: number;
|
|
225
|
+
costUsd: number;
|
|
226
|
+
}>;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** AI Gateway aggregated metrics */
|
|
230
|
+
export interface AIGatewaySummary {
|
|
231
|
+
totalRequests: number;
|
|
232
|
+
totalCachedRequests: number;
|
|
233
|
+
cacheHitRate: number;
|
|
234
|
+
tokensIn: number;
|
|
235
|
+
tokensOut: number;
|
|
236
|
+
totalCostUsd: number;
|
|
237
|
+
byProvider: Record<
|
|
238
|
+
string,
|
|
239
|
+
{
|
|
240
|
+
requests: number;
|
|
241
|
+
cachedRequests: number;
|
|
242
|
+
tokensIn: number;
|
|
243
|
+
tokensOut: number;
|
|
244
|
+
costUsd: number;
|
|
245
|
+
}
|
|
246
|
+
>;
|
|
247
|
+
byModel: Array<{
|
|
248
|
+
provider: string;
|
|
249
|
+
model: string;
|
|
250
|
+
requests: number;
|
|
251
|
+
cachedRequests: number;
|
|
252
|
+
tokensIn: number;
|
|
253
|
+
tokensOut: number;
|
|
254
|
+
costUsd: number;
|
|
255
|
+
}>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Daily cost breakdown */
|
|
259
|
+
export interface DailyCostBreakdown {
|
|
260
|
+
date: string;
|
|
261
|
+
workers: number;
|
|
262
|
+
d1: number;
|
|
263
|
+
kv: number;
|
|
264
|
+
r2: number;
|
|
265
|
+
vectorize: number;
|
|
266
|
+
aiGateway: number;
|
|
267
|
+
durableObjects: number;
|
|
268
|
+
workersAI: number;
|
|
269
|
+
pages: number;
|
|
270
|
+
queues: number;
|
|
271
|
+
workflows: number;
|
|
272
|
+
total: number;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Daily cost data (chart data) */
|
|
276
|
+
export interface DailyCostData {
|
|
277
|
+
days: DailyCostBreakdown[];
|
|
278
|
+
totalCost: number;
|
|
279
|
+
averageDailyCost: number;
|
|
280
|
+
peakDay: DailyCostBreakdown | null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Workflows metrics */
|
|
284
|
+
export interface WorkflowsMetrics {
|
|
285
|
+
workflowName: string;
|
|
286
|
+
executions: number;
|
|
287
|
+
successes: number;
|
|
288
|
+
failures: number;
|
|
289
|
+
wallTimeMs: number;
|
|
290
|
+
cpuTimeMs: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** Workflows summary */
|
|
294
|
+
export interface WorkflowsSummary {
|
|
295
|
+
totalExecutions: number;
|
|
296
|
+
totalSuccesses: number;
|
|
297
|
+
totalFailures: number;
|
|
298
|
+
totalWallTimeMs: number;
|
|
299
|
+
totalCpuTimeMs: number;
|
|
300
|
+
byWorkflow: WorkflowsMetrics[];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Cloudflare subscription */
|
|
304
|
+
export interface CloudflareSubscription {
|
|
305
|
+
ratePlanName: string;
|
|
306
|
+
price: number;
|
|
307
|
+
frequency: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Workers Paid Plan inclusions */
|
|
311
|
+
export interface WorkersPaidPlanInclusions {
|
|
312
|
+
requestsIncluded: number;
|
|
313
|
+
cpuTimeIncluded: number;
|
|
314
|
+
d1RowsReadIncluded: number;
|
|
315
|
+
d1RowsWrittenIncluded: number;
|
|
316
|
+
d1StorageIncluded: number;
|
|
317
|
+
kvReadsIncluded: number;
|
|
318
|
+
kvWritesIncluded: number;
|
|
319
|
+
kvStorageIncluded: number;
|
|
320
|
+
r2ClassAIncluded: number;
|
|
321
|
+
r2ClassBIncluded: number;
|
|
322
|
+
r2StorageIncluded: number;
|
|
323
|
+
doRequestsIncluded: number;
|
|
324
|
+
doDurationIncluded: number;
|
|
325
|
+
doStorageIncluded: number;
|
|
326
|
+
vectorizeQueriedDimensionsIncluded: number;
|
|
327
|
+
vectorizeStoredDimensionsIncluded: number;
|
|
328
|
+
queuesOperationsIncluded: number;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** Account subscriptions response */
|
|
332
|
+
export interface CloudflareAccountSubscriptions {
|
|
333
|
+
subscriptions: CloudflareSubscription[];
|
|
334
|
+
hasWorkersPaid: boolean;
|
|
335
|
+
hasR2Paid: boolean;
|
|
336
|
+
hasAnalyticsEngine: boolean;
|
|
337
|
+
monthlyBaseCost: number;
|
|
338
|
+
planInclusions: WorkersPaidPlanInclusions;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** Billing profile */
|
|
342
|
+
export interface CloudflareBillingProfile {
|
|
343
|
+
accountId: string;
|
|
344
|
+
currency: string;
|
|
345
|
+
paymentMethod: string;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// =============================================================================
|
|
349
|
+
// CLOUDFLARE GRAPHQL CLIENT
|
|
350
|
+
// =============================================================================
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* CloudflareGraphQL client for querying Cloudflare's Analytics API.
|
|
354
|
+
*
|
|
355
|
+
* TODO: Implement your GraphQL queries for the Cloudflare Analytics API.
|
|
356
|
+
* This is a minimal stub — add methods as needed for your metrics collection.
|
|
357
|
+
*
|
|
358
|
+
* @see https://developers.cloudflare.com/analytics/graphql-api/
|
|
359
|
+
*/
|
|
360
|
+
export class CloudflareGraphQL {
|
|
361
|
+
private accountId: string;
|
|
362
|
+
private apiToken: string;
|
|
363
|
+
|
|
364
|
+
constructor(env: { CLOUDFLARE_ACCOUNT_ID: string; CLOUDFLARE_API_TOKEN: string }) {
|
|
365
|
+
this.accountId = env.CLOUDFLARE_ACCOUNT_ID;
|
|
366
|
+
this.apiToken = env.CLOUDFLARE_API_TOKEN;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Fetch all account metrics for a given period.
|
|
371
|
+
* TODO: Implement GraphQL queries for your needed metrics.
|
|
372
|
+
*/
|
|
373
|
+
async getAllMetrics(period: TimePeriod): Promise<AccountUsage> {
|
|
374
|
+
// TODO: Implement Cloudflare GraphQL queries
|
|
375
|
+
// @see https://developers.cloudflare.com/analytics/graphql-api/
|
|
376
|
+
throw new Error('CloudflareGraphQL.getAllMetrics() not implemented — add your GraphQL queries');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Get Workflows metrics from GraphQL.
|
|
381
|
+
*/
|
|
382
|
+
async getWorkflowsMetrics(_period: TimePeriod): Promise<WorkflowsSummary> {
|
|
383
|
+
throw new Error(
|
|
384
|
+
'CloudflareGraphQL.getWorkflowsMetrics() not implemented — add your GraphQL queries'
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get Queues metrics from GraphQL.
|
|
390
|
+
*/
|
|
391
|
+
async getQueuesMetrics(_period: TimePeriod): Promise<QueuesMetrics[]> {
|
|
392
|
+
throw new Error(
|
|
393
|
+
'CloudflareGraphQL.getQueuesMetrics() not implemented — add your GraphQL queries'
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get Workers AI metrics from Analytics Engine.
|
|
399
|
+
*/
|
|
400
|
+
async getWorkersAIMetrics(
|
|
401
|
+
_period: TimePeriod
|
|
402
|
+
): Promise<{ metrics: WorkersAIMetrics[]; totalRequests: number }> {
|
|
403
|
+
throw new Error(
|
|
404
|
+
'CloudflareGraphQL.getWorkersAIMetrics() not implemented — add your GraphQL queries'
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get Workers AI neuron data from GraphQL.
|
|
410
|
+
*/
|
|
411
|
+
async getWorkersAINeuronsGraphQL(_dateRange: DateRange): Promise<{
|
|
412
|
+
totalNeurons: number;
|
|
413
|
+
totalInputTokens: number;
|
|
414
|
+
totalOutputTokens: number;
|
|
415
|
+
byModel: Array<{
|
|
416
|
+
modelId: string;
|
|
417
|
+
requestCount: number;
|
|
418
|
+
inputTokens: number;
|
|
419
|
+
outputTokens: number;
|
|
420
|
+
neurons: number;
|
|
421
|
+
}>;
|
|
422
|
+
}> {
|
|
423
|
+
throw new Error(
|
|
424
|
+
'CloudflareGraphQL.getWorkersAINeuronsGraphQL() not implemented — add your GraphQL queries'
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get Vectorize query metrics from GraphQL.
|
|
430
|
+
*/
|
|
431
|
+
async getVectorizeQueriesGraphQL(_dateRange: DateRange): Promise<{
|
|
432
|
+
totalQueriedDimensions: number;
|
|
433
|
+
totalServedVectors: number;
|
|
434
|
+
byIndex: Array<{ indexName: string; queriedDimensions: number }>;
|
|
435
|
+
}> {
|
|
436
|
+
throw new Error(
|
|
437
|
+
'CloudflareGraphQL.getVectorizeQueriesGraphQL() not implemented — add your GraphQL queries'
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Get Vectorize storage metrics from GraphQL.
|
|
443
|
+
*/
|
|
444
|
+
async getVectorizeStorageGraphQL(_dateRange: DateRange): Promise<{
|
|
445
|
+
totalStoredDimensions: number;
|
|
446
|
+
totalVectorCount: number;
|
|
447
|
+
byIndex: Array<{ indexName: string; storedDimensions: number; vectorCount: number }>;
|
|
448
|
+
}> {
|
|
449
|
+
throw new Error(
|
|
450
|
+
'CloudflareGraphQL.getVectorizeStorageGraphQL() not implemented — add your GraphQL queries'
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get Cloudflare account subscriptions.
|
|
456
|
+
*/
|
|
457
|
+
async getAccountSubscriptions(): Promise<CloudflareAccountSubscriptions | null> {
|
|
458
|
+
throw new Error(
|
|
459
|
+
'CloudflareGraphQL.getAccountSubscriptions() not implemented — add your REST API calls'
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// =============================================================================
|
|
465
|
+
// COST CALCULATOR
|
|
466
|
+
// =============================================================================
|
|
467
|
+
|
|
468
|
+
/** Cloudflare pricing constants (Workers Paid plan) */
|
|
469
|
+
export const CF_PRICING = {
|
|
470
|
+
workers: {
|
|
471
|
+
baseCostMonthly: 5.0,
|
|
472
|
+
includedRequests: 10_000_000,
|
|
473
|
+
requestsPerMillion: 0.3,
|
|
474
|
+
cpuMsPerMillion: 0.02,
|
|
475
|
+
},
|
|
476
|
+
d1: {
|
|
477
|
+
rowsReadPerBillion: 0.001,
|
|
478
|
+
rowsWrittenPerMillion: 1.0,
|
|
479
|
+
storagePerGb: 0.75,
|
|
480
|
+
},
|
|
481
|
+
kv: {
|
|
482
|
+
readsPerMillion: 0.5,
|
|
483
|
+
writesPerMillion: 5.0,
|
|
484
|
+
deletesPerMillion: 5.0,
|
|
485
|
+
listsPerMillion: 5.0,
|
|
486
|
+
storagePerGb: 0.5,
|
|
487
|
+
},
|
|
488
|
+
r2: {
|
|
489
|
+
storagePerGbMonth: 0.015,
|
|
490
|
+
classAPerMillion: 4.5,
|
|
491
|
+
classBPerMillion: 0.36,
|
|
492
|
+
},
|
|
493
|
+
durableObjects: {
|
|
494
|
+
requestsPerMillion: 0.15,
|
|
495
|
+
gbSecondsPerMillion: 12.5,
|
|
496
|
+
storagePerGbMonth: 0.2,
|
|
497
|
+
readsPerMillion: 0.2,
|
|
498
|
+
writesPerMillion: 1.0,
|
|
499
|
+
deletesPerMillion: 1.0,
|
|
500
|
+
},
|
|
501
|
+
vectorize: {
|
|
502
|
+
storedDimensionsPerMillion: 0.01,
|
|
503
|
+
queriedDimensionsPerMillion: 0.01,
|
|
504
|
+
},
|
|
505
|
+
aiGateway: { free: true },
|
|
506
|
+
workersAI: {
|
|
507
|
+
models: {
|
|
508
|
+
default: { input: 0.2, output: 0.4 },
|
|
509
|
+
} as Record<string, { input: number; output: number }>,
|
|
510
|
+
neuronsPerThousand: 0.011,
|
|
511
|
+
},
|
|
512
|
+
pages: {
|
|
513
|
+
buildCost: 0.15,
|
|
514
|
+
bandwidthPerGb: 0.02,
|
|
515
|
+
},
|
|
516
|
+
queues: {
|
|
517
|
+
messagesPerMillion: 0.4,
|
|
518
|
+
operationsPerMillion: 0.4,
|
|
519
|
+
},
|
|
520
|
+
workflows: { free: true },
|
|
521
|
+
} as const;
|
|
522
|
+
|
|
523
|
+
/** Workers Paid Plan monthly allowances */
|
|
524
|
+
export const CF_PAID_ALLOWANCES = {
|
|
525
|
+
d1: { rowsRead: 25_000_000_000, rowsWritten: 50_000_000 },
|
|
526
|
+
kv: { reads: 10_000_000, writes: 1_000_000, deletes: 1_000_000, lists: 1_000_000 },
|
|
527
|
+
r2: { storage: 10_000_000_000, classA: 1_000_000, classB: 10_000_000 },
|
|
528
|
+
durableObjects: { requests: 1_000_000, gbSeconds: 400_000 },
|
|
529
|
+
vectorize: { storedDimensions: 10_000_000, queriedDimensions: 50_000_000 },
|
|
530
|
+
queues: { operations: 1_000_000 },
|
|
531
|
+
pages: { builds: 500 },
|
|
532
|
+
workersAI: { neurons: 0 },
|
|
533
|
+
} as const;
|
|
534
|
+
|
|
535
|
+
/** Free tier limits */
|
|
536
|
+
export const CF_FREE_LIMITS = {
|
|
537
|
+
workers: { requestsPerDay: 100_000, cpuMsPerInvocation: 10 },
|
|
538
|
+
d1: { rowsReadPerDay: 5_000_000, rowsWrittenPerDay: 100_000, storageGb: 5 },
|
|
539
|
+
kv: { readsPerDay: 100_000, writesPerDay: 1_000, storageGb: 1 },
|
|
540
|
+
r2: { storageGb: 10, classAPerMonth: 1_000_000, classBPerMonth: 10_000_000 },
|
|
541
|
+
durableObjects: { requestsPerMonth: 1_000_000, gbSecondsPerMonth: 400_000, storageGb: 1 },
|
|
542
|
+
vectorize: { storedDimensionsPerMonth: 10_000_000, queriesPerMonth: 50_000_000 },
|
|
543
|
+
pages: { buildsPerMonth: 500, bandwidthGbPerMonth: 100 },
|
|
544
|
+
queues: { messagesPerMonth: 1_000_000, operationsPerMonth: 1_000_000 },
|
|
545
|
+
workflows: { cpuMsPerMonth: 10_000_000 },
|
|
546
|
+
workersAI: { neuronsPerDay: 10_000 },
|
|
547
|
+
} as const;
|
|
548
|
+
|
|
549
|
+
/** Cost breakdown by resource type */
|
|
550
|
+
export interface CostBreakdown {
|
|
551
|
+
workers: number;
|
|
552
|
+
d1: number;
|
|
553
|
+
kv: number;
|
|
554
|
+
r2: number;
|
|
555
|
+
durableObjects: number;
|
|
556
|
+
vectorize: number;
|
|
557
|
+
aiGateway: number;
|
|
558
|
+
workersAI: number;
|
|
559
|
+
pages: number;
|
|
560
|
+
queues: number;
|
|
561
|
+
workflows: number;
|
|
562
|
+
total: number;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/** Cost breakdown by project */
|
|
566
|
+
export interface ProjectCostBreakdown {
|
|
567
|
+
project: string;
|
|
568
|
+
workers: number;
|
|
569
|
+
d1: number;
|
|
570
|
+
kv: number;
|
|
571
|
+
r2: number;
|
|
572
|
+
durableObjects: number;
|
|
573
|
+
vectorize: number;
|
|
574
|
+
aiGateway: number;
|
|
575
|
+
workersAI: number;
|
|
576
|
+
total: number;
|
|
577
|
+
doRequests: number;
|
|
578
|
+
doGbSeconds: number;
|
|
579
|
+
workersRequests: number;
|
|
580
|
+
workersErrors: number;
|
|
581
|
+
workersCpuTimeMs: number;
|
|
582
|
+
d1RowsRead: number;
|
|
583
|
+
d1RowsWritten: number;
|
|
584
|
+
kvReads: number;
|
|
585
|
+
kvWrites: number;
|
|
586
|
+
kvDeletes: number;
|
|
587
|
+
kvLists: number;
|
|
588
|
+
r2ClassAOps: number;
|
|
589
|
+
r2ClassBOps: number;
|
|
590
|
+
r2StorageBytes: number;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/** Threshold levels */
|
|
594
|
+
export type ThresholdLevel = 'normal' | 'warning' | 'high' | 'critical';
|
|
595
|
+
|
|
596
|
+
/** Threshold warning */
|
|
597
|
+
export interface ThresholdWarning {
|
|
598
|
+
resource: string;
|
|
599
|
+
metric: string;
|
|
600
|
+
current: number;
|
|
601
|
+
limit: number;
|
|
602
|
+
percentage: number;
|
|
603
|
+
level: ThresholdLevel;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
/** Threshold analysis result */
|
|
607
|
+
export interface ThresholdAnalysis {
|
|
608
|
+
warnings: ThresholdWarning[];
|
|
609
|
+
hasWarnings: boolean;
|
|
610
|
+
hasCritical: boolean;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/** Service type for alert thresholds */
|
|
614
|
+
export type AlertServiceType =
|
|
615
|
+
| 'workers'
|
|
616
|
+
| 'd1'
|
|
617
|
+
| 'kv'
|
|
618
|
+
| 'r2'
|
|
619
|
+
| 'durableObjects'
|
|
620
|
+
| 'vectorize'
|
|
621
|
+
| 'aiGateway'
|
|
622
|
+
| 'workersAI'
|
|
623
|
+
| 'pages'
|
|
624
|
+
| 'queues'
|
|
625
|
+
| 'workflows';
|
|
626
|
+
|
|
627
|
+
/** Configurable alert thresholds per service type */
|
|
628
|
+
export interface ServiceThreshold {
|
|
629
|
+
warningPct: number;
|
|
630
|
+
highPct: number;
|
|
631
|
+
criticalPct: number;
|
|
632
|
+
absoluteMax: number;
|
|
633
|
+
enabled: boolean;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** All configurable alert thresholds */
|
|
637
|
+
export interface AlertThresholds {
|
|
638
|
+
[key: string]: ServiceThreshold;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Daily usage metrics input for cost calculation */
|
|
642
|
+
export interface DailyUsageMetrics {
|
|
643
|
+
workersRequests: number;
|
|
644
|
+
workersCpuMs: number;
|
|
645
|
+
d1Reads: number;
|
|
646
|
+
d1Writes: number;
|
|
647
|
+
kvReads: number;
|
|
648
|
+
kvWrites: number;
|
|
649
|
+
kvDeletes?: number;
|
|
650
|
+
kvLists?: number;
|
|
651
|
+
r2ClassA: number;
|
|
652
|
+
r2ClassB: number;
|
|
653
|
+
vectorizeQueries: number;
|
|
654
|
+
aiGatewayRequests: number;
|
|
655
|
+
durableObjectsRequests: number;
|
|
656
|
+
durableObjectsGbSeconds?: number;
|
|
657
|
+
workersAITokens?: number;
|
|
658
|
+
queuesMessages?: number;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/** Default alert thresholds */
|
|
662
|
+
export const DEFAULT_ALERT_THRESHOLDS: AlertThresholds = {
|
|
663
|
+
workers: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 5, enabled: true },
|
|
664
|
+
d1: { warningPct: 40, highPct: 60, criticalPct: 80, absoluteMax: 20, enabled: true },
|
|
665
|
+
kv: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 5, enabled: true },
|
|
666
|
+
r2: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 20, enabled: true },
|
|
667
|
+
durableObjects: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 10, enabled: true },
|
|
668
|
+
vectorize: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 5, enabled: true },
|
|
669
|
+
aiGateway: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 0, enabled: false },
|
|
670
|
+
workersAI: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 10, enabled: true },
|
|
671
|
+
pages: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 5, enabled: true },
|
|
672
|
+
queues: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 5, enabled: true },
|
|
673
|
+
workflows: { warningPct: 50, highPct: 75, criticalPct: 90, absoluteMax: 0, enabled: false },
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
/**
|
|
677
|
+
* Project patterns for resource identification.
|
|
678
|
+
*
|
|
679
|
+
* TODO: Customise these for your projects.
|
|
680
|
+
* Workers are matched by name (case-insensitive contains) or regex.
|
|
681
|
+
*/
|
|
682
|
+
export const PROJECT_PATTERNS: Record<
|
|
683
|
+
string,
|
|
684
|
+
{
|
|
685
|
+
workers: (string | RegExp)[];
|
|
686
|
+
d1: (string | RegExp)[];
|
|
687
|
+
kv: (string | RegExp)[];
|
|
688
|
+
r2: (string | RegExp)[];
|
|
689
|
+
vectorize: (string | RegExp)[];
|
|
690
|
+
aiGateway: (string | RegExp)[];
|
|
691
|
+
}
|
|
692
|
+
> = {
|
|
693
|
+
// TODO: Add your project patterns here. Example:
|
|
694
|
+
// 'my-project': {
|
|
695
|
+
// workers: [/^my-project/],
|
|
696
|
+
// d1: [/^my-project/],
|
|
697
|
+
// kv: [/^MY_PROJECT/],
|
|
698
|
+
// r2: [/^my-project/],
|
|
699
|
+
// vectorize: [/^my-project/],
|
|
700
|
+
// aiGateway: ['my-project'],
|
|
701
|
+
// },
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
/** Identify which project a resource belongs to */
|
|
705
|
+
export function identifyProject(resourceName: string): string | null {
|
|
706
|
+
for (const [project, patterns] of Object.entries(PROJECT_PATTERNS)) {
|
|
707
|
+
const allPatterns = [
|
|
708
|
+
...patterns.workers,
|
|
709
|
+
...patterns.d1,
|
|
710
|
+
...patterns.kv,
|
|
711
|
+
...patterns.r2,
|
|
712
|
+
...patterns.vectorize,
|
|
713
|
+
...patterns.aiGateway,
|
|
714
|
+
];
|
|
715
|
+
|
|
716
|
+
for (const pattern of allPatterns) {
|
|
717
|
+
if (typeof pattern === 'string') {
|
|
718
|
+
if (
|
|
719
|
+
resourceName === pattern ||
|
|
720
|
+
resourceName.toLowerCase().includes(pattern.toLowerCase())
|
|
721
|
+
) {
|
|
722
|
+
return project;
|
|
723
|
+
}
|
|
724
|
+
} else if (pattern.test(resourceName)) {
|
|
725
|
+
return project;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return null;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function calculateOverage(usage: number, included: number): number {
|
|
734
|
+
return Math.max(0, usage - included);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function calculateWorkersUsageCost(workers: WorkersMetrics[]): number {
|
|
738
|
+
const totalRequests = workers.reduce((sum, w) => sum + w.requests, 0);
|
|
739
|
+
const totalCpuMs = workers.reduce((sum, w) => sum + w.cpuTimeMs, 0);
|
|
740
|
+
let cost = 0;
|
|
741
|
+
const overageRequests = Math.max(0, totalRequests - CF_PRICING.workers.includedRequests);
|
|
742
|
+
cost += (overageRequests / 1_000_000) * CF_PRICING.workers.requestsPerMillion;
|
|
743
|
+
cost += (totalCpuMs / 1_000_000) * CF_PRICING.workers.cpuMsPerMillion;
|
|
744
|
+
return cost;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function calculateD1Cost(d1: D1Metrics[]): number {
|
|
748
|
+
const totalRowsRead = d1.reduce((sum, db) => sum + db.rowsRead, 0);
|
|
749
|
+
const totalRowsWritten = d1.reduce((sum, db) => sum + db.rowsWritten, 0);
|
|
750
|
+
let cost = 0;
|
|
751
|
+
cost +=
|
|
752
|
+
(calculateOverage(totalRowsRead, CF_PAID_ALLOWANCES.d1.rowsRead) / 1_000_000_000) *
|
|
753
|
+
CF_PRICING.d1.rowsReadPerBillion;
|
|
754
|
+
cost +=
|
|
755
|
+
(calculateOverage(totalRowsWritten, CF_PAID_ALLOWANCES.d1.rowsWritten) / 1_000_000) *
|
|
756
|
+
CF_PRICING.d1.rowsWrittenPerMillion;
|
|
757
|
+
return cost;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
function calculateKVCost(kv: KVMetrics[]): number {
|
|
761
|
+
const totalReads = kv.reduce((sum, ns) => sum + ns.reads, 0);
|
|
762
|
+
const totalWrites = kv.reduce((sum, ns) => sum + ns.writes, 0);
|
|
763
|
+
const totalDeletes = kv.reduce((sum, ns) => sum + ns.deletes, 0);
|
|
764
|
+
const totalLists = kv.reduce((sum, ns) => sum + ns.lists, 0);
|
|
765
|
+
let cost = 0;
|
|
766
|
+
cost +=
|
|
767
|
+
(calculateOverage(totalReads, CF_PAID_ALLOWANCES.kv.reads) / 1_000_000) *
|
|
768
|
+
CF_PRICING.kv.readsPerMillion;
|
|
769
|
+
cost +=
|
|
770
|
+
(calculateOverage(totalWrites, CF_PAID_ALLOWANCES.kv.writes) / 1_000_000) *
|
|
771
|
+
CF_PRICING.kv.writesPerMillion;
|
|
772
|
+
cost +=
|
|
773
|
+
(calculateOverage(totalDeletes, CF_PAID_ALLOWANCES.kv.deletes) / 1_000_000) *
|
|
774
|
+
CF_PRICING.kv.deletesPerMillion;
|
|
775
|
+
cost +=
|
|
776
|
+
(calculateOverage(totalLists, CF_PAID_ALLOWANCES.kv.lists) / 1_000_000) *
|
|
777
|
+
CF_PRICING.kv.listsPerMillion;
|
|
778
|
+
return cost;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function calculateR2Cost(r2: R2Metrics[]): number {
|
|
782
|
+
const totalStorage = r2.reduce((sum, b) => sum + b.storageBytes, 0);
|
|
783
|
+
const totalClassA = r2.reduce((sum, b) => sum + b.classAOperations, 0);
|
|
784
|
+
const totalClassB = r2.reduce((sum, b) => sum + b.classBOperations, 0);
|
|
785
|
+
let cost = 0;
|
|
786
|
+
cost +=
|
|
787
|
+
(calculateOverage(totalStorage, CF_PAID_ALLOWANCES.r2.storage) / 1_000_000_000) *
|
|
788
|
+
CF_PRICING.r2.storagePerGbMonth;
|
|
789
|
+
cost +=
|
|
790
|
+
(calculateOverage(totalClassA, CF_PAID_ALLOWANCES.r2.classA) / 1_000_000) *
|
|
791
|
+
CF_PRICING.r2.classAPerMillion;
|
|
792
|
+
cost +=
|
|
793
|
+
(calculateOverage(totalClassB, CF_PAID_ALLOWANCES.r2.classB) / 1_000_000) *
|
|
794
|
+
CF_PRICING.r2.classBPerMillion;
|
|
795
|
+
return cost;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function calculateDOCost(doMetrics: DOMetrics): number {
|
|
799
|
+
let cost = 0;
|
|
800
|
+
cost +=
|
|
801
|
+
(calculateOverage(doMetrics.requests, CF_PAID_ALLOWANCES.durableObjects.requests) / 1_000_000) *
|
|
802
|
+
CF_PRICING.durableObjects.requestsPerMillion;
|
|
803
|
+
cost +=
|
|
804
|
+
(calculateOverage(doMetrics.gbSeconds, CF_PAID_ALLOWANCES.durableObjects.gbSeconds) /
|
|
805
|
+
1_000_000) *
|
|
806
|
+
CF_PRICING.durableObjects.gbSecondsPerMillion;
|
|
807
|
+
cost += (doMetrics.storageReadUnits / 1_000_000) * CF_PRICING.durableObjects.readsPerMillion;
|
|
808
|
+
cost += (doMetrics.storageWriteUnits / 1_000_000) * CF_PRICING.durableObjects.writesPerMillion;
|
|
809
|
+
cost += (doMetrics.storageDeleteUnits / 1_000_000) * CF_PRICING.durableObjects.deletesPerMillion;
|
|
810
|
+
return cost;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function calculateVectorizeCost(vectorize: VectorizeInfo[]): number {
|
|
814
|
+
const totalDimensions = vectorize.reduce((sum, v) => sum + v.vectorCount * v.dimensions, 0);
|
|
815
|
+
const overageDimensions = calculateOverage(
|
|
816
|
+
totalDimensions,
|
|
817
|
+
CF_PAID_ALLOWANCES.vectorize.storedDimensions
|
|
818
|
+
);
|
|
819
|
+
return (overageDimensions / 1_000_000) * CF_PRICING.vectorize.storedDimensionsPerMillion;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function calculatePagesCost(pages: PagesMetrics[]): number {
|
|
823
|
+
const totalBuilds = pages.reduce((sum, p) => sum + p.totalBuilds, 0);
|
|
824
|
+
const overageBuilds = Math.max(0, totalBuilds - CF_FREE_LIMITS.pages.buildsPerMonth);
|
|
825
|
+
return overageBuilds * CF_PRICING.pages.buildCost;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
function calculateQueuesCost(queues: QueuesMetrics[]): number {
|
|
829
|
+
const totalMessages = queues.reduce((sum, q) => sum + q.messagesProduced + q.messagesConsumed, 0);
|
|
830
|
+
const overageMessages = Math.max(0, totalMessages - CF_FREE_LIMITS.queues.messagesPerMonth);
|
|
831
|
+
return (overageMessages / 1_000_000) * CF_PRICING.queues.messagesPerMillion;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/** Calculate cost breakdown for account usage */
|
|
835
|
+
export function calculateMonthlyCosts(
|
|
836
|
+
usage: AccountUsage & { queues?: QueuesMetrics[] }
|
|
837
|
+
): CostBreakdown {
|
|
838
|
+
const periodDays = usage.period === '24h' ? 1 : usage.period === '7d' ? 7 : 30;
|
|
839
|
+
const baseProration = periodDays / 30;
|
|
840
|
+
const workersUsage = calculateWorkersUsageCost(usage.workers);
|
|
841
|
+
const workersBase = CF_PRICING.workers.baseCostMonthly * baseProration;
|
|
842
|
+
const workers = workersBase + workersUsage;
|
|
843
|
+
const d1 = calculateD1Cost(usage.d1);
|
|
844
|
+
const kv = calculateKVCost(usage.kv);
|
|
845
|
+
const r2 = calculateR2Cost(usage.r2);
|
|
846
|
+
const durableObjects = calculateDOCost(usage.durableObjects);
|
|
847
|
+
const vectorize = calculateVectorizeCost(usage.vectorize);
|
|
848
|
+
const aiGateway = 0;
|
|
849
|
+
const pages = calculatePagesCost(usage.pages);
|
|
850
|
+
const queues = calculateQueuesCost(usage.queues ?? []);
|
|
851
|
+
const workflows = 0;
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
workers,
|
|
855
|
+
d1,
|
|
856
|
+
kv,
|
|
857
|
+
r2,
|
|
858
|
+
durableObjects,
|
|
859
|
+
vectorize,
|
|
860
|
+
aiGateway,
|
|
861
|
+
pages,
|
|
862
|
+
queues,
|
|
863
|
+
workflows,
|
|
864
|
+
workersAI: 0,
|
|
865
|
+
total:
|
|
866
|
+
workers + d1 + kv + r2 + durableObjects + vectorize + aiGateway + pages + queues + workflows,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/** Calculate cost breakdown by project */
|
|
871
|
+
export function calculateProjectCosts(usage: AccountUsage): ProjectCostBreakdown[] {
|
|
872
|
+
const projectCosts = new Map<string, ProjectCostBreakdown>();
|
|
873
|
+
|
|
874
|
+
const createEmptyBreakdown = (projectName: string): ProjectCostBreakdown => ({
|
|
875
|
+
project: projectName,
|
|
876
|
+
workers: 0,
|
|
877
|
+
d1: 0,
|
|
878
|
+
kv: 0,
|
|
879
|
+
r2: 0,
|
|
880
|
+
durableObjects: 0,
|
|
881
|
+
vectorize: 0,
|
|
882
|
+
aiGateway: 0,
|
|
883
|
+
workersAI: 0,
|
|
884
|
+
total: 0,
|
|
885
|
+
doRequests: 0,
|
|
886
|
+
doGbSeconds: 0,
|
|
887
|
+
workersRequests: 0,
|
|
888
|
+
workersErrors: 0,
|
|
889
|
+
workersCpuTimeMs: 0,
|
|
890
|
+
d1RowsRead: 0,
|
|
891
|
+
d1RowsWritten: 0,
|
|
892
|
+
kvReads: 0,
|
|
893
|
+
kvWrites: 0,
|
|
894
|
+
kvDeletes: 0,
|
|
895
|
+
kvLists: 0,
|
|
896
|
+
r2ClassAOps: 0,
|
|
897
|
+
r2ClassBOps: 0,
|
|
898
|
+
r2StorageBytes: 0,
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
for (const project of Object.keys(PROJECT_PATTERNS)) {
|
|
902
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
903
|
+
}
|
|
904
|
+
projectCosts.set('other', createEmptyBreakdown('other'));
|
|
905
|
+
|
|
906
|
+
const periodDays = usage.period === '24h' ? 1 : usage.period === '7d' ? 7 : 30;
|
|
907
|
+
const baseProration = periodDays / 30;
|
|
908
|
+
const proratedBaseCost = CF_PRICING.workers.baseCostMonthly * baseProration;
|
|
909
|
+
const totalRequests = usage.workers.reduce((sum, w) => sum + w.requests, 0);
|
|
910
|
+
const projectRequests = new Map<string, number>();
|
|
911
|
+
|
|
912
|
+
for (const worker of usage.workers) {
|
|
913
|
+
const project = identifyProject(worker.scriptName) ?? 'other';
|
|
914
|
+
projectRequests.set(project, (projectRequests.get(project) ?? 0) + worker.requests);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for (const worker of usage.workers) {
|
|
918
|
+
const project = identifyProject(worker.scriptName) ?? 'other';
|
|
919
|
+
const usageCost = calculateWorkersUsageCost([worker]);
|
|
920
|
+
if (!projectCosts.has(project)) {
|
|
921
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
922
|
+
}
|
|
923
|
+
const entry = projectCosts.get(project)!;
|
|
924
|
+
entry.workers += usageCost;
|
|
925
|
+
entry.total += usageCost;
|
|
926
|
+
entry.workersRequests += worker.requests;
|
|
927
|
+
entry.workersErrors += worker.errors;
|
|
928
|
+
entry.workersCpuTimeMs += worker.cpuTimeMs;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (totalRequests > 0) {
|
|
932
|
+
for (const [project, requests] of Array.from(projectRequests.entries())) {
|
|
933
|
+
const proportion = requests / totalRequests;
|
|
934
|
+
const baseCostShare = proratedBaseCost * proportion;
|
|
935
|
+
if (!projectCosts.has(project)) {
|
|
936
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
937
|
+
}
|
|
938
|
+
const entry = projectCosts.get(project)!;
|
|
939
|
+
entry.workers += baseCostShare;
|
|
940
|
+
entry.total += baseCostShare;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// D1 cost distribution
|
|
945
|
+
const totalD1Cost = calculateD1Cost(usage.d1);
|
|
946
|
+
const totalD1Usage = usage.d1.reduce((sum, db) => sum + db.rowsRead + db.rowsWritten, 0);
|
|
947
|
+
for (const db of usage.d1) {
|
|
948
|
+
const project = identifyProject(db.databaseName) ?? 'other';
|
|
949
|
+
if (!projectCosts.has(project)) {
|
|
950
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
951
|
+
}
|
|
952
|
+
const entry = projectCosts.get(project)!;
|
|
953
|
+
if (totalD1Usage > 0 && totalD1Cost > 0) {
|
|
954
|
+
const dbUsage = db.rowsRead + db.rowsWritten;
|
|
955
|
+
const proportion = dbUsage / totalD1Usage;
|
|
956
|
+
const cost = totalD1Cost * proportion;
|
|
957
|
+
entry.d1 += cost;
|
|
958
|
+
entry.total += cost;
|
|
959
|
+
}
|
|
960
|
+
entry.d1RowsRead += db.rowsRead;
|
|
961
|
+
entry.d1RowsWritten += db.rowsWritten;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
// KV cost distribution
|
|
965
|
+
const totalKVCost = calculateKVCost(usage.kv);
|
|
966
|
+
const totalKVUsage = usage.kv.reduce(
|
|
967
|
+
(sum, ns) => sum + ns.reads + ns.writes + ns.deletes + ns.lists,
|
|
968
|
+
0
|
|
969
|
+
);
|
|
970
|
+
for (const ns of usage.kv) {
|
|
971
|
+
const project = identifyProject(ns.namespaceName) ?? 'other';
|
|
972
|
+
if (!projectCosts.has(project)) {
|
|
973
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
974
|
+
}
|
|
975
|
+
const entry = projectCosts.get(project)!;
|
|
976
|
+
if (totalKVUsage > 0 && totalKVCost > 0) {
|
|
977
|
+
const nsUsage = ns.reads + ns.writes + ns.deletes + ns.lists;
|
|
978
|
+
const proportion = nsUsage / totalKVUsage;
|
|
979
|
+
const cost = totalKVCost * proportion;
|
|
980
|
+
entry.kv += cost;
|
|
981
|
+
entry.total += cost;
|
|
982
|
+
}
|
|
983
|
+
entry.kvReads += ns.reads;
|
|
984
|
+
entry.kvWrites += ns.writes;
|
|
985
|
+
entry.kvDeletes += ns.deletes;
|
|
986
|
+
entry.kvLists += ns.lists;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// R2 cost distribution
|
|
990
|
+
const totalR2Cost = calculateR2Cost(usage.r2);
|
|
991
|
+
const totalR2Usage = usage.r2.reduce(
|
|
992
|
+
(sum, b) =>
|
|
993
|
+
sum +
|
|
994
|
+
b.storageBytes / 1_000_000_000 +
|
|
995
|
+
b.classAOperations * 0.001 +
|
|
996
|
+
b.classBOperations * 0.0001,
|
|
997
|
+
0
|
|
998
|
+
);
|
|
999
|
+
for (const bucket of usage.r2) {
|
|
1000
|
+
const project = identifyProject(bucket.bucketName) ?? 'other';
|
|
1001
|
+
if (!projectCosts.has(project)) {
|
|
1002
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
1003
|
+
}
|
|
1004
|
+
const entry = projectCosts.get(project)!;
|
|
1005
|
+
if (totalR2Usage > 0 && totalR2Cost > 0) {
|
|
1006
|
+
const bucketUsage =
|
|
1007
|
+
bucket.storageBytes / 1_000_000_000 +
|
|
1008
|
+
bucket.classAOperations * 0.001 +
|
|
1009
|
+
bucket.classBOperations * 0.0001;
|
|
1010
|
+
const proportion = bucketUsage / totalR2Usage;
|
|
1011
|
+
const cost = totalR2Cost * proportion;
|
|
1012
|
+
entry.r2 += cost;
|
|
1013
|
+
entry.total += cost;
|
|
1014
|
+
}
|
|
1015
|
+
entry.r2ClassAOps += bucket.classAOperations;
|
|
1016
|
+
entry.r2ClassBOps += bucket.classBOperations;
|
|
1017
|
+
entry.r2StorageBytes += bucket.storageBytes;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// DO cost distribution
|
|
1021
|
+
const totalDOCost = calculateDOCost(usage.durableObjects);
|
|
1022
|
+
if (usage.durableObjects.byScript && usage.durableObjects.byScript.length > 0) {
|
|
1023
|
+
const totalWeight = usage.durableObjects.byScript.reduce(
|
|
1024
|
+
(sum, script) => sum + script.requests * 0.3 + script.gbSeconds * 0.7,
|
|
1025
|
+
0
|
|
1026
|
+
);
|
|
1027
|
+
if (totalWeight > 0) {
|
|
1028
|
+
for (const script of usage.durableObjects.byScript) {
|
|
1029
|
+
const project = identifyProject(script.scriptName) ?? 'other';
|
|
1030
|
+
const scriptWeight = script.requests * 0.3 + script.gbSeconds * 0.7;
|
|
1031
|
+
const scriptCost = totalDOCost > 0 ? totalDOCost * (scriptWeight / totalWeight) : 0;
|
|
1032
|
+
if (!projectCosts.has(project)) {
|
|
1033
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
1034
|
+
}
|
|
1035
|
+
const entry = projectCosts.get(project)!;
|
|
1036
|
+
entry.durableObjects += scriptCost;
|
|
1037
|
+
entry.total += scriptCost;
|
|
1038
|
+
entry.doRequests += script.requests;
|
|
1039
|
+
entry.doGbSeconds += script.gbSeconds;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Vectorize cost distribution
|
|
1045
|
+
const totalVectorizeCost = calculateVectorizeCost(usage.vectorize);
|
|
1046
|
+
const totalVectorizeUsage = usage.vectorize.reduce(
|
|
1047
|
+
(sum, v) => sum + v.vectorCount * v.dimensions,
|
|
1048
|
+
0
|
|
1049
|
+
);
|
|
1050
|
+
for (const index of usage.vectorize) {
|
|
1051
|
+
const project = identifyProject(index.name) ?? 'other';
|
|
1052
|
+
if (!projectCosts.has(project)) {
|
|
1053
|
+
projectCosts.set(project, createEmptyBreakdown(project));
|
|
1054
|
+
}
|
|
1055
|
+
const entry = projectCosts.get(project)!;
|
|
1056
|
+
if (totalVectorizeUsage > 0 && totalVectorizeCost > 0) {
|
|
1057
|
+
const indexUsage = index.vectorCount * index.dimensions;
|
|
1058
|
+
const proportion = indexUsage / totalVectorizeUsage;
|
|
1059
|
+
const cost = totalVectorizeCost * proportion;
|
|
1060
|
+
entry.vectorize += cost;
|
|
1061
|
+
entry.total += cost;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
return Array.from(projectCosts.values()).filter((p) => p.total > 0);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/** Get threshold level */
|
|
1069
|
+
export function getThresholdLevel(
|
|
1070
|
+
percentage: number,
|
|
1071
|
+
thresholds?: ServiceThreshold
|
|
1072
|
+
): ThresholdLevel {
|
|
1073
|
+
const criticalPct = thresholds?.criticalPct ?? 90;
|
|
1074
|
+
const highPct = thresholds?.highPct ?? 75;
|
|
1075
|
+
const warningPct = thresholds?.warningPct ?? 50;
|
|
1076
|
+
if (percentage >= criticalPct) return 'critical';
|
|
1077
|
+
if (percentage >= highPct) return 'high';
|
|
1078
|
+
if (percentage >= warningPct) return 'warning';
|
|
1079
|
+
return 'normal';
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/** Merge custom thresholds with defaults */
|
|
1083
|
+
export function mergeThresholds(custom?: Partial<AlertThresholds>): AlertThresholds {
|
|
1084
|
+
if (!custom) return DEFAULT_ALERT_THRESHOLDS;
|
|
1085
|
+
const merged: AlertThresholds = { ...DEFAULT_ALERT_THRESHOLDS };
|
|
1086
|
+
for (const key of Object.keys(custom)) {
|
|
1087
|
+
if (merged[key] && custom[key]) {
|
|
1088
|
+
merged[key] = { ...merged[key], ...custom[key] };
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
return merged;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
/** Analyse usage against thresholds */
|
|
1095
|
+
export function analyseThresholds(
|
|
1096
|
+
usage: AccountUsage,
|
|
1097
|
+
customThresholds?: Partial<AlertThresholds>
|
|
1098
|
+
): ThresholdAnalysis {
|
|
1099
|
+
const warnings: ThresholdWarning[] = [];
|
|
1100
|
+
const thresholds = mergeThresholds(customThresholds);
|
|
1101
|
+
const dailyScale = usage.period === '24h' ? 1 : usage.period === '7d' ? 1 / 7 : 1 / 30;
|
|
1102
|
+
|
|
1103
|
+
const shouldWarn = (serviceType: string, percentage: number): boolean => {
|
|
1104
|
+
const t = thresholds[serviceType];
|
|
1105
|
+
if (!t || !t.enabled) return false;
|
|
1106
|
+
return percentage >= t.warningPct;
|
|
1107
|
+
};
|
|
1108
|
+
|
|
1109
|
+
if (thresholds.workers.enabled) {
|
|
1110
|
+
const totalRequests = usage.workers.reduce((sum, w) => sum + w.requests, 0);
|
|
1111
|
+
const dailyRequests = totalRequests * dailyScale;
|
|
1112
|
+
const pct = (dailyRequests / CF_FREE_LIMITS.workers.requestsPerDay) * 100;
|
|
1113
|
+
if (shouldWarn('workers', pct)) {
|
|
1114
|
+
warnings.push({
|
|
1115
|
+
resource: 'Workers',
|
|
1116
|
+
metric: 'Requests/day',
|
|
1117
|
+
current: dailyRequests,
|
|
1118
|
+
limit: CF_FREE_LIMITS.workers.requestsPerDay,
|
|
1119
|
+
percentage: pct,
|
|
1120
|
+
level: getThresholdLevel(pct, thresholds.workers),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
if (thresholds.d1.enabled) {
|
|
1126
|
+
const totalRowsRead = usage.d1.reduce((sum, db) => sum + db.rowsRead, 0);
|
|
1127
|
+
const dailyRowsRead = totalRowsRead * dailyScale;
|
|
1128
|
+
const pct = (dailyRowsRead / CF_FREE_LIMITS.d1.rowsReadPerDay) * 100;
|
|
1129
|
+
if (shouldWarn('d1', pct)) {
|
|
1130
|
+
warnings.push({
|
|
1131
|
+
resource: 'D1',
|
|
1132
|
+
metric: 'Rows Read/day',
|
|
1133
|
+
current: dailyRowsRead,
|
|
1134
|
+
limit: CF_FREE_LIMITS.d1.rowsReadPerDay,
|
|
1135
|
+
percentage: pct,
|
|
1136
|
+
level: getThresholdLevel(pct, thresholds.d1),
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return {
|
|
1142
|
+
warnings,
|
|
1143
|
+
hasWarnings: warnings.length > 0,
|
|
1144
|
+
hasCritical: warnings.some((w) => w.level === 'critical'),
|
|
1145
|
+
};
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Calculate costs for a single day's usage metrics */
|
|
1149
|
+
export function calculateDailyCosts(usage: DailyUsageMetrics): Omit<DailyCostBreakdown, 'date'> {
|
|
1150
|
+
const dailyIncludedRequests = CF_PRICING.workers.includedRequests / 30;
|
|
1151
|
+
const overageRequests = Math.max(0, usage.workersRequests - dailyIncludedRequests);
|
|
1152
|
+
const workersCost =
|
|
1153
|
+
(overageRequests / 1_000_000) * CF_PRICING.workers.requestsPerMillion +
|
|
1154
|
+
(usage.workersCpuMs / 1_000_000) * CF_PRICING.workers.cpuMsPerMillion;
|
|
1155
|
+
const d1Cost =
|
|
1156
|
+
(usage.d1Reads / 1_000_000_000) * CF_PRICING.d1.rowsReadPerBillion +
|
|
1157
|
+
(usage.d1Writes / 1_000_000) * CF_PRICING.d1.rowsWrittenPerMillion;
|
|
1158
|
+
const kvCost =
|
|
1159
|
+
(usage.kvReads / 1_000_000) * CF_PRICING.kv.readsPerMillion +
|
|
1160
|
+
(usage.kvWrites / 1_000_000) * CF_PRICING.kv.writesPerMillion +
|
|
1161
|
+
((usage.kvDeletes ?? 0) / 1_000_000) * CF_PRICING.kv.deletesPerMillion +
|
|
1162
|
+
((usage.kvLists ?? 0) / 1_000_000) * CF_PRICING.kv.listsPerMillion;
|
|
1163
|
+
const r2Cost =
|
|
1164
|
+
(usage.r2ClassA / 1_000_000) * CF_PRICING.r2.classAPerMillion +
|
|
1165
|
+
(usage.r2ClassB / 1_000_000) * CF_PRICING.r2.classBPerMillion;
|
|
1166
|
+
const vectorizeCost =
|
|
1167
|
+
(usage.vectorizeQueries / 1_000_000) * CF_PRICING.vectorize.queriedDimensionsPerMillion;
|
|
1168
|
+
const durableObjectsCost =
|
|
1169
|
+
(usage.durableObjectsRequests / 1_000_000) * CF_PRICING.durableObjects.requestsPerMillion +
|
|
1170
|
+
((usage.durableObjectsGbSeconds ?? 0) / 1_000_000) *
|
|
1171
|
+
CF_PRICING.durableObjects.gbSecondsPerMillion;
|
|
1172
|
+
const workersAITokens = usage.workersAITokens ?? 0;
|
|
1173
|
+
const workersAICost =
|
|
1174
|
+
((workersAITokens / 1_000_000) *
|
|
1175
|
+
(CF_PRICING.workersAI.models['default'].input +
|
|
1176
|
+
CF_PRICING.workersAI.models['default'].output)) /
|
|
1177
|
+
2;
|
|
1178
|
+
const queuesMessages = usage.queuesMessages ?? 0;
|
|
1179
|
+
const queuesCost = (queuesMessages / 1_000_000) * CF_PRICING.queues.messagesPerMillion;
|
|
1180
|
+
const total =
|
|
1181
|
+
workersCost +
|
|
1182
|
+
d1Cost +
|
|
1183
|
+
kvCost +
|
|
1184
|
+
r2Cost +
|
|
1185
|
+
vectorizeCost +
|
|
1186
|
+
durableObjectsCost +
|
|
1187
|
+
workersAICost +
|
|
1188
|
+
queuesCost;
|
|
1189
|
+
|
|
1190
|
+
return {
|
|
1191
|
+
workers: workersCost,
|
|
1192
|
+
d1: d1Cost,
|
|
1193
|
+
kv: kvCost,
|
|
1194
|
+
r2: r2Cost,
|
|
1195
|
+
vectorize: vectorizeCost,
|
|
1196
|
+
aiGateway: 0,
|
|
1197
|
+
durableObjects: durableObjectsCost,
|
|
1198
|
+
workersAI: workersAICost,
|
|
1199
|
+
pages: 0,
|
|
1200
|
+
queues: queuesCost,
|
|
1201
|
+
workflows: 0,
|
|
1202
|
+
total,
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/** Format number for display */
|
|
1207
|
+
export function formatNumber(n: number): string {
|
|
1208
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(2)}B`;
|
|
1209
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(2)}M`;
|
|
1210
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(2)}K`;
|
|
1211
|
+
return n.toLocaleString('en-AU');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
/** Format currency for display */
|
|
1215
|
+
export function formatCurrency(amount: number): string {
|
|
1216
|
+
return `$${amount.toFixed(2)}`;
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// =============================================================================
|
|
1220
|
+
// PROJECT REGISTRY (D1-backed)
|
|
1221
|
+
// =============================================================================
|
|
1222
|
+
|
|
1223
|
+
// Re-export from shared types (already defined there)
|
|
1224
|
+
export type { Project, ResourceMapping, ResourceType } from './types';
|
|
1225
|
+
|
|
1226
|
+
/** In-memory cache for registry data */
|
|
1227
|
+
interface RegistryCache {
|
|
1228
|
+
projects: Map<string, import('./types').Project>;
|
|
1229
|
+
loadedAt: number;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
let registryCache: RegistryCache | null = null;
|
|
1233
|
+
const REGISTRY_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
1234
|
+
|
|
1235
|
+
/** Clear the in-memory cache */
|
|
1236
|
+
export function clearRegistryCache(): void {
|
|
1237
|
+
registryCache = null;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/** Get all projects from D1 */
|
|
1241
|
+
export async function getProjects(db: D1Database): Promise<import('./types').Project[]> {
|
|
1242
|
+
if (registryCache && Date.now() - registryCache.loadedAt < REGISTRY_CACHE_TTL_MS) {
|
|
1243
|
+
return Array.from(registryCache.projects.values());
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
try {
|
|
1247
|
+
const result = await db
|
|
1248
|
+
.prepare(
|
|
1249
|
+
`SELECT project_id, display_name, description, color, icon, owner,
|
|
1250
|
+
repo_path, status, primary_resource, custom_limit, repo_url, github_repo_id
|
|
1251
|
+
FROM project_registry
|
|
1252
|
+
WHERE status != 'archived'
|
|
1253
|
+
ORDER BY display_name`
|
|
1254
|
+
)
|
|
1255
|
+
.all<{
|
|
1256
|
+
project_id: string;
|
|
1257
|
+
display_name: string;
|
|
1258
|
+
description: string | null;
|
|
1259
|
+
color: string | null;
|
|
1260
|
+
icon: string | null;
|
|
1261
|
+
owner: string | null;
|
|
1262
|
+
repo_path: string | null;
|
|
1263
|
+
status: string;
|
|
1264
|
+
primary_resource: string | null;
|
|
1265
|
+
custom_limit: number | null;
|
|
1266
|
+
repo_url: string | null;
|
|
1267
|
+
github_repo_id: string | null;
|
|
1268
|
+
}>();
|
|
1269
|
+
|
|
1270
|
+
const projects: import('./types').Project[] = (result.results ?? []).map((row) => ({
|
|
1271
|
+
projectId: row.project_id,
|
|
1272
|
+
displayName: row.display_name,
|
|
1273
|
+
description: row.description,
|
|
1274
|
+
color: row.color,
|
|
1275
|
+
icon: row.icon,
|
|
1276
|
+
owner: row.owner,
|
|
1277
|
+
repoPath: row.repo_path,
|
|
1278
|
+
status: row.status as 'active' | 'archived' | 'development',
|
|
1279
|
+
primaryResource: row.primary_resource as import('./types').ResourceType | null,
|
|
1280
|
+
customLimit: row.custom_limit,
|
|
1281
|
+
repoUrl: row.repo_url,
|
|
1282
|
+
githubRepoId: row.github_repo_id,
|
|
1283
|
+
}));
|
|
1284
|
+
|
|
1285
|
+
// Update cache
|
|
1286
|
+
const projectMap = new Map<string, import('./types').Project>();
|
|
1287
|
+
for (const p of projects) {
|
|
1288
|
+
projectMap.set(p.projectId, p);
|
|
1289
|
+
}
|
|
1290
|
+
registryCache = { projects: projectMap, loadedAt: Date.now() };
|
|
1291
|
+
|
|
1292
|
+
return projects;
|
|
1293
|
+
} catch {
|
|
1294
|
+
return [];
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** Get a single project by ID */
|
|
1299
|
+
export async function getProject(
|
|
1300
|
+
db: D1Database,
|
|
1301
|
+
projectId: string
|
|
1302
|
+
): Promise<import('./types').Project | null> {
|
|
1303
|
+
const projects = await getProjects(db);
|
|
1304
|
+
return projects.find((p) => p.projectId === projectId) ?? null;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
/** Identify project from D1 registry */
|
|
1308
|
+
export async function identifyProjectFromRegistry(
|
|
1309
|
+
db: D1Database,
|
|
1310
|
+
resourceType: string,
|
|
1311
|
+
resourceName: string
|
|
1312
|
+
): Promise<string | null> {
|
|
1313
|
+
try {
|
|
1314
|
+
const result = await db
|
|
1315
|
+
.prepare(
|
|
1316
|
+
`SELECT project_id FROM resource_project_mapping
|
|
1317
|
+
WHERE resource_type = ? AND (resource_name = ? OR resource_id = ?)
|
|
1318
|
+
LIMIT 1`
|
|
1319
|
+
)
|
|
1320
|
+
.bind(resourceType, resourceName, resourceName)
|
|
1321
|
+
.first<{ project_id: string }>();
|
|
1322
|
+
|
|
1323
|
+
return result?.project_id ?? identifyProject(resourceName);
|
|
1324
|
+
} catch {
|
|
1325
|
+
return identifyProject(resourceName);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// =============================================================================
|
|
1330
|
+
// D1 HELPERS
|
|
1331
|
+
// =============================================================================
|
|
1332
|
+
|
|
1333
|
+
/** Health check record from system_health_checks table */
|
|
1334
|
+
export interface HealthCheckRecord {
|
|
1335
|
+
service_name: string;
|
|
1336
|
+
status: string;
|
|
1337
|
+
last_check: string;
|
|
1338
|
+
details: string | null;
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
/** Project health map */
|
|
1342
|
+
export type ProjectHealthMap = Map<string, HealthCheckRecord>;
|
|
1343
|
+
|
|
1344
|
+
/** Get system health from D1 */
|
|
1345
|
+
export async function getSystemHealth(db: D1Database): Promise<ProjectHealthMap> {
|
|
1346
|
+
const healthMap: ProjectHealthMap = new Map();
|
|
1347
|
+
try {
|
|
1348
|
+
const result = await db
|
|
1349
|
+
.prepare(
|
|
1350
|
+
`SELECT service_name, status, last_check, details
|
|
1351
|
+
FROM system_health_checks
|
|
1352
|
+
ORDER BY last_check DESC`
|
|
1353
|
+
)
|
|
1354
|
+
.all<HealthCheckRecord>();
|
|
1355
|
+
for (const row of result.results ?? []) {
|
|
1356
|
+
healthMap.set(row.service_name, row);
|
|
1357
|
+
}
|
|
1358
|
+
} catch {
|
|
1359
|
+
// Table may not exist yet
|
|
1360
|
+
}
|
|
1361
|
+
return healthMap;
|
|
1362
|
+
}
|