@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,640 @@
1
+ ---
2
+ /**
3
+ * ProjectCard Component
4
+ *
5
+ * Individual project card for the usage dashboard displaying:
6
+ * - Project name and icon
7
+ * - MTD Cost (USD)
8
+ * - Utilization bar showing usage vs. primary Cloudflare resource limit
9
+ * - Traffic light status (Green <70%, Yellow 70-90%, Red >90%)
10
+ * - 7-day sparkline trend from hourly_usage_snapshots
11
+ * - Circuit breaker status indicator
12
+ *
13
+ * Props are optional - data is populated via JavaScript after API fetch.
14
+ */
15
+
16
+ interface Props {
17
+ projectId?: string;
18
+ projectName?: string;
19
+ primaryResource?: string;
20
+ }
21
+
22
+ const { projectId = '', projectName = '', primaryResource = '' } = Astro.props;
23
+ ---
24
+
25
+ <div
26
+ class="project-card"
27
+ data-project-id={projectId}
28
+ role="article"
29
+ aria-label={`${projectName} project usage`}
30
+ >
31
+ <!-- Card Header -->
32
+ <div class="card-header">
33
+ <div class="project-identity">
34
+ <span class="project-icon" data-project={projectId}></span>
35
+ <div class="project-info">
36
+ <h3 class="project-name">{projectName}</h3>
37
+ <span class="project-resource">Primary: {primaryResource}</span>
38
+ </div>
39
+ </div>
40
+ <div class="traffic-light" data-status="green">
41
+ <div class="traffic-dot"></div>
42
+ </div>
43
+ </div>
44
+
45
+ <!-- MTD Cost -->
46
+ <div class="card-cost">
47
+ <span class="cost-label">MTD Cost</span>
48
+ <div class="cost-row">
49
+ <span class="cost-value">$0.00</span>
50
+ <span class="cost-delta"></span>
51
+ </div>
52
+ </div>
53
+
54
+ <!-- Utilization Bar -->
55
+ <div class="card-utilization">
56
+ <div class="utilization-header">
57
+ <span class="utilization-label">Utilization</span>
58
+ <span class="utilization-value">0%</span>
59
+ </div>
60
+ <div class="utilization-bar-container">
61
+ <div class="utilization-bar" style="width: 0%;">
62
+ <div class="utilization-fill"></div>
63
+ </div>
64
+ <div class="utilization-markers">
65
+ <div class="marker marker-70" title="70% threshold"></div>
66
+ <div class="marker marker-90" title="90% threshold"></div>
67
+ </div>
68
+ </div>
69
+ <div class="utilization-meta">
70
+ <span class="utilization-current">0 / 0</span>
71
+ <span class="utilization-unit"></span>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- 7-Day Sparkline -->
76
+ <div class="card-sparkline">
77
+ <div class="sparkline-header">
78
+ <span class="sparkline-label">7-Day Trend</span>
79
+ </div>
80
+ <canvas class="sparkline-canvas" width="180" height="40" aria-label="7-day usage trend"
81
+ ></canvas>
82
+ </div>
83
+
84
+ <!-- Circuit Breaker Status -->
85
+ <div class="card-circuit-breaker">
86
+ <div class="cb-indicator" data-status="active">
87
+ <span class="cb-dot"></span>
88
+ <span class="cb-label">Circuit OK</span>
89
+ </div>
90
+ </div>
91
+ </div>
92
+
93
+ <style>
94
+ .project-card {
95
+ background: var(--usage-bg-secondary);
96
+ border: 1px solid var(--usage-border-default);
97
+ border-radius: var(--usage-radius-xl);
98
+ padding: 1.25rem;
99
+ display: flex;
100
+ flex-direction: column;
101
+ gap: 1rem;
102
+ transition: all var(--usage-transition-normal);
103
+ position: relative;
104
+ overflow: hidden;
105
+ }
106
+
107
+ .project-card:hover {
108
+ border-color: var(--usage-border-hover);
109
+ box-shadow: var(--usage-shadow-md);
110
+ }
111
+
112
+ .project-card[data-status='red'] {
113
+ border-color: var(--usage-status-critical);
114
+ background: linear-gradient(
115
+ 135deg,
116
+ var(--usage-bg-secondary) 0%,
117
+ var(--usage-status-critical-bg) 100%
118
+ );
119
+ }
120
+
121
+ .project-card[data-status='yellow'] {
122
+ border-color: var(--usage-status-warning);
123
+ background: linear-gradient(
124
+ 135deg,
125
+ var(--usage-bg-secondary) 0%,
126
+ var(--usage-status-warning-bg) 100%
127
+ );
128
+ }
129
+
130
+ /* Card Header */
131
+ .card-header {
132
+ display: flex;
133
+ justify-content: space-between;
134
+ align-items: flex-start;
135
+ }
136
+
137
+ .project-identity {
138
+ display: flex;
139
+ align-items: center;
140
+ gap: 0.75rem;
141
+ }
142
+
143
+ .project-icon {
144
+ width: 40px;
145
+ height: 40px;
146
+ border-radius: var(--usage-radius-lg);
147
+ background: var(--usage-bg-tertiary);
148
+ display: flex;
149
+ align-items: center;
150
+ justify-content: center;
151
+ font-size: 1.25rem;
152
+ }
153
+
154
+ .project-icon[data-project='scout']::before {
155
+ content: '🔍';
156
+ }
157
+ .project-icon[data-project='brand-copilot']::before {
158
+ content: '🎨';
159
+ }
160
+ .project-icon[data-project='australian-history-mcp']::before {
161
+ content: '📚';
162
+ }
163
+ .project-icon[data-project='platform']::before {
164
+ content: '🏗️';
165
+ }
166
+
167
+ .project-info {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 0.125rem;
171
+ }
172
+
173
+ .project-name {
174
+ font-size: 1rem;
175
+ font-weight: 600;
176
+ color: var(--usage-text-primary);
177
+ margin: 0;
178
+ line-height: 1.2;
179
+ }
180
+
181
+ .project-resource {
182
+ font-size: 0.6875rem;
183
+ color: var(--usage-text-muted);
184
+ text-transform: uppercase;
185
+ letter-spacing: 0.03em;
186
+ }
187
+
188
+ /* Traffic Light */
189
+ .traffic-light {
190
+ width: 24px;
191
+ height: 24px;
192
+ border-radius: 50%;
193
+ display: flex;
194
+ align-items: center;
195
+ justify-content: center;
196
+ background: var(--usage-bg-tertiary);
197
+ border: 2px solid var(--usage-border-default);
198
+ }
199
+
200
+ .traffic-dot {
201
+ width: 12px;
202
+ height: 12px;
203
+ border-radius: 50%;
204
+ transition: all var(--usage-transition-normal);
205
+ }
206
+
207
+ .traffic-light[data-status='green'] {
208
+ border-color: var(--usage-status-ok);
209
+ }
210
+ .traffic-light[data-status='green'] .traffic-dot {
211
+ background: var(--usage-status-ok);
212
+ box-shadow: 0 0 8px var(--usage-status-ok);
213
+ }
214
+
215
+ .traffic-light[data-status='yellow'] {
216
+ border-color: var(--usage-status-warning);
217
+ }
218
+ .traffic-light[data-status='yellow'] .traffic-dot {
219
+ background: var(--usage-status-warning);
220
+ box-shadow: 0 0 8px var(--usage-status-warning);
221
+ }
222
+
223
+ .traffic-light[data-status='red'] {
224
+ border-color: var(--usage-status-critical);
225
+ }
226
+ .traffic-light[data-status='red'] .traffic-dot {
227
+ background: var(--usage-status-critical);
228
+ box-shadow: 0 0 8px var(--usage-status-critical);
229
+ }
230
+
231
+ /* MTD Cost */
232
+ .card-cost {
233
+ padding: 0.75rem;
234
+ background: var(--usage-bg-tertiary);
235
+ border-radius: var(--usage-radius-lg);
236
+ }
237
+
238
+ .cost-label {
239
+ font-size: 0.625rem;
240
+ font-weight: 600;
241
+ text-transform: uppercase;
242
+ letter-spacing: 0.05em;
243
+ color: var(--usage-text-muted);
244
+ }
245
+
246
+ .cost-row {
247
+ display: flex;
248
+ align-items: baseline;
249
+ gap: 0.5rem;
250
+ margin-top: 0.25rem;
251
+ }
252
+
253
+ .cost-value {
254
+ font-family: var(--usage-font-mono);
255
+ font-size: 1.5rem;
256
+ font-weight: 700;
257
+ color: var(--usage-text-primary);
258
+ line-height: 1;
259
+ }
260
+
261
+ .cost-delta {
262
+ font-size: 0.75rem;
263
+ font-weight: 600;
264
+ padding: 0.125rem 0.375rem;
265
+ border-radius: var(--usage-radius-sm);
266
+ }
267
+
268
+ .cost-delta.up {
269
+ background: var(--usage-status-critical-bg);
270
+ color: var(--usage-status-critical);
271
+ }
272
+
273
+ .cost-delta.down {
274
+ background: var(--usage-status-ok-bg);
275
+ color: var(--usage-status-ok);
276
+ }
277
+
278
+ /* Utilization Bar */
279
+ .card-utilization {
280
+ display: flex;
281
+ flex-direction: column;
282
+ gap: 0.375rem;
283
+ }
284
+
285
+ .utilization-header {
286
+ display: flex;
287
+ justify-content: space-between;
288
+ align-items: center;
289
+ }
290
+
291
+ .utilization-label {
292
+ font-size: 0.6875rem;
293
+ font-weight: 600;
294
+ text-transform: uppercase;
295
+ letter-spacing: 0.03em;
296
+ color: var(--usage-text-muted);
297
+ }
298
+
299
+ .utilization-value {
300
+ font-family: var(--usage-font-mono);
301
+ font-size: 0.875rem;
302
+ font-weight: 700;
303
+ color: var(--usage-text-primary);
304
+ }
305
+
306
+ .utilization-bar-container {
307
+ position: relative;
308
+ height: 12px;
309
+ background: var(--usage-bg-tertiary);
310
+ border-radius: var(--usage-radius-md);
311
+ overflow: visible;
312
+ }
313
+
314
+ .utilization-bar {
315
+ height: 100%;
316
+ border-radius: var(--usage-radius-md);
317
+ transition: width var(--usage-transition-slow);
318
+ overflow: hidden;
319
+ }
320
+
321
+ .utilization-fill {
322
+ height: 100%;
323
+ background: linear-gradient(
324
+ 90deg,
325
+ var(--usage-status-ok) 0%,
326
+ var(--usage-status-ok) 70%,
327
+ var(--usage-status-warning) 70%,
328
+ var(--usage-status-warning) 90%,
329
+ var(--usage-status-critical) 90%
330
+ );
331
+ background-size: 143% 100%; /* 100/70 to align gradient with fill */
332
+ }
333
+
334
+ .utilization-markers {
335
+ position: absolute;
336
+ top: 0;
337
+ left: 0;
338
+ right: 0;
339
+ bottom: 0;
340
+ pointer-events: none;
341
+ }
342
+
343
+ .marker {
344
+ position: absolute;
345
+ top: -2px;
346
+ bottom: -2px;
347
+ width: 2px;
348
+ background: var(--usage-bg-primary);
349
+ opacity: 0.6;
350
+ }
351
+
352
+ .marker-70 {
353
+ left: 70%;
354
+ }
355
+
356
+ .marker-90 {
357
+ left: 90%;
358
+ }
359
+
360
+ .utilization-meta {
361
+ display: flex;
362
+ justify-content: space-between;
363
+ align-items: center;
364
+ }
365
+
366
+ .utilization-current {
367
+ font-family: var(--usage-font-mono);
368
+ font-size: 0.6875rem;
369
+ color: var(--usage-text-secondary);
370
+ }
371
+
372
+ .utilization-unit {
373
+ font-size: 0.6875rem;
374
+ color: var(--usage-text-muted);
375
+ }
376
+
377
+ /* Sparkline */
378
+ .card-sparkline {
379
+ border-top: 1px solid var(--usage-border-subtle);
380
+ padding-top: 0.75rem;
381
+ }
382
+
383
+ .sparkline-header {
384
+ margin-bottom: 0.375rem;
385
+ }
386
+
387
+ .sparkline-label {
388
+ font-size: 0.625rem;
389
+ font-weight: 600;
390
+ text-transform: uppercase;
391
+ letter-spacing: 0.05em;
392
+ color: var(--usage-text-muted);
393
+ }
394
+
395
+ .sparkline-canvas {
396
+ width: 100%;
397
+ height: 40px;
398
+ }
399
+
400
+ /* Circuit Breaker */
401
+ .card-circuit-breaker {
402
+ border-top: 1px solid var(--usage-border-subtle);
403
+ padding-top: 0.5rem;
404
+ }
405
+
406
+ .cb-indicator {
407
+ display: flex;
408
+ align-items: center;
409
+ gap: 0.375rem;
410
+ }
411
+
412
+ .cb-dot {
413
+ width: 8px;
414
+ height: 8px;
415
+ border-radius: 50%;
416
+ transition: all var(--usage-transition-normal);
417
+ }
418
+
419
+ .cb-indicator[data-status='active'] .cb-dot {
420
+ background: var(--usage-status-ok);
421
+ }
422
+
423
+ .cb-indicator[data-status='tripped'] .cb-dot {
424
+ background: var(--usage-status-critical);
425
+ animation: pulse 1s ease-in-out infinite;
426
+ }
427
+
428
+ .cb-indicator[data-status='degraded'] .cb-dot {
429
+ background: var(--usage-status-warning);
430
+ }
431
+
432
+ @keyframes pulse {
433
+ 0%,
434
+ 100% {
435
+ opacity: 1;
436
+ }
437
+ 50% {
438
+ opacity: 0.5;
439
+ }
440
+ }
441
+
442
+ .cb-label {
443
+ font-size: 0.6875rem;
444
+ color: var(--usage-text-muted);
445
+ }
446
+
447
+ /* Responsive */
448
+ @media (max-width: 480px) {
449
+ .project-card {
450
+ padding: 1rem;
451
+ }
452
+
453
+ .cost-value {
454
+ font-size: 1.25rem;
455
+ }
456
+
457
+ .project-icon {
458
+ width: 32px;
459
+ height: 32px;
460
+ font-size: 1rem;
461
+ }
462
+ }
463
+ </style>
464
+
465
+ <script>
466
+ /**
467
+ * ProjectCard JavaScript
468
+ *
469
+ * Handles sparkline drawing and data updates.
470
+ */
471
+
472
+ interface ProjectCardData {
473
+ projectId: string;
474
+ projectName: string;
475
+ primaryResource: string;
476
+ mtdCost: number;
477
+ costDelta?: number;
478
+ costDeltaPct?: number;
479
+ utilizationPct: number;
480
+ utilizationCurrent: number;
481
+ utilizationLimit: number;
482
+ utilizationUnit: string;
483
+ status: 'green' | 'yellow' | 'red';
484
+ sparklineData: number[];
485
+ circuitBreakerStatus: 'active' | 'tripped' | 'degraded';
486
+ circuitBreakerLabel: string;
487
+ }
488
+
489
+ // Draw sparkline on canvas
490
+ function drawCardSparkline(
491
+ canvas: HTMLCanvasElement,
492
+ data: number[],
493
+ status: 'green' | 'yellow' | 'red'
494
+ ) {
495
+ const ctx = canvas.getContext('2d');
496
+ if (!ctx || data.length < 2) return;
497
+
498
+ const width = canvas.width;
499
+ const height = canvas.height;
500
+ const padding = 4;
501
+
502
+ ctx.clearRect(0, 0, width, height);
503
+
504
+ const min = Math.min(...data);
505
+ const max = Math.max(...data);
506
+ const range = max - min || 1;
507
+
508
+ const xStep = (width - padding * 2) / (data.length - 1);
509
+
510
+ // Color based on status
511
+ const colors = {
512
+ green:
513
+ getComputedStyle(document.documentElement).getPropertyValue('--usage-status-ok').trim() ||
514
+ '#3fb950',
515
+ yellow:
516
+ getComputedStyle(document.documentElement)
517
+ .getPropertyValue('--usage-status-warning')
518
+ .trim() || '#d29922',
519
+ red:
520
+ getComputedStyle(document.documentElement)
521
+ .getPropertyValue('--usage-status-critical')
522
+ .trim() || '#f85149',
523
+ };
524
+ const color = colors[status];
525
+
526
+ // Draw line
527
+ ctx.beginPath();
528
+ ctx.strokeStyle = color;
529
+ ctx.lineWidth = 2;
530
+ ctx.lineCap = 'round';
531
+ ctx.lineJoin = 'round';
532
+
533
+ data.forEach((value, index) => {
534
+ const x = padding + index * xStep;
535
+ const y = height - padding - ((value - min) / range) * (height - padding * 2);
536
+
537
+ if (index === 0) {
538
+ ctx.moveTo(x, y);
539
+ } else {
540
+ ctx.lineTo(x, y);
541
+ }
542
+ });
543
+
544
+ ctx.stroke();
545
+
546
+ // Draw gradient fill
547
+ const gradient = ctx.createLinearGradient(0, 0, 0, height);
548
+ gradient.addColorStop(0, color + '30');
549
+ gradient.addColorStop(1, color + '00');
550
+
551
+ ctx.lineTo(padding + (data.length - 1) * xStep, height);
552
+ ctx.lineTo(padding, height);
553
+ ctx.closePath();
554
+ ctx.fillStyle = gradient;
555
+ ctx.fill();
556
+ }
557
+
558
+ // Format large numbers
559
+ function formatNumber(n: number): string {
560
+ if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
561
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
562
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
563
+ return n.toLocaleString();
564
+ }
565
+
566
+ // Update a single project card
567
+ window.updateProjectCard = function (data: ProjectCardData) {
568
+ const card = document.querySelector(`.project-card[data-project-id="${data.projectId}"]`);
569
+ if (!card) return;
570
+
571
+ // Update card status
572
+ card.setAttribute('data-status', data.status);
573
+
574
+ // Project name and resource (if changed)
575
+ const nameEl = card.querySelector('.project-name');
576
+ const resourceEl = card.querySelector('.project-resource');
577
+ if (nameEl) nameEl.textContent = data.projectName;
578
+ if (resourceEl) resourceEl.textContent = `Primary: ${data.primaryResource}`;
579
+
580
+ // Traffic light
581
+ const trafficLight = card.querySelector('.traffic-light');
582
+ if (trafficLight) {
583
+ trafficLight.setAttribute('data-status', data.status);
584
+ }
585
+
586
+ // MTD Cost
587
+ const costValueEl = card.querySelector('.cost-value');
588
+ const costDeltaEl = card.querySelector('.cost-delta');
589
+ if (costValueEl) {
590
+ costValueEl.textContent = `$${data.mtdCost.toFixed(2)}`;
591
+ }
592
+ if (costDeltaEl && data.costDeltaPct !== undefined) {
593
+ const isUp = data.costDeltaPct > 0;
594
+ costDeltaEl.className = 'cost-delta ' + (isUp ? 'up' : 'down');
595
+ costDeltaEl.textContent = `${isUp ? '+' : ''}${data.costDeltaPct.toFixed(1)}%`;
596
+ }
597
+
598
+ // Utilization
599
+ const utilValueEl = card.querySelector('.utilization-value');
600
+ const utilBarEl = card.querySelector('.utilization-bar') as HTMLElement;
601
+ const utilCurrentEl = card.querySelector('.utilization-current');
602
+ const utilUnitEl = card.querySelector('.utilization-unit');
603
+
604
+ if (utilValueEl) {
605
+ utilValueEl.textContent = `${Math.min(data.utilizationPct, 999).toFixed(1)}%`;
606
+ }
607
+ if (utilBarEl) {
608
+ utilBarEl.style.width = `${Math.min(data.utilizationPct, 100)}%`;
609
+ }
610
+ if (utilCurrentEl) {
611
+ utilCurrentEl.textContent = `${formatNumber(data.utilizationCurrent)} / ${formatNumber(data.utilizationLimit)}`;
612
+ }
613
+ if (utilUnitEl) {
614
+ utilUnitEl.textContent = data.utilizationUnit;
615
+ }
616
+
617
+ // Sparkline
618
+ const canvas = card.querySelector('.sparkline-canvas') as HTMLCanvasElement;
619
+ if (canvas && data.sparklineData && data.sparklineData.length > 1) {
620
+ drawCardSparkline(canvas, data.sparklineData, data.status);
621
+ }
622
+
623
+ // Circuit breaker
624
+ const cbIndicator = card.querySelector('.cb-indicator');
625
+ const cbLabel = card.querySelector('.cb-label');
626
+ if (cbIndicator) {
627
+ cbIndicator.setAttribute('data-status', data.circuitBreakerStatus);
628
+ }
629
+ if (cbLabel) {
630
+ cbLabel.textContent = data.circuitBreakerLabel;
631
+ }
632
+ };
633
+
634
+ // Type declaration for global window
635
+ declare global {
636
+ interface Window {
637
+ updateProjectCard: (data: ProjectCardData) => void;
638
+ }
639
+ }
640
+ </script>