@littlebearapps/platform-admin-sdk 2.0.0 → 2.2.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 (185) hide show
  1. package/README.md +4 -7
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +206 -4
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  6. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  7. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  8. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  9. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  10. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  11. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  12. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  13. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  14. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  15. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  18. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  19. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  20. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  21. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  22. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  23. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  24. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  25. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  26. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  27. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  28. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  30. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  31. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  32. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  34. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  35. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  36. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  37. package/templates/full/dashboard/src/pages/map.astro +561 -0
  38. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  39. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  40. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  41. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  42. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  43. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  44. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  45. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  46. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  47. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  48. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  49. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  50. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  51. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  52. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  53. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  54. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  55. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  56. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  57. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  58. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  59. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  60. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  61. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  62. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  63. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  64. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  65. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  66. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  67. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  68. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  69. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  70. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  71. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  72. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  73. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  74. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  75. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  76. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  77. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  78. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  79. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  80. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  81. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  82. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  83. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  84. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  85. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  86. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  87. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  88. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  89. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  90. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  91. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  92. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  93. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  94. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  95. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  96. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  97. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  98. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  99. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  100. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  101. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  102. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  103. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  104. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  105. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  106. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  107. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  108. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  109. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  110. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  111. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  112. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  113. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  114. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  115. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  116. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  117. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  118. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  119. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  120. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  121. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  122. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  123. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  124. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  125. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  126. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  127. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  128. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  129. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  130. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  131. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  132. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  133. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  134. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  135. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  136. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  137. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  138. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  139. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  140. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  141. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  142. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  143. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  144. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  145. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  146. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  147. package/templates/shared/tests/unit/billing.test.ts +331 -0
  148. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  149. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  150. package/templates/shared/tests/unit/control.test.ts +226 -0
  151. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  152. package/templates/shared/tests/unit/economics.test.ts +365 -0
  153. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  154. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  155. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  156. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  157. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  158. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  159. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  160. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  161. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  162. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  163. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  164. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  165. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  166. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  167. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  168. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  169. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  170. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  171. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  172. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  173. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  174. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  175. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  176. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  177. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  178. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  179. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  180. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  181. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
  182. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  183. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  184. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  185. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Usage Dashboard Color System
