@littlebearapps/platform-admin-sdk 2.0.0 → 2.1.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 +2 -2
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +86 -2
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Allowance & Licensing Configuration
|
|
3
|
+
*
|
|
4
|
+
* Defines monthly limits for Cloudflare services to track "Usage vs. Included Thresholds".
|
|
5
|
+
* These values represent the Workers Paid plan allowances and free tier limits.
|
|
6
|
+
*
|
|
7
|
+
* @see https://developers.cloudflare.com/workers/platform/pricing/
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Service type identifiers
|
|
12
|
+
*/
|
|
13
|
+
export type ServiceType =
|
|
14
|
+
| 'workers'
|
|
15
|
+
| 'd1'
|
|
16
|
+
| 'kv'
|
|
17
|
+
| 'r2'
|
|
18
|
+
| 'durableObjects'
|
|
19
|
+
| 'vectorize'
|
|
20
|
+
| 'aiGateway'
|
|
21
|
+
| 'workersAI'
|
|
22
|
+
| 'pages'
|
|
23
|
+
| 'queues';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Utilization status based on percentage thresholds
|
|
27
|
+
*/
|
|
28
|
+
export type UtilizationStatus = 'green' | 'yellow' | 'red';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Service allowance definition
|
|
32
|
+
*/
|
|
33
|
+
export interface ServiceAllowance {
|
|
34
|
+
/** Human-readable service name */
|
|
35
|
+
name: string;
|
|
36
|
+
/** Monthly allowance value (in native units) */
|
|
37
|
+
monthlyLimit: number;
|
|
38
|
+
/** Unit of measurement */
|
|
39
|
+
unit: string;
|
|
40
|
+
/** Whether this is a paid plan limit (vs free tier) */
|
|
41
|
+
isPaidPlan: boolean;
|
|
42
|
+
/** Description of the allowance */
|
|
43
|
+
description: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Project-specific allowance overrides
|
|
48
|
+
*/
|
|
49
|
+
export interface ProjectAllowance {
|
|
50
|
+
projectId: string;
|
|
51
|
+
projectName: string;
|
|
52
|
+
/** Primary resource type for this project (e.g., D1 writes for database-heavy projects) */
|
|
53
|
+
primaryResource: ServiceType;
|
|
54
|
+
/** Custom limits that override account-level defaults */
|
|
55
|
+
overrides?: Partial<Record<ServiceType, number>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Account-level Cloudflare service allowances (Workers Paid Plan)
|
|
60
|
+
*
|
|
61
|
+
* Based on Cloudflare pricing as of January 2026:
|
|
62
|
+
* - Workers Paid: $5/month includes 10M requests
|
|
63
|
+
* - D1: 50M writes/month for paid accounts
|
|
64
|
+
* - R2: 10 GB Class A operations included
|
|
65
|
+
*/
|
|
66
|
+
export const CF_ALLOWANCES: Record<ServiceType, ServiceAllowance> = {
|
|
67
|
+
workers: {
|
|
68
|
+
name: 'Workers Requests',
|
|
69
|
+
monthlyLimit: 10_000_000, // 10M requests/month included
|
|
70
|
+
unit: 'requests',
|
|
71
|
+
isPaidPlan: true,
|
|
72
|
+
description: '10M requests included with Workers Paid ($5/mo)',
|
|
73
|
+
},
|
|
74
|
+
d1: {
|
|
75
|
+
name: 'D1 Writes',
|
|
76
|
+
monthlyLimit: 50_000_000, // 50M writes/month threshold (primary metric for utilization)
|
|
77
|
+
unit: 'rows written',
|
|
78
|
+
isPaidPlan: true,
|
|
79
|
+
description: '25B rows read + 50M rows written per month included',
|
|
80
|
+
},
|
|
81
|
+
kv: {
|
|
82
|
+
name: 'KV Writes',
|
|
83
|
+
monthlyLimit: 1_000_000, // 1M writes/month threshold (primary metric for utilization)
|
|
84
|
+
unit: 'writes',
|
|
85
|
+
isPaidPlan: true,
|
|
86
|
+
description: '10M reads + 1M writes/deletes/lists per month included',
|
|
87
|
+
},
|
|
88
|
+
r2: {
|
|
89
|
+
name: 'R2 Storage',
|
|
90
|
+
monthlyLimit: 10_000_000_000, // 10GB storage (primary metric for utilization)
|
|
91
|
+
unit: 'bytes',
|
|
92
|
+
isPaidPlan: true,
|
|
93
|
+
description: '10GB storage + 1M Class A + 10M Class B ops per month included',
|
|
94
|
+
},
|
|
95
|
+
durableObjects: {
|
|
96
|
+
name: 'Durable Objects Requests',
|
|
97
|
+
monthlyLimit: 1_000_000, // 1M requests/month Workers Paid Plan
|
|
98
|
+
unit: 'requests',
|
|
99
|
+
isPaidPlan: true,
|
|
100
|
+
description: '1M requests included in Workers Paid Plan',
|
|
101
|
+
},
|
|
102
|
+
vectorize: {
|
|
103
|
+
name: 'Vectorize Stored Dimensions',
|
|
104
|
+
monthlyLimit: 10_000_000, // 10M stored dimensions/month Workers Paid Plan
|
|
105
|
+
unit: 'dimensions',
|
|
106
|
+
isPaidPlan: true,
|
|
107
|
+
description:
|
|
108
|
+
'10M stored dimensions + 50M queried dimensions per month included in Workers Paid Plan',
|
|
109
|
+
},
|
|
110
|
+
aiGateway: {
|
|
111
|
+
name: 'AI Gateway Requests',
|
|
112
|
+
monthlyLimit: Infinity, // Unlimited (free service)
|
|
113
|
+
unit: 'requests',
|
|
114
|
+
isPaidPlan: false,
|
|
115
|
+
description: 'AI Gateway is free (cost is from underlying AI provider)',
|
|
116
|
+
},
|
|
117
|
+
// CONSERVATIVE: Set to 0 allowance for accurate billing visibility on Workers Paid Plan.
|
|
118
|
+
// Cloudflare provides a 10K neurons/day free tier (resets daily at midnight UTC),
|
|
119
|
+
// but we report from first neuron to ensure costs are never hidden.
|
|
120
|
+
// Better to over-report than under-report for budget tracking.
|
|
121
|
+
workersAI: {
|
|
122
|
+
name: 'Workers AI Neurons',
|
|
123
|
+
monthlyLimit: 0, // Pay-as-you-go from first neuron on Workers Paid
|
|
124
|
+
unit: 'neurons',
|
|
125
|
+
isPaidPlan: true,
|
|
126
|
+
description:
|
|
127
|
+
'Pay-as-you-go: $0.011/1K neurons. No paid plan inclusion (free tier may apply but not tracked).',
|
|
128
|
+
},
|
|
129
|
+
pages: {
|
|
130
|
+
name: 'Pages Builds',
|
|
131
|
+
monthlyLimit: 500, // 500 builds/month free
|
|
132
|
+
unit: 'builds',
|
|
133
|
+
isPaidPlan: false,
|
|
134
|
+
description: '500 builds per month in free tier',
|
|
135
|
+
},
|
|
136
|
+
queues: {
|
|
137
|
+
name: 'Queues Messages',
|
|
138
|
+
monthlyLimit: 1_000_000, // 1M messages/month free
|
|
139
|
+
unit: 'messages',
|
|
140
|
+
isPaidPlan: false,
|
|
141
|
+
description: '1M messages per month in free tier',
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Daily limits (derived from monthly for rate limiting purposes)
|
|
147
|
+
*
|
|
148
|
+
* IMPORTANT: Most services reset MONTHLY, so daily limit = monthly / 30.
|
|
149
|
+
* Services with 0 monthly allowance (pay-as-you-go) have 0 daily limit.
|
|
150
|
+
*/
|
|
151
|
+
export const CF_DAILY_LIMITS: Record<ServiceType, number> = {
|
|
152
|
+
workers: Math.floor(CF_ALLOWANCES.workers.monthlyLimit / 30), // ~333K/day
|
|
153
|
+
d1: Math.floor(CF_ALLOWANCES.d1.monthlyLimit / 30), // ~1.67M/day
|
|
154
|
+
kv: Math.floor(CF_ALLOWANCES.kv.monthlyLimit / 30), // ~33K/day
|
|
155
|
+
r2: Math.floor(CF_ALLOWANCES.r2.monthlyLimit / 30),
|
|
156
|
+
durableObjects: Math.floor(CF_ALLOWANCES.durableObjects.monthlyLimit / 30),
|
|
157
|
+
vectorize: 0, // Pay-as-you-go (no free tier tracked)
|
|
158
|
+
aiGateway: Infinity,
|
|
159
|
+
workersAI: 0, // Pay-as-you-go (no free tier tracked)
|
|
160
|
+
pages: Math.floor(CF_ALLOWANCES.pages.monthlyLimit / 30),
|
|
161
|
+
queues: Math.floor(CF_ALLOWANCES.queues.monthlyLimit / 30),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Project-specific configurations with primary resource assignments
|
|
166
|
+
*
|
|
167
|
+
* NOTE: Project overrides removed (2026-01-15). Utilization now calculated
|
|
168
|
+
* against account-level CF_ALLOWANCES to match actual Cloudflare billing.
|
|
169
|
+
* If project-level budgets are needed in future, implement via D1 usage_settings
|
|
170
|
+
* table with UI configuration in ThresholdSettings.
|
|
171
|
+
*/
|
|
172
|
+
export const PROJECT_ALLOWANCES: ProjectAllowance[] = [
|
|
173
|
+
{
|
|
174
|
+
projectId: '{{projectSlug}}',
|
|
175
|
+
projectName: '{{projectName}}',
|
|
176
|
+
primaryResource: 'd1',
|
|
177
|
+
// Utilization uses CF_ALLOWANCES.d1 (50M writes/month)
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
projectId: 'platform',
|
|
181
|
+
projectName: 'Platform',
|
|
182
|
+
primaryResource: 'd1',
|
|
183
|
+
// Utilization uses CF_ALLOWANCES.d1 (50M writes/month)
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Utilization threshold percentages for traffic light status
|
|
189
|
+
*/
|
|
190
|
+
export const UTILIZATION_THRESHOLDS = {
|
|
191
|
+
/** Green zone: < 70% utilization */
|
|
192
|
+
green: 70,
|
|
193
|
+
/** Yellow zone: 70-90% utilization */
|
|
194
|
+
yellow: 90,
|
|
195
|
+
/** Red zone: > 90% utilization (or overage) */
|
|
196
|
+
red: 100,
|
|
197
|
+
} as const;
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Project data from D1 registry (via /api/usage/projects)
|
|
201
|
+
*/
|
|
202
|
+
interface D1Project {
|
|
203
|
+
projectId: string;
|
|
204
|
+
displayName: string;
|
|
205
|
+
primaryResource: string | null;
|
|
206
|
+
customLimit: number | null;
|
|
207
|
+
status: string;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Map D1 resource type to ServiceType
|
|
212
|
+
*/
|
|
213
|
+
function mapResourceType(d1Resource: string | null): ServiceType {
|
|
214
|
+
if (!d1Resource) return 'workers';
|
|
215
|
+
|
|
216
|
+
const mapping: Record<string, ServiceType> = {
|
|
217
|
+
worker: 'workers',
|
|
218
|
+
workers: 'workers',
|
|
219
|
+
d1: 'd1',
|
|
220
|
+
kv: 'kv',
|
|
221
|
+
r2: 'r2',
|
|
222
|
+
vectorize: 'vectorize',
|
|
223
|
+
ai_gateway: 'aiGateway',
|
|
224
|
+
workers_ai: 'workersAI',
|
|
225
|
+
workersai: 'workersAI',
|
|
226
|
+
pages: 'pages',
|
|
227
|
+
queue: 'queues',
|
|
228
|
+
durable_object: 'durableObjects',
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
return mapping[d1Resource.toLowerCase()] ?? 'workers';
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Fetch project allowances from D1 registry via API.
|
|
236
|
+
* Falls back to static PROJECT_ALLOWANCES if the API fails.
|
|
237
|
+
*
|
|
238
|
+
* @param baseUrl - Base URL for the API
|
|
239
|
+
* @param fetchFn - Optional fetch function (for SSR environments)
|
|
240
|
+
* @returns ProjectAllowance array
|
|
241
|
+
*/
|
|
242
|
+
export async function fetchProjectAllowances(
|
|
243
|
+
baseUrl: string = '',
|
|
244
|
+
fetchFn: typeof fetch = fetch
|
|
245
|
+
): Promise<ProjectAllowance[]> {
|
|
246
|
+
try {
|
|
247
|
+
const response = await fetchFn(`${baseUrl}/api/usage/projects`, {
|
|
248
|
+
credentials: 'include',
|
|
249
|
+
headers: {
|
|
250
|
+
Accept: 'application/json',
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (!response.ok) {
|
|
255
|
+
console.warn(`[allowance-config] API returned ${response.status}, using static fallback`);
|
|
256
|
+
return PROJECT_ALLOWANCES;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const json = (await response.json()) as {
|
|
260
|
+
success: boolean;
|
|
261
|
+
projects?: D1Project[];
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
if (!json.success || !json.projects) {
|
|
265
|
+
console.warn('[allowance-config] Invalid API response, using static fallback');
|
|
266
|
+
return PROJECT_ALLOWANCES;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Convert D1 projects to ProjectAllowance format
|
|
270
|
+
const dynamicAllowances: ProjectAllowance[] = json.projects
|
|
271
|
+
.filter((p) => p.status === 'active' && p.primaryResource)
|
|
272
|
+
.map((p) => {
|
|
273
|
+
const primaryResource = mapResourceType(p.primaryResource);
|
|
274
|
+
const allowance: ProjectAllowance = {
|
|
275
|
+
projectId: p.projectId,
|
|
276
|
+
projectName: p.displayName,
|
|
277
|
+
primaryResource,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// Add custom limit as override if specified
|
|
281
|
+
if (p.customLimit && p.customLimit > 0) {
|
|
282
|
+
allowance.overrides = {
|
|
283
|
+
[primaryResource]: p.customLimit,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return allowance;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// If we got valid data, return it; otherwise fall back
|
|
291
|
+
if (dynamicAllowances.length > 0) {
|
|
292
|
+
console.log(
|
|
293
|
+
`[allowance-config] Loaded ${dynamicAllowances.length} projects from D1 registry`
|
|
294
|
+
);
|
|
295
|
+
return dynamicAllowances;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return PROJECT_ALLOWANCES;
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.warn('[allowance-config] Failed to fetch from API, using static fallback:', error);
|
|
301
|
+
return PROJECT_ALLOWANCES;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Get project allowances (sync version, returns static config)
|
|
307
|
+
* Use fetchProjectAllowances() for dynamic D1-backed data.
|
|
308
|
+
*/
|
|
309
|
+
export function getStaticProjectAllowances(): ProjectAllowance[] {
|
|
310
|
+
return PROJECT_ALLOWANCES;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Calculate utilization percentage
|
|
315
|
+
*/
|
|
316
|
+
export function calculateUtilizationPct(current: number, limit: number): number {
|
|
317
|
+
if (limit === Infinity || limit === 0) return 0;
|
|
318
|
+
return Math.min((current / limit) * 100, 999); // Cap at 999% for display
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get utilization status (traffic light) based on percentage
|
|
323
|
+
*/
|
|
324
|
+
export function getUtilizationStatus(percentage: number): UtilizationStatus {
|
|
325
|
+
if (percentage < UTILIZATION_THRESHOLDS.green) return 'green';
|
|
326
|
+
if (percentage < UTILIZATION_THRESHOLDS.yellow) return 'yellow';
|
|
327
|
+
return 'red';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Get the effective limit for a project/service combination
|
|
332
|
+
*/
|
|
333
|
+
export function getEffectiveLimit(_projectId: string, serviceType: ServiceType): number {
|
|
334
|
+
// Always use account-level CF allowances for utilization calculations.
|
|
335
|
+
// This matches actual Cloudflare billing and provides accurate visibility.
|
|
336
|
+
// Project-specific budgets were removed 2026-01-15 - see PROJECT_ALLOWANCES comment.
|
|
337
|
+
return CF_ALLOWANCES[serviceType].monthlyLimit;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Calculate utilization data for a project
|
|
342
|
+
*/
|
|
343
|
+
export interface ProjectUtilization {
|
|
344
|
+
projectId: string;
|
|
345
|
+
projectName: string;
|
|
346
|
+
primaryResource: ServiceType;
|
|
347
|
+
currentUsage: number;
|
|
348
|
+
monthlyLimit: number;
|
|
349
|
+
utilizationPct: number;
|
|
350
|
+
status: UtilizationStatus;
|
|
351
|
+
unit: string;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Build utilization data for a project given current usage
|
|
356
|
+
*/
|
|
357
|
+
export function buildProjectUtilization(
|
|
358
|
+
projectId: string,
|
|
359
|
+
currentUsage: Record<ServiceType, number>
|
|
360
|
+
): ProjectUtilization | null {
|
|
361
|
+
const projectConfig = PROJECT_ALLOWANCES.find((p) => p.projectId === projectId);
|
|
362
|
+
if (!projectConfig) return null;
|
|
363
|
+
|
|
364
|
+
const primaryResource = projectConfig.primaryResource;
|
|
365
|
+
const usage = currentUsage[primaryResource] ?? 0;
|
|
366
|
+
const limit = getEffectiveLimit(projectId, primaryResource);
|
|
367
|
+
const utilizationPct = calculateUtilizationPct(usage, limit);
|
|
368
|
+
|
|
369
|
+
return {
|
|
370
|
+
projectId,
|
|
371
|
+
projectName: projectConfig.projectName,
|
|
372
|
+
primaryResource,
|
|
373
|
+
currentUsage: usage,
|
|
374
|
+
monthlyLimit: limit,
|
|
375
|
+
utilizationPct,
|
|
376
|
+
status: getUtilizationStatus(utilizationPct),
|
|
377
|
+
unit: CF_ALLOWANCES[primaryResource].unit,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Calculate aggregate account-level utilization across all services
|
|
383
|
+
*/
|
|
384
|
+
export interface AccountUtilization {
|
|
385
|
+
totalMTDCost: number;
|
|
386
|
+
projectedMonthlyCost: number;
|
|
387
|
+
dailyBurnRate: number;
|
|
388
|
+
daysIntoMonth: number;
|
|
389
|
+
services: Record<
|
|
390
|
+
ServiceType,
|
|
391
|
+
{
|
|
392
|
+
current: number;
|
|
393
|
+
limit: number;
|
|
394
|
+
pct: number;
|
|
395
|
+
status: UtilizationStatus;
|
|
396
|
+
}
|
|
397
|
+
>;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Get the list of tracked project IDs
|
|
402
|
+
*/
|
|
403
|
+
export function getTrackedProjectIds(): string[] {
|
|
404
|
+
return PROJECT_ALLOWANCES.map((p) => p.projectId);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Get project display name by ID
|
|
409
|
+
*/
|
|
410
|
+
export function getProjectDisplayName(projectId: string): string {
|
|
411
|
+
const project = PROJECT_ALLOWANCES.find((p) => p.projectId === projectId);
|
|
412
|
+
return project?.projectName ?? projectId;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Simplified allowances for worker usage (limit + unit only)
|
|
417
|
+
* Used by platform-usage worker for utilization calculations.
|
|
418
|
+
*/
|
|
419
|
+
export type SimpleAllowanceType =
|
|
420
|
+
| 'workers'
|
|
421
|
+
| 'd1'
|
|
422
|
+
| 'kv'
|
|
423
|
+
| 'r2'
|
|
424
|
+
| 'vectorize'
|
|
425
|
+
| 'workersAI'
|
|
426
|
+
| 'durableObjects'
|
|
427
|
+
| 'queues';
|
|
428
|
+
|
|
429
|
+
export const CF_SIMPLE_ALLOWANCES: Record<SimpleAllowanceType, { limit: number; unit: string }> = {
|
|
430
|
+
workers: { limit: CF_ALLOWANCES.workers.monthlyLimit, unit: CF_ALLOWANCES.workers.unit },
|
|
431
|
+
d1: { limit: CF_ALLOWANCES.d1.monthlyLimit, unit: CF_ALLOWANCES.d1.unit },
|
|
432
|
+
kv: { limit: CF_ALLOWANCES.kv.monthlyLimit, unit: CF_ALLOWANCES.kv.unit },
|
|
433
|
+
r2: { limit: CF_ALLOWANCES.r2.monthlyLimit, unit: CF_ALLOWANCES.r2.unit },
|
|
434
|
+
vectorize: { limit: CF_ALLOWANCES.vectorize.monthlyLimit, unit: CF_ALLOWANCES.vectorize.unit },
|
|
435
|
+
workersAI: { limit: CF_ALLOWANCES.workersAI.monthlyLimit, unit: CF_ALLOWANCES.workersAI.unit },
|
|
436
|
+
durableObjects: {
|
|
437
|
+
limit: CF_ALLOWANCES.durableObjects.monthlyLimit,
|
|
438
|
+
unit: CF_ALLOWANCES.durableObjects.unit,
|
|
439
|
+
},
|
|
440
|
+
queues: {
|
|
441
|
+
limit: CF_ALLOWANCES.queues.monthlyLimit,
|
|
442
|
+
unit: CF_ALLOWANCES.queues.unit,
|
|
443
|
+
},
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// =============================================================================
|
|
447
|
+
// GITHUB ALLOWANCES & PRICING
|
|
448
|
+
// =============================================================================
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* GitHub plan allowances (defaults by plan tier)
|
|
452
|
+
* These are dynamically fetched from the API but have fallbacks here.
|
|
453
|
+
*
|
|
454
|
+
* @see https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions
|
|
455
|
+
*/
|
|
456
|
+
export const GITHUB_ALLOWANCES = {
|
|
457
|
+
/** GitHub Free plan */
|
|
458
|
+
free: {
|
|
459
|
+
actionsMinutes: 2000, // 2,000 minutes/month (private repos)
|
|
460
|
+
actionsStorageGb: 0.5, // 500 MB storage
|
|
461
|
+
packagesStorageGb: 0.5, // 500 MB packages
|
|
462
|
+
packagesBandwidthGb: 1, // 1 GB bandwidth
|
|
463
|
+
lfsStorageGb: 1, // 1 GiB storage
|
|
464
|
+
lfsBandwidthGb: 1, // 1 GiB bandwidth/month
|
|
465
|
+
},
|
|
466
|
+
/** GitHub Pro plan (individual) */
|
|
467
|
+
pro: {
|
|
468
|
+
actionsMinutes: 3000, // 3,000 minutes/month
|
|
469
|
+
actionsStorageGb: 2, // 2 GB storage
|
|
470
|
+
packagesStorageGb: 2, // 2 GB packages
|
|
471
|
+
packagesBandwidthGb: 10, // 10 GB bandwidth
|
|
472
|
+
lfsStorageGb: 1, // 1 GiB storage
|
|
473
|
+
lfsBandwidthGb: 1, // 1 GiB bandwidth/month
|
|
474
|
+
},
|
|
475
|
+
/** GitHub Team plan */
|
|
476
|
+
team: {
|
|
477
|
+
actionsMinutes: 3000, // 3,000 minutes/month
|
|
478
|
+
actionsStorageGb: 2, // 2 GB storage
|
|
479
|
+
packagesStorageGb: 2, // 2 GB packages
|
|
480
|
+
packagesBandwidthGb: 10, // 10 GB bandwidth
|
|
481
|
+
lfsStorageGb: 1, // 1 GiB storage
|
|
482
|
+
lfsBandwidthGb: 1, // 1 GiB bandwidth/month
|
|
483
|
+
},
|
|
484
|
+
/** GitHub Enterprise Cloud plan */
|
|
485
|
+
enterprise: {
|
|
486
|
+
actionsMinutes: 50000, // 50,000 minutes/month
|
|
487
|
+
actionsStorageGb: 50, // 50 GB storage
|
|
488
|
+
packagesStorageGb: 50, // 50 GB packages
|
|
489
|
+
packagesBandwidthGb: 100, // 100 GB bandwidth
|
|
490
|
+
lfsStorageGb: 250, // 250 GiB storage
|
|
491
|
+
lfsBandwidthGb: 250, // 250 GiB bandwidth/month
|
|
492
|
+
},
|
|
493
|
+
} as const;
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* GitHub pricing for overages and paid features
|
|
497
|
+
* Prices are in USD per unit
|
|
498
|
+
*
|
|
499
|
+
* @see https://docs.github.com/en/billing/managing-billing-for-your-products/managing-billing-for-github-actions/about-billing-for-github-actions
|
|
500
|
+
*/
|
|
501
|
+
export const GITHUB_PRICING = {
|
|
502
|
+
/** Actions minute overage pricing (per minute, by runner type)
|
|
503
|
+
* Updated Jan 2026: GitHub reduced Actions pricing by up to 39%
|
|
504
|
+
* @see https://docs.github.com/en/billing/reference/actions-runner-pricing */
|
|
505
|
+
actions: {
|
|
506
|
+
linux: 0.006, // $0.006/minute for Linux (was $0.008)
|
|
507
|
+
macos: 0.062, // $0.062/minute for macOS (was $0.08)
|
|
508
|
+
windows: 0.01, // $0.010/minute for Windows (was $0.016)
|
|
509
|
+
/** Larger runners have multiplied pricing */
|
|
510
|
+
linuxLarge: 0.012, // 2x Linux for 4-core
|
|
511
|
+
linuxXLarge: 0.024, // 4x Linux for 8-core
|
|
512
|
+
gpuLinux: 0.07, // GPU-powered runners
|
|
513
|
+
},
|
|
514
|
+
/** Actions storage overage (per GB/month) */
|
|
515
|
+
actionsStorageGb: 0.25, // $0.25/GB/month
|
|
516
|
+
/** Packages storage overage (per GB/month) */
|
|
517
|
+
packagesStorageGb: 0.25, // $0.25/GB/month
|
|
518
|
+
/** Packages bandwidth overage (per GB) */
|
|
519
|
+
packagesBandwidthGb: 0.5, // $0.50/GB
|
|
520
|
+
/** Git LFS storage overage (per GiB/month) */
|
|
521
|
+
lfsStorageGb: 0.07, // $0.07/GiB/month
|
|
522
|
+
/** Git LFS bandwidth overage (per GiB) */
|
|
523
|
+
lfsBandwidthGb: 0.007, // $0.007/GiB
|
|
524
|
+
/** GitHub Enterprise Cloud (per user/month) */
|
|
525
|
+
ghecPerUser: 21, // $21/user/month
|
|
526
|
+
/** GitHub Advanced Security - Code Security (per active committer/month) */
|
|
527
|
+
ghasCodeSecurity: 49, // $49/active committer/month
|
|
528
|
+
/** GitHub Advanced Security - Secret Protection (per active committer/month) */
|
|
529
|
+
ghasSecretProtection: 31, // $31/active committer/month
|
|
530
|
+
/** GitHub Copilot Business (per user/month) */
|
|
531
|
+
copilotBusiness: 19, // $19/user/month
|
|
532
|
+
/** GitHub Copilot Enterprise (per user/month) */
|
|
533
|
+
copilotEnterprise: 39, // $39/user/month
|
|
534
|
+
} as const;
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Get GitHub plan allowances by plan name
|
|
538
|
+
*/
|
|
539
|
+
export function getGitHubPlanAllowances(
|
|
540
|
+
planName: string
|
|
541
|
+
): (typeof GITHUB_ALLOWANCES)[keyof typeof GITHUB_ALLOWANCES] {
|
|
542
|
+
const lowerPlan = planName.toLowerCase();
|
|
543
|
+
if (lowerPlan.includes('enterprise')) return GITHUB_ALLOWANCES.enterprise;
|
|
544
|
+
if (lowerPlan.includes('team')) return GITHUB_ALLOWANCES.team;
|
|
545
|
+
if (lowerPlan.includes('pro')) return GITHUB_ALLOWANCES.pro;
|
|
546
|
+
return GITHUB_ALLOWANCES.free;
|
|
547
|
+
}
|