@littlebearapps/platform-admin-sdk 2.1.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 (115) hide show
  1. package/README.md +2 -5
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +121 -3
  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/FeatureUsageReport.tsx +339 -0
  9. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  10. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  11. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  12. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  13. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  14. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  15. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  16. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  17. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  18. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  19. package/templates/full/dashboard/src/pages/map.astro +561 -0
  20. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  21. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  22. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  23. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  24. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  25. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  26. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  27. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  28. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  29. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  30. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  31. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  32. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  33. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  34. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  35. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  36. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  37. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  38. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  39. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  40. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  41. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  42. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  43. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  44. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  45. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  46. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  47. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  48. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  49. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  50. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  51. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  52. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  53. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  54. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  55. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  56. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  57. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  58. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  59. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  60. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  61. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  62. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  63. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  64. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  65. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  66. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  67. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  68. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  69. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  70. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  71. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  72. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  73. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  74. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  75. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  76. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  77. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  78. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  79. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  80. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  81. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  82. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  83. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  84. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  85. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  86. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  87. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  88. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  89. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  90. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  91. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  92. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  93. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  94. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  95. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  96. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  97. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  98. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  99. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  100. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  101. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  102. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  103. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  104. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  105. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  106. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  107. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  108. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  109. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  110. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  111. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  112. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  113. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  114. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  115. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,632 @@
