@littlebearapps/platform-admin-sdk 2.1.0 → 2.3.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 (122) hide show
  1. package/README.md +2 -5
  2. package/dist/check-upgrade.d.ts +29 -0
  3. package/dist/check-upgrade.js +97 -0
  4. package/dist/index.js +59 -4
  5. package/dist/manifest.d.ts +2 -0
  6. package/dist/scaffold.js +5 -1
  7. package/dist/templates.d.ts +6 -1
  8. package/dist/templates.js +141 -3
  9. package/dist/upgrade.d.ts +1 -0
  10. package/dist/upgrade.js +21 -2
  11. package/package.json +1 -1
  12. package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
  13. package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
  14. package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
  15. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  16. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  17. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  18. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  19. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  20. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  21. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  22. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  23. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  24. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  25. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  26. package/templates/full/dashboard/src/pages/map.astro +561 -0
  27. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  28. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  29. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  30. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  31. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  32. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  33. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  34. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  35. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  36. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  37. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  38. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  39. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  40. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  41. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  42. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  43. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  44. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  45. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  46. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  47. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  48. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  49. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  50. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  51. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  52. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  53. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  54. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  55. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  56. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  57. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  58. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  59. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  60. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  61. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  62. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  63. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  64. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  65. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  66. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  67. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  68. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  69. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  70. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  71. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  72. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  73. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  74. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  75. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  76. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  77. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  78. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  79. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  80. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  81. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  82. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  83. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  84. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  85. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  86. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  87. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  88. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  89. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  90. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  91. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  92. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  93. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  94. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  95. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  96. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  97. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  98. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  99. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  100. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  101. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  102. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  103. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  104. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  105. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  106. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  107. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  108. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  109. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  110. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  111. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  112. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  113. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  114. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  115. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  116. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  117. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  118. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  119. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  120. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  121. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  122. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
