@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,600 @@
1
+ ---
2
+ /**
3
+ * StatsHero Component
4
+ *
5
+ * Hero section for the Usage Dashboard displaying:
6
+ * - Large total cost with comparison period delta
7
+ * - Inline mini sparkline showing trend
8
+ * - Quick stat pills for resource type counts
9
+ * - Overall threshold status indicator
10
+ *
11
+ * Data is populated via JavaScript after API fetch.
12
+ */
13
+
14
+ interface Props {
15
+ period: string;
16
+ periodLabel: string;
17
+ }
18
+
19
+ const { period, periodLabel } = Astro.props;
20
+ ---
21
+
22
+ <div class="stats-hero" role="region" aria-label="Usage summary">
23
+ <!-- Primary Cost Display -->
24
+ <div class="hero-primary">
25
+ <div class="cost-block">
26
+ <div class="cost-header">
27
+ <span class="cost-period">{periodLabel}</span>
28
+ <span id="hero-cache-badge" class="cache-badge" style="display: none;">Cached</span>
29
+ </div>
30
+
31
+ <div class="cost-value-row">
32
+ <span id="hero-cost-value" class="cost-value">$0.00</span>
33
+ <div id="hero-cost-trend" class="cost-trend">
34
+ <span id="hero-trend-arrow" class="trend-arrow"></span>
35
+ <span id="hero-trend-pct" class="trend-pct"></span>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="cost-delta-row">
40
+ <span id="hero-cost-delta" class="cost-delta"></span>
41
+ <span id="hero-comparison-label" class="comparison-label"></span>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- Mini Sparkline -->
46
+ <div class="sparkline-block">
47
+ <canvas
48
+ id="hero-sparkline"
49
+ class="hero-sparkline"
50
+ width="120"
51
+ height="40"
52
+ aria-label="Cost trend over time"></canvas>
53
+ <span class="sparkline-label">7-day trend</span>
54
+ </div>
55
+
56
+ <!-- Status Indicator -->
57
+ <div class="status-block">
58
+ <div id="hero-status-dot" class="status-dot status-ok" aria-label="System status"></div>
59
+ <span id="hero-status-label" class="status-label">All OK</span>
60
+ <span id="hero-alert-count" class="alert-count" style="display: none;">0 alerts</span>
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Quick Stats Pills -->
65
+ <div class="hero-stats-row">
66
+ <div class="stat-pill" data-type="workers">
67
+ <span class="pill-icon">&#x2699;&#xFE0F;</span>
68
+ <span id="stat-workers" class="pill-value">0</span>
69
+ <span class="pill-label">Workers</span>
70
+ </div>
71
+ <div class="stat-pill" data-type="d1">
72
+ <span class="pill-icon">&#x1F5C3;</span>
73
+ <span id="stat-d1" class="pill-value">0</span>
74
+ <span class="pill-label">D1</span>
75
+ </div>
76
+ <div class="stat-pill" data-type="kv">
77
+ <span class="pill-icon">&#x1F511;</span>
78
+ <span id="stat-kv" class="pill-value">0</span>
79
+ <span class="pill-label">KV</span>
80
+ </div>
81
+ <div class="stat-pill" data-type="r2">
82
+ <span class="pill-icon">&#x1F4E6;</span>
83
+ <span id="stat-r2" class="pill-value">0</span>
84
+ <span class="pill-label">R2</span>
85
+ </div>
86
+ <div class="stat-pill" data-type="pages">
87
+ <span class="pill-icon">&#x1F4C4;</span>
88
+ <span id="stat-pages" class="pill-value">0</span>
89
+ <span class="pill-label">Pages</span>
90
+ </div>
91
+ <div class="stat-pill" data-type="vectorize">
92
+ <span class="pill-icon">&#x1F9ED;</span>
93
+ <span id="stat-vectorize" class="pill-value">0</span>
94
+ <span class="pill-label">Vectorize</span>
95
+ </div>
96
+ <div class="stat-pill" data-type="aigateway">
97
+ <span class="pill-icon">&#x1F916;</span>
98
+ <span id="stat-aigateway" class="pill-value">0</span>
99
+ <span class="pill-label">AI Gateway</span>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Response Time / Meta -->
104
+ <div class="hero-meta">
105
+ <span id="hero-response-time" class="response-time"></span>
106
+ <span id="hero-last-updated" class="last-updated"></span>
107
+ </div>
108
+ </div>
109
+
110
+ <style>
111
+ .stats-hero {
112
+ background: var(--usage-bg-secondary);
113
+ border: 1px solid var(--usage-border-default);
114
+ border-radius: var(--usage-radius-xl);
115
+ padding: 1.5rem 2rem;
116
+ margin-bottom: 1.5rem;
117
+ transition:
118
+ background-color var(--usage-transition-normal),
119
+ border-color var(--usage-transition-normal);
120
+ }
121
+
122
+ .hero-primary {
123
+ display: flex;
124
+ align-items: flex-start;
125
+ gap: 2rem;
126
+ margin-bottom: 1.5rem;
127
+ flex-wrap: wrap;
128
+ }
129
+
130
+ /* Cost Block */
131
+ .cost-block {
132
+ flex: 1;
133
+ min-width: 200px;
134
+ }
135
+
136
+ .cost-header {
137
+ display: flex;
138
+ align-items: center;
139
+ gap: 0.5rem;
140
+ margin-bottom: 0.25rem;
141
+ }
142
+
143
+ .cost-period {
144
+ font-size: 0.75rem;
145
+ font-weight: 600;
146
+ text-transform: uppercase;
147
+ letter-spacing: 0.05em;
148
+ color: var(--usage-text-secondary);
149
+ }
150
+
151
+ .cache-badge {
152
+ font-size: 0.625rem;
153
+ font-weight: 600;
154
+ text-transform: uppercase;
155
+ padding: 0.125rem 0.375rem;
156
+ background: var(--usage-accent-purple);
157
+ color: white;
158
+ border-radius: var(--usage-radius-sm);
159
+ }
160
+
161
+ .cost-value-row {
162
+ display: flex;
163
+ align-items: baseline;
164
+ gap: 0.75rem;
165
+ }
166
+
167
+ .cost-value {
168
+ font-family: var(--usage-font-mono);
169
+ font-size: 2.5rem;
170
+ font-weight: 700;
171
+ color: var(--usage-text-primary);
172
+ line-height: 1;
173
+ }
174
+
175
+ .cost-trend {
176
+ display: flex;
177
+ align-items: center;
178
+ gap: 0.25rem;
179
+ padding: 0.25rem 0.5rem;
180
+ border-radius: var(--usage-radius-md);
181
+ font-size: 0.875rem;
182
+ font-weight: 600;
183
+ }
184
+
185
+ .cost-trend.up {
186
+ background: var(--usage-status-critical-bg);
187
+ color: var(--usage-status-critical);
188
+ }
189
+
190
+ .cost-trend.down {
191
+ background: var(--usage-status-ok-bg);
192
+ color: var(--usage-status-ok);
193
+ }
194
+
195
+ .cost-trend.stable {
196
+ background: var(--usage-bg-tertiary);
197
+ color: var(--usage-text-secondary);
198
+ }
199
+
200
+ .trend-arrow {
201
+ font-size: 1rem;
202
+ }
203
+
204
+ .cost-delta-row {
205
+ margin-top: 0.5rem;
206
+ display: flex;
207
+ align-items: center;
208
+ gap: 0.5rem;
209
+ }
210
+
211
+ .cost-delta {
212
+ font-family: var(--usage-font-mono);
213
+ font-size: 0.875rem;
214
+ color: var(--usage-text-secondary);
215
+ }
216
+
217
+ .comparison-label {
218
+ font-size: 0.75rem;
219
+ color: var(--usage-text-muted);
220
+ }
221
+
222
+ /* Sparkline Block */
223
+ .sparkline-block {
224
+ display: flex;
225
+ flex-direction: column;
226
+ align-items: center;
227
+ gap: 0.25rem;
228
+ }
229
+
230
+ .hero-sparkline {
231
+ width: 120px;
232
+ height: 40px;
233
+ }
234
+
235
+ .sparkline-label {
236
+ font-size: 0.625rem;
237
+ text-transform: uppercase;
238
+ letter-spacing: 0.05em;
239
+ color: var(--usage-text-muted);
240
+ }
241
+
242
+ /* Status Block */
243
+ .status-block {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 0.5rem;
247
+ padding: 0.75rem 1rem;
248
+ background: var(--usage-bg-tertiary);
249
+ border-radius: var(--usage-radius-lg);
250
+ margin-left: auto;
251
+ }
252
+
253
+ .status-dot {
254
+ width: 12px;
255
+ height: 12px;
256
+ border-radius: 50%;
257
+ flex-shrink: 0;
258
+ }
259
+
260
+ .status-dot.status-ok {
261
+ background: var(--usage-status-ok);
262
+ box-shadow: 0 0 8px var(--usage-status-ok);
263
+ }
264
+
265
+ .status-dot.status-warning {
266
+ background: var(--usage-status-warning);
267
+ box-shadow: 0 0 8px var(--usage-status-warning);
268
+ }
269
+
270
+ .status-dot.status-critical {
271
+ background: var(--usage-status-critical);
272
+ box-shadow: 0 0 8px var(--usage-status-critical);
273
+ }
274
+
275
+ .status-label {
276
+ font-size: 0.875rem;
277
+ font-weight: 600;
278
+ color: var(--usage-text-primary);
279
+ }
280
+
281
+ .alert-count {
282
+ font-size: 0.75rem;
283
+ color: var(--usage-text-secondary);
284
+ padding: 0.125rem 0.5rem;
285
+ background: var(--usage-status-warning-bg);
286
+ color: var(--usage-status-warning);
287
+ border-radius: var(--usage-radius-sm);
288
+ }
289
+
290
+ /* Quick Stats Row */
291
+ .hero-stats-row {
292
+ display: flex;
293
+ gap: 0.75rem;
294
+ flex-wrap: wrap;
295
+ padding-top: 1rem;
296
+ border-top: 1px solid var(--usage-border-subtle);
297
+ }
298
+
299
+ .stat-pill {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 0.375rem;
303
+ padding: 0.5rem 0.75rem;
304
+ background: var(--usage-bg-tertiary);
305
+ border: 1px solid var(--usage-border-default);
306
+ border-radius: var(--usage-radius-lg);
307
+ cursor: pointer;
308
+ transition: all var(--usage-transition-fast);
309
+ }
310
+
311
+ .stat-pill:hover {
312
+ background: var(--usage-bg-hover);
313
+ border-color: var(--usage-border-hover);
314
+ }
315
+
316
+ .stat-pill.active {
317
+ background: var(--usage-accent-blue);
318
+ border-color: var(--usage-accent-blue);
319
+ }
320
+
321
+ .stat-pill.active .pill-value,
322
+ .stat-pill.active .pill-label {
323
+ color: white;
324
+ }
325
+
326
+ .pill-icon {
327
+ font-size: 1rem;
328
+ }
329
+
330
+ .pill-value {
331
+ font-family: var(--usage-font-mono);
332
+ font-size: 0.875rem;
333
+ font-weight: 700;
334
+ color: var(--usage-text-primary);
335
+ }
336
+
337
+ .pill-label {
338
+ font-size: 0.75rem;
339
+ color: var(--usage-text-secondary);
340
+ }
341
+
342
+ /* Hero Meta */
343
+ .hero-meta {
344
+ display: flex;
345
+ gap: 1rem;
346
+ margin-top: 1rem;
347
+ padding-top: 0.75rem;
348
+ border-top: 1px solid var(--usage-border-subtle);
349
+ }
350
+
351
+ .response-time,
352
+ .last-updated {
353
+ font-size: 0.6875rem;
354
+ color: var(--usage-text-muted);
355
+ }
356
+
357
+ /* Responsive */
358
+ @media (max-width: 768px) {
359
+ .stats-hero {
360
+ padding: 1rem;
361
+ }
362
+
363
+ .hero-primary {
364
+ flex-direction: column;
365
+ gap: 1rem;
366
+ }
367
+
368
+ .cost-value {
369
+ font-size: 2rem;
370
+ }
371
+
372
+ .status-block {
373
+ margin-left: 0;
374
+ width: 100%;
375
+ justify-content: center;
376
+ }
377
+
378
+ .hero-stats-row {
379
+ justify-content: center;
380
+ }
381
+
382
+ .stat-pill {
383
+ flex: 1;
384
+ min-width: 80px;
385
+ justify-content: center;
386
+ }
387
+ }
388
+
389
+ @media (max-width: 480px) {
390
+ .cost-value {
391
+ font-size: 1.75rem;
392
+ }
393
+
394
+ .hero-stats-row {
395
+ gap: 0.5rem;
396
+ }
397
+
398
+ .stat-pill {
399
+ padding: 0.375rem 0.5rem;
400
+ flex-direction: column;
401
+ gap: 0.125rem;
402
+ }
403
+
404
+ .pill-label {
405
+ font-size: 0.625rem;
406
+ }
407
+ }
408
+ </style>
409
+
410
+ <script>
411
+ /**
412
+ * StatsHero JavaScript
413
+ *
414
+ * Handles sparkline rendering and stat pill click-to-filter functionality.
415
+ */
416
+
417
+ // Draw mini sparkline on canvas
418
+ function drawHeroSparkline(canvas: HTMLCanvasElement, data: number[], color: string = '#58a6ff') {
419
+ const ctx = canvas.getContext('2d');
420
+ if (!ctx || data.length < 2) return;
421
+
422
+ const width = canvas.width;
423
+ const height = canvas.height;
424
+ const padding = 4;
425
+
426
+ ctx.clearRect(0, 0, width, height);
427
+
428
+ const min = Math.min(...data);
429
+ const max = Math.max(...data);
430
+ const range = max - min || 1;
431
+
432
+ const xStep = (width - padding * 2) / (data.length - 1);
433
+
434
+ // Draw line
435
+ ctx.beginPath();
436
+ ctx.strokeStyle = color;
437
+ ctx.lineWidth = 2;
438
+ ctx.lineCap = 'round';
439
+ ctx.lineJoin = 'round';
440
+
441
+ data.forEach((value, index) => {
442
+ const x = padding + index * xStep;
443
+ const y = height - padding - ((value - min) / range) * (height - padding * 2);
444
+
445
+ if (index === 0) {
446
+ ctx.moveTo(x, y);
447
+ } else {
448
+ ctx.lineTo(x, y);
449
+ }
450
+ });
451
+
452
+ ctx.stroke();
453
+
454
+ // Draw gradient fill
455
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
456
+ gradient.addColorStop(0, color + '30');
457
+ gradient.addColorStop(1, color + '00');
458
+
459
+ ctx.lineTo(padding + (data.length - 1) * xStep, height);
460
+ ctx.lineTo(padding, height);
461
+ ctx.closePath();
462
+ ctx.fillStyle = gradient;
463
+ ctx.fill();
464
+ }
465
+
466
+ // Stat pill click handler - filters table by resource type
467
+ function initStatPillClicks() {
468
+ const pills = document.querySelectorAll('.stat-pill');
469
+
470
+ pills.forEach((pill) => {
471
+ pill.addEventListener('click', () => {
472
+ const type = (pill as HTMLElement).dataset.type;
473
+ if (!type) return;
474
+
475
+ // Toggle active state
476
+ const wasActive = pill.classList.contains('active');
477
+ pills.forEach((p) => p.classList.remove('active'));
478
+
479
+ if (!wasActive) {
480
+ pill.classList.add('active');
481
+ // Dispatch event to filter table
482
+ window.dispatchEvent(new CustomEvent('usage:filter-type', { detail: { type } }));
483
+ } else {
484
+ // Clear filter
485
+ window.dispatchEvent(new CustomEvent('usage:filter-type', { detail: { type: null } }));
486
+ }
487
+ });
488
+ });
489
+ }
490
+
491
+ // Update hero stats from API data
492
+ window.updateStatsHero = function (data: {
493
+ cost: number;
494
+ costDelta?: number;
495
+ costDeltaPct?: number;
496
+ trend?: 'up' | 'down' | 'stable';
497
+ sparklineData?: number[];
498
+ resourceCounts: Record<string, number>;
499
+ alertCount?: number;
500
+ alertStatus?: 'ok' | 'warning' | 'critical';
501
+ cached?: boolean;
502
+ responseTime?: number;
503
+ }) {
504
+ // Cost value
505
+ const costEl = document.getElementById('hero-cost-value');
506
+ if (costEl) {
507
+ costEl.textContent = `$${data.cost.toFixed(2)}`;
508
+ }
509
+
510
+ // Cost trend
511
+ const trendEl = document.getElementById('hero-cost-trend');
512
+ const trendArrowEl = document.getElementById('hero-trend-arrow');
513
+ const trendPctEl = document.getElementById('hero-trend-pct');
514
+
515
+ if (trendEl && trendArrowEl && trendPctEl && data.trend) {
516
+ trendEl.className = 'cost-trend ' + data.trend;
517
+
518
+ if (data.trend === 'up') {
519
+ trendArrowEl.textContent = '↑';
520
+ } else if (data.trend === 'down') {
521
+ trendArrowEl.textContent = '↓';
522
+ } else {
523
+ trendArrowEl.textContent = '→';
524
+ }
525
+
526
+ trendPctEl.textContent = data.costDeltaPct
527
+ ? `${Math.abs(data.costDeltaPct).toFixed(1)}%`
528
+ : '';
529
+ }
530
+
531
+ // Cost delta
532
+ const deltaEl = document.getElementById('hero-cost-delta');
533
+ if (deltaEl && data.costDelta !== undefined) {
534
+ const sign = data.costDelta >= 0 ? '+' : '';
535
+ deltaEl.textContent = `${sign}$${data.costDelta.toFixed(2)}`;
536
+ }
537
+
538
+ // Resource counts
539
+ const countKeys = ['workers', 'd1', 'kv', 'r2', 'pages', 'vectorize', 'aigateway'];
540
+ countKeys.forEach((key) => {
541
+ const el = document.getElementById(`stat-${key}`);
542
+ if (el) {
543
+ el.textContent = String(data.resourceCounts[key] || 0);
544
+ }
545
+ });
546
+
547
+ // Status
548
+ const statusDotEl = document.getElementById('hero-status-dot');
549
+ const statusLabelEl = document.getElementById('hero-status-label');
550
+ const alertCountEl = document.getElementById('hero-alert-count');
551
+
552
+ if (statusDotEl && statusLabelEl) {
553
+ statusDotEl.className = 'status-dot';
554
+
555
+ if (data.alertStatus === 'critical') {
556
+ statusDotEl.classList.add('status-critical');
557
+ statusLabelEl.textContent = 'Critical';
558
+ } else if (data.alertStatus === 'warning') {
559
+ statusDotEl.classList.add('status-warning');
560
+ statusLabelEl.textContent = 'Warning';
561
+ } else {
562
+ statusDotEl.classList.add('status-ok');
563
+ statusLabelEl.textContent = 'All OK';
564
+ }
565
+ }
566
+
567
+ if (alertCountEl && data.alertCount && data.alertCount > 0) {
568
+ alertCountEl.style.display = 'inline';
569
+ alertCountEl.textContent = `${data.alertCount} alert${data.alertCount > 1 ? 's' : ''}`;
570
+ }
571
+
572
+ // Sparkline
573
+ const sparklineCanvas = document.getElementById('hero-sparkline') as HTMLCanvasElement;
574
+ if (sparklineCanvas && data.sparklineData && data.sparklineData.length > 1) {
575
+ const color = data.trend === 'up' ? '#f85149' : data.trend === 'down' ? '#3fb950' : '#58a6ff';
576
+ drawHeroSparkline(sparklineCanvas, data.sparklineData, color);
577
+ }
578
+
579
+ // Cache badge
580
+ const cacheBadgeEl = document.getElementById('hero-cache-badge');
581
+ if (cacheBadgeEl) {
582
+ cacheBadgeEl.style.display = data.cached ? 'inline' : 'none';
583
+ }
584
+
585
+ // Response time
586
+ const responseTimeEl = document.getElementById('hero-response-time');
587
+ if (responseTimeEl && data.responseTime) {
588
+ responseTimeEl.textContent = `Response: ${data.responseTime}ms`;
589
+ }
590
+
591
+ // Last updated
592
+ const lastUpdatedEl = document.getElementById('hero-last-updated');
593
+ if (lastUpdatedEl) {
594
+ lastUpdatedEl.textContent = `Updated: ${new Date().toLocaleTimeString()}`;
595
+ }
596
+ };
597
+
598
+ // Initialize on DOM ready
599
+ document.addEventListener('DOMContentLoaded', initStatPillClicks);
600
+ </script>