@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,542 @@
1
+ ---
2
+ /**
3
+ * CircuitBreakerEventLog Component
4
+ *
5
+ * Displays recent circuit breaker trip/reset events.
6
+ * Data is populated via JavaScript after API fetch.
7
+ *
8
+ * Features:
9
+ * - Recent trip/reset events from D1
10
+ * - Filter by event type
11
+ * - Click to filter by feature
12
+ */
13
+ ---
14
+
15
+ <div class="circuit-breaker-log-panel" role="region" aria-label="Circuit breaker event log">
16
+ <!-- Header -->
17
+ <div class="panel-header">
18
+ <div class="header-left">
19
+ <h3 class="panel-title">Circuit Breaker Events</h3>
20
+ <span class="event-count" id="cb-event-count">0 events</span>
21
+ </div>
22
+ <div class="header-actions">
23
+ <select id="cb-event-type-filter" class="filter-select" title="Filter by event type">
24
+ <option value="">All events</option>
25
+ <option value="trip">Trips only</option>
26
+ <option value="reset">Resets only</option>
27
+ </select>
28
+ <button type="button" class="refresh-btn" id="refresh-cb-events-btn" title="Refresh">
29
+ <svg
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ width="14"
32
+ height="14"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="2"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ >
40
+ <polyline points="23 4 23 10 17 10"></polyline>
41
+ <polyline points="1 20 1 14 7 14"></polyline>
42
+ <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
43
+ </svg>
44
+ </button>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- Empty state -->
49
+ <div id="cb-events-empty-state" class="empty-state">
50
+ <div class="empty-icon">⚡</div>
51
+ <p class="empty-title">No circuit breaker events</p>
52
+ <p class="empty-desc">
53
+ Events will appear here when circuit breakers trip or reset due to budget violations.
54
+ </p>
55
+ </div>
56
+
57
+ <!-- Events list -->
58
+ <div id="cb-events-container" class="events-container hidden">
59
+ <div id="cb-events-list" class="events-list">
60
+ <!-- Events populated via JavaScript -->
61
+ </div>
62
+ </div>
63
+
64
+ <!-- Last updated -->
65
+ <div class="panel-footer">
66
+ <span id="cb-events-last-updated" class="last-updated">Last updated: --</span>
67
+ </div>
68
+ </div>
69
+
70
+ <style>
71
+ .circuit-breaker-log-panel {
72
+ background: var(--usage-bg-secondary);
73
+ border: 1px solid var(--usage-border-default);
74
+ border-radius: var(--usage-radius-xl);
75
+ padding: 1.25rem;
76
+ display: flex;
77
+ flex-direction: column;
78
+ gap: 1rem;
79
+ }
80
+
81
+ .panel-header {
82
+ display: flex;
83
+ justify-content: space-between;
84
+ align-items: center;
85
+ gap: 1rem;
86
+ }
87
+
88
+ .header-left {
89
+ display: flex;
90
+ align-items: baseline;
91
+ gap: 0.75rem;
92
+ }
93
+
94
+ .panel-title {
95
+ font-size: 1rem;
96
+ font-weight: 600;
97
+ color: var(--usage-text-primary);
98
+ margin: 0;
99
+ }
100
+
101
+ .event-count {
102
+ font-size: 0.75rem;
103
+ color: var(--usage-text-muted);
104
+ }
105
+
106
+ .header-actions {
107
+ display: flex;
108
+ gap: 0.5rem;
109
+ align-items: center;
110
+ }
111
+
112
+ .filter-select {
113
+ font-size: 0.75rem;
114
+ padding: 0.25rem 0.5rem;
115
+ border: 1px solid var(--usage-border-default);
116
+ border-radius: var(--usage-radius-sm);
117
+ background: var(--usage-bg-primary);
118
+ color: var(--usage-text-primary);
119
+ cursor: pointer;
120
+ }
121
+
122
+ .refresh-btn {
123
+ display: flex;
124
+ align-items: center;
125
+ justify-content: center;
126
+ padding: 0.375rem;
127
+ border: 1px solid var(--usage-border-default);
128
+ border-radius: var(--usage-radius-md);
129
+ background: var(--usage-bg-primary);
130
+ color: var(--usage-text-secondary);
131
+ cursor: pointer;
132
+ transition: all 0.15s ease;
133
+ }
134
+
135
+ .refresh-btn:hover {
136
+ border-color: var(--usage-primary);
137
+ color: var(--usage-primary);
138
+ background: var(--usage-primary-muted);
139
+ }
140
+
141
+ .refresh-btn.loading svg {
142
+ animation: spin 1s linear infinite;
143
+ }
144
+
145
+ @keyframes spin {
146
+ from {
147
+ transform: rotate(0deg);
148
+ }
149
+ to {
150
+ transform: rotate(360deg);
151
+ }
152
+ }
153
+
154
+ .empty-state {
155
+ text-align: center;
156
+ padding: 2rem 1rem;
157
+ }
158
+
159
+ .empty-icon {
160
+ font-size: 2rem;
161
+ margin-bottom: 0.5rem;
162
+ }
163
+
164
+ .empty-title {
165
+ font-size: 0.875rem;
166
+ font-weight: 500;
167
+ color: var(--usage-text-primary);
168
+ margin: 0 0 0.25rem;
169
+ }
170
+
171
+ .empty-desc {
172
+ font-size: 0.75rem;
173
+ color: var(--usage-text-muted);
174
+ margin: 0;
175
+ max-width: 300px;
176
+ margin-inline: auto;
177
+ }
178
+
179
+ .events-container {
180
+ max-height: 400px;
181
+ overflow-y: auto;
182
+ }
183
+
184
+ .events-container.hidden {
185
+ display: none;
186
+ }
187
+
188
+ .events-list {
189
+ display: flex;
190
+ flex-direction: column;
191
+ gap: 0.5rem;
192
+ }
193
+
194
+ .panel-footer {
195
+ padding-top: 0.5rem;
196
+ border-top: 1px solid var(--usage-border-subtle);
197
+ }
198
+
199
+ .last-updated {
200
+ font-size: 0.7rem;
201
+ color: var(--usage-text-muted);
202
+ }
203
+
204
+ /* Event item styles */
205
+ :global(.cb-event-item) {
206
+ display: flex;
207
+ align-items: flex-start;
208
+ gap: 0.75rem;
209
+ padding: 0.75rem;
210
+ background: var(--usage-bg-primary);
211
+ border: 1px solid var(--usage-border-subtle);
212
+ border-radius: var(--usage-radius-md);
213
+ transition: border-color 0.15s ease;
214
+ }
215
+
216
+ :global(.cb-event-item:hover) {
217
+ border-color: var(--usage-border-default);
218
+ }
219
+
220
+ :global(.cb-event-icon) {
221
+ flex-shrink: 0;
222
+ width: 28px;
223
+ height: 28px;
224
+ display: flex;
225
+ align-items: center;
226
+ justify-content: center;
227
+ border-radius: 50%;
228
+ font-size: 0.875rem;
229
+ }
230
+
231
+ :global(.cb-event-icon.trip) {
232
+ background: rgba(239, 68, 68, 0.1);
233
+ color: #ef4444;
234
+ }
235
+
236
+ :global(.cb-event-icon.reset) {
237
+ background: rgba(34, 197, 94, 0.1);
238
+ color: #22c55e;
239
+ }
240
+
241
+ :global(.cb-event-icon.manual_disable) {
242
+ background: rgba(251, 191, 36, 0.1);
243
+ color: #fbbf24;
244
+ }
245
+
246
+ :global(.cb-event-icon.manual_enable) {
247
+ background: rgba(59, 130, 246, 0.1);
248
+ color: #3b82f6;
249
+ }
250
+
251
+ :global(.cb-event-content) {
252
+ flex: 1;
253
+ min-width: 0;
254
+ }
255
+
256
+ :global(.cb-event-header) {
257
+ display: flex;
258
+ align-items: baseline;
259
+ gap: 0.5rem;
260
+ margin-bottom: 0.25rem;
261
+ }
262
+
263
+ :global(.cb-event-feature) {
264
+ font-size: 0.8125rem;
265
+ font-weight: 500;
266
+ color: var(--usage-text-primary);
267
+ word-break: break-word;
268
+ }
269
+
270
+ :global(.cb-event-type) {
271
+ font-size: 0.6875rem;
272
+ padding: 0.125rem 0.375rem;
273
+ border-radius: var(--usage-radius-sm);
274
+ text-transform: uppercase;
275
+ font-weight: 600;
276
+ letter-spacing: 0.02em;
277
+ }
278
+
279
+ :global(.cb-event-type.trip) {
280
+ background: rgba(239, 68, 68, 0.1);
281
+ color: #ef4444;
282
+ }
283
+
284
+ :global(.cb-event-type.reset) {
285
+ background: rgba(34, 197, 94, 0.1);
286
+ color: #22c55e;
287
+ }
288
+
289
+ :global(.cb-event-type.manual_disable) {
290
+ background: rgba(251, 191, 36, 0.1);
291
+ color: #fbbf24;
292
+ }
293
+
294
+ :global(.cb-event-type.manual_enable) {
295
+ background: rgba(59, 130, 246, 0.1);
296
+ color: #3b82f6;
297
+ }
298
+
299
+ :global(.cb-event-reason) {
300
+ font-size: 0.75rem;
301
+ color: var(--usage-text-secondary);
302
+ margin-bottom: 0.25rem;
303
+ word-break: break-word;
304
+ }
305
+
306
+ :global(.cb-event-meta) {
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 0.75rem;
310
+ font-size: 0.6875rem;
311
+ color: var(--usage-text-muted);
312
+ }
313
+
314
+ :global(.cb-event-meta-item) {
315
+ display: flex;
316
+ align-items: center;
317
+ gap: 0.25rem;
318
+ }
319
+
320
+ :global(.cb-event-badge) {
321
+ padding: 0.0625rem 0.25rem;
322
+ border-radius: var(--usage-radius-xs);
323
+ font-size: 0.625rem;
324
+ text-transform: uppercase;
325
+ font-weight: 500;
326
+ }
327
+
328
+ :global(.cb-event-badge.auto-reset) {
329
+ background: rgba(99, 102, 241, 0.1);
330
+ color: #6366f1;
331
+ }
332
+
333
+ :global(.cb-event-badge.alert-sent) {
334
+ background: rgba(236, 72, 153, 0.1);
335
+ color: #ec4899;
336
+ }
337
+ </style>
338
+
339
+ <script>
340
+ interface CircuitBreakerEvent {
341
+ id: string;
342
+ featureKey: string;
343
+ eventType: 'trip' | 'reset' | 'manual_disable' | 'manual_enable';
344
+ reason: string | null;
345
+ violatedResource: string | null;
346
+ currentValue: number | null;
347
+ budgetLimit: number | null;
348
+ autoReset: boolean;
349
+ alertSent: boolean;
350
+ createdAt: string;
351
+ }
352
+
353
+ interface EventsResponse {
354
+ success: boolean;
355
+ events: CircuitBreakerEvent[];
356
+ count: number;
357
+ timestamp: string;
358
+ }
359
+
360
+ const eventsList = document.getElementById('cb-events-list');
361
+ const eventsContainer = document.getElementById('cb-events-container');
362
+ const emptyState = document.getElementById('cb-events-empty-state');
363
+ const eventCount = document.getElementById('cb-event-count');
364
+ const lastUpdated = document.getElementById('cb-events-last-updated');
365
+ const refreshBtn = document.getElementById('refresh-cb-events-btn');
366
+ const typeFilter = document.getElementById('cb-event-type-filter') as HTMLSelectElement | null;
367
+
368
+ function getEventIcon(eventType: string): string {
369
+ switch (eventType) {
370
+ case 'trip':
371
+ return '⚡';
372
+ case 'reset':
373
+ return '✓';
374
+ case 'manual_disable':
375
+ return '⏸';
376
+ case 'manual_enable':
377
+ return '▶';
378
+ default:
379
+ return '•';
380
+ }
381
+ }
382
+
383
+ function getEventTypeLabel(eventType: string): string {
384
+ switch (eventType) {
385
+ case 'trip':
386
+ return 'Trip';
387
+ case 'reset':
388
+ return 'Reset';
389
+ case 'manual_disable':
390
+ return 'Manual Disable';
391
+ case 'manual_enable':
392
+ return 'Manual Enable';
393
+ default:
394
+ return eventType;
395
+ }
396
+ }
397
+
398
+ function formatTimeAgo(dateString: string): string {
399
+ const date = new Date(dateString + 'Z'); // Assume UTC
400
+ const now = new Date();
401
+ const diffMs = now.getTime() - date.getTime();
402
+ const diffMins = Math.floor(diffMs / 60000);
403
+ const diffHours = Math.floor(diffMins / 60);
404
+ const diffDays = Math.floor(diffHours / 24);
405
+
406
+ if (diffMins < 1) return 'Just now';
407
+ if (diffMins < 60) return `${diffMins}m ago`;
408
+ if (diffHours < 24) return `${diffHours}h ago`;
409
+ if (diffDays < 7) return `${diffDays}d ago`;
410
+ return date.toLocaleDateString();
411
+ }
412
+
413
+ function createEventItem(event: CircuitBreakerEvent): HTMLElement {
414
+ const item = document.createElement('div');
415
+ item.className = 'cb-event-item';
416
+
417
+ // Icon
418
+ const icon = document.createElement('div');
419
+ icon.className = `cb-event-icon ${event.eventType}`;
420
+ icon.textContent = getEventIcon(event.eventType);
421
+ item.appendChild(icon);
422
+
423
+ // Content
424
+ const content = document.createElement('div');
425
+ content.className = 'cb-event-content';
426
+
427
+ // Header (feature + type badge)
428
+ const header = document.createElement('div');
429
+ header.className = 'cb-event-header';
430
+
431
+ const feature = document.createElement('span');
432
+ feature.className = 'cb-event-feature';
433
+ feature.textContent = event.featureKey;
434
+ header.appendChild(feature);
435
+
436
+ const typeBadge = document.createElement('span');
437
+ typeBadge.className = `cb-event-type ${event.eventType}`;
438
+ typeBadge.textContent = getEventTypeLabel(event.eventType);
439
+ header.appendChild(typeBadge);
440
+
441
+ content.appendChild(header);
442
+
443
+ // Reason (if present)
444
+ if (event.reason) {
445
+ const reason = document.createElement('div');
446
+ reason.className = 'cb-event-reason';
447
+ reason.textContent = event.reason;
448
+ content.appendChild(reason);
449
+ }
450
+
451
+ // Meta (time + badges)
452
+ const meta = document.createElement('div');
453
+ meta.className = 'cb-event-meta';
454
+
455
+ const timeItem = document.createElement('span');
456
+ timeItem.className = 'cb-event-meta-item';
457
+ timeItem.textContent = formatTimeAgo(event.createdAt);
458
+ meta.appendChild(timeItem);
459
+
460
+ if (event.autoReset) {
461
+ const autoResetBadge = document.createElement('span');
462
+ autoResetBadge.className = 'cb-event-badge auto-reset';
463
+ autoResetBadge.textContent = 'Auto-reset';
464
+ meta.appendChild(autoResetBadge);
465
+ }
466
+
467
+ if (event.alertSent) {
468
+ const alertBadge = document.createElement('span');
469
+ alertBadge.className = 'cb-event-badge alert-sent';
470
+ alertBadge.textContent = 'Alert sent';
471
+ meta.appendChild(alertBadge);
472
+ }
473
+
474
+ content.appendChild(meta);
475
+ item.appendChild(content);
476
+
477
+ return item;
478
+ }
479
+
480
+ function clearEventsList(): void {
481
+ if (!eventsList) return;
482
+ while (eventsList.firstChild) {
483
+ eventsList.removeChild(eventsList.firstChild);
484
+ }
485
+ }
486
+
487
+ async function fetchEvents(): Promise<void> {
488
+ if (!eventsList || !eventsContainer || !emptyState || !eventCount || !lastUpdated) return;
489
+
490
+ refreshBtn?.classList.add('loading');
491
+
492
+ try {
493
+ const eventType = typeFilter?.value || '';
494
+ const params = new URLSearchParams({ limit: '50' });
495
+ if (eventType) params.set('eventType', eventType);
496
+
497
+ const res = await fetch(`/api/usage/features/circuit-breaker-events?${params}`, {
498
+ credentials: 'include',
499
+ });
500
+
501
+ if (!res.ok) {
502
+ throw new Error(`Failed to fetch events: ${res.status}`);
503
+ }
504
+
505
+ const data: EventsResponse = await res.json();
506
+
507
+ if (!data.success || !data.events || data.events.length === 0) {
508
+ clearEventsList();
509
+ eventsContainer.classList.add('hidden');
510
+ emptyState.classList.remove('hidden');
511
+ eventCount.textContent = '0 events';
512
+ } else {
513
+ clearEventsList();
514
+ data.events.forEach((event) => {
515
+ eventsList.appendChild(createEventItem(event));
516
+ });
517
+ eventsContainer.classList.remove('hidden');
518
+ emptyState.classList.add('hidden');
519
+ eventCount.textContent = `${data.events.length} event${data.events.length !== 1 ? 's' : ''}`;
520
+ }
521
+
522
+ lastUpdated.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
523
+ } catch (error) {
524
+ console.error('Error fetching circuit breaker events:', error);
525
+ eventCount.textContent = 'Error';
526
+ } finally {
527
+ refreshBtn?.classList.remove('loading');
528
+ }
529
+ }
530
+
531
+ // Initial fetch
532
+ fetchEvents();
533
+
534
+ // Refresh button
535
+ refreshBtn?.addEventListener('click', () => fetchEvents());
536
+
537
+ // Filter change
538
+ typeFilter?.addEventListener('change', () => fetchEvents());
539
+
540
+ // Auto-refresh every 60 seconds
541
+ setInterval(fetchEvents, 60000);
542
+ </script>