@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,419 @@
1
+ /**
2
+ * AI Tab Controller
3
+ *
4
+ * Handles AI Gateway and Workers AI data loading and rendering.
5
+ * Extracted from index.astro for task-22.5 (slim to <300 lines)
6
+ */
7
+
8
+ import { formatAINumber, formatAICurrency } from './formatters';
9
+
10
+ // ========== Types ==========
11
+
12
+ export interface AIGatewayData {
13
+ totalRequests: number;
14
+ totalCachedRequests: number;
15
+ cacheHitRate: number;
16
+ tokensIn: number;
17
+ tokensOut: number;
18
+ totalCostUsd: number;
19
+ byProvider: Record<
20
+ string,
21
+ {
22
+ requests: number;
23
+ cachedRequests: number;
24
+ tokensIn: number;
25
+ tokensOut: number;
26
+ costUsd: number;
27
+ }
28
+ >;
29
+ byModel: Record<
30
+ string,
31
+ {
32
+ requests: number;
33
+ cachedRequests: number;
34
+ tokensIn: number;
35
+ tokensOut: number;
36
+ costUsd: number;
37
+ }
38
+ >;
39
+ }
40
+
41
+ export interface WorkersAIData {
42
+ totalRequests: number;
43
+ totalInputTokens: number;
44
+ totalOutputTokens: number;
45
+ totalCostUsd: number;
46
+ byProject: Record<string, { requests: number; costUsd: number; isEstimated: boolean }>;
47
+ byModel: Record<string, { requests: number; costUsd: number }>;
48
+ aiGateway?: AIGatewayData;
49
+ }
50
+
51
+ // ========== State ==========
52
+
53
+ let aiDataLoaded = false;
54
+ let aiDataCache: WorkersAIData | null = null;
55
+
56
+ // ========== DOM Helpers ==========
57
+
58
+ function clearElement(el: HTMLElement): void {
59
+ while (el.firstChild) {
60
+ el.removeChild(el.firstChild);
61
+ }
62
+ }
63
+
64
+ function createLoadingIndicator(message: string): HTMLElement {
65
+ const div = document.createElement('div');
66
+ div.className = 'ai-loading-indicator';
67
+ div.textContent = message;
68
+ return div;
69
+ }
70
+
71
+ function createErrorState(message: string): HTMLElement {
72
+ const div = document.createElement('div');
73
+ div.className = 'ai-error-state';
74
+ div.textContent = message;
75
+ return div;
76
+ }
77
+
78
+ function createEmptyState(message: string): HTMLElement {
79
+ const div = document.createElement('div');
80
+ div.className = 'ai-empty-state';
81
+ div.textContent = message;
82
+ return div;
83
+ }
84
+
85
+ // ========== Data Loading ==========
86
+
87
+ export async function loadAITabData(period: string = '7d'): Promise<void> {
88
+ if (aiDataLoaded && aiDataCache) {
89
+ renderAIData(aiDataCache);
90
+ return;
91
+ }
92
+
93
+ // Show loading state
94
+ const containers = ['ai-gateway-content', 'ai-by-project', 'ai-by-model'];
95
+ containers.forEach((id) => {
96
+ const el = document.getElementById(id);
97
+ if (el) {
98
+ clearElement(el);
99
+ el.appendChild(createLoadingIndicator('Loading AI data...'));
100
+ }
101
+ });
102
+
103
+ try {
104
+ // Include credentials to pass Cloudflare Access JWT cookie
105
+ const response = await fetch(`/api/usage/workersai?period=${period}`, {
106
+ credentials: 'include',
107
+ });
108
+
109
+ if (!response.ok) {
110
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
111
+ }
112
+
113
+ const json = await response.json();
114
+
115
+ if (!json.success || !json.data) {
116
+ throw new Error(json.error || 'Invalid response');
117
+ }
118
+
119
+ aiDataCache = json.data;
120
+ aiDataLoaded = true;
121
+ renderAIData(json.data);
122
+ } catch (error) {
123
+ console.error('Failed to load AI data:', error);
124
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
125
+ containers.forEach((id) => {
126
+ const el = document.getElementById(id);
127
+ if (el) {
128
+ clearElement(el);
129
+ el.appendChild(createErrorState('Failed to load AI data: ' + errorMsg));
130
+ }
131
+ });
132
+
133
+ const statIds = ['ai-total-requests', 'ai-total-cost', 'ai-input-tokens', 'ai-output-tokens'];
134
+ statIds.forEach((id) => {
135
+ const el = document.getElementById(id);
136
+ if (el) el.textContent = 'Error';
137
+ });
138
+ }
139
+ }
140
+
141
+ // ========== Rendering ==========
142
+
143
+ function renderAIData(data: WorkersAIData): void {
144
+ // Update stats
145
+ const totalRequestsEl = document.getElementById('ai-total-requests');
146
+ const totalCostEl = document.getElementById('ai-total-cost');
147
+ const inputTokensEl = document.getElementById('ai-input-tokens');
148
+ const outputTokensEl = document.getElementById('ai-output-tokens');
149
+
150
+ if (totalRequestsEl) totalRequestsEl.textContent = formatAINumber(data.totalRequests);
151
+ if (totalCostEl) totalCostEl.textContent = formatAICurrency(data.totalCostUsd);
152
+ if (inputTokensEl) inputTokensEl.textContent = formatAINumber(data.totalInputTokens);
153
+ if (outputTokensEl) outputTokensEl.textContent = formatAINumber(data.totalOutputTokens);
154
+
155
+ // Render AI Gateway content with real data if available
156
+ renderAIGatewaySection(data.aiGateway);
157
+
158
+ // Render by project table
159
+ renderByProjectTable(data.byProject);
160
+
161
+ // Render by model table
162
+ renderByModelTable(data.byModel);
163
+ }
164
+
165
+ function renderAIGatewaySection(aiGateway?: AIGatewayData): void {
166
+ const gatewayEl = document.getElementById('ai-gateway-content');
167
+ if (!gatewayEl) return;
168
+
169
+ clearElement(gatewayEl);
170
+
171
+ // If no AI Gateway data, show placeholder
172
+ if (!aiGateway) {
173
+ const emptyDiv = createEmptyState('No AI Gateway data available for this period');
174
+ gatewayEl.appendChild(emptyDiv);
175
+ return;
176
+ }
177
+
178
+ // Create metrics grid
179
+ const metricsGrid = document.createElement('div');
180
+ metricsGrid.className = 'ai-gateway-metrics-grid';
181
+
182
+ const metrics = [
183
+ { value: formatAINumber(aiGateway.totalRequests), label: 'Gateway Requests' },
184
+ { value: `${aiGateway.cacheHitRate.toFixed(1)}%`, label: 'Cache Hit Rate' },
185
+ { value: formatAINumber(aiGateway.tokensIn), label: 'Tokens In' },
186
+ { value: formatAINumber(aiGateway.tokensOut), label: 'Tokens Out' },
187
+ ];
188
+
189
+ metrics.forEach((metric) => {
190
+ const metricDiv = document.createElement('div');
191
+ metricDiv.className = 'ai-gateway-metric';
192
+
193
+ const valueDiv = document.createElement('div');
194
+ valueDiv.className = 'metric-value';
195
+ valueDiv.textContent = metric.value;
196
+ metricDiv.appendChild(valueDiv);
197
+
198
+ const labelDiv = document.createElement('div');
199
+ labelDiv.className = 'metric-label';
200
+ labelDiv.textContent = metric.label;
201
+ metricDiv.appendChild(labelDiv);
202
+
203
+ metricsGrid.appendChild(metricDiv);
204
+ });
205
+
206
+ gatewayEl.appendChild(metricsGrid);
207
+
208
+ // Add provider breakdown if available
209
+ const providers = Object.entries(aiGateway.byProvider);
210
+ if (providers.length > 0) {
211
+ const providerSection = document.createElement('div');
212
+ providerSection.className = 'ai-gateway-providers';
213
+
214
+ const providerHeader = document.createElement('h4');
215
+ providerHeader.textContent = 'By Provider';
216
+ providerSection.appendChild(providerHeader);
217
+
218
+ const providerTable = document.createElement('table');
219
+ providerTable.className = 'ai-table ai-table-compact';
220
+
221
+ // Header
222
+ const thead = document.createElement('thead');
223
+ const headerRow = document.createElement('tr');
224
+ ['Provider', 'Requests', 'Cached', 'Cost'].forEach((text) => {
225
+ const th = document.createElement('th');
226
+ th.textContent = text;
227
+ headerRow.appendChild(th);
228
+ });
229
+ thead.appendChild(headerRow);
230
+ providerTable.appendChild(thead);
231
+
232
+ // Body
233
+ const tbody = document.createElement('tbody');
234
+ providers.sort((a, b) => b[1].requests - a[1].requests);
235
+ providers.forEach(([provider, data]) => {
236
+ const row = document.createElement('tr');
237
+
238
+ const nameCell = document.createElement('td');
239
+ nameCell.className = 'provider-name';
240
+ nameCell.textContent = provider;
241
+ row.appendChild(nameCell);
242
+
243
+ const requestsCell = document.createElement('td');
244
+ requestsCell.textContent = formatAINumber(data.requests);
245
+ row.appendChild(requestsCell);
246
+
247
+ const cachedCell = document.createElement('td');
248
+ cachedCell.textContent = formatAINumber(data.cachedRequests);
249
+ row.appendChild(cachedCell);
250
+
251
+ const costCell = document.createElement('td');
252
+ costCell.className = 'cost-cell';
253
+ costCell.textContent = formatAICurrency(data.costUsd);
254
+ row.appendChild(costCell);
255
+
256
+ tbody.appendChild(row);
257
+ });
258
+ providerTable.appendChild(tbody);
259
+ providerSection.appendChild(providerTable);
260
+ gatewayEl.appendChild(providerSection);
261
+ }
262
+ }
263
+
264
+ function renderByProjectTable(
265
+ byProject: Record<string, { requests: number; costUsd: number; isEstimated: boolean }>
266
+ ): void {
267
+ const containerEl = document.getElementById('ai-by-project');
268
+ if (!containerEl) return;
269
+
270
+ clearElement(containerEl);
271
+
272
+ const projects = Object.entries(byProject);
273
+ if (projects.length === 0) {
274
+ containerEl.appendChild(createEmptyState('No Workers AI usage data available'));
275
+ return;
276
+ }
277
+
278
+ // Sort by cost descending
279
+ projects.sort((a, b) => b[1].costUsd - a[1].costUsd);
280
+
281
+ const table = document.createElement('table');
282
+ table.className = 'ai-table';
283
+
284
+ // Header
285
+ const thead = document.createElement('thead');
286
+ const headerRow = document.createElement('tr');
287
+ ['Project', 'Requests', 'Est. Cost'].forEach((text) => {
288
+ const th = document.createElement('th');
289
+ th.textContent = text;
290
+ headerRow.appendChild(th);
291
+ });
292
+ thead.appendChild(headerRow);
293
+ table.appendChild(thead);
294
+
295
+ // Body
296
+ const tbody = document.createElement('tbody');
297
+ let hasEstimated = false;
298
+
299
+ projects.forEach(([projectName, projectData]) => {
300
+ const row = document.createElement('tr');
301
+
302
+ // Project name cell
303
+ const nameCell = document.createElement('td');
304
+ nameCell.className = 'project-name';
305
+ nameCell.textContent = projectName;
306
+
307
+ if (projectData.isEstimated) {
308
+ hasEstimated = true;
309
+ const badge = document.createElement('span');
310
+ badge.className = 'estimated-badge';
311
+ badge.textContent = '~estimated';
312
+ nameCell.appendChild(badge);
313
+ }
314
+ row.appendChild(nameCell);
315
+
316
+ // Requests cell
317
+ const requestsCell = document.createElement('td');
318
+ requestsCell.textContent = formatAINumber(projectData.requests);
319
+ row.appendChild(requestsCell);
320
+
321
+ // Cost cell
322
+ const costCell = document.createElement('td');
323
+ costCell.className = 'cost-cell';
324
+ costCell.textContent = formatAICurrency(projectData.costUsd);
325
+ row.appendChild(costCell);
326
+
327
+ tbody.appendChild(row);
328
+ });
329
+
330
+ table.appendChild(tbody);
331
+ containerEl.appendChild(table);
332
+
333
+ if (hasEstimated) {
334
+ const note = document.createElement('p');
335
+ note.className = 'ai-note';
336
+ note.textContent =
337
+ '* Estimated costs are calculated from request counts and average token usage.';
338
+ containerEl.appendChild(note);
339
+ }
340
+ }
341
+
342
+ function renderByModelTable(byModel: Record<string, { requests: number; costUsd: number }>): void {
343
+ const containerEl = document.getElementById('ai-by-model');
344
+ if (!containerEl) return;
345
+
346
+ clearElement(containerEl);
347
+
348
+ const models = Object.entries(byModel);
349
+ if (models.length === 0) {
350
+ containerEl.appendChild(createEmptyState('No model-level data available'));
351
+ return;
352
+ }
353
+
354
+ // Sort by cost descending
355
+ models.sort((a, b) => b[1].costUsd - a[1].costUsd);
356
+
357
+ const table = document.createElement('table');
358
+ table.className = 'ai-table';
359
+
360
+ // Header
361
+ const thead = document.createElement('thead');
362
+ const headerRow = document.createElement('tr');
363
+ ['Model', 'Requests', 'Est. Cost'].forEach((text) => {
364
+ const th = document.createElement('th');
365
+ th.textContent = text;
366
+ headerRow.appendChild(th);
367
+ });
368
+ thead.appendChild(headerRow);
369
+ table.appendChild(thead);
370
+
371
+ // Body
372
+ const tbody = document.createElement('tbody');
373
+
374
+ models.forEach(([modelName, modelData]) => {
375
+ const row = document.createElement('tr');
376
+
377
+ // Model name cell (formatted)
378
+ const nameCell = document.createElement('td');
379
+ nameCell.className = 'model-name';
380
+ nameCell.textContent = modelName.replace('@cf/', '').replace('meta/', '');
381
+ row.appendChild(nameCell);
382
+
383
+ // Requests cell
384
+ const requestsCell = document.createElement('td');
385
+ requestsCell.textContent = formatAINumber(modelData.requests);
386
+ row.appendChild(requestsCell);
387
+
388
+ // Cost cell
389
+ const costCell = document.createElement('td');
390
+ costCell.className = 'cost-cell';
391
+ costCell.textContent = formatAICurrency(modelData.costUsd);
392
+ row.appendChild(costCell);
393
+
394
+ tbody.appendChild(row);
395
+ });
396
+
397
+ table.appendChild(tbody);
398
+ containerEl.appendChild(table);
399
+ }
400
+
401
+ // ========== Reset State ==========
402
+
403
+ export function resetAITabState(): void {
404
+ aiDataLoaded = false;
405
+ aiDataCache = null;
406
+ }
407
+
408
+ // ========== Event Listener Setup ==========
409
+
410
+ export function setupAITabEventListeners(): void {
411
+ // Reload AI data when period changes
412
+ document.addEventListener('period-change', () => {
413
+ resetAITabState();
414
+ const aiPanel = document.getElementById('tab-ai');
415
+ if (aiPanel && aiPanel.style.display !== 'none') {
416
+ loadAITabData();
417
+ }
418
+ });
419
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Usage Dashboard Constants
3
+ *
4
+ * Static label mappings and configuration.
5
+ * Extracted from index.astro for task-22.5 (slim to <300 lines)
6
+ */
7
+
8
+ // Period display labels
9
+ export const PERIOD_LABELS: Record<string, string> = {
10
+ '24h': 'Last 24 Hours',
11
+ '7d': 'Last 7 Days',
12
+ '30d': 'Last 30 Days',
13
+ custom: 'Custom Range',
14
+ };
15
+
16
+ // Project display labels
17
+ export const PROJECT_LABELS: Record<string, string> = {
18
+ all: 'All Projects',
19
+ 'brand-copilot': 'Brand Copilot',
20
+ scout: 'Scout',
21
+ platform: 'Platform',
22
+ };
23
+
24
+ // Valid period values
25
+ export const VALID_PERIODS = ['24h', '7d', '30d', 'custom'] as const;
26
+ export type Period = (typeof VALID_PERIODS)[number];
27
+
28
+ // Valid project values
29
+ export const VALID_PROJECTS = ['all', 'brand-copilot', 'scout', 'platform'] as const;
30
+ export type Project = (typeof VALID_PROJECTS)[number];
31
+
32
+ // Valid compare mode values
33
+ export const VALID_COMPARE_MODES = ['none', 'lastMonth', 'custom'] as const;
34
+ export type CompareMode = (typeof VALID_COMPARE_MODES)[number];
35
+
36
+ // Get SSR config from data attributes (set by Astro template)
37
+ export function getSSRConfig(): { period: string; project: string; compare: CompareMode } {
38
+ const configEl = document.getElementById('usage-ssr-config');
39
+ if (!configEl) {
40
+ return { period: '30d', project: 'all', compare: 'none' };
41
+ }
42
+ const compareValue = configEl.dataset.compare || 'none';
43
+ return {
44
+ period: configEl.dataset.period || '30d',
45
+ project: configEl.dataset.project || 'all',
46
+ compare: (VALID_COMPARE_MODES.includes(compareValue as CompareMode)
47
+ ? compareValue
48
+ : 'none') as CompareMode,
49
+ };
50
+ }
51
+
52
+ // Get period label
53
+ export function getPeriodLabel(period: string): string {
54
+ return PERIOD_LABELS[period] || 'Last 30 Days';
55
+ }
56
+
57
+ // Get project label
58
+ export function getProjectLabel(project: string): string {
59
+ return PROJECT_LABELS[project] || 'All Projects';
60
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Usage Dashboard - Format Utilities
3
+ *
4
+ * Extracted from index.astro for task-22.5 (slim to <300 lines)
5
+ */
6
+
7
+ /**
8
+ * Format a number with K/M/B suffixes
9
+ */
10
+ export function formatNumber(num: number | undefined | null): string {
11
+ if (num === undefined || num === null || isNaN(num)) return '0';
12
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
13
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
14
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
15
+ return num.toLocaleString();
16
+ }
17
+
18
+ /**
19
+ * Format bytes to human-readable string using decimal (SI) units.
20
+ *
21
+ * IMPORTANT: Uses decimal (SI) units because Cloudflare bills in decimal GB:
22
+ * - 1 GB = 1,000,000,000 bytes (decimal/SI - used for billing)
23
+ * - 1 GiB = 1,073,741,824 bytes (binary - NOT used by Cloudflare)
24
+ *
25
+ * This ensures displayed values match Cloudflare billing/CSV exports.
26
+ */
27
+ export function formatBytes(bytes: number): string {
28
+ if (bytes >= 1_000_000_000) return `${(bytes / 1_000_000_000).toFixed(2)} GB`;
29
+ if (bytes >= 1_000_000) return `${(bytes / 1_000_000).toFixed(2)} MB`;
30
+ if (bytes >= 1_000) return `${(bytes / 1_000).toFixed(2)} KB`;
31
+ return `${bytes} B`;
32
+ }
33
+
34
+ /**
35
+ * Format currency value
36
+ */
37
+ export function formatCurrency(amount: number, decimals = 2): string {
38
+ return '$' + amount.toFixed(decimals);
39
+ }
40
+
41
+ /**
42
+ * Format percentage
43
+ */
44
+ export function formatPercent(value: number): string {
45
+ return value.toFixed(1) + '%';
46
+ }
47
+
48
+ /**
49
+ * Format AI-specific numbers with appropriate precision
50
+ */
51
+ export function formatAINumber(num: number): string {
52
+ if (num >= 1_000_000) return (num / 1_000_000).toFixed(2) + 'M';
53
+ if (num >= 1_000) return (num / 1_000).toFixed(1) + 'K';
54
+ return num.toLocaleString();
55
+ }
56
+
57
+ /**
58
+ * Format AI currency (higher precision)
59
+ */
60
+ export function formatAICurrency(amount: number): string {
61
+ return '$' + amount.toFixed(4);
62
+ }