@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,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analytics Engine Client Library
|
|
3
|
+
*
|
|
4
|
+
* Provides a reusable client for querying Cloudflare Analytics Engine
|
|
5
|
+
* via the SQL API. Includes retry logic, error handling, and type-safe
|
|
6
|
+
* column mapping.
|
|
7
|
+
*
|
|
8
|
+
* Note: In the dashboard context, prefer using the proxy to platform-usage
|
|
9
|
+
* worker (/api/usage/query) rather than direct Analytics Engine calls.
|
|
10
|
+
* This client is provided for potential future direct queries.
|
|
11
|
+
*
|
|
12
|
+
* @module lib/cloudflare/analytics
|
|
13
|
+
* @created 2026-01-20
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// TYPES
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Analytics Engine configuration for direct queries
|
|
22
|
+
*/
|
|
23
|
+
export interface AnalyticsEngineConfig {
|
|
24
|
+
accountId: string;
|
|
25
|
+
apiToken: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Analytics Engine SQL API response structure
|
|
30
|
+
*/
|
|
31
|
+
interface AnalyticsEngineResponse {
|
|
32
|
+
// Direct format (SQL API)
|
|
33
|
+
meta?: Array<{ name: string; type: string }>;
|
|
34
|
+
data?: unknown[];
|
|
35
|
+
rows?: number;
|
|
36
|
+
rows_before_limit_at_least?: number;
|
|
37
|
+
|
|
38
|
+
// Wrapped format (REST API)
|
|
39
|
+
success?: boolean;
|
|
40
|
+
errors?: Array<{ code: number; message: string }>;
|
|
41
|
+
result?: {
|
|
42
|
+
data: unknown[];
|
|
43
|
+
meta: Array<{ name: string; type: string }>;
|
|
44
|
+
rows: number;
|
|
45
|
+
rows_before_limit_at_least: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Query result with metadata
|
|
51
|
+
*/
|
|
52
|
+
export interface QueryResult<T> {
|
|
53
|
+
data: T[];
|
|
54
|
+
meta: {
|
|
55
|
+
columns: string[];
|
|
56
|
+
rowCount: number;
|
|
57
|
+
queryTimeMs: number;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Error types from Analytics Engine
|
|
63
|
+
*/
|
|
64
|
+
export type AnalyticsEngineErrorCode =
|
|
65
|
+
| 'RATE_LIMITED'
|
|
66
|
+
| 'SERVER_ERROR'
|
|
67
|
+
| 'INVALID_QUERY'
|
|
68
|
+
| 'UNAUTHORIZED'
|
|
69
|
+
| 'DATASET_NOT_FOUND'
|
|
70
|
+
| 'UNKNOWN';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Analytics Engine error with additional context
|
|
74
|
+
*/
|
|
75
|
+
export class AnalyticsEngineError extends Error {
|
|
76
|
+
constructor(
|
|
77
|
+
message: string,
|
|
78
|
+
public readonly code: AnalyticsEngineErrorCode,
|
|
79
|
+
public readonly statusCode?: number
|
|
80
|
+
) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.name = 'AnalyticsEngineError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// =============================================================================
|
|
87
|
+
// HELPERS
|
|
88
|
+
// =============================================================================
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Sleep for a given number of milliseconds
|
|
92
|
+
*/
|
|
93
|
+
function sleep(ms: number): Promise<void> {
|
|
94
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map HTTP status code to error code
|
|
99
|
+
*/
|
|
100
|
+
function statusToErrorCode(status: number): AnalyticsEngineErrorCode {
|
|
101
|
+
if (status === 429) return 'RATE_LIMITED';
|
|
102
|
+
if (status === 401 || status === 403) return 'UNAUTHORIZED';
|
|
103
|
+
if (status >= 500) return 'SERVER_ERROR';
|
|
104
|
+
if (status === 400) return 'INVALID_QUERY';
|
|
105
|
+
return 'UNKNOWN';
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// =============================================================================
|
|
109
|
+
// CLIENT
|
|
110
|
+
// =============================================================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Query Analytics Engine via the SQL API with retry logic.
|
|
114
|
+
*
|
|
115
|
+
* @param config Analytics Engine configuration
|
|
116
|
+
* @param sql SQL query to execute
|
|
117
|
+
* @param retries Maximum number of retries (default: 3)
|
|
118
|
+
* @returns Query results with metadata
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```typescript
|
|
122
|
+
* const result = await queryAnalyticsEngine<MyRow>(
|
|
123
|
+
* { accountId: '...', apiToken: '...' },
|
|
124
|
+
* 'SELECT blob1 as project, SUM(double1) as total FROM "platform-analytics" GROUP BY blob1'
|
|
125
|
+
* );
|
|
126
|
+
* console.log(result.data); // [{ project: 'scout', total: 100 }, ...]
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export async function queryAnalyticsEngine<T>(
|
|
130
|
+
config: AnalyticsEngineConfig,
|
|
131
|
+
sql: string,
|
|
132
|
+
retries = 3
|
|
133
|
+
): Promise<QueryResult<T>> {
|
|
134
|
+
const startTime = Date.now();
|
|
135
|
+
const url = `https://api.cloudflare.com/client/v4/accounts/${config.accountId}/analytics_engine/sql`;
|
|
136
|
+
|
|
137
|
+
let lastError: Error | null = null;
|
|
138
|
+
|
|
139
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetch(url, {
|
|
142
|
+
method: 'POST',
|
|
143
|
+
headers: {
|
|
144
|
+
Authorization: `Bearer ${config.apiToken}`,
|
|
145
|
+
'Content-Type': 'text/plain',
|
|
146
|
+
},
|
|
147
|
+
body: sql,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Handle rate limiting with retry
|
|
151
|
+
if (response.status === 429 && attempt < retries) {
|
|
152
|
+
const retryAfter = response.headers.get('Retry-After');
|
|
153
|
+
const delayMs = retryAfter
|
|
154
|
+
? parseInt(retryAfter, 10) * 1000
|
|
155
|
+
: Math.min(1000 * Math.pow(2, attempt), 10000);
|
|
156
|
+
await sleep(delayMs);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Handle server errors with retry
|
|
161
|
+
if (response.status >= 500 && attempt < retries) {
|
|
162
|
+
await sleep(Math.min(1000 * Math.pow(2, attempt), 10000));
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Parse response
|
|
167
|
+
const rawText = await response.text();
|
|
168
|
+
let data: AnalyticsEngineResponse;
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
data = JSON.parse(rawText) as AnalyticsEngineResponse;
|
|
172
|
+
} catch {
|
|
173
|
+
throw new AnalyticsEngineError(
|
|
174
|
+
`Invalid JSON response: ${rawText.slice(0, 200)}`,
|
|
175
|
+
'UNKNOWN',
|
|
176
|
+
response.status
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check for error response
|
|
181
|
+
if (!response.ok) {
|
|
182
|
+
const errorCode = statusToErrorCode(response.status);
|
|
183
|
+
const errorMessage = data.errors?.map((e) => e.message).join(', ') ?? rawText.slice(0, 200);
|
|
184
|
+
|
|
185
|
+
// Handle dataset not found (empty schema)
|
|
186
|
+
if (errorMessage.includes('unable to find type of column')) {
|
|
187
|
+
throw new AnalyticsEngineError(
|
|
188
|
+
'Dataset has no data yet',
|
|
189
|
+
'DATASET_NOT_FOUND',
|
|
190
|
+
response.status
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new AnalyticsEngineError(errorMessage, errorCode, response.status);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Handle both response formats:
|
|
198
|
+
// 1. Direct format: { meta, data, rows }
|
|
199
|
+
// 2. Wrapped format: { success, result: { meta, data, rows } }
|
|
200
|
+
const meta = data.meta ?? data.result?.meta;
|
|
201
|
+
const resultData = data.data ?? data.result?.data;
|
|
202
|
+
|
|
203
|
+
if (!meta || !resultData) {
|
|
204
|
+
throw new AnalyticsEngineError(
|
|
205
|
+
`Response missing expected fields: ${JSON.stringify(Object.keys(data))}`,
|
|
206
|
+
'UNKNOWN',
|
|
207
|
+
response.status
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Map the result data to typed objects using column metadata
|
|
212
|
+
// Analytics Engine can return data in two formats:
|
|
213
|
+
// 1. Array of arrays: [[val1, val2], [val1, val2]] - needs column mapping
|
|
214
|
+
// 2. Array of objects: [{col1: val1, col2: val2}, ...] - already in object format
|
|
215
|
+
const columns = meta.map((m) => m.name);
|
|
216
|
+
|
|
217
|
+
const mappedData = resultData.map((row) => {
|
|
218
|
+
// If row is already an object (not an array), return it directly
|
|
219
|
+
if (row !== null && typeof row === 'object' && !Array.isArray(row)) {
|
|
220
|
+
return row as T;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Row is an array - map using column metadata
|
|
224
|
+
const rowArray = row as unknown[];
|
|
225
|
+
const obj: Record<string, unknown> = {};
|
|
226
|
+
columns.forEach((col, i) => {
|
|
227
|
+
obj[col] = rowArray[i];
|
|
228
|
+
});
|
|
229
|
+
return obj as T;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
return {
|
|
233
|
+
data: mappedData,
|
|
234
|
+
meta: {
|
|
235
|
+
columns,
|
|
236
|
+
rowCount: mappedData.length,
|
|
237
|
+
queryTimeMs: Date.now() - startTime,
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
lastError = error as Error;
|
|
242
|
+
|
|
243
|
+
// Don't retry on non-retryable errors
|
|
244
|
+
if (error instanceof AnalyticsEngineError) {
|
|
245
|
+
if (error.code !== 'RATE_LIMITED' && error.code !== 'SERVER_ERROR') {
|
|
246
|
+
throw error;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Wait before retry
|
|
251
|
+
if (attempt < retries) {
|
|
252
|
+
await sleep(Math.min(1000 * Math.pow(2, attempt), 10000));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// All retries exhausted
|
|
258
|
+
throw lastError ?? new AnalyticsEngineError('Query failed after all retries', 'UNKNOWN');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Execute a simple SELECT query and return raw results.
|
|
263
|
+
* Convenience wrapper around queryAnalyticsEngine.
|
|
264
|
+
*
|
|
265
|
+
* @param config Analytics Engine configuration
|
|
266
|
+
* @param tableName Table/dataset name
|
|
267
|
+
* @param options Query options
|
|
268
|
+
* @returns Query results
|
|
269
|
+
*/
|
|
270
|
+
export async function selectFromAnalytics<T>(
|
|
271
|
+
config: AnalyticsEngineConfig,
|
|
272
|
+
tableName: string,
|
|
273
|
+
options: {
|
|
274
|
+
columns?: string[];
|
|
275
|
+
where?: string;
|
|
276
|
+
groupBy?: string[];
|
|
277
|
+
orderBy?: string;
|
|
278
|
+
limit?: number;
|
|
279
|
+
} = {}
|
|
280
|
+
): Promise<T[]> {
|
|
281
|
+
const { columns = ['*'], where, groupBy, orderBy, limit } = options;
|
|
282
|
+
|
|
283
|
+
// Build SQL query
|
|
284
|
+
let sql = `SELECT ${columns.join(', ')} FROM "${tableName}"`;
|
|
285
|
+
|
|
286
|
+
if (where) {
|
|
287
|
+
sql += ` WHERE ${where}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (groupBy && groupBy.length > 0) {
|
|
291
|
+
sql += ` GROUP BY ${groupBy.join(', ')}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (orderBy) {
|
|
295
|
+
sql += ` ORDER BY ${orderBy}`;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (limit) {
|
|
299
|
+
sql += ` LIMIT ${limit}`;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const result = await queryAnalyticsEngine<T>(config, sql);
|
|
303
|
+
return result.data;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// =============================================================================
|
|
307
|
+
// EXPORTS
|
|
308
|
+
// =============================================================================
|
|
309
|
+
|
|
310
|
+
export type { AnalyticsEngineResponse };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D1 Database Helpers
|
|
3
|
+
* Query system_health_checks for heartbeat data
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { D1Database } from '@cloudflare/workers-types';
|
|
7
|
+
|
|
8
|
+
export interface HealthCheckRecord {
|
|
9
|
+
project_id: string;
|
|
10
|
+
last_heartbeat: number; // Unix timestamp (seconds)
|
|
11
|
+
status: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ProjectHealthMap = Record<
|
|
15
|
+
string,
|
|
16
|
+
{
|
|
17
|
+
lastHeartbeat: string; // ISO string
|
|
18
|
+
status: string;
|
|
19
|
+
}
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get latest heartbeat per project from system_health_checks
|
|
24
|
+
* Aggregates by project_id (takes MAX last_heartbeat across all features)
|
|
25
|
+
*/
|
|
26
|
+
export async function getSystemHealth(db: D1Database): Promise<ProjectHealthMap> {
|
|
27
|
+
try {
|
|
28
|
+
const result = await db
|
|
29
|
+
.prepare(
|
|
30
|
+
`
|
|
31
|
+
SELECT
|
|
32
|
+
project_id,
|
|
33
|
+
MAX(last_heartbeat) as last_heartbeat,
|
|
34
|
+
status
|
|
35
|
+
FROM system_health_checks
|
|
36
|
+
GROUP BY project_id
|
|
37
|
+
`
|
|
38
|
+
)
|
|
39
|
+
.all<HealthCheckRecord>();
|
|
40
|
+
|
|
41
|
+
const healthMap: ProjectHealthMap = {};
|
|
42
|
+
|
|
43
|
+
for (const row of result.results ?? []) {
|
|
44
|
+
healthMap[row.project_id] = {
|
|
45
|
+
lastHeartbeat: new Date(row.last_heartbeat * 1000).toISOString(),
|
|
46
|
+
status: row.status,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return healthMap;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('[D1] Error querying system_health_checks:', error);
|
|
53
|
+
return {}; // Graceful degradation
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare Observability Library
|
|
3
|
+
*
|
|
4
|
+
* Provides unified access to Cloudflare metrics, costs, and analytics.
|
|
5
|
+
*
|
|
6
|
+
* @module cloudflare
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Cost calculator and types
|
|
10
|
+
export {
|
|
11
|
+
CF_PRICING,
|
|
12
|
+
CF_FREE_LIMITS,
|
|
13
|
+
PROJECT_PATTERNS,
|
|
14
|
+
DEFAULT_ALERT_THRESHOLDS,
|
|
15
|
+
identifyProject,
|
|
16
|
+
calculateMonthlyCosts,
|
|
17
|
+
calculateProjectCosts,
|
|
18
|
+
calculateDailyCosts,
|
|
19
|
+
getThresholdLevel,
|
|
20
|
+
analyseThresholds,
|
|
21
|
+
mergeThresholds,
|
|
22
|
+
formatNumber,
|
|
23
|
+
formatCurrency,
|
|
24
|
+
type CostBreakdown,
|
|
25
|
+
type ProjectCostBreakdown,
|
|
26
|
+
type DailyUsageMetrics,
|
|
27
|
+
type ThresholdLevel,
|
|
28
|
+
type ThresholdWarning,
|
|
29
|
+
type ThresholdAnalysis,
|
|
30
|
+
type AlertServiceType,
|
|
31
|
+
type ServiceThreshold,
|
|
32
|
+
type AlertThresholds,
|
|
33
|
+
} from './costs';
|
|
34
|
+
|
|
35
|
+
// D1 helpers
|
|
36
|
+
export { getSystemHealth, type ProjectHealthMap, type HealthCheckRecord } from './d1';
|
|
37
|
+
|
|
38
|
+
// Analytics Engine client
|
|
39
|
+
export {
|
|
40
|
+
queryAnalyticsEngine,
|
|
41
|
+
selectFromAnalytics,
|
|
42
|
+
AnalyticsEngineError,
|
|
43
|
+
type AnalyticsEngineConfig,
|
|
44
|
+
type QueryResult,
|
|
45
|
+
type AnalyticsEngineErrorCode,
|
|
46
|
+
type AnalyticsEngineResponse,
|
|
47
|
+
} from './analytics';
|
|
48
|
+
|
|
49
|
+
{{#if isFull}}
|
|
50
|
+
// GraphQL client and types (Full tier)
|
|
51
|
+
export {
|
|
52
|
+
CloudflareGraphQL,
|
|
53
|
+
type TimePeriod,
|
|
54
|
+
type DateRange,
|
|
55
|
+
type CustomDateRangeParams,
|
|
56
|
+
type CompareMode,
|
|
57
|
+
type WorkersMetrics,
|
|
58
|
+
type D1Metrics,
|
|
59
|
+
type KVMetrics,
|
|
60
|
+
type R2Metrics,
|
|
61
|
+
type DOMetrics,
|
|
62
|
+
type VectorizeInfo,
|
|
63
|
+
type AIGatewayMetrics,
|
|
64
|
+
type PagesMetrics,
|
|
65
|
+
type SparklinePoint,
|
|
66
|
+
type SparklineData,
|
|
67
|
+
type WorkersErrorBreakdown,
|
|
68
|
+
type QueuesMetrics,
|
|
69
|
+
type CacheAnalytics,
|
|
70
|
+
type PeriodComparison,
|
|
71
|
+
type AccountUsage,
|
|
72
|
+
type EnhancedAccountUsage,
|
|
73
|
+
type WorkersAIMetrics,
|
|
74
|
+
type WorkersAISummary,
|
|
75
|
+
type AIGatewaySummary,
|
|
76
|
+
type DailyCostBreakdown,
|
|
77
|
+
type DailyCostData,
|
|
78
|
+
type CloudflareSubscription,
|
|
79
|
+
type WorkersPaidPlanInclusions,
|
|
80
|
+
type CloudflareBillingProfile,
|
|
81
|
+
type CloudflareAccountSubscriptions,
|
|
82
|
+
} from './graphql';
|
|
83
|
+
|
|
84
|
+
// Alerting service and types
|
|
85
|
+
export {
|
|
86
|
+
getSeverityColour,
|
|
87
|
+
getSeverityEmoji,
|
|
88
|
+
formatPercentage,
|
|
89
|
+
generateAlertKey,
|
|
90
|
+
shouldSendAlert,
|
|
91
|
+
buildSlackMessage,
|
|
92
|
+
buildSummarySlackMessage,
|
|
93
|
+
sendSlackAlert,
|
|
94
|
+
evaluateWarning,
|
|
95
|
+
buildEmailHtml,
|
|
96
|
+
buildEmailText,
|
|
97
|
+
type CostSpikeAlert,
|
|
98
|
+
type AlertResult,
|
|
99
|
+
type SlackMessage,
|
|
100
|
+
} from './alerting';
|
|
101
|
+
|
|
102
|
+
// Project registry - D1-backed resource-to-project mapping
|
|
103
|
+
export {
|
|
104
|
+
getProjects,
|
|
105
|
+
getProject,
|
|
106
|
+
identifyProjectFromRegistry,
|
|
107
|
+
getProjectResources,
|
|
108
|
+
getProjectResourcesByType,
|
|
109
|
+
getResourceCountsByProject,
|
|
110
|
+
getResourceCountByType,
|
|
111
|
+
upsertResourceMapping,
|
|
112
|
+
deleteResourceMapping,
|
|
113
|
+
createProject,
|
|
114
|
+
updateProjectStatus,
|
|
115
|
+
clearRegistryCache,
|
|
116
|
+
type Project,
|
|
117
|
+
type ResourceMapping,
|
|
118
|
+
type ResourceType,
|
|
119
|
+
} from './project-registry';
|
|
120
|
+
{{/if}}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Infrastructure Types
|
|
3
|
+
* TypeScript types for infrastructure monitoring dashboard
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export type ServiceStatus = 'deployed' | 'development' | 'deprecated' | 'paused';
|
|
7
|
+
export type ServiceType = 'worker' | 'vps-app' | 'd1' | 'kv' | 'r2' | 'vectorize' | 'queue';
|
|
8
|
+
export type MonitorStatus = 'up' | 'down' | 'paused' | 'unknown';
|
|
9
|
+
export type HealthcheckStatus = 'up' | 'down' | 'grace' | 'paused' | 'new';
|
|
10
|
+
|
|
11
|
+
export interface Service {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
type: ServiceType;
|
|
15
|
+
status: ServiceStatus;
|
|
16
|
+
project: string;
|
|
17
|
+
scriptName?: string;
|
|
18
|
+
schedule?: string | null;
|
|
19
|
+
lastDeployed?: number | null;
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface ServiceRegistryStats {
|
|
24
|
+
total: number;
|
|
25
|
+
byStatus: Record<ServiceStatus, number>;
|
|
26
|
+
byType: Record<ServiceType, number>;
|
|
27
|
+
byProject: Record<string, number>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UptimeMonitor {
|
|
31
|
+
id: string;
|
|
32
|
+
name: string;
|
|
33
|
+
url: string;
|
|
34
|
+
status: MonitorStatus;
|
|
35
|
+
uptimeRatio: number; // Last 7 days (from Gatus)
|
|
36
|
+
responseTime: number; // Latest ms
|
|
37
|
+
lastCheckAt: number;
|
|
38
|
+
createdAt: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface HealthcheckJob {
|
|
42
|
+
id: string;
|
|
43
|
+
name: string;
|
|
44
|
+
slug: string;
|
|
45
|
+
status: HealthcheckStatus;
|
|
46
|
+
lastPing: number | null;
|
|
47
|
+
nextPing: number | null;
|
|
48
|
+
period: number; // seconds
|
|
49
|
+
grace: number; // seconds
|
|
50
|
+
nPings: number;
|
|
51
|
+
tags: string[];
|
|
52
|
+
// Optional flip summary (populated by enhanced API)
|
|
53
|
+
recentFlips?: FlipEvent[];
|
|
54
|
+
flipsToday?: number;
|
|
55
|
+
lastFailure?: number | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A flip represents a status change (up→down or down→up)
|
|
60
|
+
* Retrieved from Gatus events API
|
|
61
|
+
*/
|
|
62
|
+
export interface FlipEvent {
|
|
63
|
+
timestamp: number; // Unix timestamp
|
|
64
|
+
up: boolean; // true = went up, false = went down
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A response time data point from Gatus
|
|
69
|
+
*/
|
|
70
|
+
export interface ResponseTimeDataPoint {
|
|
71
|
+
timestamp: number; // Unix timestamp
|
|
72
|
+
value: number; // Response time in ms
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Response time stats summary
|
|
77
|
+
*/
|
|
78
|
+
export interface ResponseTimeStats {
|
|
79
|
+
avg: number;
|
|
80
|
+
min: number;
|
|
81
|
+
max: number;
|
|
82
|
+
trend: 'improving' | 'stable' | 'degrading';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface Alert {
|
|
86
|
+
id: string;
|
|
87
|
+
type: string;
|
|
88
|
+
severity: 'info' | 'warning' | 'critical';
|
|
89
|
+
source: string;
|
|
90
|
+
message: string;
|
|
91
|
+
createdAt: number;
|
|
92
|
+
acknowledgedAt: number | null;
|
|
93
|
+
resolvedAt: number | null;
|
|
94
|
+
metadata?: Record<string, unknown>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface InfrastructureStats {
|
|
98
|
+
services: ServiceRegistryStats;
|
|
99
|
+
monitors: {
|
|
100
|
+
total: number;
|
|
101
|
+
up: number;
|
|
102
|
+
down: number;
|
|
103
|
+
averageUptime: number;
|
|
104
|
+
};
|
|
105
|
+
healthchecks: {
|
|
106
|
+
total: number;
|
|
107
|
+
up: number;
|
|
108
|
+
down: number;
|
|
109
|
+
grace: number;
|
|
110
|
+
};
|
|
111
|
+
alerts: {
|
|
112
|
+
total: number;
|
|
113
|
+
unacknowledged: number;
|
|
114
|
+
critical: number;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetch with Request Deduplication
|
|
3
|
+
*
|
|
4
|
+
* Prevents duplicate API calls by:
|
|
5
|
+
* 1. Deduplicating concurrent requests to the same URL
|
|
6
|
+
* 2. Caching responses for a short TTL
|
|
7
|
+
*
|
|
8
|
+
* This is used by the unified dashboard components to prevent
|
|
9
|
+
* multiple components from making redundant API calls.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
interface CacheEntry<T> {
|
|
13
|
+
data: T;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// In-flight request tracking (prevents concurrent duplicate requests)
|
|
18
|
+
const pendingRequests = new Map<string, Promise<Response>>();
|
|
19
|
+
|
|
20
|
+
// Response cache with TTL
|
|
21
|
+
const responseCache = new Map<string, CacheEntry<unknown>>();
|
|
22
|
+
|
|
23
|
+
// Default cache TTL: 5 seconds (short enough to stay fresh, long enough to dedupe)
|
|
24
|
+
const DEFAULT_CACHE_TTL = 5000;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch with automatic request deduplication and short-term caching
|
|
28
|
+
*/
|
|
29
|
+
export async function fetchWithDedup<T>(
|
|
30
|
+
url: string,
|
|
31
|
+
options: RequestInit = {},
|
|
32
|
+
cacheTtl: number = DEFAULT_CACHE_TTL
|
|
33
|
+
): Promise<T> {
|
|
34
|
+
const cacheKey = `${options.method || 'GET'}:${url}`;
|
|
35
|
+
|
|
36
|
+
// Check response cache first
|
|
37
|
+
const cached = responseCache.get(cacheKey);
|
|
38
|
+
if (cached && Date.now() < cached.expiresAt) {
|
|
39
|
+
return cached.data as T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check for in-flight request
|
|
43
|
+
const pending = pendingRequests.get(cacheKey);
|
|
44
|
+
if (pending) {
|
|
45
|
+
const response = await pending;
|
|
46
|
+
const data = await response.clone().json();
|
|
47
|
+
return data as T;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Create new request
|
|
51
|
+
const request = fetch(url, {
|
|
52
|
+
credentials: 'include',
|
|
53
|
+
...options,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
pendingRequests.set(cacheKey, request);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await request;
|
|
60
|
+
pendingRequests.delete(cacheKey);
|
|
61
|
+
|
|
62
|
+
if (!response.ok) {
|
|
63
|
+
throw new Error(`HTTP ${response.status}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const data = await response.json();
|
|
67
|
+
|
|
68
|
+
// Cache the response
|
|
69
|
+
responseCache.set(cacheKey, {
|
|
70
|
+
data,
|
|
71
|
+
expiresAt: Date.now() + cacheTtl,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return data as T;
|
|
75
|
+
} catch (error) {
|
|
76
|
+
pendingRequests.delete(cacheKey);
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Clear the response cache (useful for forced refresh)
|
|
83
|
+
*/
|
|
84
|
+
export function clearFetchCache(urlPattern?: string): void {
|
|
85
|
+
if (urlPattern) {
|
|
86
|
+
for (const key of responseCache.keys()) {
|
|
87
|
+
if (key.includes(urlPattern)) {
|
|
88
|
+
responseCache.delete(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
responseCache.clear();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Check if a URL is currently being fetched
|
|
98
|
+
*/
|
|
99
|
+
export function isFetching(url: string): boolean {
|
|
100
|
+
return pendingRequests.has(`GET:${url}`);
|
|
101
|
+
}
|