@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.
- package/README.md +4 -7
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +206 -4
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -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/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -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/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -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/components/usage/usage-colors.ts +292 -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/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -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/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -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/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -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,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
|
+
}
|