3
+ *
4
+ * Consistent, accessible color palette for resource types.
5
+ * Part of task-18: Usage Dashboard Interactive Chart & Table Enhancement
6
+ *
7
+ * Each resource type has light and dark mode variants optimised for:
8
+ * - WCAG 2.1 AA contrast on backgrounds
9
+ * - Visual distinction between resource types
10
+ * - Consistency with the existing design token system
11
+ */
12
+
13
+ /**
14
+ * Resource type identifiers matching DailyCostBreakdown keys
15
+ */
16
+ export type ResourceType =
17
+ | 'workers'
18
+ | 'd1'
19
+ | 'kv'
20
+ | 'r2'
21
+ | 'vectorize'
22
+ | 'aiGateway'
23
+ | 'durableObjects'
24
+ | 'workersAI'
25
+ | 'queues';
26
+
27
+ /**
28
+ * Theme-aware color definition
29
+ */
30
+ export interface ResourceColor {
31
+ light: {
32
+ fill: string;
33
+ fillHover: string;
34
+ text: string;
35
+ };
36
+ dark: {
37
+ fill: string;
38
+ fillHover: string;
39
+ text: string;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Resource display metadata
45
+ */
46
+ export interface ResourceInfo {
47
+ key: ResourceType;
48
+ label: string;
49
+ shortLabel: string;
50
+ color: ResourceColor;
51
+ }
52
+
53
+ /**
54
+ * Color palette for each resource type
55
+ *
56
+ * Colours chosen for:
57
+ * - High contrast in both light and dark modes
58
+ * - Distinct hues for easy differentiation
59
+ * - Consistent saturation and brightness levels
60
+ */
61
+ export const RESOURCE_COLORS: Record<ResourceType, ResourceColor> = {
62
+ workers: {
63
+ light: {
64
+ fill: '#f59e0b', // amber-500
65
+ fillHover: '#d97706', // amber-600
66
+ text: '#92400e', // amber-800
67
+ },
68
+ dark: {
69
+ fill: '#fbbf24', // amber-400
70
+ fillHover: '#f59e0b', // amber-500
71
+ text: '#fcd34d', // amber-300
72
+ },
73
+ },
74
+ d1: {
75
+ light: {
76
+ fill: '#3b82f6', // blue-500
77
+ fillHover: '#2563eb', // blue-600
78
+ text: '#1e40af', // blue-800
79
+ },
80
+ dark: {
81
+ fill: '#60a5fa', // blue-400
82
+ fillHover: '#3b82f6', // blue-500
83
+ text: '#93c5fd', // blue-300
84
+ },
85
+ },
86
+ kv: {
87
+ light: {
88
+ fill: '#10b981', // emerald-500
89
+ fillHover: '#059669', // emerald-600
90
+ text: '#065f46', // emerald-800
91
+ },
92
+ dark: {
93
+ fill: '#34d399', // emerald-400
94
+ fillHover: '#10b981', // emerald-500
95
+ text: '#6ee7b7', // emerald-300
96
+ },
97
+ },
98
+ r2: {
99
+ light: {
100
+ fill: '#8b5cf6', // violet-500
101
+ fillHover: '#7c3aed', // violet-600
102
+ text: '#5b21b6', // violet-800
103
+ },
104
+ dark: {
105
+ fill: '#a78bfa', // violet-400
106
+ fillHover: '#8b5cf6', // violet-500
107
+ text: '#c4b5fd', // violet-300
108
+ },
109
+ },
110
+ vectorize: {
111
+ light: {
112
+ fill: '#ec4899', // pink-500
113
+ fillHover: '#db2777', // pink-600
114
+ text: '#9d174d', // pink-800
115
+ },
116
+ dark: {
117
+ fill: '#f472b6', // pink-400
118
+ fillHover: '#ec4899', // pink-500
119
+ text: '#f9a8d4', // pink-300
120
+ },
121
+ },
122
+ aiGateway: {
123
+ light: {
124
+ fill: '#06b6d4', // cyan-500
125
+ fillHover: '#0891b2', // cyan-600
126
+ text: '#155e75', // cyan-800
127
+ },
128
+ dark: {
129
+ fill: '#22d3ee', // cyan-400
130
+ fillHover: '#06b6d4', // cyan-500
131
+ text: '#67e8f9', // cyan-300
132
+ },
133
+ },
134
+ durableObjects: {
135
+ light: {
136
+ fill: '#f97316', // orange-500
137
+ fillHover: '#ea580c', // orange-600
138
+ text: '#9a3412', // orange-800
139
+ },
140
+ dark: {
141
+ fill: '#fb923c', // orange-400
142
+ fillHover: '#f97316', // orange-500
143
+ text: '#fdba74', // orange-300
144
+ },
145
+ },
146
+ workersAI: {
147
+ light: {
148
+ fill: '#6366f1', // indigo-500
149
+ fillHover: '#4f46e5', // indigo-600
150
+ text: '#3730a3', // indigo-800
151
+ },
152
+ dark: {
153
+ fill: '#818cf8', // indigo-400
154
+ fillHover: '#6366f1', // indigo-500
155
+ text: '#a5b4fc', // indigo-300
156
+ },
157
+ },
158
+ queues: {
159
+ light: {
160
+ fill: '#14b8a6', // teal-500
161
+ fillHover: '#0d9488', // teal-600
162
+ text: '#115e59', // teal-800
163
+ },
164
+ dark: {
165
+ fill: '#2dd4bf', // teal-400
166
+ fillHover: '#14b8a6', // teal-500
167
+ text: '#5eead4', // teal-300
168
+ },
169
+ },
170
+ };
171
+
172
+ /**
173
+ * Resource display information
174
+ */
175
+ export const RESOURCE_INFO: ResourceInfo[] = [
176
+ {
177
+ key: 'workers',
178
+ label: 'Workers',
179
+ shortLabel: 'WK',
180
+ color: RESOURCE_COLORS.workers,
181
+ },
182
+ {
183
+ key: 'd1',
184
+ label: 'D1 Database',
185
+ shortLabel: 'D1',
186
+ color: RESOURCE_COLORS.d1,
187
+ },
188
+ {
189
+ key: 'kv',
190
+ label: 'KV Storage',
191
+ shortLabel: 'KV',
192
+ color: RESOURCE_COLORS.kv,
193
+ },
194
+ {
195
+ key: 'r2',
196
+ label: 'R2 Storage',
197
+ shortLabel: 'R2',
198
+ color: RESOURCE_COLORS.r2,
199
+ },
200
+ {
201
+ key: 'vectorize',
202
+ label: 'Vectorize',
203
+ shortLabel: 'VZ',
204
+ color: RESOURCE_COLORS.vectorize,
205
+ },
206
+ {
207
+ key: 'aiGateway',
208
+ label: 'AI Gateway',
209
+ shortLabel: 'AI',
210
+ color: RESOURCE_COLORS.aiGateway,
211
+ },
212
+ {
213
+ key: 'durableObjects',
214
+ label: 'Durable Objects',
215
+ shortLabel: 'DO',
216
+ color: RESOURCE_COLORS.durableObjects,
217
+ },
218
+ {
219
+ key: 'workersAI',
220
+ label: 'Workers AI',
221
+ shortLabel: 'AI',
222
+ color: RESOURCE_COLORS.workersAI,
223
+ },
224
+ {
225
+ key: 'queues',
226
+ label: 'Queues',
227
+ shortLabel: 'Q',
228
+ color: RESOURCE_COLORS.queues,
229
+ },
230
+ ];
231
+
232
+ /**
233
+ * Get Chart.js compatible color arrays for a given theme
234
+ */
235
+ export function getChartColors(theme: 'light' | 'dark'): {
236
+ backgroundColor: string[];
237
+ borderColor: string[];
238
+ hoverBackgroundColor: string[];
239
+ } {
240
+ const fills = RESOURCE_INFO.map((r) => r.color[theme].fill);
241
+ const hovers = RESOURCE_INFO.map((r) => r.color[theme].fillHover);
242
+
243
+ return {
244
+ backgroundColor: fills,
245
+ borderColor: fills.map((c) => c + 'cc'), // Add some transparency for border
246
+ hoverBackgroundColor: hovers,
247
+ };
248
+ }
249
+
250
+ /**
251
+ * Get color for a specific resource type
252
+ */
253
+ export function getResourceColor(
254
+ resourceType: ResourceType,
255
+ theme: 'light' | 'dark'
256
+ ): ResourceColor['light'] | ResourceColor['dark'] {
257
+ return RESOURCE_COLORS[resourceType][theme];
258
+ }
259
+
260
+ /**
261
+ * Get resource info by key
262
+ */
263
+ export function getResourceInfo(resourceType: ResourceType): ResourceInfo | undefined {
264
+ return RESOURCE_INFO.find((r) => r.key === resourceType);
265
+ }
266
+
267
+ /**
268
+ * CSS custom properties for resource colours (for injection into stylesheets)
269
+ */
270
+ export function generateResourceColorCSS(): string {
271
+ const lines: string[] = [':root {'];
272
+
273
+ for (const [key, color] of Object.entries(RESOURCE_COLORS)) {
274
+ lines.push(` --usage-resource-${key}: ${color.light.fill};`);
275
+ lines.push(` --usage-resource-${key}-hover: ${color.light.fillHover};`);
276
+ lines.push(` --usage-resource-${key}-text: ${color.light.text};`);
277
+ }
278
+
279
+ lines.push('}');
280
+ lines.push('');
281
+ lines.push('[data-theme="dark"] {');
282
+
283
+ for (const [key, color] of Object.entries(RESOURCE_COLORS)) {
284
+ lines.push(` --usage-resource-${key}: ${color.dark.fill};`);
285
+ lines.push(` --usage-resource-${key}-hover: ${color.dark.fillHover};`);
286
+ lines.push(` --usage-resource-${key}-text: ${color.dark.text};`);
287
+ }
288
+
289
+ lines.push('}');
290
+
291
+ return lines.join('\n');
292
+ }
@@ -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
+ }