@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,135 @@
1
+ ---
2
+ /**
3
+ * UsageCard Component
4
+ *
5
+ * Displays a resource type usage card with primary metric and secondary items.
6
+ */
7
+
8
+ interface SecondaryItem {
9
+ label: string;
10
+ value: string;
11
+ }
12
+
13
+ interface Props {
14
+ label: string;
15
+ icon: string;
16
+ primaryValue: string;
17
+ primaryLabel: string;
18
+ secondaryItems?: SecondaryItem[];
19
+ }
20
+
21
+ const { label, icon, primaryValue, primaryLabel, secondaryItems = [] } = Astro.props;
22
+ ---
23
+
24
+ <div class="usage-card" role="region" aria-label={label}>
25
+ <div class="card-header">
26
+ <span class="card-icon" aria-hidden="true" set:html={icon} />
27
+ <span class="card-label">{label}</span>
28
+ </div>
29
+
30
+ <div class="card-primary">
31
+ <span class="primary-value">{primaryValue}</span>
32
+ <span class="primary-label">{primaryLabel}</span>
33
+ </div>
34
+
35
+ {
36
+ secondaryItems.length > 0 && (
37
+ <div class="card-secondary">
38
+ {secondaryItems.map((item) => (
39
+ <div class="secondary-item">
40
+ <span class="secondary-label">{item.label}</span>
41
+ <span class="secondary-value">{item.value}</span>
42
+ </div>
43
+ ))}
44
+ </div>
45
+ )
46
+ }
47
+ </div>
48
+
49
+ <style>
50
+ .usage-card {
51
+ background-color: white;
52
+ border: 1px solid #e5e7eb;
53
+ border-radius: 0.5rem;
54
+ padding: 1.25rem;
55
+ transition: all 0.15s ease;
56
+ }
57
+
58
+ .usage-card:hover {
59
+ border-color: #d1d5db;
60
+ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
61
+ }
62
+
63
+ .card-header {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 0.5rem;
67
+ margin-bottom: 1rem;
68
+ }
69
+
70
+ .card-icon {
71
+ font-size: 1.25rem;
72
+ line-height: 1;
73
+ }
74
+
75
+ .card-label {
76
+ font-size: 0.875rem;
77
+ font-weight: 600;
78
+ color: #374151;
79
+ }
80
+
81
+ .card-primary {
82
+ margin-bottom: 1rem;
83
+ }
84
+
85
+ .primary-value {
86
+ display: block;
87
+ font-size: 2rem;
88
+ font-weight: 700;
89
+ color: #1f2937;
90
+ line-height: 1;
91
+ margin-bottom: 0.25rem;
92
+ }
93
+
94
+ .primary-label {
95
+ font-size: 0.75rem;
96
+ color: #6b7280;
97
+ text-transform: uppercase;
98
+ letter-spacing: 0.05em;
99
+ }
100
+
101
+ .card-secondary {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: 0.5rem;
105
+ padding-top: 0.75rem;
106
+ border-top: 1px solid #f3f4f6;
107
+ }
108
+
109
+ .secondary-item {
110
+ display: flex;
111
+ justify-content: space-between;
112
+ align-items: center;
113
+ }
114
+
115
+ .secondary-label {
116
+ font-size: 0.75rem;
117
+ color: #6b7280;
118
+ }
119
+
120
+ .secondary-value {
121
+ font-size: 0.875rem;
122
+ font-weight: 600;
123
+ color: #374151;
124
+ }
125
+
126
+ @media (max-width: 640px) {
127
+ .usage-card {
128
+ padding: 1rem;
129
+ }
130
+
131
+ .primary-value {
132
+ font-size: 1.5rem;
133
+ }
134
+ }
135
+ </style>
@@ -0,0 +1,387 @@
1
+ ---
2
+ /**
3
+ * UsageHealthBanner - Top-level health status banner
4
+ *
5
+ * Shows combined health status for Cloudflare and GitHub with:
6
+ * - Colour-coded status pill for each provider
7
+ * - Warning count badges
8
+ * - Overall utilisation percentage
9
+ * - Quick visual at-a-glance health status
10
+ */
11
+ import type { UtilizationStatus } from './types';
12
+ import UtilizationBar from './UtilizationBar.astro';
13
+
14
+ interface ProviderHealth {
15
+ provider: 'cloudflare' | 'github';
16
+ percentage: number;
17
+ warnings: number;
18
+ status: UtilizationStatus;
19
+ }
20
+
21
+ interface Props {
22
+ /** Cloudflare health data */
23
+ cloudflare?: ProviderHealth;
24
+ /** GitHub health data */
25
+ github?: ProviderHealth;
26
+ /** Last updated timestamp */
27
+ lastUpdated?: string;
28
+ }
29
+
30
+ const { cloudflare, github, lastUpdated } = Astro.props;
31
+
32
+ // Calculate overall health (worst status wins)
33
+ const getOverallStatus = (): UtilizationStatus => {
34
+ const statuses: UtilizationStatus[] = [];
35
+ if (cloudflare) statuses.push(cloudflare.status);
36
+ if (github) statuses.push(github.status);
37
+
38
+ if (statuses.includes('overage')) return 'overage';
39
+ if (statuses.includes('critical')) return 'critical';
40
+ if (statuses.includes('warning')) return 'warning';
41
+ return 'ok';
42
+ };
43
+
44
+ const overallStatus = getOverallStatus();
45
+ const totalWarnings = (cloudflare?.warnings ?? 0) + (github?.warnings ?? 0);
46
+
47
+ const statusLabels: Record<UtilizationStatus, string> = {
48
+ ok: 'All Systems OK',
49
+ warning: 'Approaching Limits',
50
+ critical: 'Near Capacity',
51
+ overage: 'Over Limit',
52
+ };
53
+
54
+ const statusIcons: Record<UtilizationStatus, string> = {
55
+ ok: '&#x2705;',
56
+ warning: '&#x26A0;&#xFE0F;',
57
+ critical: '&#x1F6A8;',
58
+ overage: '&#x274C;',
59
+ };
60
+ ---
61
+
62
+ <div class="health-banner" id="usage-health-banner" data-status={overallStatus}>
63
+ <div class="health-header">
64
+ <div class="health-status">
65
+ <span class="status-icon" set:html={statusIcons[overallStatus]} />
66
+ <span class="status-label" id="health-status-label">{statusLabels[overallStatus]}</span>
67
+ {
68
+ totalWarnings > 0 && (
69
+ <span class="warning-badge" id="health-warning-count">
70
+ {totalWarnings} warning{totalWarnings !== 1 ? 's' : ''}
71
+ </span>
72
+ )
73
+ }
74
+ </div>
75
+ {
76
+ lastUpdated && (
77
+ <span class="last-updated" id="health-last-updated">
78
+ Updated: {lastUpdated}
79
+ </span>
80
+ )
81
+ }
82
+ </div>
83
+
84
+ <div class="providers-row">
85
+ {/* Cloudflare Provider */}
86
+ <div
87
+ class="provider-card"
88
+ id="health-cloudflare"
89
+ data-percentage={cloudflare?.percentage ?? 0}
90
+ data-status={cloudflare?.status ?? 'ok'}
91
+ >
92
+ <div class="provider-header">
93
+ <span class="provider-icon">☁️</span>
94
+ <span class="provider-name">Cloudflare</span>
95
+ {
96
+ cloudflare?.warnings && cloudflare.warnings > 0 && (
97
+ <span class="provider-warnings">{cloudflare.warnings}</span>
98
+ )
99
+ }
100
+ </div>
101
+ <UtilizationBar
102
+ percentage={cloudflare?.percentage ?? 0}
103
+ status={cloudflare?.status ?? 'ok'}
104
+ height={6}
105
+ showLabel
106
+ />
107
+ </div>
108
+
109
+ {/* GitHub Provider */}
110
+ <div
111
+ class="provider-card"
112
+ id="health-github"
113
+ data-percentage={github?.percentage ?? 0}
114
+ data-status={github?.status ?? 'ok'}
115
+ >
116
+ <div class="provider-header">
117
+ <span class="provider-icon">🐙</span>
118
+ <span class="provider-name">GitHub</span>
119
+ {
120
+ github?.warnings && github.warnings > 0 && (
121
+ <span class="provider-warnings">{github.warnings}</span>
122
+ )
123
+ }
124
+ </div>
125
+ <UtilizationBar
126
+ percentage={github?.percentage ?? 0}
127
+ status={github?.status ?? 'ok'}
128
+ height={6}
129
+ showLabel
130
+ />
131
+ </div>
132
+ </div>
133
+ </div>
134
+
135
+ <style>
136
+ .health-banner {
137
+ background: var(--color-surface, #ffffff);
138
+ border: 1px solid var(--color-border, #e5e7eb);
139
+ border-radius: 12px;
140
+ padding: 16px 20px;
141
+ margin-bottom: 20px;
142
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
143
+ }
144
+
145
+ :global(.dark) .health-banner {
146
+ background: var(--color-surface-dark, #1f2937);
147
+ border-color: var(--color-border-dark, #374151);
148
+ }
149
+
150
+ .health-banner[data-status='ok'] {
151
+ border-left: 4px solid var(--color-success, #22c55e);
152
+ }
153
+
154
+ .health-banner[data-status='warning'] {
155
+ border-left: 4px solid var(--color-warning, #eab308);
156
+ }
157
+
158
+ .health-banner[data-status='critical'],
159
+ .health-banner[data-status='overage'] {
160
+ border-left: 4px solid var(--color-critical, #ef4444);
161
+ }
162
+
163
+ .health-header {
164
+ display: flex;
165
+ justify-content: space-between;
166
+ align-items: center;
167
+ margin-bottom: 12px;
168
+ }
169
+
170
+ .health-status {
171
+ display: flex;
172
+ align-items: center;
173
+ gap: 8px;
174
+ }
175
+
176
+ .status-icon {
177
+ font-size: 18px;
178
+ }
179
+
180
+ .status-label {
181
+ font-size: 16px;
182
+ font-weight: 600;
183
+ color: var(--color-text, #111827);
184
+ }
185
+
186
+ :global(.dark) .status-label {
187
+ color: var(--color-text-dark, #f9fafb);
188
+ }
189
+
190
+ .warning-badge {
191
+ background: var(--color-warning-bg, #fef3c7);
192
+ color: var(--color-warning-text, #92400e);
193
+ font-size: 12px;
194
+ font-weight: 500;
195
+ padding: 2px 8px;
196
+ border-radius: 9999px;
197
+ }
198
+
199
+ :global(.dark) .warning-badge {
200
+ background: rgba(234, 179, 8, 0.2);
201
+ color: #fcd34d;
202
+ }
203
+
204
+ .last-updated {
205
+ font-size: 12px;
206
+ color: var(--color-text-secondary, #6b7280);
207
+ }
208
+
209
+ :global(.dark) .last-updated {
210
+ color: var(--color-text-secondary-dark, #9ca3af);
211
+ }
212
+
213
+ .providers-row {
214
+ display: grid;
215
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
216
+ gap: 16px;
217
+ }
218
+
219
+ .provider-card {
220
+ background: var(--color-surface-secondary, #f9fafb);
221
+ border-radius: 8px;
222
+ padding: 12px;
223
+ }
224
+
225
+ :global(.dark) .provider-card {
226
+ background: var(--color-surface-secondary-dark, #111827);
227
+ }
228
+
229
+ .provider-header {
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 6px;
233
+ margin-bottom: 8px;
234
+ }
235
+
236
+ .provider-icon {
237
+ font-size: 16px;
238
+ }
239
+
240
+ .provider-name {
241
+ font-size: 14px;
242
+ font-weight: 500;
243
+ color: var(--color-text, #111827);
244
+ flex: 1;
245
+ }
246
+
247
+ :global(.dark) .provider-name {
248
+ color: var(--color-text-dark, #f9fafb);
249
+ }
250
+
251
+ .provider-warnings {
252
+ background: var(--color-critical, #ef4444);
253
+ color: white;
254
+ font-size: 11px;
255
+ font-weight: 600;
256
+ padding: 2px 6px;
257
+ border-radius: 9999px;
258
+ min-width: 18px;
259
+ text-align: center;
260
+ }
261
+
262
+ /* Skeleton loading state */
263
+ .health-banner.loading .status-label,
264
+ .health-banner.loading .provider-name {
265
+ background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%);
266
+ background-size: 200% 100%;
267
+ animation: shimmer 1.5s infinite;
268
+ color: transparent;
269
+ border-radius: 4px;
270
+ }
271
+
272
+ @keyframes shimmer {
273
+ 0% {
274
+ background-position: 200% 0;
275
+ }
276
+ 100% {
277
+ background-position: -200% 0;
278
+ }
279
+ }
280
+ </style>
281
+
282
+ <script>
283
+ /**
284
+ * Update health banner with new data
285
+ * Called from usage-overview-controller.ts
286
+ */
287
+ interface ProviderHealth {
288
+ provider: 'cloudflare' | 'github';
289
+ percentage: number;
290
+ warnings: number;
291
+ status: 'ok' | 'warning' | 'critical' | 'overage';
292
+ }
293
+
294
+ interface HealthData {
295
+ cloudflare: ProviderHealth;
296
+ github: ProviderHealth;
297
+ lastUpdated?: string;
298
+ }
299
+
300
+ const statusLabels: Record<string, string> = {
301
+ ok: 'All Systems OK',
302
+ warning: 'Approaching Limits',
303
+ critical: 'Near Capacity',
304
+ overage: 'Over Limit',
305
+ };
306
+
307
+ const statusIcons: Record<string, string> = {
308
+ ok: '&#x2705;',
309
+ warning: '&#x26A0;&#xFE0F;',
310
+ critical: '&#x1F6A8;',
311
+ overage: '&#x274C;',
312
+ };
313
+
314
+ (window as unknown as { updateHealthBanner: (data: HealthData) => void }).updateHealthBanner = (
315
+ data: HealthData
316
+ ) => {
317
+ const banner = document.getElementById('usage-health-banner');
318
+ if (!banner) return;
319
+
320
+ // Calculate overall status
321
+ const statuses = [data.cloudflare.status, data.github.status];
322
+ let overallStatus = 'ok';
323
+ if (statuses.includes('overage')) overallStatus = 'overage';
324
+ else if (statuses.includes('critical')) overallStatus = 'critical';
325
+ else if (statuses.includes('warning')) overallStatus = 'warning';
326
+
327
+ banner.dataset.status = overallStatus;
328
+
329
+ // Update status label
330
+ const statusLabel = document.getElementById('health-status-label');
331
+ if (statusLabel) {
332
+ statusLabel.textContent = statusLabels[overallStatus];
333
+ }
334
+
335
+ // Update warning count
336
+ const totalWarnings = data.cloudflare.warnings + data.github.warnings;
337
+ const warningBadge = document.getElementById('health-warning-count');
338
+ if (warningBadge) {
339
+ if (totalWarnings > 0) {
340
+ warningBadge.textContent = `${totalWarnings} warning${totalWarnings !== 1 ? 's' : ''}`;
341
+ warningBadge.style.display = '';
342
+ } else {
343
+ warningBadge.style.display = 'none';
344
+ }
345
+ }
346
+
347
+ // Update last updated
348
+ if (data.lastUpdated) {
349
+ const lastUpdated = document.getElementById('health-last-updated');
350
+ if (lastUpdated) {
351
+ lastUpdated.textContent = `Updated: ${data.lastUpdated}`;
352
+ }
353
+ }
354
+
355
+ // Update Cloudflare card
356
+ const cfCard = document.getElementById('health-cloudflare');
357
+ if (cfCard) {
358
+ cfCard.dataset.percentage = String(data.cloudflare.percentage);
359
+ cfCard.dataset.status = data.cloudflare.status;
360
+ const cfBar = cfCard.querySelector('.utilization-bar-fill') as HTMLElement;
361
+ if (cfBar) {
362
+ cfBar.style.width = `${Math.min(data.cloudflare.percentage, 100)}%`;
363
+ }
364
+ const cfLabel = cfCard.querySelector('.utilization-bar-label');
365
+ if (cfLabel) {
366
+ cfLabel.textContent = `${data.cloudflare.percentage.toFixed(0)}%`;
367
+ }
368
+ }
369
+
370
+ // Update GitHub card
371
+ const ghCard = document.getElementById('health-github');
372
+ if (ghCard) {
373
+ ghCard.dataset.percentage = String(data.github.percentage);
374
+ ghCard.dataset.status = data.github.status;
375
+ const ghBar = ghCard.querySelector('.utilization-bar-fill') as HTMLElement;
376
+ if (ghBar) {
377
+ ghBar.style.width = `${Math.min(data.github.percentage, 100)}%`;
378
+ }
379
+ const ghLabel = ghCard.querySelector('.utilization-bar-label');
380
+ if (ghLabel) {
381
+ ghLabel.textContent = `${data.github.percentage.toFixed(0)}%`;
382
+ }
383
+ }
384
+
385
+ banner.classList.remove('loading');
386
+ };
387
+ </script>
@@ -0,0 +1,159 @@
1
+ ---
2
+ /**
3
+ * UtilizationBar - Reusable progress bar for utilization display
4
+ *
5
+ * Displays a progress bar with:
6
+ * - Colour-coded status (green/yellow/red based on percentage)
7
+ * - Optional label showing percentage
8
+ * - Animated fill on mount
9
+ * - Overage indicator (>100%)
10
+ */
11
+ import type { UtilizationStatus } from './types';
12
+
13
+ interface Props {
14
+ /** Current percentage (0-100+, can exceed for overage) */
15
+ percentage: number;
16
+ /** Status for colour coding */
17
+ status: UtilizationStatus;
18
+ /** Optional height in pixels (default: 8) */
19
+ height?: number;
20
+ /** Show percentage label */
21
+ showLabel?: boolean;
22
+ /** Custom CSS class */
23
+ class?: string;
24
+ }
25
+
26
+ const { percentage, status, height = 8, showLabel = false, class: className = '' } = Astro.props;
27
+
28
+ // Clamp display percentage to 100 for bar width, but track overage
29
+ const displayPct = Math.min(percentage, 100);
30
+ const hasOverage = percentage > 100;
31
+
32
+ // Status colours
33
+ const statusColours: Record<UtilizationStatus, string> = {
34
+ ok: 'var(--color-success, #22c55e)',
35
+ warning: 'var(--color-warning, #eab308)',
36
+ critical: 'var(--color-critical, #ef4444)',
37
+ overage: 'var(--color-overage, #dc2626)',
38
+ };
39
+
40
+ const barColour = statusColours[status];
41
+ ---
42
+
43
+ <div
44
+ class:list={['utilization-bar-container', className]}
45
+ data-percentage={percentage}
46
+ data-status={status}
47
+ >
48
+ <div class="utilization-bar-track" style={`height: ${height}px;`}>
49
+ <div
50
+ class="utilization-bar-fill"
51
+ style={`
52
+ width: ${displayPct}%;
53
+ background-color: ${barColour};
54
+ `}
55
+ role="progressbar"
56
+ aria-valuenow={percentage}
57
+ aria-valuemin="0"
58
+ aria-valuemax="100"
59
+ >
60
+ </div>
61
+ {
62
+ hasOverage && (
63
+ <div class="utilization-bar-overage-indicator">
64
+ <span class="overage-stripes" />
65
+ </div>
66
+ )
67
+ }
68
+ </div>
69
+ {
70
+ showLabel && (
71
+ <span
72
+ class="utilization-bar-label"
73
+ class:list={{ overage: hasOverage, [`status-${status}`]: true }}
74
+ >
75
+ {percentage.toFixed(0)}%
76
+ </span>
77
+ )
78
+ }
79
+ </div>
80
+
81
+ <style>
82
+ .utilization-bar-container {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 8px;
86
+ width: 100%;
87
+ }
88
+
89
+ .utilization-bar-track {
90
+ flex: 1;
91
+ background-color: var(--color-border, #e5e7eb);
92
+ border-radius: 4px;
93
+ overflow: hidden;
94
+ position: relative;
95
+ }
96
+
97
+ :global(.dark) .utilization-bar-track {
98
+ background-color: var(--color-border-dark, #374151);
99
+ }
100
+
101
+ .utilization-bar-fill {
102
+ height: 100%;
103
+ border-radius: 4px;
104
+ transition: width 0.3s ease-out;
105
+ min-width: 2px;
106
+ }
107
+
108
+ .utilization-bar-overage-indicator {
109
+ position: absolute;
110
+ top: 0;
111
+ right: 0;
112
+ width: 8px;
113
+ height: 100%;
114
+ display: flex;
115
+ align-items: center;
116
+ justify-content: center;
117
+ }
118
+
119
+ .overage-stripes {
120
+ width: 100%;
121
+ height: 100%;
122
+ background: repeating-linear-gradient(
123
+ 45deg,
124
+ transparent,
125
+ transparent 2px,
126
+ rgba(220, 38, 38, 0.5) 2px,
127
+ rgba(220, 38, 38, 0.5) 4px
128
+ );
129
+ }
130
+
131
+ .utilization-bar-label {
132
+ font-size: 12px;
133
+ font-weight: 500;
134
+ min-width: 40px;
135
+ text-align: right;
136
+ color: var(--color-text-secondary, #6b7280);
137
+ }
138
+
139
+ .utilization-bar-label.status-ok {
140
+ color: var(--color-success, #22c55e);
141
+ }
142
+
143
+ .utilization-bar-label.status-warning {
144
+ color: var(--color-warning, #eab308);
145
+ }
146
+
147
+ .utilization-bar-label.status-critical,
148
+ .utilization-bar-label.status-overage {
149
+ color: var(--color-critical, #ef4444);
150
+ }
151
+
152
+ .utilization-bar-label.overage {
153
+ font-weight: 600;
154
+ }
155
+
156
+ :global(.dark) .utilization-bar-label {
157
+ color: var(--color-text-secondary-dark, #9ca3af);
158
+ }
159
+ </style>