@@ -0,0 +1,659 @@
1
+ ---
2
+ /**
3
+ * WorkersBreakdownTable Component
4
+ *
5
+ * Shows Workers usage breakdown by project with individual worker details.
6
+ * Part of Enhancement #1: Per-project operational breakdown.
7
+ *
8
+ * Features:
9
+ * - Group workers by project
10
+ * - Show total requests per project
11
+ * - Expand/collapse to see individual workers
12
+ * - Sort by project or requests
13
+ */
14
+
15
+ export interface WorkerData {
16
+ scriptName: string;
17
+ requests: number;
18
+ errors: number;
19
+ cpuTime: number;
20
+ }
21
+
22
+ export interface ProjectBreakdown {
23
+ project: string;
24
+ totalRequests: number;
25
+ totalErrors: number;
26
+ workerCount: number;
27
+ workers: WorkerData[];
28
+ }
29
+
30
+ interface Props {
31
+ /** Pre-grouped breakdown data (if provided by server) */
32
+ breakdown?: ProjectBreakdown[];
33
+ }
34
+
35
+ const { breakdown = [] } = Astro.props;
36
+
37
+ // Format large numbers for display
38
+ function formatNumber(num: number): string {
39
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
40
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
41
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
42
+ return num.toLocaleString();
43
+ }
44
+
45
+ // Calculate error rate percentage
46
+ function calcErrorRate(errors: number, requests: number): string {
47
+ if (requests === 0) return '0%';
48
+ const rate = (errors / requests) * 100;
49
+ return rate < 0.01 ? '<0.01%' : `${rate.toFixed(2)}%`;
50
+ }
51
+ ---
52
+
53
+ <section class="workers-breakdown-section" data-component="workers-breakdown">
54
+ <div class="section-header">
55
+ <h3 class="section-title">
56
+ <span class="title-icon">⚡</span>
57
+ Workers by Project
58
+ </h3>
59
+ <p class="section-description">
60
+ Request breakdown showing which projects consume the most Workers resources
61
+ </p>
62
+ </div>
63
+
64
+ <div class="breakdown-table-container" id="workers-breakdown-container">
65
+ <!-- Loading state -->
66
+ <div class="loading-state" id="workers-breakdown-loading">
67
+ <div class="loading-spinner"></div>
68
+ <span>Loading Workers breakdown...</span>
69
+ </div>
70
+
71
+ <!-- Empty state -->
72
+ <div class="empty-state" id="workers-breakdown-empty" style="display: none;">
73
+ <span class="empty-icon">📊</span>
74
+ <span class="empty-text">No Workers data available</span>
75
+ </div>
76
+
77
+ <!-- Data table (populated by JavaScript) -->
78
+ <table class="breakdown-table" id="workers-breakdown-table" role="grid" style="display: none;">
79
+ <thead>
80
+ <tr>
81
+ <th scope="col" class="col-expand" aria-label="Expand"></th>
82
+ <th scope="col" class="col-project">Project</th>
83
+ <th scope="col" class="col-workers">Workers</th>
84
+ <th scope="col" class="col-requests">Requests</th>
85
+ <th scope="col" class="col-errors">Error Rate</th>
86
+ <th scope="col" class="col-pct">% of Total</th>
87
+ </tr>
88
+ </thead>
89
+ <tbody id="workers-breakdown-tbody">
90
+ <!-- Rows populated by JavaScript -->
91
+ </tbody>
92
+ <tfoot>
93
+ <tr class="totals-row">
94
+ <td></td>
95
+ <td class="totals-label">Total</td>
96
+ <td id="total-workers">-</td>
97
+ <td id="total-requests">-</td>
98
+ <td id="total-error-rate">-</td>
99
+ <td>100%</td>
100
+ </tr>
101
+ </tfoot>
102
+ </table>
103
+ </div>
104
+ </section>
105
+
106
+ <style>
107
+ .workers-breakdown-section {
108
+ margin-top: 1.5rem;
109
+ padding: 1.5rem;
110
+ background: var(--usage-bg-secondary, #ffffff);
111
+ border: 1px solid var(--usage-border-default, #e5e7eb);
112
+ border-radius: var(--usage-radius-xl, 12px);
113
+ }
114
+
115
+ .section-header {
116
+ margin-bottom: 1rem;
117
+ }
118
+
119
+ .section-title {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 0.5rem;
123
+ font-size: 1rem;
124
+ font-weight: 600;
125
+ color: var(--usage-text-primary, #111827);
126
+ margin: 0 0 0.25rem 0;
127
+ }
128
+
129
+ .title-icon {
130
+ font-size: 1.125rem;
131
+ }
132
+
133
+ .section-description {
134
+ font-size: 0.8125rem;
135
+ color: var(--usage-text-secondary, #6b7280);
136
+ margin: 0;
137
+ }
138
+
139
+ .breakdown-table-container {
140
+ position: relative;
141
+ min-height: 100px;
142
+ }
143
+
144
+ /* Loading state */
145
+ .loading-state {
146
+ display: flex;
147
+ flex-direction: column;
148
+ align-items: center;
149
+ justify-content: center;
150
+ padding: 2rem;
151
+ color: var(--usage-text-secondary, #6b7280);
152
+ gap: 0.75rem;
153
+ }
154
+
155
+ .loading-spinner {
156
+ width: 24px;
157
+ height: 24px;
158
+ border: 2px solid var(--usage-border-default, #e5e7eb);
159
+ border-top-color: var(--usage-accent-blue, #3b82f6);
160
+ border-radius: 50%;
161
+ animation: spin 1s linear infinite;
162
+ }
163
+
164
+ @keyframes spin {
165
+ to {
166
+ transform: rotate(360deg);
167
+ }
168
+ }
169
+
170
+ /* Empty state */
171
+ .empty-state {
172
+ display: flex;
173
+ flex-direction: column;
174
+ align-items: center;
175
+ padding: 2rem;
176
+ color: var(--usage-text-tertiary, #9ca3af);
177
+ }
178
+
179
+ .empty-icon {
180
+ font-size: 2rem;
181
+ margin-bottom: 0.5rem;
182
+ }
183
+
184
+ /* Table styles */
185
+ .breakdown-table {
186
+ width: 100%;
187
+ border-collapse: collapse;
188
+ font-size: 0.875rem;
189
+ }
190
+
191
+ .breakdown-table th {
192
+ text-align: left;
193
+ padding: 0.75rem 1rem;
194
+ font-weight: 600;
195
+ color: var(--usage-text-secondary, #6b7280);
196
+ border-bottom: 1px solid var(--usage-border-default, #e5e7eb);
197
+ background: var(--usage-bg-tertiary, #f9fafb);
198
+ font-size: 0.75rem;
199
+ text-transform: uppercase;
200
+ letter-spacing: 0.025em;
201
+ }
202
+
203
+ .breakdown-table td {
204
+ padding: 0.75rem 1rem;
205
+ border-bottom: 1px solid var(--usage-border-light, #f3f4f6);
206
+ color: var(--usage-text-primary, #111827);
207
+ }
208
+
209
+ .col-expand {
210
+ width: 40px;
211
+ text-align: center;
212
+ }
213
+
214
+ .col-project {
215
+ min-width: 140px;
216
+ }
217
+
218
+ .col-workers,
219
+ .col-requests,
220
+ .col-errors,
221
+ .col-pct {
222
+ text-align: right;
223
+ width: 100px;
224
+ }
225
+
226
+ /* Project row */
227
+ :global(.project-row) {
228
+ cursor: pointer;
229
+ transition: background-color 0.15s ease;
230
+ }
231
+
232
+ :global(.project-row:hover) {
233
+ background: var(--usage-bg-hover, #f9fafb);
234
+ }
235
+
236
+ :global(.project-row.expanded) {
237
+ background: var(--usage-bg-tertiary, #f9fafb);
238
+ }
239
+
240
+ :global(.expand-btn) {
241
+ display: inline-flex;
242
+ align-items: center;
243
+ justify-content: center;
244
+ width: 24px;
245
+ height: 24px;
246
+ border: none;
247
+ background: transparent;
248
+ cursor: pointer;
249
+ color: var(--usage-text-tertiary, #9ca3af);
250
+ transition: all 0.15s ease;
251
+ border-radius: 4px;
252
+ }
253
+
254
+ :global(.expand-btn:hover) {
255
+ background: var(--usage-bg-hover, #e5e7eb);
256
+ color: var(--usage-text-primary, #111827);
257
+ }
258
+
259
+ :global(.expand-btn.expanded) {
260
+ transform: rotate(90deg);
261
+ }
262
+
263
+ :global(.project-name) {
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 0.5rem;
267
+ font-weight: 500;
268
+ }
269
+
270
+ :global(.project-badge) {
271
+ display: inline-block;
272
+ padding: 0.125rem 0.375rem;
273
+ font-size: 0.625rem;
274
+ font-weight: 600;
275
+ text-transform: uppercase;
276
+ border-radius: 4px;
277
+ background: var(--usage-accent-blue-light, #dbeafe);
278
+ color: var(--usage-accent-blue, #2563eb);
279
+ }
280
+
281
+ :global(.project-badge.other) {
282
+ background: var(--usage-bg-tertiary, #f3f4f6);
283
+ color: var(--usage-text-tertiary, #6b7280);
284
+ }
285
+
286
+ /* Worker sub-row */
287
+ :global(.worker-row) {
288
+ background: var(--usage-bg-tertiary, #f9fafb);
289
+ }
290
+
291
+ :global(.worker-row td) {
292
+ padding: 0.5rem 1rem;
293
+ font-size: 0.8125rem;
294
+ color: var(--usage-text-secondary, #6b7280);
295
+ }
296
+
297
+ :global(.worker-row td:first-child) {
298
+ padding-left: 2.5rem;
299
+ }
300
+
301
+ :global(.worker-name) {
302
+ font-family: ui-monospace, monospace;
303
+ font-size: 0.75rem;
304
+ }
305
+
306
+ /* Numeric values */
307
+ :global(.requests-value) {
308
+ font-variant-numeric: tabular-nums;
309
+ font-weight: 500;
310
+ }
311
+
312
+ :global(.error-rate) {
313
+ font-variant-numeric: tabular-nums;
314
+ }
315
+
316
+ :global(.error-rate.high) {
317
+ color: var(--usage-status-red, #ef4444);
318
+ font-weight: 500;
319
+ }
320
+
321
+ :global(.error-rate.warning) {
322
+ color: var(--usage-status-yellow, #f59e0b);
323
+ }
324
+
325
+ :global(.pct-value) {
326
+ font-variant-numeric: tabular-nums;
327
+ color: var(--usage-text-tertiary, #9ca3af);
328
+ }
329
+
330
+ /* Totals row */
331
+ .totals-row {
332
+ font-weight: 600;
333
+ background: var(--usage-bg-tertiary, #f9fafb);
334
+ }
335
+
336
+ .totals-row td {
337
+ border-top: 2px solid var(--usage-border-default, #e5e7eb);
338
+ border-bottom: none;
339
+ }
340
+
341
+ .totals-label {
342
+ text-transform: uppercase;
343
+ font-size: 0.75rem;
344
+ letter-spacing: 0.025em;
345
+ color: var(--usage-text-secondary, #6b7280);
346
+ }
347
+
348
+ /* Responsive */
349
+ @media (max-width: 640px) {
350
+ .breakdown-table {
351
+ font-size: 0.8125rem;
352
+ }
353
+
354
+ .breakdown-table th,
355
+ .breakdown-table td {
356
+ padding: 0.5rem 0.75rem;
357
+ }
358
+
359
+ .col-errors,
360
+ .col-pct {
361
+ display: none;
362
+ }
363
+ }
364
+
365
+ /* Dark mode support */
366
+ :global(.dark) .workers-breakdown-section {
367
+ background: var(--usage-bg-secondary-dark, #1f2937);
368
+ border-color: var(--usage-border-default-dark, #374151);
369
+ }
370
+
371
+ :global(.dark) .section-title {
372
+ color: var(--usage-text-primary-dark, #f9fafb);
373
+ }
374
+
375
+ :global(.dark) .breakdown-table th {
376
+ background: var(--usage-bg-tertiary-dark, #111827);
377
+ border-color: var(--usage-border-default-dark, #374151);
378
+ color: var(--usage-text-secondary-dark, #9ca3af);
379
+ }
380
+
381
+ :global(.dark) .breakdown-table td {
382
+ color: var(--usage-text-primary-dark, #f9fafb);
383
+ border-color: var(--usage-border-light-dark, #1f2937);
384
+ }
385
+
386
+ :global(.dark .project-row:hover),
387
+ :global(.dark .project-row.expanded) {
388
+ background: var(--usage-bg-hover-dark, #111827);
389
+ }
390
+
391
+ :global(.dark .worker-row) {
392
+ background: var(--usage-bg-tertiary-dark, #111827);
393
+ }
394
+
395
+ :global(.dark) .totals-row {
396
+ background: var(--usage-bg-tertiary-dark, #111827);
397
+ }
398
+ </style>
399
+
400
+ <script>
401
+ /**
402
+ * WorkersBreakdownTable JavaScript
403
+ *
404
+ * Handles data population and expand/collapse functionality.
405
+ * Uses safe DOM methods (createElement, textContent) instead of innerHTML.
406
+ */
407
+
408
+ interface WorkerData {
409
+ scriptName: string;
410
+ requests: number;
411
+ errors: number;
412
+ cpuTime: number;
413
+ }
414
+
415
+ interface ProjectBreakdown {
416
+ project: string;
417
+ totalRequests: number;
418
+ totalErrors: number;
419
+ workerCount: number;
420
+ workers: WorkerData[];
421
+ }
422
+
423
+ // State
424
+ let breakdownData: ProjectBreakdown[] = [];
425
+ let expandedProjects: Set<string> = new Set();
426
+
427
+ // Format large numbers
428
+ function formatNumber(num: number): string {
429
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(2)}B`;
430
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(2)}M`;
431
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
432
+ return num.toLocaleString();
433
+ }
434
+
435
+ // Calculate error rate
436
+ function calcErrorRate(errors: number, requests: number): string {
437
+ if (requests === 0) return '0%';
438
+ const rate = (errors / requests) * 100;
439
+ return rate < 0.01 ? '<0.01%' : `${rate.toFixed(2)}%`;
440
+ }
441
+
442
+ // Get error rate class
443
+ function getErrorRateClass(errors: number, requests: number): string {
444
+ if (requests === 0) return '';
445
+ const rate = (errors / requests) * 100;
446
+ if (rate >= 5) return 'high';
447
+ if (rate >= 1) return 'warning';
448
+ return '';
449
+ }
450
+
451
+ // Create a table row element
452
+ function createProjectRow(
453
+ project: ProjectBreakdown,
454
+ totalRequests: number,
455
+ isExpanded: boolean
456
+ ): HTMLTableRowElement {
457
+ const row = document.createElement('tr');
458
+ row.className = `project-row${isExpanded ? ' expanded' : ''}`;
459
+ row.dataset.project = project.project;
460
+
461
+ const pctOfTotal =
462
+ totalRequests > 0 ? ((project.totalRequests / totalRequests) * 100).toFixed(1) : '0';
463
+ const isOther = project.project === 'other' || project.project === 'Other';
464
+
465
+ // Expand button cell
466
+ const expandCell = document.createElement('td');
467
+ expandCell.className = 'col-expand';
468
+ const expandBtn = document.createElement('button');
469
+ expandBtn.className = `expand-btn${isExpanded ? ' expanded' : ''}`;
470
+ expandBtn.setAttribute('aria-expanded', String(isExpanded));
471
+ expandBtn.setAttribute('aria-label', `Expand ${project.project} workers`);
472
+ expandBtn.textContent = '▶';
473
+ expandCell.appendChild(expandBtn);
474
+ row.appendChild(expandCell);
475
+
476
+ // Project name cell
477
+ const projectCell = document.createElement('td');
478
+ projectCell.className = 'col-project';
479
+ const projectName = document.createElement('span');
480
+ projectName.className = 'project-name';
481
+ const badge = document.createElement('span');
482
+ badge.className = `project-badge${isOther ? ' other' : ''}`;
483
+ badge.textContent = project.project;
484
+ projectName.appendChild(badge);
485
+ projectCell.appendChild(projectName);
486
+ row.appendChild(projectCell);
487
+
488
+ // Workers count cell
489
+ const workersCell = document.createElement('td');
490
+ workersCell.className = 'col-workers';
491
+ workersCell.textContent = String(project.workerCount);
492
+ row.appendChild(workersCell);
493
+
494
+ // Requests cell
495
+ const requestsCell = document.createElement('td');
496
+ requestsCell.className = 'col-requests';
497
+ const requestsValue = document.createElement('span');
498
+ requestsValue.className = 'requests-value';
499
+ requestsValue.textContent = formatNumber(project.totalRequests);
500
+ requestsCell.appendChild(requestsValue);
501
+ row.appendChild(requestsCell);
502
+
503
+ // Error rate cell
504
+ const errorCell = document.createElement('td');
505
+ errorCell.className = 'col-errors';
506
+ const errorRate = document.createElement('span');
507
+ errorRate.className = `error-rate ${getErrorRateClass(project.totalErrors, project.totalRequests)}`;
508
+ errorRate.textContent = calcErrorRate(project.totalErrors, project.totalRequests);
509
+ errorCell.appendChild(errorRate);
510
+ row.appendChild(errorCell);
511
+
512
+ // Percentage cell
513
+ const pctCell = document.createElement('td');
514
+ pctCell.className = 'col-pct';
515
+ const pctValue = document.createElement('span');
516
+ pctValue.className = 'pct-value';
517
+ pctValue.textContent = `${pctOfTotal}%`;
518
+ pctCell.appendChild(pctValue);
519
+ row.appendChild(pctCell);
520
+
521
+ return row;
522
+ }
523
+
524
+ // Create a worker sub-row element
525
+ function createWorkerRow(worker: WorkerData, projectTotalRequests: number): HTMLTableRowElement {
526
+ const row = document.createElement('tr');
527
+ row.className = 'worker-row';
528
+
529
+ const workerPct =
530
+ projectTotalRequests > 0 ? ((worker.requests / projectTotalRequests) * 100).toFixed(1) : '0';
531
+
532
+ // Empty expand cell
533
+ const emptyCell1 = document.createElement('td');
534
+ row.appendChild(emptyCell1);
535
+
536
+ // Worker name cell
537
+ const nameCell = document.createElement('td');
538
+ nameCell.className = 'worker-name';
539
+ nameCell.textContent = worker.scriptName;
540
+ row.appendChild(nameCell);
541
+
542
+ // Empty workers count cell
543
+ const emptyCell2 = document.createElement('td');
544
+ row.appendChild(emptyCell2);
545
+
546
+ // Requests cell
547
+ const requestsCell = document.createElement('td');
548
+ requestsCell.className = 'col-requests';
549
+ const requestsValue = document.createElement('span');
550
+ requestsValue.className = 'requests-value';
551
+ requestsValue.textContent = formatNumber(worker.requests);
552
+ requestsCell.appendChild(requestsValue);
553
+ row.appendChild(requestsCell);
554
+
555
+ // Error rate cell
556
+ const errorCell = document.createElement('td');
557
+ errorCell.className = 'col-errors';
558
+ const errorRate = document.createElement('span');
559
+ errorRate.className = `error-rate ${getErrorRateClass(worker.errors, worker.requests)}`;
560
+ errorRate.textContent = calcErrorRate(worker.errors, worker.requests);
561
+ errorCell.appendChild(errorRate);
562
+ row.appendChild(errorCell);
563
+
564
+ // Percentage cell
565
+ const pctCell = document.createElement('td');
566
+ pctCell.className = 'col-pct';
567
+ const pctValue = document.createElement('span');
568
+ pctValue.className = 'pct-value';
569
+ pctValue.textContent = `${workerPct}%`;
570
+ pctCell.appendChild(pctValue);
571
+ row.appendChild(pctCell);
572
+
573
+ return row;
574
+ }
575
+
576
+ // Render table
577
+ function renderTable() {
578
+ const tbody = document.getElementById('workers-breakdown-tbody');
579
+ const table = document.getElementById('workers-breakdown-table');
580
+ const loading = document.getElementById('workers-breakdown-loading');
581
+ const empty = document.getElementById('workers-breakdown-empty');
582
+
583
+ if (!tbody || !table || !loading || !empty) return;
584
+
585
+ // Hide loading
586
+ loading.style.display = 'none';
587
+
588
+ // Check for empty data
589
+ if (breakdownData.length === 0) {
590
+ table.style.display = 'none';
591
+ empty.style.display = 'flex';
592
+ return;
593
+ }
594
+
595
+ // Show table
596
+ empty.style.display = 'none';
597
+ table.style.display = 'table';
598
+
599
+ // Calculate totals
600
+ const totalRequests = breakdownData.reduce((sum, p) => sum + p.totalRequests, 0);
601
+ const totalErrors = breakdownData.reduce((sum, p) => sum + p.totalErrors, 0);
602
+ const totalWorkers = breakdownData.reduce((sum, p) => sum + p.workerCount, 0);
603
+
604
+ // Clear tbody and rebuild using DOM methods
605
+ tbody.replaceChildren();
606
+
607
+ for (const project of breakdownData) {
608
+ const isExpanded = expandedProjects.has(project.project);
609
+
610
+ // Add project row
611
+ const projectRow = createProjectRow(project, totalRequests, isExpanded);
612
+ projectRow.addEventListener('click', () => {
613
+ if (expandedProjects.has(project.project)) {
614
+ expandedProjects.delete(project.project);
615
+ } else {
616
+ expandedProjects.add(project.project);
617
+ }
618
+ renderTable();
619
+ });
620
+ tbody.appendChild(projectRow);
621
+
622
+ // Add worker sub-rows if expanded
623
+ if (isExpanded) {
624
+ for (const worker of project.workers) {
625
+ const workerRow = createWorkerRow(worker, project.totalRequests);
626
+ tbody.appendChild(workerRow);
627
+ }
628
+ }
629
+ }
630
+
631
+ // Update totals using textContent (safe)
632
+ const totalWorkersEl = document.getElementById('total-workers');
633
+ const totalRequestsEl = document.getElementById('total-requests');
634
+ const totalErrorRateEl = document.getElementById('total-error-rate');
635
+
636
+ if (totalWorkersEl) totalWorkersEl.textContent = totalWorkers.toString();
637
+ if (totalRequestsEl) totalRequestsEl.textContent = formatNumber(totalRequests);
638
+ if (totalErrorRateEl) totalErrorRateEl.textContent = calcErrorRate(totalErrors, totalRequests);
639
+ }
640
+
641
+ // Public method to update data
642
+ window.updateWorkersBreakdown = function (data: ProjectBreakdown[]) {
643
+ breakdownData = data.sort((a, b) => b.totalRequests - a.totalRequests);
644
+ renderTable();
645
+ };
646
+
647
+ // Type declarations
648
+ declare global {
649
+ interface Window {
650
+ updateWorkersBreakdown: (data: ProjectBreakdown[]) => void;
651
+ }
652
+ }
653
+
654
+ // Initialize on load
655
+ document.addEventListener('DOMContentLoaded', () => {
656
+ // Initial render (will show loading state)
657
+ renderTable();
658
+ });
659
+ </script>