@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,872 @@
1
+ ---
2
+ /**
3
+ * FeatureBudgetsTable Component
4
+ *
5
+ * Displays feature-level usage metrics and circuit breaker status.
6
+ * Data is populated via JavaScript after API fetch.
7
+ *
8
+ * Features:
9
+ * - Feature usage metrics from Analytics Engine
10
+ * - Circuit breaker status per feature
11
+ * - Toggle circuit breaker (enable/disable)
12
+ * - Budget percentage indicators
13
+ */
14
+ ---
15
+
16
+ <div class="feature-budgets-panel" role="region" aria-label="Feature budgets">
17
+ <!-- Header -->
18
+ <div class="panel-header">
19
+ <div class="header-left">
20
+ <h3 class="panel-title">Feature Budgets</h3>
21
+ <span class="feature-count" id="feature-count">0 features</span>
22
+ </div>
23
+ <div class="header-actions">
24
+ <button type="button" class="refresh-btn" id="refresh-features-btn" title="Refresh">
25
+ <svg
26
+ xmlns="http://www.w3.org/2000/svg"
27
+ width="14"
28
+ height="14"
29
+ viewBox="0 0 24 24"
30
+ fill="none"
31
+ stroke="currentColor"
32
+ stroke-width="2"
33
+ stroke-linecap="round"
34
+ stroke-linejoin="round"
35
+ >
36
+ <polyline points="23 4 23 10 17 10"></polyline>
37
+ <polyline points="1 20 1 14 7 14"></polyline>
38
+ <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>
39
+ </svg>
40
+ </button>
41
+ </div>
42
+ </div>
43
+
44
+ <!-- Empty state -->
45
+ <div id="features-empty-state" class="empty-state">
46
+ <div class="empty-icon">📊</div>
47
+ <p class="empty-title">No feature telemetry yet</p>
48
+ <p class="empty-desc">
49
+ Feature usage data will appear once projects start reporting telemetry using the <code
50
+ >withFeatureBudget()</code
51
+ > wrapper.
52
+ </p>
53
+ </div>
54
+
55
+ <!-- Features table -->
56
+ <div id="features-table-container" class="table-container hidden">
57
+ <table class="features-table">
58
+ <thead>
59
+ <tr>
60
+ <th class="col-feature">Feature</th>
61
+ <th class="col-project">Project</th>
62
+ <th class="col-metrics">D1 Writes</th>
63
+ <th class="col-metrics">KV Ops</th>
64
+ <th class="col-metrics">Requests</th>
65
+ <th class="col-trend">7d Trend</th>
66
+ <th class="col-status">Status</th>
67
+ <th class="col-action">Action</th>
68
+ </tr>
69
+ </thead>
70
+ <tbody id="features-tbody">
71
+ <!-- Rows populated via JavaScript -->
72
+ </tbody>
73
+ </table>
74
+ </div>
75
+
76
+ <!-- Last updated -->
77
+ <div class="panel-footer">
78
+ <span id="features-last-updated" class="last-updated">Last updated: --</span>
79
+ </div>
80
+ </div>
81
+
82
+ <style>
83
+ .feature-budgets-panel {
84
+ background: var(--usage-bg-secondary);
85
+ border: 1px solid var(--usage-border-default);
86
+ border-radius: var(--usage-radius-xl);
87
+ padding: 1.25rem;
88
+ display: flex;
89
+ flex-direction: column;
90
+ gap: 1rem;
91
+ }
92
+
93
+ .panel-header {
94
+ display: flex;
95
+ justify-content: space-between;
96
+ align-items: center;
97
+ }
98
+
99
+ .header-left {
100
+ display: flex;
101
+ align-items: center;
102
+ gap: 0.75rem;
103
+ }
104
+
105
+ .panel-title {
106
+ font-size: 0.875rem;
107
+ font-weight: 600;
108
+ color: var(--usage-text-primary);
109
+ margin: 0;
110
+ }
111
+
112
+ .feature-count {
113
+ font-size: 0.6875rem;
114
+ color: var(--usage-text-muted);
115
+ background: var(--usage-bg-tertiary);
116
+ padding: 0.125rem 0.5rem;
117
+ border-radius: var(--usage-radius-sm);
118
+ }
119
+
120
+ .header-actions {
121
+ display: flex;
122
+ align-items: center;
123
+ gap: 0.5rem;
124
+ }
125
+
126
+ .refresh-btn {
127
+ display: flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ width: 28px;
131
+ height: 28px;
132
+ padding: 0;
133
+ background: var(--usage-bg-tertiary);
134
+ border: 1px solid var(--usage-border-subtle);
135
+ border-radius: var(--usage-radius-md);
136
+ color: var(--usage-text-secondary);
137
+ cursor: pointer;
138
+ transition: all var(--usage-transition-fast);
139
+ }
140
+
141
+ .refresh-btn:hover {
142
+ background: var(--usage-bg-hover);
143
+ color: var(--usage-text-primary);
144
+ }
145
+
146
+ .refresh-btn.loading svg {
147
+ animation: spin 1s linear infinite;
148
+ }
149
+
150
+ @keyframes spin {
151
+ from {
152
+ transform: rotate(0deg);
153
+ }
154
+ to {
155
+ transform: rotate(360deg);
156
+ }
157
+ }
158
+
159
+ /* Empty state */
160
+ .empty-state {
161
+ display: flex;
162
+ flex-direction: column;
163
+ align-items: center;
164
+ justify-content: center;
165
+ padding: 2rem;
166
+ text-align: center;
167
+ }
168
+
169
+ .empty-icon {
170
+ font-size: 2rem;
171
+ margin-bottom: 0.75rem;
172
+ }
173
+
174
+ .empty-title {
175
+ font-size: 0.875rem;
176
+ font-weight: 600;
177
+ color: var(--usage-text-primary);
178
+ margin: 0 0 0.25rem 0;
179
+ }
180
+
181
+ .empty-desc {
182
+ font-size: 0.75rem;
183
+ color: var(--usage-text-muted);
184
+ margin: 0;
185
+ max-width: 300px;
186
+ }
187
+
188
+ .empty-desc code {
189
+ font-family: var(--usage-font-mono);
190
+ font-size: 0.6875rem;
191
+ background: var(--usage-bg-tertiary);
192
+ padding: 0.125rem 0.25rem;
193
+ border-radius: var(--usage-radius-sm);
194
+ }
195
+
196
+ /* Table */
197
+ .table-container {
198
+ overflow-x: auto;
199
+ }
200
+
201
+ .features-table {
202
+ width: 100%;
203
+ border-collapse: collapse;
204
+ font-size: 0.75rem;
205
+ }
206
+
207
+ .features-table th {
208
+ text-align: left;
209
+ font-size: 0.625rem;
210
+ font-weight: 600;
211
+ text-transform: uppercase;
212
+ letter-spacing: 0.03em;
213
+ color: var(--usage-text-muted);
214
+ padding: 0.5rem 0.75rem;
215
+ border-bottom: 1px solid var(--usage-border-subtle);
216
+ white-space: nowrap;
217
+ }
218
+
219
+ .features-table td {
220
+ padding: 0.625rem 0.75rem;
221
+ border-bottom: 1px solid var(--usage-border-subtle);
222
+ color: var(--usage-text-primary);
223
+ }
224
+
225
+ .features-table tbody tr:hover {
226
+ background: var(--usage-bg-hover);
227
+ }
228
+
229
+ .features-table tbody tr:last-child td {
230
+ border-bottom: none;
231
+ }
232
+
233
+ .col-feature {
234
+ width: 25%;
235
+ }
236
+ .col-project {
237
+ width: 12%;
238
+ }
239
+ .col-metrics {
240
+ width: 10%;
241
+ text-align: right;
242
+ }
243
+ .col-trend {
244
+ width: 12%;
245
+ text-align: center;
246
+ }
247
+ .col-status {
248
+ width: 10%;
249
+ text-align: center;
250
+ }
251
+ .col-action {
252
+ width: 10%;
253
+ text-align: center;
254
+ }
255
+
256
+ /* Sparkline */
257
+ .sparkline {
258
+ display: block;
259
+ }
260
+
261
+ .sparkline-line {
262
+ fill: none;
263
+ stroke: var(--usage-primary);
264
+ stroke-width: 1.5;
265
+ stroke-linecap: round;
266
+ stroke-linejoin: round;
267
+ }
268
+
269
+ .sparkline-area {
270
+ fill: var(--usage-primary);
271
+ opacity: 0.1;
272
+ }
273
+
274
+ .sparkline-empty {
275
+ font-size: 0.625rem;
276
+ color: var(--usage-text-muted);
277
+ }
278
+
279
+ /* Feature key display */
280
+ .feature-key {
281
+ display: flex;
282
+ flex-direction: column;
283
+ gap: 0.125rem;
284
+ }
285
+
286
+ .feature-name {
287
+ font-weight: 500;
288
+ color: var(--usage-text-primary);
289
+ }
290
+
291
+ .feature-category {
292
+ font-size: 0.625rem;
293
+ color: var(--usage-text-muted);
294
+ }
295
+
296
+ /* Metrics cells */
297
+ .metric-cell {
298
+ text-align: right;
299
+ font-family: var(--usage-font-mono);
300
+ font-size: 0.6875rem;
301
+ }
302
+
303
+ .metric-value {
304
+ display: block;
305
+ font-weight: 500;
306
+ }
307
+
308
+ .metric-pct {
309
+ display: block;
310
+ font-size: 0.5625rem;
311
+ color: var(--usage-text-muted);
312
+ }
313
+
314
+ .metric-pct.warning {
315
+ color: var(--usage-status-warning);
316
+ }
317
+
318
+ .metric-pct.critical {
319
+ color: var(--usage-status-critical);
320
+ }
321
+
322
+ /* Budget progress bars */
323
+ .budget-bar {
324
+ width: 100%;
325
+ height: 3px;
326
+ background: var(--usage-border-subtle, #e5e7eb);
327
+ border-radius: 2px;
328
+ margin-top: 3px;
329
+ overflow: hidden;
330
+ }
331
+
332
+ .budget-bar-fill {
333
+ height: 100%;
334
+ border-radius: 2px;
335
+ transition: width 0.3s ease;
336
+ }
337
+
338
+ .budget-bar-fill.ok {
339
+ background: var(--usage-status-ok, #10b981);
340
+ }
341
+
342
+ .budget-bar-fill.warning {
343
+ background: var(--usage-status-warning, #f59e0b);
344
+ }
345
+
346
+ .budget-bar-fill.critical {
347
+ background: var(--usage-status-critical, #ef4444);
348
+ }
349
+
350
+ /* Status badges */
351
+ .status-badge {
352
+ display: inline-flex;
353
+ align-items: center;
354
+ gap: 0.25rem;
355
+ padding: 0.125rem 0.5rem;
356
+ font-size: 0.625rem;
357
+ font-weight: 600;
358
+ border-radius: var(--usage-radius-sm);
359
+ }
360
+
361
+ .status-badge.enabled {
362
+ background: var(--usage-status-ok-bg);
363
+ color: var(--usage-status-ok);
364
+ }
365
+
366
+ .status-badge.disabled {
367
+ background: var(--usage-status-critical-bg);
368
+ color: var(--usage-status-critical);
369
+ }
370
+
371
+ .status-dot {
372
+ width: 6px;
373
+ height: 6px;
374
+ border-radius: 50%;
375
+ }
376
+
377
+ .status-badge.enabled .status-dot {
378
+ background: var(--usage-status-ok);
379
+ }
380
+
381
+ .status-badge.disabled .status-dot {
382
+ background: var(--usage-status-critical);
383
+ }
384
+
385
+ /* Action buttons */
386
+ .toggle-btn {
387
+ padding: 0.25rem 0.5rem;
388
+ font-size: 0.625rem;
389
+ font-weight: 600;
390
+ text-transform: uppercase;
391
+ letter-spacing: 0.03em;
392
+ border-radius: var(--usage-radius-sm);
393
+ cursor: pointer;
394
+ transition: all var(--usage-transition-fast);
395
+ }
396
+
397
+ .toggle-btn.enable {
398
+ color: var(--usage-status-ok);
399
+ background: var(--usage-status-ok-bg);
400
+ border: 1px solid var(--usage-status-ok);
401
+ }
402
+
403
+ .toggle-btn.enable:hover {
404
+ background: var(--usage-status-ok);
405
+ color: white;
406
+ }
407
+
408
+ .toggle-btn.disable {
409
+ color: var(--usage-status-critical);
410
+ background: var(--usage-status-critical-bg);
411
+ border: 1px solid var(--usage-status-critical);
412
+ }
413
+
414
+ .toggle-btn.disable:hover {
415
+ background: var(--usage-status-critical);
416
+ color: white;
417
+ }
418
+
419
+ .toggle-btn:disabled {
420
+ opacity: 0.5;
421
+ cursor: not-allowed;
422
+ }
423
+
424
+ /* Footer */
425
+ .panel-footer {
426
+ border-top: 1px solid var(--usage-border-subtle);
427
+ padding-top: 0.5rem;
428
+ }
429
+
430
+ .last-updated {
431
+ font-size: 0.625rem;
432
+ color: var(--usage-text-muted);
433
+ }
434
+
435
+ .hidden {
436
+ display: none !important;
437
+ }
438
+
439
+ /* Responsive */
440
+ @media (max-width: 768px) {
441
+ .features-table th,
442
+ .features-table td {
443
+ padding: 0.5rem;
444
+ }
445
+
446
+ .col-metrics {
447
+ display: none;
448
+ }
449
+ }
450
+ </style>
451
+
452
+ <script>
453
+ /**
454
+ * FeatureBudgetsTable JavaScript
455
+ *
456
+ * Handles fetching feature data and updating the table using safe DOM methods.
457
+ */
458
+
459
+ interface FeatureMetrics {
460
+ d1Writes: number;
461
+ d1Reads: number;
462
+ kvReads: number;
463
+ kvWrites: number;
464
+ doRequests: number;
465
+ doGbSeconds: number;
466
+ r2ClassA: number;
467
+ r2ClassB: number;
468
+ aiNeurons: number;
469
+ queueMessages: number;
470
+ requests: number;
471
+ cpuMs: number;
472
+ }
473
+
474
+ interface FeatureCircuitBreaker {
475
+ enabled: boolean;
476
+ disabledReason?: string;
477
+ disabledAt?: string;
478
+ autoResetAt?: string;
479
+ }
480
+
481
+ interface FeatureData {
482
+ featureKey: string;
483
+ project: string;
484
+ category: string;
485
+ feature: string;
486
+ metrics: FeatureMetrics;
487
+ circuitBreaker: FeatureCircuitBreaker;
488
+ }
489
+
490
+ interface HistoryDataPoint {
491
+ date: string;
492
+ d1Writes: number;
493
+ d1Reads: number;
494
+ kvReads: number;
495
+ kvWrites: number;
496
+ doRequests: number;
497
+ doGbSeconds: number;
498
+ r2ClassA: number;
499
+ r2ClassB: number;
500
+ aiNeurons: number;
501
+ queueMessages: number;
502
+ requests: number;
503
+ timesDisabled: number;
504
+ }
505
+
506
+ type HistoryByFeature = Record<string, HistoryDataPoint[]>;
507
+
508
+ interface FeatureBudgets {
509
+ _defaults: Record<string, { hourly: number; daily: number }>;
510
+ features: Record<string, Record<string, { hourly: number; daily: number }>>;
511
+ }
512
+
513
+ // Format numbers with K/M suffix
514
+ function formatNumber(n: number): string {
515
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
516
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
517
+ return n.toFixed(0);
518
+ }
519
+
520
+ // Calculate budget percentage
521
+ function getBudgetPct(value: number, limit: number): number {
522
+ if (limit <= 0) return 0;
523
+ return (value / limit) * 100;
524
+ }
525
+
526
+ // Get status class based on percentage
527
+ function getPctClass(pct: number): string {
528
+ if (pct >= 90) return 'critical';
529
+ if (pct >= 70) return 'warning';
530
+ return '';
531
+ }
532
+
533
+ // Get bar fill class based on percentage
534
+ function getBarClass(pct: number): string {
535
+ if (pct >= 90) return 'critical';
536
+ if (pct >= 70) return 'warning';
537
+ return 'ok';
538
+ }
539
+
540
+ // Create a budget progress bar element
541
+ function createBudgetBar(pct: number): HTMLDivElement {
542
+ const bar = document.createElement('div');
543
+ bar.className = 'budget-bar';
544
+ const fill = document.createElement('div');
545
+ fill.className = `budget-bar-fill ${getBarClass(pct)}`;
546
+ fill.style.width = `${Math.min(pct, 100)}%`;
547
+ bar.appendChild(fill);
548
+ return bar;
549
+ }
550
+
551
+ // Cached budgets and history
552
+ let cachedBudgets: FeatureBudgets | null = null;
553
+ let cachedHistory: HistoryByFeature | null = null;
554
+
555
+ /**
556
+ * Create SVG sparkline from data points
557
+ */
558
+ function createSparkline(data: number[], width = 60, height = 20): SVGElement {
559
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
560
+ svg.setAttribute('class', 'sparkline');
561
+ svg.setAttribute('viewBox', `0 0 ${width} ${height}`);
562
+ svg.setAttribute('width', String(width));
563
+ svg.setAttribute('height', String(height));
564
+
565
+ if (data.length < 2) {
566
+ return svg; // Empty sparkline
567
+ }
568
+
569
+ const max = Math.max(...data, 1);
570
+ const min = Math.min(...data);
571
+ const range = max - min || 1;
572
+
573
+ // Calculate points
574
+ const points: string[] = [];
575
+ const xStep = width / (data.length - 1);
576
+
577
+ for (let i = 0; i < data.length; i++) {
578
+ const x = i * xStep;
579
+ const y = height - ((data[i] - min) / range) * (height - 4) - 2;
580
+ points.push(`${x.toFixed(1)},${y.toFixed(1)}`);
581
+ }
582
+
583
+ // Create area path (filled area under line)
584
+ const areaPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
585
+ const areaD = `M0,${height} L${points.join(' L')} L${width},${height} Z`;
586
+ areaPath.setAttribute('d', areaD);
587
+ areaPath.setAttribute('class', 'sparkline-area');
588
+ svg.appendChild(areaPath);
589
+
590
+ // Create line path
591
+ const linePath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
592
+ linePath.setAttribute('d', `M${points.join(' L')}`);
593
+ linePath.setAttribute('class', 'sparkline-line');
594
+ svg.appendChild(linePath);
595
+
596
+ return svg;
597
+ }
598
+
599
+ // Create a table row element safely
600
+ function createFeatureRow(
601
+ f: FeatureData,
602
+ defaults: Record<string, { hourly: number; daily?: number }>
603
+ ): HTMLTableRowElement {
604
+ const row = document.createElement('tr');
605
+ row.dataset.featureKey = f.featureKey;
606
+
607
+ // Feature column
608
+ const featureCell = document.createElement('td');
609
+ const featureDiv = document.createElement('div');
610
+ featureDiv.className = 'feature-key';
611
+ const nameSpan = document.createElement('span');
612
+ nameSpan.className = 'feature-name';
613
+ nameSpan.textContent = f.feature;
614
+ const catSpan = document.createElement('span');
615
+ catSpan.className = 'feature-category';
616
+ catSpan.textContent = f.category;
617
+ featureDiv.appendChild(nameSpan);
618
+ featureDiv.appendChild(catSpan);
619
+ featureCell.appendChild(featureDiv);
620
+ row.appendChild(featureCell);
621
+
622
+ // Project column
623
+ const projectCell = document.createElement('td');
624
+ projectCell.textContent = f.project;
625
+ row.appendChild(projectCell);
626
+
627
+ // D1 Writes column
628
+ const d1WritesPct = getBudgetPct(f.metrics.d1Writes, defaults.d1Writes?.hourly ?? 10000);
629
+ const d1Cell = document.createElement('td');
630
+ d1Cell.className = 'metric-cell';
631
+ const d1Value = document.createElement('span');
632
+ d1Value.className = 'metric-value';
633
+ d1Value.textContent = formatNumber(f.metrics.d1Writes);
634
+ const d1Pct = document.createElement('span');
635
+ d1Pct.className = `metric-pct ${getPctClass(d1WritesPct)}`;
636
+ d1Pct.textContent = `${d1WritesPct.toFixed(0)}%`;
637
+ d1Cell.appendChild(d1Value);
638
+ d1Cell.appendChild(d1Pct);
639
+ d1Cell.appendChild(createBudgetBar(d1WritesPct));
640
+ row.appendChild(d1Cell);
641
+
642
+ // KV Ops column
643
+ const kvOps = f.metrics.kvReads + f.metrics.kvWrites;
644
+ const kvLimit = (defaults.kvReads?.hourly ?? 50000) + (defaults.kvWrites?.hourly ?? 5000);
645
+ const kvOpsPct = getBudgetPct(kvOps, kvLimit);
646
+ const kvCell = document.createElement('td');
647
+ kvCell.className = 'metric-cell';
648
+ const kvValue = document.createElement('span');
649
+ kvValue.className = 'metric-value';
650
+ kvValue.textContent = formatNumber(kvOps);
651
+ const kvPct = document.createElement('span');
652
+ kvPct.className = `metric-pct ${getPctClass(kvOpsPct)}`;
653
+ kvPct.textContent = `${kvOpsPct.toFixed(0)}%`;
654
+ kvCell.appendChild(kvValue);
655
+ kvCell.appendChild(kvPct);
656
+ kvCell.appendChild(createBudgetBar(kvOpsPct));
657
+ row.appendChild(kvCell);
658
+
659
+ // Requests column
660
+ const reqCell = document.createElement('td');
661
+ reqCell.className = 'metric-cell';
662
+ const reqValue = document.createElement('span');
663
+ reqValue.className = 'metric-value';
664
+ reqValue.textContent = formatNumber(f.metrics.requests);
665
+ reqCell.appendChild(reqValue);
666
+ row.appendChild(reqCell);
667
+
668
+ // Trend column (7-day sparkline)
669
+ const trendCell = document.createElement('td');
670
+ trendCell.className = 'metric-cell';
671
+ const historyData = cachedHistory?.[f.featureKey];
672
+ if (historyData && historyData.length > 0) {
673
+ // Use requests for the sparkline (or sum of activity)
674
+ const values = historyData.map((d) => d.requests || d.d1Writes + d.kvReads + d.kvWrites);
675
+ const sparkline = createSparkline(values);
676
+ trendCell.appendChild(sparkline);
677
+ } else {
678
+ const noData = document.createElement('span');
679
+ noData.className = 'sparkline-empty';
680
+ noData.textContent = '—';
681
+ trendCell.appendChild(noData);
682
+ }
683
+ row.appendChild(trendCell);
684
+
685
+ // Status column
686
+ const statusCell = document.createElement('td');
687
+ const statusBadge = document.createElement('span');
688
+ statusBadge.className = `status-badge ${f.circuitBreaker.enabled ? 'enabled' : 'disabled'}`;
689
+ const statusDot = document.createElement('span');
690
+ statusDot.className = 'status-dot';
691
+ statusBadge.appendChild(statusDot);
692
+ statusBadge.appendChild(
693
+ document.createTextNode(f.circuitBreaker.enabled ? 'Active' : 'Disabled')
694
+ );
695
+ statusCell.appendChild(statusBadge);
696
+ row.appendChild(statusCell);
697
+
698
+ // Action column
699
+ const actionCell = document.createElement('td');
700
+ const toggleBtn = document.createElement('button');
701
+ toggleBtn.type = 'button';
702
+ toggleBtn.className = `toggle-btn ${f.circuitBreaker.enabled ? 'disable' : 'enable'}`;
703
+ toggleBtn.dataset.featureKey = f.featureKey;
704
+ toggleBtn.dataset.currentState = String(f.circuitBreaker.enabled);
705
+ toggleBtn.textContent = f.circuitBreaker.enabled ? 'Disable' : 'Enable';
706
+ toggleBtn.addEventListener('click', handleToggle);
707
+ actionCell.appendChild(toggleBtn);
708
+ row.appendChild(actionCell);
709
+
710
+ return row;
711
+ }
712
+
713
+ // Fetch features data from API
714
+ async function fetchFeatures(): Promise<void> {
715
+ const refreshBtn = document.getElementById('refresh-features-btn');
716
+ refreshBtn?.classList.add('loading');
717
+
718
+ try {
719
+ // Fetch features, budgets, and history in parallel
720
+ const [featuresRes, budgetsRes, historyRes] = await Promise.all([
721
+ fetch('/api/usage/features', { credentials: 'include' }),
722
+ cachedBudgets
723
+ ? Promise.resolve(null)
724
+ : fetch('/api/usage/features/budgets', { credentials: 'include' }),
725
+ cachedHistory
726
+ ? Promise.resolve(null)
727
+ : fetch('/api/usage/features/history?days=7', { credentials: 'include' }),
728
+ ]);
729
+
730
+ if (!featuresRes.ok) {
731
+ throw new Error(`Features API returned ${featuresRes.status}`);
732
+ }
733
+
734
+ const featuresData = await featuresRes.json();
735
+
736
+ // Cache budgets
737
+ if (budgetsRes) {
738
+ const budgetsData = await budgetsRes.json();
739
+ if (budgetsData.success) {
740
+ cachedBudgets = budgetsData.budgets;
741
+ }
742
+ }
743
+
744
+ // Cache history
745
+ if (historyRes) {
746
+ const historyData = await historyRes.json();
747
+ if (historyData.success) {
748
+ cachedHistory = historyData.features;
749
+ }
750
+ }
751
+
752
+ if (featuresData.success) {
753
+ renderFeatures(featuresData.features, cachedBudgets);
754
+
755
+ // Update last updated
756
+ const lastUpdated = document.getElementById('features-last-updated');
757
+ if (lastUpdated) {
758
+ lastUpdated.textContent = `Last updated: ${new Date().toLocaleTimeString()}`;
759
+ }
760
+ }
761
+ } catch (error) {
762
+ console.error('[Features] Error fetching data:', error);
763
+ } finally {
764
+ refreshBtn?.classList.remove('loading');
765
+ }
766
+ }
767
+
768
+ // Render features table using safe DOM methods
769
+ function renderFeatures(features: FeatureData[], budgets: FeatureBudgets | null): void {
770
+ const emptyState = document.getElementById('features-empty-state');
771
+ const tableContainer = document.getElementById('features-table-container');
772
+ const tbody = document.getElementById('features-tbody');
773
+ const countEl = document.getElementById('feature-count');
774
+
775
+ if (!tbody) return;
776
+
777
+ // Update count
778
+ if (countEl) {
779
+ countEl.textContent = `${features.length} feature${features.length !== 1 ? 's' : ''}`;
780
+ }
781
+
782
+ // Show empty state or table
783
+ if (features.length === 0) {
784
+ emptyState?.classList.remove('hidden');
785
+ tableContainer?.classList.add('hidden');
786
+ return;
787
+ }
788
+
789
+ emptyState?.classList.add('hidden');
790
+ tableContainer?.classList.remove('hidden');
791
+
792
+ // Get default budgets
793
+ const defaults = budgets?._defaults ?? {
794
+ d1Writes: { hourly: 10000 },
795
+ kvReads: { hourly: 50000 },
796
+ kvWrites: { hourly: 5000 },
797
+ };
798
+
799
+ // Clear existing rows
800
+ while (tbody.firstChild) {
801
+ tbody.removeChild(tbody.firstChild);
802
+ }
803
+
804
+ // Build table rows using safe DOM methods
805
+ for (const f of features) {
806
+ const row = createFeatureRow(f, defaults);
807
+ tbody.appendChild(row);
808
+ }
809
+ }
810
+
811
+ // Handle toggle button click
812
+ async function handleToggle(event: Event): Promise<void> {
813
+ const button = event.currentTarget as HTMLButtonElement;
814
+ const featureKey = button.dataset.featureKey;
815
+ const currentState = button.dataset.currentState === 'true';
816
+
817
+ if (!featureKey) return;
818
+
819
+ const action = currentState ? 'disable' : 'enable';
820
+ const confirmed = confirm(
821
+ `${action.charAt(0).toUpperCase() + action.slice(1)} feature "${featureKey}"?`
822
+ );
823
+ if (!confirmed) return;
824
+
825
+ button.disabled = true;
826
+ button.textContent = '...';
827
+
828
+ try {
829
+ const response = await fetch('/api/usage/features/circuit-breakers', {
830
+ method: 'PUT',
831
+ headers: { 'Content-Type': 'application/json' },
832
+ body: JSON.stringify({
833
+ featureKey,
834
+ enabled: !currentState,
835
+ }),
836
+ credentials: 'include',
837
+ });
838
+
839
+ const result = await response.json();
840
+
841
+ if (result.success) {
842
+ // Refresh the table
843
+ await fetchFeatures();
844
+ } else {
845
+ alert(`Failed to ${action}: ${result.error || 'Unknown error'}`);
846
+ button.disabled = false;
847
+ button.textContent = currentState ? 'Disable' : 'Enable';
848
+ }
849
+ } catch (error) {
850
+ console.error('[Features] Toggle error:', error);
851
+ alert(`Failed to ${action} feature`);
852
+ button.disabled = false;
853
+ button.textContent = currentState ? 'Disable' : 'Enable';
854
+ }
855
+ }
856
+
857
+ // Export for external triggering
858
+ window.fetchFeatureBudgets = fetchFeatures;
859
+
860
+ // Refresh button handler
861
+ document.getElementById('refresh-features-btn')?.addEventListener('click', fetchFeatures);
862
+
863
+ // Initial load when tab becomes visible
864
+ window.addEventListener('usage:features-tab-visible', fetchFeatures);
865
+
866
+ // Type declarations
867
+ declare global {
868
+ interface Window {
869
+ fetchFeatureBudgets: () => Promise<void>;
870
+ }
871
+ }
872
+ </script>