@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,478 @@
1
+ /**
2
+ * Usage Data Transformers
3
+ *
4
+ * Functions to transform API response data into component formats.
5
+ * Part of task-17.9 (Restructure page layout).
6
+ */
7
+
8
+ import type { UnifiedResource, ResourceType, ResourceStatus } from './types';
9
+ import { CF_PRICING } from '../../lib/cloudflare/costs';
10
+
11
+ /**
12
+ * Transform API usage data into UnifiedResource array for the table.
13
+ * Consolidates all resource types (workers, D1, KV, R2, etc.) into a single list.
14
+ */
15
+ export function transformToUnifiedResources(
16
+ data: {
17
+ workers?: Array<{
18
+ scriptName: string;
19
+ requests: number;
20
+ cpuTime: number;
21
+ duration: number;
22
+ errors: number;
23
+ }>;
24
+ d1?: Array<{
25
+ databaseId: string;
26
+ databaseName: string;
27
+ rowsRead: number;
28
+ rowsWritten: number;
29
+ queryCount: number;
30
+ }>;
31
+ kv?: Array<{
32
+ namespaceId: string;
33
+ namespaceName: string;
34
+ reads: number;
35
+ writes: number;
36
+ deletes: number;
37
+ lists: number;
38
+ }>;
39
+ r2?: Array<{
40
+ bucketName: string;
41
+ storageBytes: number;
42
+ objectCount: number;
43
+ classAOperations: number;
44
+ classBOperations: number;
45
+ }>;
46
+ vectorize?: Array<{
47
+ id: string;
48
+ name: string;
49
+ vectorCount: number;
50
+ dimensions: number;
51
+ }>;
52
+ pages?: Array<{
53
+ projectName: string;
54
+ deployments: number;
55
+ requests: number;
56
+ bandwidth: number;
57
+ }>;
58
+ durableObjects?: Array<{
59
+ name: string;
60
+ requests: number;
61
+ duration: number;
62
+ storageBytes: number;
63
+ }>;
64
+ aiGateway?: {
65
+ totalRequests: number;
66
+ totalTokens: number;
67
+ cachedRequests: number;
68
+ modelBreakdown: Array<{
69
+ model: string;
70
+ requests: number;
71
+ tokens: number;
72
+ }>;
73
+ };
74
+ },
75
+ costs: {
76
+ workers: number;
77
+ d1: number;
78
+ kv: number;
79
+ r2: number;
80
+ vectorize: number;
81
+ pages: number;
82
+ queues: number;
83
+ workflows: number;
84
+ durableObjects: number;
85
+ aiGateway: number;
86
+ total: number;
87
+ },
88
+ projectMapping: (name: string) => string
89
+ ): UnifiedResource[] {
90
+ const resources: UnifiedResource[] = [];
91
+
92
+ // Transform Workers - calculate cost based on actual requests and CPU time
93
+ if (data.workers) {
94
+ for (const worker of data.workers) {
95
+ // Workers pricing: $0.30 per million requests + $0.02 per million CPU ms
96
+ // Note: $5/mo base cost is account-level, not per-worker
97
+ const requestCost = (worker.requests / 1_000_000) * CF_PRICING.workers.requestsPerMillion;
98
+ const cpuCost = (worker.cpuTime / 1_000_000) * CF_PRICING.workers.cpuMsPerMillion;
99
+ const costPerWorker = requestCost + cpuCost;
100
+ resources.push({
101
+ id: `worker-${worker.scriptName}`,
102
+ name: worker.scriptName,
103
+ type: 'worker' as ResourceType,
104
+ project: projectMapping(worker.scriptName),
105
+ usage: {
106
+ value: worker.requests,
107
+ unit: 'requests',
108
+ formatted: formatNumber(worker.requests),
109
+ },
110
+ costCurrent: costPerWorker,
111
+ costPrior: 0, // Will be filled by comparison data
112
+ costDelta: 0,
113
+ costDeltaPct: null,
114
+ status: getStatusFromThreshold(worker.errors, worker.requests),
115
+ });
116
+ }
117
+ }
118
+
119
+ // Transform D1 databases - calculate cost based on actual rowsRead/rowsWritten
120
+ if (data.d1) {
121
+ for (const db of data.d1) {
122
+ // D1 pricing: $0.001 per billion rows read + $1.00 per million rows written
123
+ const costPerDb =
124
+ (db.rowsRead / 1_000_000_000) * CF_PRICING.d1.rowsReadPerBillion +
125
+ (db.rowsWritten / 1_000_000) * CF_PRICING.d1.rowsWrittenPerMillion;
126
+ resources.push({
127
+ id: `d1-${db.databaseId}`,
128
+ name: db.databaseName || db.databaseId,
129
+ type: 'd1' as ResourceType,
130
+ project: projectMapping(db.databaseName || db.databaseId),
131
+ usage: {
132
+ value: db.rowsRead,
133
+ unit: 'rows read',
134
+ formatted: formatNumber(db.rowsRead),
135
+ },
136
+ costCurrent: costPerDb,
137
+ costPrior: 0,
138
+ costDelta: 0,
139
+ costDeltaPct: null,
140
+ status: 'healthy' as ResourceStatus,
141
+ });
142
+ }
143
+ }
144
+
145
+ // Transform KV namespaces - calculate cost based on actual reads/writes/deletes/lists
146
+ if (data.kv) {
147
+ for (const kv of data.kv) {
148
+ // KV pricing: $0.50/M reads + $5.00/M writes + $5.00/M deletes + $5.00/M lists
149
+ const costPerKv =
150
+ (kv.reads / 1_000_000) * CF_PRICING.kv.readsPerMillion +
151
+ (kv.writes / 1_000_000) * CF_PRICING.kv.writesPerMillion +
152
+ (kv.deletes / 1_000_000) * CF_PRICING.kv.deletesPerMillion +
153
+ (kv.lists / 1_000_000) * CF_PRICING.kv.listsPerMillion;
154
+ const totalOps = kv.reads + kv.writes + kv.deletes + kv.lists;
155
+ resources.push({
156
+ id: `kv-${kv.namespaceId}`,
157
+ name: kv.namespaceName || kv.namespaceId,
158
+ type: 'kv' as ResourceType,
159
+ project: projectMapping(kv.namespaceName || kv.namespaceId),
160
+ usage: {
161
+ value: totalOps,
162
+ unit: 'operations',
163
+ formatted: formatNumber(totalOps),
164
+ },
165
+ costCurrent: costPerKv,
166
+ costPrior: 0,
167
+ costDelta: 0,
168
+ costDeltaPct: null,
169
+ status: 'healthy' as ResourceStatus,
170
+ });
171
+ }
172
+ }
173
+
174
+ // Transform R2 buckets - calculate cost based on actual storage and operations
175
+ if (data.r2) {
176
+ for (const bucket of data.r2) {
177
+ // R2 pricing: $0.015/GB storage + $4.50/M Class A ops + $0.36/M Class B ops
178
+ // Note: Cloudflare uses decimal GB (1 GB = 1,000,000,000 bytes) for billing
179
+ const costPerBucket =
180
+ (bucket.storageBytes / 1_000_000_000) * CF_PRICING.r2.storagePerGbMonth +
181
+ (bucket.classAOperations / 1_000_000) * CF_PRICING.r2.classAPerMillion +
182
+ (bucket.classBOperations / 1_000_000) * CF_PRICING.r2.classBPerMillion;
183
+ resources.push({
184
+ id: `r2-${bucket.bucketName}`,
185
+ name: bucket.bucketName,
186
+ type: 'r2' as ResourceType,
187
+ project: projectMapping(bucket.bucketName),
188
+ usage: {
189
+ value: bucket.storageBytes,
190
+ unit: 'storage',
191
+ formatted: formatBytes(bucket.storageBytes),
192
+ },
193
+ costCurrent: costPerBucket,
194
+ costPrior: 0,
195
+ costDelta: 0,
196
+ costDeltaPct: null,
197
+ status: 'healthy' as ResourceStatus,
198
+ });
199
+ }
200
+ }
201
+
202
+ // Transform Vectorize indexes - calculate cost based on actual dimensions stored
203
+ if (data.vectorize) {
204
+ for (const index of data.vectorize) {
205
+ // Vectorize pricing: $0.01 per million stored dimensions
206
+ const storedDimensions = index.vectorCount * index.dimensions;
207
+ const costPerIndex =
208
+ (storedDimensions / 1_000_000) * CF_PRICING.vectorize.storedDimensionsPerMillion;
209
+ resources.push({
210
+ id: `vectorize-${index.id}`,
211
+ name: index.name || index.id,
212
+ type: 'vectorize' as ResourceType,
213
+ project: projectMapping(index.name || index.id),
214
+ usage: {
215
+ value: index.vectorCount,
216
+ unit: 'vectors',
217
+ formatted: formatNumber(index.vectorCount),
218
+ },
219
+ costCurrent: costPerIndex,
220
+ costPrior: 0,
221
+ costDelta: 0,
222
+ costDeltaPct: null,
223
+ status: 'healthy' as ResourceStatus,
224
+ });
225
+ }
226
+ }
227
+
228
+ // Transform Pages projects - calculate cost based on deployments and bandwidth
229
+ if (data.pages) {
230
+ for (const page of data.pages) {
231
+ // Pages pricing: $0.15 per build after 500 free + $0.02 per GB bandwidth
232
+ // Note: First 500 builds/month are free - assuming passed in usage is billable
233
+ // Note: Cloudflare uses decimal GB (1 GB = 1,000,000,000 bytes) for billing
234
+ const buildCost = page.deployments * CF_PRICING.pages.buildCost;
235
+ const bandwidthCost = (page.bandwidth / 1_000_000_000) * CF_PRICING.pages.bandwidthPerGb;
236
+ const costPerPage = buildCost + bandwidthCost;
237
+ resources.push({
238
+ id: `pages-${page.projectName}`,
239
+ name: page.projectName,
240
+ type: 'pages' as ResourceType,
241
+ project: projectMapping(page.projectName),
242
+ usage: {
243
+ value: page.requests,
244
+ unit: 'requests',
245
+ formatted: formatNumber(page.requests),
246
+ },
247
+ costCurrent: costPerPage,
248
+ costPrior: 0,
249
+ costDelta: 0,
250
+ costDeltaPct: null,
251
+ status: 'healthy' as ResourceStatus,
252
+ });
253
+ }
254
+ }
255
+
256
+ // Transform Durable Objects - calculate cost based on requests, duration, storage
257
+ if (data.durableObjects) {
258
+ for (const obj of data.durableObjects) {
259
+ // DO pricing: $0.15/M requests + $12.50/M GB-seconds + $0.20/GB storage
260
+ // Note: Cloudflare uses decimal GB (1 GB = 1,000,000,000 bytes) for billing
261
+ const requestCost = (obj.requests / 1_000_000) * CF_PRICING.durableObjects.requestsPerMillion;
262
+ const durationCost =
263
+ (obj.duration / 1_000_000) * CF_PRICING.durableObjects.gbSecondsPerMillion;
264
+ const storageCost =
265
+ (obj.storageBytes / 1_000_000_000) * CF_PRICING.durableObjects.storagePerGbMonth;
266
+ const costPerObj = requestCost + durationCost + storageCost;
267
+ resources.push({
268
+ id: `do-${obj.name}`,
269
+ name: obj.name,
270
+ type: 'do' as ResourceType,
271
+ project: projectMapping(obj.name),
272
+ usage: {
273
+ value: obj.requests,
274
+ unit: 'requests',
275
+ formatted: formatNumber(obj.requests),
276
+ },
277
+ costCurrent: costPerObj,
278
+ costPrior: 0,
279
+ costDelta: 0,
280
+ costDeltaPct: null,
281
+ status: 'healthy' as ResourceStatus,
282
+ });
283
+ }
284
+ }
285
+
286
+ // Transform AI Gateway (single entry)
287
+ if (data.aiGateway && data.aiGateway.totalRequests > 0) {
288
+ resources.push({
289
+ id: 'ai-gateway-main',
290
+ name: 'AI Gateway',
291
+ type: 'ai-gateway' as ResourceType,
292
+ project: 'platform', // AI Gateway project attribution
293
+ usage: {
294
+ value: data.aiGateway.totalTokens,
295
+ unit: 'tokens',
296
+ formatted: formatNumber(data.aiGateway.totalTokens),
297
+ },
298
+ costCurrent: costs.aiGateway,
299
+ costPrior: 0,
300
+ costDelta: 0,
301
+ costDeltaPct: null,
302
+ status: 'healthy' as ResourceStatus,
303
+ });
304
+ }
305
+
306
+ return resources;
307
+ }
308
+
309
+ /**
310
+ * Apply comparison data to resources.
311
+ * Updates costPrior, costDelta, and costDeltaPct for each resource.
312
+ */
313
+ export function applyComparisonData(
314
+ resources: UnifiedResource[],
315
+ priorResources: UnifiedResource[]
316
+ ): UnifiedResource[] {
317
+ const priorMap = new Map(priorResources.map((r) => [r.id, r]));
318
+
319
+ return resources.map((resource) => {
320
+ const prior = priorMap.get(resource.id);
321
+
322
+ if (!prior) {
323
+ // New resource - mark as NEW
324
+ return {
325
+ ...resource,
326
+ costDeltaPct: 'NEW' as const,
327
+ };
328
+ }
329
+
330
+ const costDelta = resource.costCurrent - prior.costCurrent;
331
+ // Use $0.01 threshold to avoid extreme percentages from near-zero baselines
332
+ // Cap at 999% for display sanity
333
+ const costDeltaPct =
334
+ prior.costCurrent >= 0.01
335
+ ? Math.min((costDelta / prior.costCurrent) * 100, 999)
336
+ : resource.costCurrent > 0
337
+ ? 'NEW'
338
+ : 0;
339
+
340
+ return {
341
+ ...resource,
342
+ costPrior: prior.costCurrent,
343
+ costDelta,
344
+ costDeltaPct:
345
+ typeof costDeltaPct === 'number' ? Math.round(costDeltaPct * 10) / 10 : costDeltaPct,
346
+ };
347
+ });
348
+ }
349
+
350
+ /**
351
+ * Filter resources based on filter state.
352
+ */
353
+ export function filterResources(
354
+ resources: UnifiedResource[],
355
+ filters: {
356
+ project?: string;
357
+ serviceTypes?: string[];
358
+ searchQuery?: string;
359
+ onlyChanged?: boolean;
360
+ nonZeroCost?: boolean;
361
+ }
362
+ ): UnifiedResource[] {
363
+ return resources.filter((resource) => {
364
+ // Project filter
365
+ if (filters.project && filters.project !== 'all' && resource.project !== filters.project) {
366
+ return false;
367
+ }
368
+
369
+ // Service type filter
370
+ if (filters.serviceTypes && filters.serviceTypes.length > 0) {
371
+ if (!filters.serviceTypes.includes(resource.type)) {
372
+ return false;
373
+ }
374
+ }
375
+
376
+ // Search query filter
377
+ if (filters.searchQuery) {
378
+ const query = filters.searchQuery.toLowerCase();
379
+ if (!resource.name.toLowerCase().includes(query)) {
380
+ return false;
381
+ }
382
+ }
383
+
384
+ // Only changed filter (>5% change or NEW)
385
+ if (filters.onlyChanged) {
386
+ if (resource.costDeltaPct === 'NEW') return true;
387
+ if (typeof resource.costDeltaPct !== 'number') return false;
388
+ if (Math.abs(resource.costDeltaPct) <= 5) return false;
389
+ }
390
+
391
+ // Non-zero cost filter
392
+ if (filters.nonZeroCost && resource.costCurrent === 0) {
393
+ return false;
394
+ }
395
+
396
+ return true;
397
+ });
398
+ }
399
+
400
+ /**
401
+ * Sort resources by column.
402
+ */
403
+ export function sortResources(
404
+ resources: UnifiedResource[],
405
+ column: string,
406
+ direction: 'asc' | 'desc'
407
+ ): UnifiedResource[] {
408
+ const sorted = [...resources].sort((a, b) => {
409
+ let comparison = 0;
410
+
411
+ switch (column) {
412
+ case 'name':
413
+ comparison = a.name.localeCompare(b.name);
414
+ break;
415
+ case 'type':
416
+ comparison = a.type.localeCompare(b.type);
417
+ break;
418
+ case 'project':
419
+ comparison = a.project.localeCompare(b.project);
420
+ break;
421
+ case 'usage':
422
+ comparison = a.usage.value - b.usage.value;
423
+ break;
424
+ case 'costCurrent':
425
+ comparison = a.costCurrent - b.costCurrent;
426
+ break;
427
+ case 'costDeltaPct': {
428
+ const aPct = a.costDeltaPct === 'NEW' ? Infinity : (a.costDeltaPct ?? -Infinity);
429
+ const bPct = b.costDeltaPct === 'NEW' ? Infinity : (b.costDeltaPct ?? -Infinity);
430
+ comparison = aPct - bPct;
431
+ break;
432
+ }
433
+ case 'status': {
434
+ const statusOrder = { critical: 4, high: 3, warning: 2, healthy: 1 };
435
+ comparison = statusOrder[a.status] - statusOrder[b.status];
436
+ break;
437
+ }
438
+ default:
439
+ comparison = 0;
440
+ }
441
+
442
+ return direction === 'asc' ? comparison : -comparison;
443
+ });
444
+
445
+ return sorted;
446
+ }
447
+
448
+ // Helper functions
449
+ function formatNumber(num: number): string {
450
+ if (num === undefined || num === null || isNaN(num)) return '0';
451
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
452
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
453
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
454
+ return num.toLocaleString();
455
+ }
456
+
457
+ /**
458
+ * Format bytes to human-readable string using decimal (SI) units.
459
+ *
460
+ * IMPORTANT: Uses decimal (SI) units because Cloudflare bills in decimal GB:
461
+ * - 1 GB = 1,000,000,000 bytes (decimal/SI - used for billing)
462
+ * - 1 GiB = 1,073,741,824 bytes (binary - NOT used by Cloudflare)
463
+ */
464
+ function formatBytes(bytes: number): string {
465
+ if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
466
+ if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(2)} MB`;
467
+ if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(2)} KB`;
468
+ return `${bytes} B`;
469
+ }
470
+
471
+ function getStatusFromThreshold(errors: number, requests: number): ResourceStatus {
472
+ if (requests === 0) return 'healthy';
473
+ const errorRate = (errors / requests) * 100;
474
+ if (errorRate >= 10) return 'critical';
475
+ if (errorRate >= 5) return 'high';
476
+ if (errorRate >= 1) return 'warning';
477
+ return 'healthy';
478
+ }