@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.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +86 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  6. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  7. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  8. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  9. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  10. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  11. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  12. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  13. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  14. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  15. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  16. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  17. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  18. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  19. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  20. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  21. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  22. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  23. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  24. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  25. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  26. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  27. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  28. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  29. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  30. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  31. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  32. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  33. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  34. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  35. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  36. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  37. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  38. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  39. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  40. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  41. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  42. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  43. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  44. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  45. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  46. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  47. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  48. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  49. package/templates/shared/tests/unit/billing.test.ts +331 -0
  50. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  51. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  52. package/templates/shared/tests/unit/control.test.ts +226 -0
  53. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  54. package/templates/shared/tests/unit/economics.test.ts +365 -0
  55. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  56. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  57. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  58. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  59. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  60. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  61. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  62. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  63. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  64. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  65. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  66. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  67. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  68. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  69. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  70. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  71. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  72. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  73. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  74. 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
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Usage Library Exports
3
+ *
4
+ * Centralised exports for usage-related configuration and utilities.
5
+ */
6
+
7
+ export * from './fetchWithDedup';
8
+
9
+ {{#if isFull}}
10
+ export * from './allowance-config';
11
+ export * from './providers';
12
+ {{/if}}