1
+ ---
2
+ /**
3
+ * DailyCostChart.astro
4
+ *
5
+ * Interactive stacked bar chart showing daily cost breakdown by resource type.
6
+ * - Chart.js v4 stacked bar chart
7
+ * - Theme-aware colors using CSS custom properties
8
+ * - Hover tooltips with date, per-resource costs, and total
9
+ * - Click emits `usage:date-selected` event
10
+ * - Visual highlight for selected bar
11
+ *
12
+ * Part of task-18: Usage Dashboard Interactive Chart & Table Enhancement
13
+ */
14
+
15
+ import type { DailyCostBreakdown } from '../../lib/cloudflare/graphql';
16
+ import { RESOURCE_INFO, type ResourceType } from './usage-colors';
17
+
18
+ interface Props {
19
+ data: DailyCostBreakdown[];
20
+ selectedDate?: string;
21
+ class?: string;
22
+ }
23
+
24
+ const { data = [], selectedDate, class: className = '' } = Astro.props;
25
+
26
+ // Prepare data for Chart.js
27
+ const labels = data.map((d) => d.date);
28
+ const chartId = `daily-cost-chart-${Math.random().toString(36).slice(2, 9)}`;
29
+
30
+ // Resource keys in display order
31
+ const resourceKeys: ResourceType[] = [
32
+ 'workers',
33
+ 'd1',
34
+ 'kv',
35
+ 'r2',
36
+ 'vectorize',
37
+ 'aiGateway',
38
+ 'durableObjects',
39
+ ];
40
+
41
+ // Calculate total across all days
42
+ const grandTotal = data.reduce((sum, d) => sum + d.total, 0);
43
+
44
+ function formatCost(value: number): string {
45
+ return new Intl.NumberFormat('en-US', {
46
+ style: 'currency',
47
+ currency: 'USD',
48
+ minimumFractionDigits: 2,
49
+ maximumFractionDigits: 2,
50
+ }).format(value);
51
+ }
52
+
53
+ function formatDate(dateStr: string): string {
54
+ const date = new Date(dateStr + 'T00:00:00');
55
+ return date.toLocaleDateString('en-US', {
56
+ weekday: 'short',
57
+ month: 'short',
58
+ day: 'numeric',
59
+ });
60
+ }
61
+
62
+ // Prepare dataset for each resource type
63
+ const datasets = resourceKeys.map((key) => {
64
+ const info = RESOURCE_INFO.find((r) => r.key === key);
65
+ return {
66
+ key,
67
+ label: info?.label || key,
68
+ data: data.map((d) => d[key]),
69
+ };
70
+ });
71
+
72
+ // Serialize data for client-side script
73
+ const chartData = JSON.stringify({
74
+ labels,
75
+ datasets,
76
+ resourceInfo: RESOURCE_INFO,
77
+ selectedDate,
78
+ });
79
+ ---
80
+
81
+ <div class:list={['daily-cost-chart', className]} data-component="daily-cost-chart">
82
+ <div class="chart-header">
83
+ <h3 class="chart-title">Daily Cost Breakdown</h3>
84
+ <span class="chart-total">{formatCost(grandTotal)} total</span>
85
+ </div>
86
+
87
+ <div class="chart-container">
88
+ {
89
+ data.length === 0 ? (
90
+ <div class="empty-state">
91
+ <svg
92
+ class="empty-icon"
93
+ viewBox="0 0 24 24"
94
+ fill="none"
95
+ stroke="currentColor"
96
+ stroke-width="1.5"
97
+ >
98
+ <path
99
+ stroke-linecap="round"
100
+ stroke-linejoin="round"
101
+ d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
102
+ />
103
+ </svg>
104
+ <span class="empty-text">No cost data available for this period</span>
105
+ </div>
106
+ ) : (
107
+ <canvas id={chartId} class="chart-canvas" />
108
+ )
109
+ }
110
+ </div>
111
+
112
+ <div class="chart-legend">
113
+ {
114
+ RESOURCE_INFO.map((resource) => (
115
+ <div class="legend-item" data-resource={resource.key}>
116
+ <span
117
+ class="legend-color"
118
+ style={`background-color: var(--usage-resource-${resource.key}, ${resource.color.light.fill})`}
119
+ />
120
+ <span class="legend-label">{resource.label}</span>
121
+ </div>
122
+ ))
123
+ }
124
+ </div>
125
+
126
+ {
127
+ selectedDate && (
128
+ <div class="selection-indicator">
129
+ <span class="selection-text">Showing: {formatDate(selectedDate)}</span>
130
+ <button type="button" class="clear-selection" data-action="clear-selection">
131
+ <svg viewBox="0 0 16 16" fill="currentColor" class="clear-icon">
132
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z" />
133
+ </svg>
134
+ Clear
135
+ </button>
136
+ </div>
137
+ )
138
+ }
139
+ </div>
140
+
141
+ <style>
142
+ .daily-cost-chart {
143
+ background: var(--usage-bg-secondary, #f6f8fa);
144
+ border: 1px solid var(--usage-border-default, #d1d9e0);
145
+ border-radius: var(--usage-radius-lg, 8px);
146
+ padding: var(--usage-spacing-lg, 1.5rem);
147
+ transition:
148
+ background-color var(--usage-transition-normal, 200ms ease),
149
+ border-color var(--usage-transition-normal, 200ms ease);
150
+ }
151
+
152
+ .chart-header {
153
+ display: flex;
154
+ align-items: baseline;
155
+ justify-content: space-between;
156
+ margin-bottom: var(--usage-spacing-lg, 1.5rem);
157
+ padding-bottom: var(--usage-spacing-md, 1rem);
158
+ border-bottom: 1px solid var(--usage-border-subtle, #e8ecef);
159
+ }
160
+
161
+ .chart-title {
162
+ margin: 0;
163
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
164
+ font-size: 1rem;
165
+ font-weight: 600;
166
+ color: var(--usage-text-primary, #1f2328);
167
+ letter-spacing: -0.01em;
168
+ }
169
+
170
+ .chart-total {
171
+ font-family: var(--usage-font-mono, 'JetBrains Mono', monospace);
172
+ font-size: 0.875rem;
173
+ font-weight: 500;
174
+ color: var(--usage-text-secondary, #656d76);
175
+ }
176
+
177
+ .chart-container {
178
+ position: relative;
179
+ height: 280px;
180
+ margin-bottom: var(--usage-spacing-md, 1rem);
181
+ }
182
+
183
+ .chart-canvas {
184
+ width: 100% !important;
185
+ height: 100% !important;
186
+ }
187
+
188
+ .chart-legend {
189
+ display: flex;
190
+ flex-wrap: wrap;
191
+ gap: var(--usage-spacing-md, 1rem);
192
+ padding-top: var(--usage-spacing-md, 1rem);
193
+ border-top: 1px solid var(--usage-border-subtle, #e8ecef);
194
+ }
195
+
196
+ .legend-item {
197
+ display: flex;
198
+ align-items: center;
199
+ gap: var(--usage-spacing-xs, 0.25rem);
200
+ cursor: pointer;
201
+ padding: 2px 6px;
202
+ border-radius: var(--usage-radius-sm, 4px);
203
+ transition: background-color var(--usage-transition-fast, 150ms ease);
204
+ }
205
+
206
+ .legend-item:hover {
207
+ background: var(--usage-bg-hover, #eaeef2);
208
+ }
209
+
210
+ .legend-item.dimmed {
211
+ opacity: 0.4;
212
+ }
213
+
214
+ .legend-color {
215
+ width: 12px;
216
+ height: 12px;
217
+ border-radius: 2px;
218
+ flex-shrink: 0;
219
+ }
220
+
221
+ .legend-label {
222
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
223
+ font-size: 0.75rem;
224
+ font-weight: 500;
225
+ color: var(--usage-text-secondary, #656d76);
226
+ }
227
+
228
+ .selection-indicator {
229
+ display: flex;
230
+ align-items: center;
231
+ justify-content: space-between;
232
+ margin-top: var(--usage-spacing-md, 1rem);
233
+ padding: var(--usage-spacing-sm, 0.5rem) var(--usage-spacing-md, 1rem);
234
+ background: var(--usage-accent-blue, #0969da);
235
+ border-radius: var(--usage-radius-md, 6px);
236
+ color: white;
237
+ }
238
+
239
+ .selection-text {
240
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
241
+ font-size: 0.8125rem;
242
+ font-weight: 500;
243
+ }
244
+
245
+ .clear-selection {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 4px;
249
+ padding: 4px 8px;
250
+ background: rgba(255, 255, 255, 0.2);
251
+ border: none;
252
+ border-radius: var(--usage-radius-sm, 4px);
253
+ color: white;
254
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
255
+ font-size: 0.75rem;
256
+ font-weight: 500;
257
+ cursor: pointer;
258
+ transition: background-color var(--usage-transition-fast, 150ms ease);
259
+ }
260
+
261
+ .clear-selection:hover {
262
+ background: rgba(255, 255, 255, 0.3);
263
+ }
264
+
265
+ .clear-icon {
266
+ width: 14px;
267
+ height: 14px;
268
+ }
269
+
270
+ .empty-state {
271
+ display: flex;
272
+ flex-direction: column;
273
+ align-items: center;
274
+ justify-content: center;
275
+ height: 100%;
276
+ gap: var(--usage-spacing-sm, 0.5rem);
277
+ }
278
+
279
+ .empty-icon {
280
+ width: 48px;
281
+ height: 48px;
282
+ color: var(--usage-text-muted, #8c959f);
283
+ opacity: 0.5;
284
+ }
285
+
286
+ .empty-text {
287
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
288
+ font-size: 0.875rem;
289
+ color: var(--usage-text-muted, #8c959f);
290
+ }
291
+
292
+ /* Responsive adjustments */
293
+ @media (max-width: 768px) {
294
+ .chart-container {
295
+ height: 220px;
296
+ }
297
+
298
+ .chart-legend {
299
+ gap: var(--usage-spacing-sm, 0.5rem);
300
+ }
301
+
302
+ .legend-label {
303
+ font-size: 0.6875rem;
304
+ }
305
+ }
306
+
307
+ @media (max-width: 480px) {
308
+ .chart-header {
309
+ flex-direction: column;
310
+ gap: var(--usage-spacing-xs, 0.25rem);
311
+ }
312
+
313
+ .chart-container {
314
+ height: 180px;
315
+ }
316
+ }
317
+ </style>
318
+
319
+ <script define:vars={{ chartId, chartData }}>
320
+ // Initialize chart when DOM is ready
321
+ document.addEventListener('DOMContentLoaded', () => {
322
+ initDailyCostChart();
323
+ });
324
+
325
+ // Also handle Astro page transitions
326
+ document.addEventListener('astro:page-load', () => {
327
+ initDailyCostChart();
328
+ });
329
+
330
+ function initDailyCostChart() {
331
+ const canvas = document.getElementById(chartId);
332
+ if (!canvas || !(canvas instanceof HTMLCanvasElement)) return;
333
+
334
+ // Parse the serialized data
335
+ const data = JSON.parse(chartData);
336
+ if (!data.labels || data.labels.length === 0) return;
337
+
338
+ // Detect theme
339
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
340
+
341
+ // Color palettes for light and dark modes
342
+ const colorPalettes = {
343
+ workers: { light: '#f59e0b', dark: '#fbbf24' },
344
+ d1: { light: '#3b82f6', dark: '#60a5fa' },
345
+ kv: { light: '#10b981', dark: '#34d399' },
346
+ r2: { light: '#8b5cf6', dark: '#a78bfa' },
347
+ vectorize: { light: '#ec4899', dark: '#f472b6' },
348
+ aiGateway: { light: '#06b6d4', dark: '#22d3ee' },
349
+ durableObjects: { light: '#f97316', dark: '#fb923c' },
350
+ };
351
+
352
+ // Build datasets for Chart.js
353
+ const chartDatasets = data.datasets.map((ds, index) => ({
354
+ label: ds.label,
355
+ data: ds.data,
356
+ backgroundColor: colorPalettes[ds.key]?.[isDark ? 'dark' : 'light'] || '#888',
357
+ borderColor: 'transparent',
358
+ borderWidth: 0,
359
+ borderRadius: index === data.datasets.length - 1 ? { topLeft: 4, topRight: 4 } : 0,
360
+ borderSkipped: false,
361
+ }));
362
+
363
+ // Chart.js configuration
364
+ const config = {
365
+ type: 'bar',
366
+ data: {
367
+ labels: data.labels.map((d) => formatDateShort(d)),
368
+ datasets: chartDatasets,
369
+ },
370
+ options: {
371
+ responsive: true,
372
+ maintainAspectRatio: false,
373
+ interaction: {
374
+ mode: 'index',
375
+ intersect: false,
376
+ },
377
+ plugins: {
378
+ legend: {
379
+ display: false, // We use custom legend
380
+ },
381
+ tooltip: {
382
+ enabled: true,
383
+ backgroundColor: isDark ? '#21262d' : '#ffffff',
384
+ titleColor: isDark ? '#e6edf3' : '#1f2328',
385
+ bodyColor: isDark ? '#8b949e' : '#656d76',
386
+ borderColor: isDark ? '#30363d' : '#d1d9e0',
387
+ borderWidth: 1,
388
+ padding: 12,
389
+ cornerRadius: 6,
390
+ titleFont: {
391
+ family: "'Inter', sans-serif",
392
+ size: 13,
393
+ weight: 600,
394
+ },
395
+ bodyFont: {
396
+ family: "'JetBrains Mono', monospace",
397
+ size: 11,
398
+ },
399
+ callbacks: {
400
+ title: (items) => {
401
+ if (!items.length) return '';
402
+ const idx = items[0].dataIndex;
403
+ return formatDateLong(data.labels[idx]);
404
+ },
405
+ label: (context) => {
406
+ const value = context.raw || 0;
407
+ return `${context.dataset.label}: ${formatCurrency(value)}`;
408
+ },
409
+ afterBody: (items) => {
410
+ if (!items.length) return '';
411
+ const idx = items[0].dataIndex;
412
+ const total = data.datasets.reduce((sum, ds) => sum + (ds.data[idx] || 0), 0);
413
+ return `\nTotal: ${formatCurrency(total)}`;
414
+ },
415
+ },
416
+ },
417
+ },
418
+ scales: {
419
+ x: {
420
+ stacked: true,
421
+ grid: {
422
+ display: false,
423
+ },
424
+ ticks: {
425
+ color: isDark ? '#8b949e' : '#656d76',
426
+ font: {
427
+ family: "'Inter', sans-serif",
428
+ size: 10,
429
+ },
430
+ maxRotation: 45,
431
+ minRotation: 0,
432
+ },
433
+ border: {
434
+ display: false,
435
+ },
436
+ },
437
+ y: {
438
+ stacked: true,
439
+ beginAtZero: true,
440
+ grid: {
441
+ color: isDark ? '#21262d' : '#e8ecef',
442
+ },
443
+ ticks: {
444
+ color: isDark ? '#8b949e' : '#656d76',
445
+ font: {
446
+ family: "'JetBrains Mono', monospace",
447
+ size: 10,
448
+ },
449
+ callback: (value) => formatCurrency(value),
450
+ },
451
+ border: {
452
+ display: false,
453
+ },
454
+ },
455
+ },
456
+ onClick: (event, elements) => {
457
+ if (elements.length > 0) {
458
+ const idx = elements[0].index;
459
+ const date = data.labels[idx];
460
+
461
+ // Dispatch custom event
462
+ document.dispatchEvent(
463
+ new CustomEvent('usage:date-selected', {
464
+ detail: { date },
465
+ bubbles: true,
466
+ })
467
+ );
468
+
469
+ // Update URL
470
+ const url = new URL(window.location.href);
471
+ url.searchParams.set('selectedDate', date);
472
+ window.history.replaceState({}, '', url.toString());
473
+
474
+ // Highlight selected bar
475
+ highlightBar(chart, idx);
476
+ }
477
+ },
478
+ onHover: (event, elements) => {
479
+ const canvas = event.native?.target;
480
+ if (canvas) {
481
+ canvas.style.cursor = elements.length > 0 ? 'pointer' : 'default';
482
+ }
483
+ },
484
+ },
485
+ };
486
+
487
+ // Create chart
488
+ const chart = new Chart(canvas, config);
489
+
490
+ // Highlight selected date if present
491
+ if (data.selectedDate) {
492
+ const idx = data.labels.indexOf(data.selectedDate);
493
+ if (idx !== -1) {
494
+ highlightBar(chart, idx);
495
+ }
496
+ }
497
+
498
+ // Handle clear selection
499
+ const clearBtn = document.querySelector('[data-action="clear-selection"]');
500
+ if (clearBtn) {
501
+ clearBtn.addEventListener('click', () => {
502
+ document.dispatchEvent(
503
+ new CustomEvent('usage:clear-selection', {
504
+ bubbles: true,
505
+ })
506
+ );
507
+
508
+ const url = new URL(window.location.href);
509
+ url.searchParams.delete('selectedDate');
510
+ window.history.replaceState({}, '', url.toString());
511
+
512
+ // Reset bar highlighting
513
+ chart.data.datasets.forEach((ds) => {
514
+ ds.backgroundColor = colorPalettes[ds.key]?.[isDark ? 'dark' : 'light'] || '#888';
515
+ });
516
+ chart.update();
517
+ });
518
+ }
519
+
520
+ // Handle legend clicks for filtering
521
+ const legendItems = document.querySelectorAll('.legend-item');
522
+ const hiddenDatasets = new Set();
523
+
524
+ legendItems.forEach((item) => {
525
+ item.addEventListener('click', () => {
526
+ const resourceKey = item.getAttribute('data-resource');
527
+ const dsIndex = data.datasets.findIndex((ds) => ds.key === resourceKey);
528
+
529
+ if (dsIndex !== -1) {
530
+ const isHidden = chart.getDatasetMeta(dsIndex).hidden;
531
+ chart.getDatasetMeta(dsIndex).hidden = !isHidden;
532
+
533
+ if (isHidden) {
534
+ hiddenDatasets.delete(resourceKey);
535
+ item.classList.remove('dimmed');
536
+ } else {
537
+ hiddenDatasets.add(resourceKey);
538
+ item.classList.add('dimmed');
539
+ }
540
+
541
+ chart.update();
542
+ }
543
+ });
544
+ });
545
+
546
+ // Handle theme changes
547
+ const observer = new MutationObserver((mutations) => {
548
+ mutations.forEach((mutation) => {
549
+ if (mutation.attributeName === 'data-theme') {
550
+ const newIsDark = document.documentElement.getAttribute('data-theme') === 'dark';
551
+
552
+ chart.data.datasets.forEach((ds, i) => {
553
+ const key = data.datasets[i].key;
554
+ ds.backgroundColor = colorPalettes[key]?.[newIsDark ? 'dark' : 'light'] || '#888';
555
+ });
556
+
557
+ chart.options.plugins.tooltip.backgroundColor = newIsDark ? '#21262d' : '#ffffff';
558
+ chart.options.plugins.tooltip.titleColor = newIsDark ? '#e6edf3' : '#1f2328';
559
+ chart.options.plugins.tooltip.bodyColor = newIsDark ? '#8b949e' : '#656d76';
560
+ chart.options.plugins.tooltip.borderColor = newIsDark ? '#30363d' : '#d1d9e0';
561
+ chart.options.scales.x.ticks.color = newIsDark ? '#8b949e' : '#656d76';
562
+ chart.options.scales.y.ticks.color = newIsDark ? '#8b949e' : '#656d76';
563
+ chart.options.scales.y.grid.color = newIsDark ? '#21262d' : '#e8ecef';
564
+
565
+ chart.update();
566
+ }
567
+ });
568
+ });
569
+
570
+ observer.observe(document.documentElement, { attributes: true });
571
+
572
+ // Store chart instance for cleanup
573
+ canvas._chartInstance = chart;
574
+ }
575
+
576
+ function highlightBar(chart, selectedIndex) {
577
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
578
+
579
+ chart.data.datasets.forEach((ds, dsIdx) => {
580
+ const key =
581
+ chart.config.data.datasets[dsIdx].key ||
582
+ ['workers', 'd1', 'kv', 'r2', 'vectorize', 'aiGateway', 'durableObjects'][dsIdx];
583
+ const baseColor =
584
+ {
585
+ workers: { light: '#f59e0b', dark: '#fbbf24' },
586
+ d1: { light: '#3b82f6', dark: '#60a5fa' },
587
+ kv: { light: '#10b981', dark: '#34d399' },
588
+ r2: { light: '#8b5cf6', dark: '#a78bfa' },
589
+ vectorize: { light: '#ec4899', dark: '#f472b6' },
590
+ aiGateway: { light: '#06b6d4', dark: '#22d3ee' },
591
+ durableObjects: { light: '#f97316', dark: '#fb923c' },
592
+ }[key]?.[isDark ? 'dark' : 'light'] || '#888';
593
+
594
+ // Create array of colors for each bar
595
+ const colors = [];
596
+ for (let i = 0; i < ds.data.length; i++) {
597
+ if (i === selectedIndex) {
598
+ colors.push(baseColor);
599
+ } else {
600
+ colors.push(baseColor + '40'); // 25% opacity for non-selected
601
+ }
602
+ }
603
+ ds.backgroundColor = colors;
604
+ });
605
+
606
+ chart.update();
607
+ }
608
+
609
+ function formatDateShort(dateStr) {
610
+ const date = new Date(dateStr + 'T00:00:00');
611
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
612
+ }
613
+
614
+ function formatDateLong(dateStr) {
615
+ const date = new Date(dateStr + 'T00:00:00');
616
+ return date.toLocaleDateString('en-US', {
617
+ weekday: 'long',
618
+ month: 'long',
619
+ day: 'numeric',
620
+ year: 'numeric',
621
+ });
622
+ }
623
+
624
+ function formatCurrency(value) {
625
+ return new Intl.NumberFormat('en-US', {
626
+ style: 'currency',
627
+ currency: 'USD',
628
+ minimumFractionDigits: 2,
629
+ maximumFractionDigits: 2,
630
+ }).format(value);
631
+ }
632
+ </script>