@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,669 @@
1
+ ---
2
+ /**
3
+ * CircuitBreakerStatus Component
4
+ *
5
+ * Displays the status of KV circuit breakers for all tracked projects.
6
+ * Shows:
7
+ * - Global stop flag status
8
+ * - Per-project circuit breaker status (Scout, Brand Copilot, Australian History MCP, Platform)
9
+ * - Adaptive sampling mode for platform-usage worker
10
+ * - D1 write usage and limits
11
+ *
12
+ * Data is populated via JavaScript after API fetch.
13
+ */
14
+
15
+ // Projects with circuit breakers
16
+ const projects = [
17
+ { id: 'scout', name: 'Scout', kvKey: 'PROJECT:SCOUT:STATUS' },
18
+ { id: 'brand-copilot', name: 'Brand Copilot', kvKey: 'PROJECT:BRAND-COPILOT:STATUS' },
19
+ {
20
+ id: 'australian-history-mcp',
21
+ name: 'Australian History MCP',
22
+ kvKey: 'PROJECT:AUSTRALIAN-HISTORY-MCP:STATUS',
23
+ },
24
+ { id: 'platform', name: 'Platform', kvKey: 'PROJECT:PLATFORM:STATUS' },
25
+ ];
26
+ ---
27
+
28
+ <div class="circuit-breaker-panel" role="region" aria-label="Circuit breaker status">
29
+ <div class="panel-header">
30
+ <h3 class="panel-title">Circuit Breaker Status</h3>
31
+ <div class="global-status" id="global-status">
32
+ <span class="global-dot" data-status="active"></span>
33
+ <span class="global-label">System Active</span>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- D1 Write Usage -->
38
+ <div class="d1-usage-section">
39
+ <div class="d1-header">
40
+ <span class="d1-label">D1 Writes (24h)</span>
41
+ <span id="d1-sampling-mode" class="sampling-badge" data-mode="full">FULL</span>
42
+ </div>
43
+ <div class="d1-bar-container">
44
+ <div id="d1-usage-bar" class="d1-bar" style="width: 0%;"></div>
45
+ <div class="d1-markers">
46
+ <div class="d1-marker" style="left: 60%;" title="HALF sampling (60%)"></div>
47
+ <div class="d1-marker" style="left: 80%;" title="QUARTER sampling (80%)"></div>
48
+ <div class="d1-marker" style="left: 90%;" title="MINIMAL sampling (90%)"></div>
49
+ </div>
50
+ </div>
51
+ <div class="d1-meta">
52
+ <span id="d1-usage-current">0</span>
53
+ <span class="d1-separator">/</span>
54
+ <span id="d1-usage-limit">1,000,000</span>
55
+ <span class="d1-percentage" id="d1-usage-pct">(0%)</span>
56
+ </div>
57
+ </div>
58
+
59
+ <!-- Project Circuit Breakers -->
60
+ <div class="project-breakers">
61
+ {
62
+ projects.map((project) => (
63
+ <div class="breaker-row" data-project={project.id}>
64
+ <div class="breaker-identity">
65
+ <span class="breaker-dot" data-status="active" />
66
+ <span class="breaker-name">{project.name}</span>
67
+ </div>
68
+ <div class="breaker-status">
69
+ <span class="status-label">Active</span>
70
+ </div>
71
+ <div class="breaker-actions">
72
+ <button
73
+ type="button"
74
+ class="reset-btn"
75
+ data-project={project.id}
76
+ title="Reset circuit breaker"
77
+ style="display: none;"
78
+ >
79
+ Reset
80
+ </button>
81
+ </div>
82
+ <div class="breaker-kv-key">
83
+ <code class="kv-key">{project.kvKey}</code>
84
+ </div>
85
+ </div>
86
+ ))
87
+ }
88
+ </div>
89
+
90
+ <!-- Adaptive Sampling Info -->
91
+ <div class="sampling-info">
92
+ <div class="sampling-header">
93
+ <span class="sampling-title">Adaptive Sampling</span>
94
+ </div>
95
+ <div class="sampling-modes">
96
+ <div class="mode-item" data-mode="full">
97
+ <span class="mode-dot"></span>
98
+ <span class="mode-label">FULL</span>
99
+ <span class="mode-desc">&lt;60%: Hourly</span>
100
+ </div>
101
+ <div class="mode-item" data-mode="half">
102
+ <span class="mode-dot"></span>
103
+ <span class="mode-label">HALF</span>
104
+ <span class="mode-desc">60-80%: Every 2h</span>
105
+ </div>
106
+ <div class="mode-item" data-mode="quarter">
107
+ <span class="mode-dot"></span>
108
+ <span class="mode-label">QUARTER</span>
109
+ <span class="mode-desc">80-90%: Every 4h</span>
110
+ </div>
111
+ <div class="mode-item" data-mode="minimal">
112
+ <span class="mode-dot"></span>
113
+ <span class="mode-label">MINIMAL</span>
114
+ <span class="mode-desc">&gt;90%: Daily</span>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- Last Updated -->
120
+ <div class="panel-footer">
121
+ <span id="cb-last-updated" class="last-updated">Last checked: --</span>
122
+ </div>
123
+ </div>
124
+
125
+ <style>
126
+ .circuit-breaker-panel {
127
+ background: var(--usage-bg-secondary);
128
+ border: 1px solid var(--usage-border-default);
129
+ border-radius: var(--usage-radius-xl);
130
+ padding: 1.25rem;
131
+ display: flex;
132
+ flex-direction: column;
133
+ gap: 1rem;
134
+ }
135
+
136
+ .panel-header {
137
+ display: flex;
138
+ justify-content: space-between;
139
+ align-items: center;
140
+ }
141
+
142
+ .panel-title {
143
+ font-size: 0.875rem;
144
+ font-weight: 600;
145
+ color: var(--usage-text-primary);
146
+ margin: 0;
147
+ }
148
+
149
+ .global-status {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 0.375rem;
153
+ padding: 0.25rem 0.5rem;
154
+ background: var(--usage-bg-tertiary);
155
+ border-radius: var(--usage-radius-md);
156
+ }
157
+
158
+ .global-dot {
159
+ width: 8px;
160
+ height: 8px;
161
+ border-radius: 50%;
162
+ transition: all var(--usage-transition-normal);
163
+ }
164
+
165
+ .global-dot[data-status='active'] {
166
+ background: var(--usage-status-ok);
167
+ box-shadow: 0 0 6px var(--usage-status-ok);
168
+ }
169
+
170
+ .global-dot[data-status='stopped'] {
171
+ background: var(--usage-status-critical);
172
+ box-shadow: 0 0 6px var(--usage-status-critical);
173
+ animation: pulse 1s ease-in-out infinite;
174
+ }
175
+
176
+ @keyframes pulse {
177
+ 0%,
178
+ 100% {
179
+ opacity: 1;
180
+ }
181
+ 50% {
182
+ opacity: 0.5;
183
+ }
184
+ }
185
+
186
+ .global-label {
187
+ font-size: 0.6875rem;
188
+ font-weight: 500;
189
+ color: var(--usage-text-secondary);
190
+ }
191
+
192
+ /* D1 Usage Section */
193
+ .d1-usage-section {
194
+ padding: 0.75rem;
195
+ background: var(--usage-bg-tertiary);
196
+ border-radius: var(--usage-radius-lg);
197
+ }
198
+
199
+ .d1-header {
200
+ display: flex;
201
+ justify-content: space-between;
202
+ align-items: center;
203
+ margin-bottom: 0.5rem;
204
+ }
205
+
206
+ .d1-label {
207
+ font-size: 0.6875rem;
208
+ font-weight: 600;
209
+ text-transform: uppercase;
210
+ letter-spacing: 0.03em;
211
+ color: var(--usage-text-muted);
212
+ }
213
+
214
+ .sampling-badge {
215
+ font-size: 0.625rem;
216
+ font-weight: 700;
217
+ padding: 0.125rem 0.375rem;
218
+ border-radius: var(--usage-radius-sm);
219
+ background: var(--usage-status-ok-bg);
220
+ color: var(--usage-status-ok);
221
+ }
222
+
223
+ .sampling-badge[data-mode='half'] {
224
+ background: var(--usage-status-warning-bg);
225
+ color: var(--usage-status-warning);
226
+ }
227
+
228
+ .sampling-badge[data-mode='quarter'] {
229
+ background: var(--usage-accent-purple);
230
+ color: white;
231
+ }
232
+
233
+ .sampling-badge[data-mode='minimal'] {
234
+ background: var(--usage-status-critical-bg);
235
+ color: var(--usage-status-critical);
236
+ }
237
+
238
+ .d1-bar-container {
239
+ position: relative;
240
+ height: 8px;
241
+ background: var(--usage-bg-hover);
242
+ border-radius: var(--usage-radius-sm);
243
+ overflow: visible;
244
+ }
245
+
246
+ .d1-bar {
247
+ height: 100%;
248
+ background: linear-gradient(
249
+ 90deg,
250
+ var(--usage-status-ok),
251
+ var(--usage-status-warning),
252
+ var(--usage-status-critical)
253
+ );
254
+ background-size: 167% 100%;
255
+ border-radius: var(--usage-radius-sm);
256
+ transition: width var(--usage-transition-slow);
257
+ }
258
+
259
+ .d1-markers {
260
+ position: absolute;
261
+ top: 0;
262
+ left: 0;
263
+ right: 0;
264
+ bottom: 0;
265
+ pointer-events: none;
266
+ }
267
+
268
+ .d1-marker {
269
+ position: absolute;
270
+ top: -2px;
271
+ bottom: -2px;
272
+ width: 1px;
273
+ background: var(--usage-text-muted);
274
+ opacity: 0.5;
275
+ }
276
+
277
+ .d1-meta {
278
+ display: flex;
279
+ align-items: center;
280
+ gap: 0.25rem;
281
+ margin-top: 0.375rem;
282
+ font-family: var(--usage-font-mono);
283
+ font-size: 0.6875rem;
284
+ color: var(--usage-text-secondary);
285
+ }
286
+
287
+ .d1-separator {
288
+ color: var(--usage-text-muted);
289
+ }
290
+
291
+ .d1-percentage {
292
+ color: var(--usage-text-muted);
293
+ margin-left: 0.25rem;
294
+ }
295
+
296
+ /* Project Breakers */
297
+ .project-breakers {
298
+ display: flex;
299
+ flex-direction: column;
300
+ gap: 0.5rem;
301
+ }
302
+
303
+ .breaker-row {
304
+ display: grid;
305
+ grid-template-columns: 1fr auto auto auto;
306
+ gap: 0.75rem;
307
+ align-items: center;
308
+ padding: 0.5rem 0.75rem;
309
+ background: var(--usage-bg-tertiary);
310
+ border-radius: var(--usage-radius-md);
311
+ transition: background var(--usage-transition-fast);
312
+ }
313
+
314
+ .breaker-row:hover {
315
+ background: var(--usage-bg-hover);
316
+ }
317
+
318
+ .breaker-identity {
319
+ display: flex;
320
+ align-items: center;
321
+ gap: 0.5rem;
322
+ }
323
+
324
+ .breaker-dot {
325
+ width: 8px;
326
+ height: 8px;
327
+ border-radius: 50%;
328
+ flex-shrink: 0;
329
+ }
330
+
331
+ .breaker-dot[data-status='active'] {
332
+ background: var(--usage-status-ok);
333
+ }
334
+
335
+ .breaker-dot[data-status='tripped'] {
336
+ background: var(--usage-status-critical);
337
+ animation: pulse 1s ease-in-out infinite;
338
+ }
339
+
340
+ .breaker-dot[data-status='degraded'] {
341
+ background: var(--usage-status-warning);
342
+ }
343
+
344
+ .breaker-name {
345
+ font-size: 0.75rem;
346
+ font-weight: 500;
347
+ color: var(--usage-text-primary);
348
+ }
349
+
350
+ .breaker-status {
351
+ text-align: right;
352
+ }
353
+
354
+ .status-label {
355
+ font-size: 0.6875rem;
356
+ font-weight: 600;
357
+ color: var(--usage-text-secondary);
358
+ }
359
+
360
+ .breaker-actions {
361
+ display: flex;
362
+ align-items: center;
363
+ }
364
+
365
+ .reset-btn {
366
+ padding: 0.25rem 0.5rem;
367
+ font-size: 0.625rem;
368
+ font-weight: 600;
369
+ text-transform: uppercase;
370
+ letter-spacing: 0.03em;
371
+ color: var(--usage-status-critical);
372
+ background: var(--usage-status-critical-bg);
373
+ border: 1px solid var(--usage-status-critical);
374
+ border-radius: var(--usage-radius-sm);
375
+ cursor: pointer;
376
+ transition: all var(--usage-transition-fast);
377
+ }
378
+
379
+ .reset-btn:hover {
380
+ background: var(--usage-status-critical);
381
+ color: white;
382
+ }
383
+
384
+ .reset-btn:disabled {
385
+ opacity: 0.5;
386
+ cursor: not-allowed;
387
+ }
388
+
389
+ .reset-btn.loading {
390
+ pointer-events: none;
391
+ opacity: 0.7;
392
+ }
393
+
394
+ .breaker-kv-key {
395
+ display: none; /* Hidden by default, shown on hover or expanded view */
396
+ }
397
+
398
+ .kv-key {
399
+ font-family: var(--usage-font-mono);
400
+ font-size: 0.625rem;
401
+ color: var(--usage-text-muted);
402
+ background: var(--usage-bg-primary);
403
+ padding: 0.125rem 0.25rem;
404
+ border-radius: var(--usage-radius-sm);
405
+ }
406
+
407
+ /* Sampling Info */
408
+ .sampling-info {
409
+ border-top: 1px solid var(--usage-border-subtle);
410
+ padding-top: 0.75rem;
411
+ }
412
+
413
+ .sampling-header {
414
+ margin-bottom: 0.5rem;
415
+ }
416
+
417
+ .sampling-title {
418
+ font-size: 0.625rem;
419
+ font-weight: 600;
420
+ text-transform: uppercase;
421
+ letter-spacing: 0.05em;
422
+ color: var(--usage-text-muted);
423
+ }
424
+
425
+ .sampling-modes {
426
+ display: grid;
427
+ grid-template-columns: repeat(4, 1fr);
428
+ gap: 0.5rem;
429
+ }
430
+
431
+ .mode-item {
432
+ display: flex;
433
+ flex-direction: column;
434
+ align-items: center;
435
+ gap: 0.25rem;
436
+ padding: 0.375rem;
437
+ background: var(--usage-bg-tertiary);
438
+ border-radius: var(--usage-radius-md);
439
+ opacity: 0.5;
440
+ transition: opacity var(--usage-transition-fast);
441
+ }
442
+
443
+ .mode-item.active {
444
+ opacity: 1;
445
+ background: var(--usage-bg-hover);
446
+ }
447
+
448
+ .mode-dot {
449
+ width: 6px;
450
+ height: 6px;
451
+ border-radius: 50%;
452
+ background: var(--usage-text-muted);
453
+ }
454
+
455
+ .mode-item[data-mode='full'] .mode-dot {
456
+ background: var(--usage-status-ok);
457
+ }
458
+ .mode-item[data-mode='half'] .mode-dot {
459
+ background: var(--usage-status-warning);
460
+ }
461
+ .mode-item[data-mode='quarter'] .mode-dot {
462
+ background: var(--usage-accent-purple);
463
+ }
464
+ .mode-item[data-mode='minimal'] .mode-dot {
465
+ background: var(--usage-status-critical);
466
+ }
467
+
468
+ .mode-label {
469
+ font-size: 0.625rem;
470
+ font-weight: 700;
471
+ color: var(--usage-text-primary);
472
+ }
473
+
474
+ .mode-desc {
475
+ font-size: 0.5625rem;
476
+ color: var(--usage-text-muted);
477
+ text-align: center;
478
+ }
479
+
480
+ /* Footer */
481
+ .panel-footer {
482
+ border-top: 1px solid var(--usage-border-subtle);
483
+ padding-top: 0.5rem;
484
+ }
485
+
486
+ .last-updated {
487
+ font-size: 0.625rem;
488
+ color: var(--usage-text-muted);
489
+ }
490
+
491
+ /* Responsive */
492
+ @media (max-width: 480px) {
493
+ .sampling-modes {
494
+ grid-template-columns: repeat(2, 1fr);
495
+ }
496
+
497
+ .breaker-row {
498
+ grid-template-columns: 1fr auto auto;
499
+ }
500
+
501
+ .breaker-kv-key {
502
+ display: none;
503
+ }
504
+
505
+ .reset-btn {
506
+ padding: 0.2rem 0.4rem;
507
+ font-size: 0.5625rem;
508
+ }
509
+ }
510
+ </style>
511
+
512
+ <script>
513
+ /**
514
+ * CircuitBreakerStatus JavaScript
515
+ *
516
+ * Updates circuit breaker display from API data.
517
+ */
518
+
519
+ interface CircuitBreakerData {
520
+ globalStop: boolean;
521
+ samplingMode: 'FULL' | 'HALF' | 'QUARTER' | 'MINIMAL';
522
+ d1Writes24h: number;
523
+ d1Limit: number;
524
+ projects: Array<{
525
+ id: string;
526
+ status: 'active' | 'tripped' | 'degraded';
527
+ label: string;
528
+ }>;
529
+ lastUpdated: string;
530
+ }
531
+
532
+ // Format large numbers
533
+ function formatNumber(n: number): string {
534
+ return n.toLocaleString();
535
+ }
536
+
537
+ // Update circuit breaker display
538
+ window.updateCircuitBreakerStatus = function (data: CircuitBreakerData) {
539
+ // Global status
540
+ const globalDot = document.querySelector('.global-dot') as HTMLElement;
541
+ const globalLabel = document.querySelector('.global-label') as HTMLElement;
542
+ if (globalDot && globalLabel) {
543
+ globalDot.setAttribute('data-status', data.globalStop ? 'stopped' : 'active');
544
+ globalLabel.textContent = data.globalStop ? 'GLOBAL STOP' : 'System Active';
545
+ }
546
+
547
+ // Sampling mode badge
548
+ const samplingBadge = document.getElementById('d1-sampling-mode');
549
+ if (samplingBadge) {
550
+ samplingBadge.setAttribute('data-mode', data.samplingMode.toLowerCase());
551
+ samplingBadge.textContent = data.samplingMode;
552
+ }
553
+
554
+ // D1 usage bar
555
+ const d1Bar = document.getElementById('d1-usage-bar');
556
+ const d1Current = document.getElementById('d1-usage-current');
557
+ const d1Limit = document.getElementById('d1-usage-limit');
558
+ const d1Pct = document.getElementById('d1-usage-pct');
559
+
560
+ const usagePct = (data.d1Writes24h / data.d1Limit) * 100;
561
+
562
+ if (d1Bar) {
563
+ d1Bar.style.width = `${Math.min(usagePct, 100)}%`;
564
+ }
565
+ if (d1Current) {
566
+ d1Current.textContent = formatNumber(data.d1Writes24h);
567
+ }
568
+ if (d1Limit) {
569
+ d1Limit.textContent = formatNumber(data.d1Limit);
570
+ }
571
+ if (d1Pct) {
572
+ d1Pct.textContent = `(${usagePct.toFixed(1)}%)`;
573
+ }
574
+
575
+ // Project statuses
576
+ for (const project of data.projects) {
577
+ const row = document.querySelector(`.breaker-row[data-project="${project.id}"]`);
578
+ if (!row) continue;
579
+
580
+ const dot = row.querySelector('.breaker-dot') as HTMLElement;
581
+ const label = row.querySelector('.status-label') as HTMLElement;
582
+ const resetBtn = row.querySelector('.reset-btn') as HTMLButtonElement;
583
+
584
+ if (dot) {
585
+ dot.setAttribute('data-status', project.status);
586
+ }
587
+ if (label) {
588
+ label.textContent = project.label;
589
+ }
590
+ // Show reset button only for paused/tripped projects
591
+ if (resetBtn) {
592
+ const isPausedOrTripped =
593
+ project.status === 'tripped' || project.label.toLowerCase() === 'paused';
594
+ resetBtn.style.display = isPausedOrTripped ? 'inline-block' : 'none';
595
+ }
596
+ }
597
+
598
+ // Sampling mode indicators
599
+ const modeItems = document.querySelectorAll('.mode-item');
600
+ modeItems.forEach((item) => {
601
+ const mode = (item as HTMLElement).dataset.mode?.toUpperCase();
602
+ if (mode === data.samplingMode) {
603
+ item.classList.add('active');
604
+ } else {
605
+ item.classList.remove('active');
606
+ }
607
+ });
608
+
609
+ // Last updated
610
+ const lastUpdatedEl = document.getElementById('cb-last-updated');
611
+ if (lastUpdatedEl) {
612
+ lastUpdatedEl.textContent = `Last checked: ${data.lastUpdated}`;
613
+ }
614
+ };
615
+
616
+ // Handle reset button clicks
617
+ async function handleResetClick(event: Event) {
618
+ const button = event.currentTarget as HTMLButtonElement;
619
+ const projectId = button.dataset.project;
620
+
621
+ if (!projectId) return;
622
+
623
+ // Confirm reset
624
+ const confirmed = confirm(`Reset circuit breaker for ${projectId}?`);
625
+ if (!confirmed) return;
626
+
627
+ // Show loading state
628
+ button.classList.add('loading');
629
+ button.textContent = '...';
630
+
631
+ try {
632
+ // Include credentials to pass Cloudflare Access JWT cookie
633
+ const response = await fetch('/api/usage/reset-circuit-breaker', {
634
+ method: 'POST',
635
+ headers: { 'Content-Type': 'application/json' },
636
+ body: JSON.stringify({ service: projectId }),
637
+ credentials: 'include',
638
+ });
639
+
640
+ const result = await response.json();
641
+
642
+ if (result.success) {
643
+ // Trigger refresh of circuit breaker data
644
+ window.dispatchEvent(new CustomEvent('usage:refresh-projects'));
645
+ button.style.display = 'none';
646
+ } else {
647
+ alert(`Failed to reset: ${result.error || 'Unknown error'}`);
648
+ }
649
+ } catch (error) {
650
+ console.error('[CircuitBreaker] Reset error:', error);
651
+ alert('Failed to reset circuit breaker');
652
+ } finally {
653
+ button.classList.remove('loading');
654
+ button.textContent = 'Reset';
655
+ }
656
+ }
657
+
658
+ // Attach click handlers to all reset buttons
659
+ document.querySelectorAll('.reset-btn').forEach((btn) => {
660
+ btn.addEventListener('click', handleResetClick);
661
+ });
662
+
663
+ // Type declaration
664
+ declare global {
665
+ interface Window {
666
+ updateCircuitBreakerStatus: (data: CircuitBreakerData) => void;
667
+ }
668
+ }
669
+ </script>