@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,633 @@
1
+ ---
2
+ /**
3
+ * AnomalyAlerts Component
4
+ *
5
+ * Displays usage anomalies detected by the platform-usage worker.
6
+ * Shows metric spikes with deviation factors and resolution status.
7
+ *
8
+ * Part of Enhancement #2: Anomaly alerts visualization.
9
+ *
10
+ * Features:
11
+ * - Lists recent anomalies with severity indicators
12
+ * - Shows deviation factor (how many stddevs above average)
13
+ * - Visual indicators for resolved/unresolved status
14
+ * - Filters by time range and resolved status
15
+ */
16
+
17
+ export interface AnomalyData {
18
+ id: string;
19
+ detectedAt: string;
20
+ metric: string;
21
+ project: string;
22
+ currentValue: number;
23
+ rollingAvg: number;
24
+ deviationFactor: number;
25
+ alertSent: boolean;
26
+ alertChannel: string | null;
27
+ resolved: boolean;
28
+ resolvedAt: string | null;
29
+ resolvedBy: string | null;
30
+ }
31
+
32
+ interface Props {
33
+ /** Initial anomalies (if server-rendered) */
34
+ anomalies?: AnomalyData[];
35
+ }
36
+
37
+ const { anomalies = [] } = Astro.props;
38
+
39
+ // Format metric names for display
40
+ function formatMetric(metric: string): string {
41
+ const map: Record<string, string> = {
42
+ workers_requests: 'Workers Requests',
43
+ d1_rows_read: 'D1 Reads',
44
+ d1_rows_written: 'D1 Writes',
45
+ kv_reads: 'KV Reads',
46
+ kv_writes: 'KV Writes',
47
+ r2_class_a: 'R2 Class A',
48
+ r2_class_b: 'R2 Class B',
49
+ total_cost_usd: 'Total Cost',
50
+ ai_requests: 'AI Requests',
51
+ ai_tokens: 'AI Tokens',
52
+ };
53
+ return map[metric] ?? metric.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
54
+ }
55
+
56
+ // Format large numbers
57
+ function formatNumber(num: number): string {
58
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
59
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
60
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
61
+ return num.toLocaleString();
62
+ }
63
+
64
+ // Get severity class based on deviation factor
65
+ function getSeverity(deviationFactor: number): 'critical' | 'high' | 'medium' | 'low' {
66
+ if (deviationFactor >= 4) return 'critical';
67
+ if (deviationFactor >= 3) return 'high';
68
+ if (deviationFactor >= 2) return 'medium';
69
+ return 'low';
70
+ }
71
+
72
+ // Format relative time
73
+ function formatRelativeTime(dateStr: string): string {
74
+ const date = new Date(dateStr);
75
+ const now = new Date();
76
+ const diffMs = now.getTime() - date.getTime();
77
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
78
+ const diffDays = Math.floor(diffHours / 24);
79
+
80
+ if (diffDays > 0) return `${diffDays}d ago`;
81
+ if (diffHours > 0) return `${diffHours}h ago`;
82
+ return 'Just now';
83
+ }
84
+ ---
85
+
86
+ <section class="anomaly-alerts-section" data-component="anomaly-alerts">
87
+ <div class="section-header">
88
+ <h3 class="section-title">
89
+ <span class="title-icon">⚠️</span>
90
+ Usage Anomalies
91
+ </h3>
92
+ <p class="section-description">Detected usage spikes based on 7-day rolling averages</p>
93
+ </div>
94
+
95
+ <div class="anomaly-controls">
96
+ <div class="filter-group">
97
+ <label for="anomaly-days">Lookback:</label>
98
+ <select id="anomaly-days" class="filter-select">
99
+ <option value="7">7 days</option>
100
+ <option value="14">14 days</option>
101
+ <option value="30">30 days</option>
102
+ </select>
103
+ </div>
104
+ <div class="filter-group">
105
+ <label for="anomaly-resolved">Status:</label>
106
+ <select id="anomaly-resolved" class="filter-select">
107
+ <option value="all">All</option>
108
+ <option value="false">Unresolved</option>
109
+ <option value="true">Resolved</option>
110
+ </select>
111
+ </div>
112
+ <button type="button" id="anomaly-refresh" class="refresh-btn" title="Refresh"> 🔄 </button>
113
+ </div>
114
+
115
+ <div class="anomaly-list-container" id="anomaly-list-container">
116
+ <!-- Loading state -->
117
+ <div class="loading-state" id="anomaly-loading">
118
+ <div class="loading-spinner"></div>
119
+ <span>Loading anomalies...</span>
120
+ </div>
121
+
122
+ <!-- Empty state -->
123
+ <div class="empty-state" id="anomaly-empty" style="display: none;">
124
+ <span class="empty-icon">✅</span>
125
+ <span class="empty-text">No anomalies detected in this period</span>
126
+ <span class="empty-subtext">Your usage is within normal ranges</span>
127
+ </div>
128
+
129
+ <!-- Anomaly cards -->
130
+ <div class="anomaly-cards" id="anomaly-cards" style="display: none;">
131
+ <!-- Cards populated by JavaScript -->
132
+ </div>
133
+ </div>
134
+ </section>
135
+
136
+ <style>
137
+ .anomaly-alerts-section {
138
+ margin-bottom: 1.5rem;
139
+ }
140
+
141
+ .section-header {
142
+ margin-bottom: 1rem;
143
+ }
144
+
145
+ .section-title {
146
+ font-size: 1.1rem;
147
+ font-weight: 600;
148
+ margin: 0 0 0.25rem;
149
+ display: flex;
150
+ align-items: center;
151
+ gap: 0.5rem;
152
+ }
153
+
154
+ .title-icon {
155
+ font-size: 1.25rem;
156
+ }
157
+
158
+ .section-description {
159
+ color: var(--text-secondary, #666);
160
+ font-size: 0.875rem;
161
+ margin: 0;
162
+ }
163
+
164
+ .anomaly-controls {
165
+ display: flex;
166
+ gap: 1rem;
167
+ align-items: center;
168
+ margin-bottom: 1rem;
169
+ flex-wrap: wrap;
170
+ }
171
+
172
+ .filter-group {
173
+ display: flex;
174
+ align-items: center;
175
+ gap: 0.5rem;
176
+ }
177
+
178
+ .filter-group label {
179
+ font-size: 0.875rem;
180
+ color: var(--text-secondary, #666);
181
+ }
182
+
183
+ .filter-select {
184
+ padding: 0.375rem 0.75rem;
185
+ border: 1px solid var(--border-color, #ddd);
186
+ border-radius: 4px;
187
+ background: var(--bg-secondary, #fff);
188
+ font-size: 0.875rem;
189
+ cursor: pointer;
190
+ }
191
+
192
+ .refresh-btn {
193
+ padding: 0.375rem 0.5rem;
194
+ border: 1px solid var(--border-color, #ddd);
195
+ border-radius: 4px;
196
+ background: var(--bg-secondary, #fff);
197
+ cursor: pointer;
198
+ font-size: 1rem;
199
+ line-height: 1;
200
+ transition: background-color 0.2s;
201
+ }
202
+
203
+ .refresh-btn:hover {
204
+ background: var(--bg-hover, #f5f5f5);
205
+ }
206
+
207
+ .refresh-btn:disabled {
208
+ opacity: 0.5;
209
+ cursor: not-allowed;
210
+ }
211
+
212
+ .anomaly-list-container {
213
+ min-height: 150px;
214
+ }
215
+
216
+ .loading-state,
217
+ .empty-state {
218
+ display: flex;
219
+ flex-direction: column;
220
+ align-items: center;
221
+ justify-content: center;
222
+ padding: 2rem;
223
+ color: var(--text-secondary, #666);
224
+ }
225
+
226
+ .loading-spinner {
227
+ width: 24px;
228
+ height: 24px;
229
+ border: 2px solid var(--border-color, #ddd);
230
+ border-top-color: var(--primary-color, #3b82f6);
231
+ border-radius: 50%;
232
+ animation: spin 0.8s linear infinite;
233
+ margin-bottom: 0.5rem;
234
+ }
235
+
236
+ @keyframes spin {
237
+ to {
238
+ transform: rotate(360deg);
239
+ }
240
+ }
241
+
242
+ .empty-icon {
243
+ font-size: 2rem;
244
+ margin-bottom: 0.5rem;
245
+ }
246
+
247
+ .empty-text {
248
+ font-weight: 500;
249
+ margin-bottom: 0.25rem;
250
+ }
251
+
252
+ .empty-subtext {
253
+ font-size: 0.8rem;
254
+ opacity: 0.8;
255
+ }
256
+
257
+ .anomaly-cards {
258
+ display: flex;
259
+ flex-direction: column;
260
+ gap: 0.75rem;
261
+ }
262
+
263
+ .anomaly-card {
264
+ display: grid;
265
+ grid-template-columns: auto 1fr auto auto;
266
+ gap: 1rem;
267
+ align-items: center;
268
+ padding: 1rem;
269
+ border: 1px solid var(--border-color, #ddd);
270
+ border-radius: 8px;
271
+ background: var(--bg-secondary, #fff);
272
+ transition: box-shadow 0.2s;
273
+ }
274
+
275
+ .anomaly-card:hover {
276
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
277
+ }
278
+
279
+ .anomaly-card.resolved {
280
+ opacity: 0.7;
281
+ border-left: 3px solid #10b981;
282
+ }
283
+
284
+ .anomaly-card.unresolved {
285
+ border-left: 3px solid #ef4444;
286
+ }
287
+
288
+ .severity-badge {
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: center;
292
+ width: 40px;
293
+ height: 40px;
294
+ border-radius: 50%;
295
+ font-weight: 600;
296
+ font-size: 0.875rem;
297
+ }
298
+
299
+ .severity-badge.critical {
300
+ background: #fee2e2;
301
+ color: #dc2626;
302
+ }
303
+
304
+ .severity-badge.high {
305
+ background: #ffedd5;
306
+ color: #ea580c;
307
+ }
308
+
309
+ .severity-badge.medium {
310
+ background: #fef3c7;
311
+ color: #d97706;
312
+ }
313
+
314
+ .severity-badge.low {
315
+ background: #dbeafe;
316
+ color: #2563eb;
317
+ }
318
+
319
+ .anomaly-info {
320
+ display: flex;
321
+ flex-direction: column;
322
+ gap: 0.25rem;
323
+ min-width: 0;
324
+ }
325
+
326
+ .anomaly-metric {
327
+ font-weight: 600;
328
+ font-size: 0.95rem;
329
+ white-space: nowrap;
330
+ overflow: hidden;
331
+ text-overflow: ellipsis;
332
+ }
333
+
334
+ .anomaly-details {
335
+ font-size: 0.8rem;
336
+ color: var(--text-secondary, #666);
337
+ }
338
+
339
+ .anomaly-project {
340
+ font-weight: 500;
341
+ }
342
+
343
+ .anomaly-values {
344
+ text-align: right;
345
+ font-size: 0.875rem;
346
+ }
347
+
348
+ .anomaly-current {
349
+ font-weight: 600;
350
+ }
351
+
352
+ .anomaly-avg {
353
+ font-size: 0.75rem;
354
+ color: var(--text-secondary, #666);
355
+ }
356
+
357
+ .anomaly-time {
358
+ font-size: 0.8rem;
359
+ color: var(--text-secondary, #666);
360
+ text-align: right;
361
+ white-space: nowrap;
362
+ }
363
+
364
+ .anomaly-status {
365
+ font-size: 0.7rem;
366
+ padding: 0.125rem 0.375rem;
367
+ border-radius: 3px;
368
+ margin-top: 0.25rem;
369
+ }
370
+
371
+ .anomaly-status.resolved {
372
+ background: #dcfce7;
373
+ color: #166534;
374
+ }
375
+
376
+ .anomaly-status.unresolved {
377
+ background: #fee2e2;
378
+ color: #991b1b;
379
+ }
380
+
381
+ @media (max-width: 768px) {
382
+ .anomaly-card {
383
+ grid-template-columns: auto 1fr;
384
+ grid-template-rows: auto auto;
385
+ }
386
+
387
+ .anomaly-values,
388
+ .anomaly-time {
389
+ grid-column: 2;
390
+ text-align: left;
391
+ }
392
+ }
393
+ </style>
394
+
395
+ <script>
396
+ // Type definitions
397
+ interface AnomalyData {
398
+ id: string;
399
+ detectedAt: string;
400
+ metric: string;
401
+ project: string;
402
+ currentValue: number;
403
+ rollingAvg: number;
404
+ deviationFactor: number;
405
+ alertSent: boolean;
406
+ alertChannel: string | null;
407
+ resolved: boolean;
408
+ resolvedAt: string | null;
409
+ resolvedBy: string | null;
410
+ }
411
+
412
+ // Global update function
413
+ declare global {
414
+ interface Window {
415
+ updateAnomalyAlerts?: (anomalies: AnomalyData[]) => void;
416
+ refreshAnomalyAlerts?: () => Promise<void>;
417
+ }
418
+ }
419
+
420
+ // Format metric names for display
421
+ function formatMetric(metric: string): string {
422
+ const map: Record<string, string> = {
423
+ workers_requests: 'Workers Requests',
424
+ d1_rows_read: 'D1 Reads',
425
+ d1_rows_written: 'D1 Writes',
426
+ kv_reads: 'KV Reads',
427
+ kv_writes: 'KV Writes',
428
+ r2_class_a: 'R2 Class A',
429
+ r2_class_b: 'R2 Class B',
430
+ total_cost_usd: 'Total Cost',
431
+ ai_requests: 'AI Requests',
432
+ ai_tokens: 'AI Tokens',
433
+ };
434
+ return map[metric] ?? metric.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
435
+ }
436
+
437
+ // Format large numbers
438
+ function formatNumber(num: number): string {
439
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
440
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
441
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
442
+ return num.toLocaleString();
443
+ }
444
+
445
+ // Get severity class based on deviation factor
446
+ function getSeverity(deviationFactor: number): string {
447
+ if (deviationFactor >= 4) return 'critical';
448
+ if (deviationFactor >= 3) return 'high';
449
+ if (deviationFactor >= 2) return 'medium';
450
+ return 'low';
451
+ }
452
+
453
+ // Format relative time
454
+ function formatRelativeTime(dateStr: string): string {
455
+ const date = new Date(dateStr);
456
+ const now = new Date();
457
+ const diffMs = now.getTime() - date.getTime();
458
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
459
+ const diffDays = Math.floor(diffHours / 24);
460
+
461
+ if (diffDays > 0) return `${diffDays}d ago`;
462
+ if (diffHours > 0) return `${diffHours}h ago`;
463
+ return 'Just now';
464
+ }
465
+
466
+ // Create anomaly card element using safe DOM methods
467
+ function createAnomalyCard(anomaly: AnomalyData): HTMLElement {
468
+ const card = document.createElement('div');
469
+ card.className = `anomaly-card ${anomaly.resolved ? 'resolved' : 'unresolved'}`;
470
+
471
+ // Severity badge
472
+ const severity = getSeverity(anomaly.deviationFactor);
473
+ const badge = document.createElement('div');
474
+ badge.className = `severity-badge ${severity}`;
475
+ badge.textContent = `${anomaly.deviationFactor}x`;
476
+ badge.title = `${anomaly.deviationFactor}x standard deviation`;
477
+ card.appendChild(badge);
478
+
479
+ // Info section
480
+ const info = document.createElement('div');
481
+ info.className = 'anomaly-info';
482
+
483
+ const metricEl = document.createElement('div');
484
+ metricEl.className = 'anomaly-metric';
485
+ metricEl.textContent = formatMetric(anomaly.metric);
486
+ info.appendChild(metricEl);
487
+
488
+ const details = document.createElement('div');
489
+ details.className = 'anomaly-details';
490
+ const projectSpan = document.createElement('span');
491
+ projectSpan.className = 'anomaly-project';
492
+ projectSpan.textContent = anomaly.project === 'all' ? 'All Projects' : anomaly.project;
493
+ details.appendChild(projectSpan);
494
+ info.appendChild(details);
495
+
496
+ card.appendChild(info);
497
+
498
+ // Values section
499
+ const values = document.createElement('div');
500
+ values.className = 'anomaly-values';
501
+
502
+ const current = document.createElement('div');
503
+ current.className = 'anomaly-current';
504
+ current.textContent = formatNumber(anomaly.currentValue);
505
+ values.appendChild(current);
506
+
507
+ const avg = document.createElement('div');
508
+ avg.className = 'anomaly-avg';
509
+ avg.textContent = `avg: ${formatNumber(anomaly.rollingAvg)}`;
510
+ values.appendChild(avg);
511
+
512
+ card.appendChild(values);
513
+
514
+ // Time section
515
+ const time = document.createElement('div');
516
+ time.className = 'anomaly-time';
517
+
518
+ const timeText = document.createElement('div');
519
+ timeText.textContent = formatRelativeTime(anomaly.detectedAt);
520
+ time.appendChild(timeText);
521
+
522
+ const status = document.createElement('div');
523
+ status.className = `anomaly-status ${anomaly.resolved ? 'resolved' : 'unresolved'}`;
524
+ status.textContent = anomaly.resolved ? 'Resolved' : 'Active';
525
+ time.appendChild(status);
526
+
527
+ card.appendChild(time);
528
+
529
+ return card;
530
+ }
531
+
532
+ // Update anomaly alerts display
533
+ function updateAnomalyAlerts(anomalies: AnomalyData[]): void {
534
+ const loading = document.getElementById('anomaly-loading');
535
+ const empty = document.getElementById('anomaly-empty');
536
+ const cards = document.getElementById('anomaly-cards');
537
+
538
+ if (!loading || !empty || !cards) return;
539
+
540
+ loading.style.display = 'none';
541
+
542
+ if (!anomalies || anomalies.length === 0) {
543
+ empty.style.display = 'flex';
544
+ cards.style.display = 'none';
545
+ return;
546
+ }
547
+
548
+ empty.style.display = 'none';
549
+ cards.style.display = 'flex';
550
+
551
+ // Clear existing cards
552
+ cards.replaceChildren();
553
+
554
+ // Create new cards using safe DOM methods
555
+ for (const anomaly of anomalies) {
556
+ const card = createAnomalyCard(anomaly);
557
+ cards.appendChild(card);
558
+ }
559
+ }
560
+
561
+ // Fetch anomalies from API
562
+ async function fetchAnomalies(): Promise<void> {
563
+ const loading = document.getElementById('anomaly-loading');
564
+ const daysSelect = document.getElementById('anomaly-days') as HTMLSelectElement | null;
565
+ const resolvedSelect = document.getElementById('anomaly-resolved') as HTMLSelectElement | null;
566
+ const refreshBtn = document.getElementById('anomaly-refresh') as HTMLButtonElement | null;
567
+
568
+ if (loading) loading.style.display = 'flex';
569
+ if (refreshBtn) refreshBtn.disabled = true;
570
+
571
+ const days = daysSelect?.value ?? '7';
572
+ const resolved = resolvedSelect?.value ?? 'all';
573
+
574
+ try {
575
+ const response = await fetch(
576
+ `/api/usage/anomalies?days=${days}&resolved=${resolved}&limit=50`,
577
+ { credentials: 'include' }
578
+ );
579
+
580
+ if (!response.ok) {
581
+ throw new Error(`HTTP ${response.status}`);
582
+ }
583
+
584
+ const data = await response.json();
585
+
586
+ if (data.success && data.anomalies) {
587
+ updateAnomalyAlerts(data.anomalies);
588
+ } else {
589
+ updateAnomalyAlerts([]);
590
+ }
591
+ } catch (error) {
592
+ console.error('[AnomalyAlerts] Fetch error:', error);
593
+ updateAnomalyAlerts([]);
594
+ } finally {
595
+ if (refreshBtn) refreshBtn.disabled = false;
596
+ }
597
+ }
598
+
599
+ // Initialize component
600
+ function init(): void {
601
+ // Export global functions
602
+ window.updateAnomalyAlerts = updateAnomalyAlerts;
603
+ window.refreshAnomalyAlerts = fetchAnomalies;
604
+
605
+ // Add event listeners for filters
606
+ const daysSelect = document.getElementById('anomaly-days');
607
+ const resolvedSelect = document.getElementById('anomaly-resolved');
608
+ const refreshBtn = document.getElementById('anomaly-refresh');
609
+
610
+ daysSelect?.addEventListener('change', fetchAnomalies);
611
+ resolvedSelect?.addEventListener('change', fetchAnomalies);
612
+ refreshBtn?.addEventListener('click', fetchAnomalies);
613
+
614
+ // Fetch initial data when Alerts tab is shown
615
+ const alertsTab = document.querySelector('[data-tab="alerts"]');
616
+ if (alertsTab) {
617
+ alertsTab.addEventListener('click', () => {
618
+ // Only fetch if we haven't loaded yet (loading state is visible)
619
+ const loading = document.getElementById('anomaly-loading');
620
+ if (loading && loading.style.display !== 'none') {
621
+ fetchAnomalies();
622
+ }
623
+ });
624
+ }
625
+ }
626
+
627
+ // Run on DOM ready
628
+ if (document.readyState === 'loading') {
629
+ document.addEventListener('DOMContentLoaded', init);
630
+ } else {
631
+ init();
632
+ }
633
+ </script>