@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,381 @@
1
+ ---
2
+ /**
3
+ * CostBreakdownChart.astro
4
+ *
5
+ * Horizontal bar chart showing cost breakdown by resource type.
6
+ * - Sorted by cost descending
7
+ * - Percentage labels and absolute values
8
+ * - Click bar to filter table to that resource type
9
+ * - Theme-aware colours using CSS custom properties
10
+ */
11
+
12
+ interface CostItem {
13
+ type: string;
14
+ label: string;
15
+ cost: number;
16
+ percentage: number;
17
+ }
18
+
19
+ interface Props {
20
+ costs: CostItem[];
21
+ totalCost: number;
22
+ class?: string;
23
+ }
24
+
25
+ const { costs = [], totalCost = 0, class: className = '' } = Astro.props;
26
+
27
+ // Sort by cost descending
28
+ const sortedCosts = [...costs].sort((a, b) => b.cost - a.cost);
29
+
30
+ // Resource type to color mapping (matches design tokens)
31
+ const typeColors: Record<string, string> = {
32
+ worker: 'var(--usage-accent-blue, #0969da)',
33
+ d1: 'var(--usage-accent-purple, #8250df)',
34
+ kv: 'var(--usage-accent-green, #1a7f37)',
35
+ r2: 'var(--usage-accent-yellow, #9a6700)',
36
+ pages: 'var(--usage-accent-red, #cf222e)',
37
+ vectorize: '#06b6d4',
38
+ 'ai-gateway': '#ec4899',
39
+ queues: '#f97316',
40
+ workflows: '#84cc16',
41
+ 'durable-objects': '#6366f1',
42
+ };
43
+
44
+ function getTypeColor(type: string): string {
45
+ return typeColors[type.toLowerCase()] || 'var(--usage-text-muted, #8c959f)';
46
+ }
47
+
48
+ function formatCost(value: number): string {
49
+ return new Intl.NumberFormat('en-US', {
50
+ style: 'currency',
51
+ currency: 'USD',
52
+ minimumFractionDigits: 2,
53
+ maximumFractionDigits: 2,
54
+ }).format(value);
55
+ }
56
+
57
+ function formatPercentage(value: number): string {
58
+ return `${value.toFixed(1)}%`;
59
+ }
60
+ ---
61
+
62
+ <div class:list={['cost-breakdown-chart', className]} data-component="cost-breakdown-chart">
63
+ <div class="chart-header">
64
+ <h3 class="chart-title">Cost Breakdown</h3>
65
+ <span class="chart-total">{formatCost(totalCost)} total</span>
66
+ </div>
67
+
68
+ <div class="chart-bars">
69
+ {
70
+ sortedCosts.length === 0 ? (
71
+ <div class="empty-state">
72
+ <svg
73
+ class="empty-icon"
74
+ viewBox="0 0 24 24"
75
+ fill="none"
76
+ stroke="currentColor"
77
+ stroke-width="1.5"
78
+ >
79
+ <path
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ d="M3 13.125C3 12.504 3.504 12 4.125 12h2.25c.621 0 1.125.504 1.125 1.125v6.75C7.5 20.496 6.996 21 6.375 21h-2.25A1.125 1.125 0 013 19.875v-6.75zM9.75 8.625c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125v11.25c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V8.625zM16.5 4.125c0-.621.504-1.125 1.125-1.125h2.25C20.496 3 21 3.504 21 4.125v15.75c0 .621-.504 1.125-1.125 1.125h-2.25a1.125 1.125 0 01-1.125-1.125V4.125z"
83
+ />
84
+ </svg>
85
+ <span class="empty-text">No cost data available</span>
86
+ </div>
87
+ ) : (
88
+ sortedCosts.map((item) => (
89
+ <button
90
+ type="button"
91
+ class="bar-row"
92
+ data-type={item.type}
93
+ title={`Click to filter by ${item.label}`}
94
+ >
95
+ <div class="bar-label">
96
+ <span class="type-indicator" style={`--bar-color: ${getTypeColor(item.type)}`} />
97
+ <span class="type-name">{item.label}</span>
98
+ </div>
99
+ <div class="bar-track">
100
+ <div
101
+ class="bar-fill"
102
+ style={`--bar-width: ${item.percentage}%; --bar-color: ${getTypeColor(item.type)}`}
103
+ >
104
+ <span class="bar-value">{formatCost(item.cost)}</span>
105
+ </div>
106
+ </div>
107
+ <div class="bar-percentage">{formatPercentage(item.percentage)}</div>
108
+ </button>
109
+ ))
110
+ )
111
+ }
112
+ </div>
113
+
114
+ {
115
+ sortedCosts.length > 0 && (
116
+ <div class="chart-legend">
117
+ <span class="legend-hint">Click a bar to filter the table</span>
118
+ </div>
119
+ )
120
+ }
121
+ </div>
122
+
123
+ <style>
124
+ .cost-breakdown-chart {
125
+ background: var(--usage-bg-secondary, #f6f8fa);
126
+ border: 1px solid var(--usage-border-default, #d1d9e0);
127
+ border-radius: var(--usage-radius-lg, 8px);
128
+ padding: var(--usage-spacing-lg, 1.5rem);
129
+ transition:
130
+ background-color var(--usage-transition-normal, 200ms ease),
131
+ border-color var(--usage-transition-normal, 200ms ease);
132
+ }
133
+
134
+ .chart-header {
135
+ display: flex;
136
+ align-items: baseline;
137
+ justify-content: space-between;
138
+ margin-bottom: var(--usage-spacing-lg, 1.5rem);
139
+ padding-bottom: var(--usage-spacing-md, 1rem);
140
+ border-bottom: 1px solid var(--usage-border-subtle, #e8ecef);
141
+ }
142
+
143
+ .chart-title {
144
+ margin: 0;
145
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
146
+ font-size: 1rem;
147
+ font-weight: 600;
148
+ color: var(--usage-text-primary, #1f2328);
149
+ letter-spacing: -0.01em;
150
+ }
151
+
152
+ .chart-total {
153
+ font-family: var(--usage-font-mono, 'JetBrains Mono', monospace);
154
+ font-size: 0.875rem;
155
+ font-weight: 500;
156
+ color: var(--usage-text-secondary, #656d76);
157
+ }
158
+
159
+ .chart-bars {
160
+ display: flex;
161
+ flex-direction: column;
162
+ gap: var(--usage-spacing-sm, 0.5rem);
163
+ }
164
+
165
+ .bar-row {
166
+ display: grid;
167
+ grid-template-columns: 140px 1fr 60px;
168
+ align-items: center;
169
+ gap: var(--usage-spacing-md, 1rem);
170
+ padding: var(--usage-spacing-sm, 0.5rem) var(--usage-spacing-sm, 0.5rem);
171
+ background: transparent;
172
+ border: none;
173
+ border-radius: var(--usage-radius-md, 6px);
174
+ cursor: pointer;
175
+ transition: background-color var(--usage-transition-fast, 150ms ease);
176
+ text-align: left;
177
+ width: 100%;
178
+ }
179
+
180
+ .bar-row:hover {
181
+ background: var(--usage-bg-hover, #eaeef2);
182
+ }
183
+
184
+ .bar-row:focus {
185
+ outline: 2px solid var(--usage-accent-blue, #0969da);
186
+ outline-offset: 2px;
187
+ }
188
+
189
+ .bar-row:active {
190
+ transform: scale(0.995);
191
+ }
192
+
193
+ .bar-label {
194
+ display: flex;
195
+ align-items: center;
196
+ gap: var(--usage-spacing-sm, 0.5rem);
197
+ min-width: 0;
198
+ }
199
+
200
+ .type-indicator {
201
+ width: 10px;
202
+ height: 10px;
203
+ border-radius: 2px;
204
+ background: var(--bar-color);
205
+ flex-shrink: 0;
206
+ }
207
+
208
+ .type-name {
209
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
210
+ font-size: 0.8125rem;
211
+ font-weight: 500;
212
+ color: var(--usage-text-primary, #1f2328);
213
+ white-space: nowrap;
214
+ overflow: hidden;
215
+ text-overflow: ellipsis;
216
+ }
217
+
218
+ .bar-track {
219
+ height: 24px;
220
+ background: var(--usage-bg-tertiary, #f0f2f4);
221
+ border-radius: var(--usage-radius-sm, 4px);
222
+ overflow: hidden;
223
+ position: relative;
224
+ }
225
+
226
+ .bar-fill {
227
+ height: 100%;
228
+ width: var(--bar-width);
229
+ min-width: fit-content;
230
+ background: var(--bar-color);
231
+ border-radius: var(--usage-radius-sm, 4px);
232
+ display: flex;
233
+ align-items: center;
234
+ padding: 0 var(--usage-spacing-sm, 0.5rem);
235
+ transition: width var(--usage-transition-slow, 300ms ease);
236
+ position: relative;
237
+ }
238
+
239
+ .bar-value {
240
+ font-family: var(--usage-font-mono, 'JetBrains Mono', monospace);
241
+ font-size: 0.6875rem;
242
+ font-weight: 600;
243
+ color: white;
244
+ white-space: nowrap;
245
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
246
+ }
247
+
248
+ /* For very small bars, show value outside */
249
+ .bar-fill[style*='--bar-width: 0'] .bar-value,
250
+ .bar-fill[style*='--bar-width: 1%'] .bar-value,
251
+ .bar-fill[style*='--bar-width: 2%'] .bar-value,
252
+ .bar-fill[style*='--bar-width: 3%'] .bar-value,
253
+ .bar-fill[style*='--bar-width: 4%'] .bar-value,
254
+ .bar-fill[style*='--bar-width: 5%'] .bar-value {
255
+ position: absolute;
256
+ left: 100%;
257
+ padding-left: var(--usage-spacing-xs, 0.25rem);
258
+ color: var(--usage-text-secondary, #656d76);
259
+ text-shadow: none;
260
+ }
261
+
262
+ .bar-percentage {
263
+ font-family: var(--usage-font-mono, 'JetBrains Mono', monospace);
264
+ font-size: 0.75rem;
265
+ font-weight: 500;
266
+ color: var(--usage-text-muted, #8c959f);
267
+ text-align: right;
268
+ white-space: nowrap;
269
+ }
270
+
271
+ .chart-legend {
272
+ margin-top: var(--usage-spacing-md, 1rem);
273
+ padding-top: var(--usage-spacing-sm, 0.5rem);
274
+ border-top: 1px solid var(--usage-border-subtle, #e8ecef);
275
+ }
276
+
277
+ .legend-hint {
278
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
279
+ font-size: 0.6875rem;
280
+ color: var(--usage-text-muted, #8c959f);
281
+ }
282
+
283
+ .empty-state {
284
+ display: flex;
285
+ flex-direction: column;
286
+ align-items: center;
287
+ justify-content: center;
288
+ padding: var(--usage-spacing-xl, 2rem);
289
+ gap: var(--usage-spacing-sm, 0.5rem);
290
+ }
291
+
292
+ .empty-icon {
293
+ width: 48px;
294
+ height: 48px;
295
+ color: var(--usage-text-muted, #8c959f);
296
+ opacity: 0.5;
297
+ }
298
+
299
+ .empty-text {
300
+ font-family: var(--usage-font-sans, 'Inter', sans-serif);
301
+ font-size: 0.875rem;
302
+ color: var(--usage-text-muted, #8c959f);
303
+ }
304
+
305
+ /* Responsive adjustments */
306
+ @media (max-width: 640px) {
307
+ .bar-row {
308
+ grid-template-columns: 100px 1fr 50px;
309
+ gap: var(--usage-spacing-sm, 0.5rem);
310
+ }
311
+
312
+ .type-name {
313
+ font-size: 0.75rem;
314
+ }
315
+
316
+ .bar-value {
317
+ font-size: 0.625rem;
318
+ }
319
+
320
+ .bar-percentage {
321
+ font-size: 0.6875rem;
322
+ }
323
+ }
324
+ </style>
325
+
326
+ <script>
327
+ // Handle bar clicks to filter the table by resource type
328
+ document.addEventListener('DOMContentLoaded', () => {
329
+ const chart = document.querySelector('[data-component="cost-breakdown-chart"]');
330
+ if (!chart) return;
331
+
332
+ chart.addEventListener('click', (e) => {
333
+ const target = e.target as HTMLElement;
334
+ const barRow = target.closest('.bar-row') as HTMLElement;
335
+
336
+ if (barRow) {
337
+ const type = barRow.dataset.type;
338
+ if (type) {
339
+ // Dispatch custom event to filter the table
340
+ document.dispatchEvent(
341
+ new CustomEvent('usage:filter-type', {
342
+ detail: { type },
343
+ bubbles: true,
344
+ })
345
+ );
346
+
347
+ // Also update URL params
348
+ const url = new URL(window.location.href);
349
+ const currentTypes = url.searchParams.get('types')?.split(',').filter(Boolean) || [];
350
+
351
+ if (currentTypes.includes(type)) {
352
+ // Already filtered by this type, remove it
353
+ const newTypes = currentTypes.filter((t) => t !== type);
354
+ if (newTypes.length > 0) {
355
+ url.searchParams.set('types', newTypes.join(','));
356
+ } else {
357
+ url.searchParams.delete('types');
358
+ }
359
+ } else {
360
+ // Add this type to filter (replace existing)
361
+ url.searchParams.set('types', type);
362
+ }
363
+
364
+ window.history.replaceState({}, '', url.toString());
365
+
366
+ // Visual feedback - add active state
367
+ chart.querySelectorAll('.bar-row').forEach((row) => row.classList.remove('active'));
368
+ barRow.classList.add('active');
369
+ }
370
+ }
371
+ });
372
+ });
373
+ </script>
374
+
375
+ <style>
376
+ /* Active state for selected bar */
377
+ .bar-row.active {
378
+ background: var(--usage-bg-hover, #eaeef2);
379
+ box-shadow: inset 0 0 0 2px var(--usage-accent-blue, #0969da);
380
+ }
381
+ </style>
@@ -0,0 +1,210 @@
1
+ ---
2
+ /**
3
+ * CostBreakdownTable Component
4
+ *
5
+ * Displays cost breakdown by project with resource type breakdown.
6
+ */
7
+
8
+ interface ProjectCost {
9
+ project: string;
10
+ workers: number;
11
+ d1: number;
12
+ kv: number;
13
+ r2: number;
14
+ vectorize: number;
15
+ aiGateway: number;
16
+ durableObjects: number;
17
+ total: number;
18
+ }
19
+
20
+ interface Props {
21
+ projectCosts: ProjectCost[];
22
+ }
23
+
24
+ const { projectCosts } = Astro.props;
25
+
26
+ function formatCurrency(amount: number): string {
27
+ if (amount === 0) return '$0.00';
28
+ if (amount < 0.01) return '<$0.01';
29
+ return `$${amount.toFixed(2)}`;
30
+ }
31
+
32
+ function getProjectLabel(project: string): string {
33
+ const labels: Record<string, string> = {
34
+ 'brand-copilot': 'Brand Copilot',
35
+ scout: 'Scout',
36
+ platform: 'Platform',
37
+ other: 'Other',
38
+ };
39
+ return labels[project] || project;
40
+ }
41
+
42
+ function getProjectIcon(project: string): string {
43
+ const icons: Record<string, string> = {
44
+ 'brand-copilot': '&#x1F43B;',
45
+ scout: '&#x1F50D;',
46
+ platform: '&#x2699;&#xFE0F;',
47
+ other: '&#x1F4E6;',
48
+ };
49
+ return icons[project] || '&#x1F4C1;';
50
+ }
51
+
52
+ // Calculate totals
53
+ const totals = projectCosts.reduce(
54
+ (acc, p) => ({
55
+ workers: acc.workers + p.workers,
56
+ d1: acc.d1 + p.d1,
57
+ kv: acc.kv + p.kv,
58
+ r2: acc.r2 + p.r2,
59
+ vectorize: acc.vectorize + p.vectorize,
60
+ aiGateway: acc.aiGateway + p.aiGateway,
61
+ durableObjects: acc.durableObjects + p.durableObjects,
62
+ total: acc.total + p.total,
63
+ }),
64
+ { workers: 0, d1: 0, kv: 0, r2: 0, vectorize: 0, aiGateway: 0, durableObjects: 0, total: 0 }
65
+ );
66
+ ---
67
+
68
+ <div class="table-container">
69
+ <table class="cost-table">
70
+ <thead>
71
+ <tr>
72
+ <th>Project</th>
73
+ <th>Workers</th>
74
+ <th>D1</th>
75
+ <th>KV</th>
76
+ <th>R2</th>
77
+ <th>Vectorize</th>
78
+ <th>AI Gateway</th>
79
+ <th class="total-col">Total</th>
80
+ </tr>
81
+ </thead>
82
+ <tbody>
83
+ {
84
+ projectCosts.map((project) => (
85
+ <tr>
86
+ <td class="project-cell">
87
+ <span class="project-icon" set:html={getProjectIcon(project.project)} />
88
+ <span class="project-name">{getProjectLabel(project.project)}</span>
89
+ </td>
90
+ <td>{formatCurrency(project.workers)}</td>
91
+ <td>{formatCurrency(project.d1)}</td>
92
+ <td>{formatCurrency(project.kv)}</td>
93
+ <td>{formatCurrency(project.r2)}</td>
94
+ <td>{formatCurrency(project.vectorize)}</td>
95
+ <td>{formatCurrency(project.aiGateway)}</td>
96
+ <td class="total-col">{formatCurrency(project.total)}</td>
97
+ </tr>
98
+ ))
99
+ }
100
+ </tbody>
101
+ <tfoot>
102
+ <tr class="totals-row">
103
+ <td class="project-cell">
104
+ <span class="project-icon">&#x2211;</span>
105
+ <span class="project-name">Total</span>
106
+ </td>
107
+ <td>{formatCurrency(totals.workers)}</td>
108
+ <td>{formatCurrency(totals.d1)}</td>
109
+ <td>{formatCurrency(totals.kv)}</td>
110
+ <td>{formatCurrency(totals.r2)}</td>
111
+ <td>{formatCurrency(totals.vectorize)}</td>
112
+ <td>{formatCurrency(totals.aiGateway)}</td>
113
+ <td class="total-col grand-total">{formatCurrency(totals.total)}</td>
114
+ </tr>
115
+ </tfoot>
116
+ </table>
117
+ </div>
118
+
119
+ <style>
120
+ .table-container {
121
+ background-color: white;
122
+ border: 1px solid #e5e7eb;
123
+ border-radius: 0.5rem;
124
+ overflow-x: auto;
125
+ }
126
+
127
+ .cost-table {
128
+ width: 100%;
129
+ border-collapse: collapse;
130
+ }
131
+
132
+ .cost-table thead {
133
+ background-color: #f9fafb;
134
+ }
135
+
136
+ .cost-table th {
137
+ padding: 0.75rem 1rem;
138
+ text-align: left;
139
+ font-size: 0.75rem;
140
+ font-weight: 600;
141
+ color: #6b7280;
142
+ text-transform: uppercase;
143
+ letter-spacing: 0.05em;
144
+ border-bottom: 1px solid #e5e7eb;
145
+ }
146
+
147
+ .cost-table td {
148
+ padding: 0.75rem 1rem;
149
+ font-size: 0.875rem;
150
+ color: #374151;
151
+ border-bottom: 1px solid #f3f4f6;
152
+ }
153
+
154
+ .cost-table tbody tr:hover {
155
+ background-color: #f9fafb;
156
+ }
157
+
158
+ .project-cell {
159
+ display: flex;
160
+ align-items: center;
161
+ gap: 0.5rem;
162
+ }
163
+
164
+ .project-icon {
165
+ font-size: 1rem;
166
+ width: 1.5rem;
167
+ text-align: center;
168
+ }
169
+
170
+ .project-name {
171
+ font-weight: 500;
172
+ }
173
+
174
+ .total-col {
175
+ font-weight: 600;
176
+ color: #1f2937;
177
+ background-color: #f9fafb;
178
+ }
179
+
180
+ .totals-row {
181
+ background-color: #f3f4f6;
182
+ font-weight: 600;
183
+ }
184
+
185
+ .totals-row td {
186
+ border-bottom: none;
187
+ border-top: 2px solid #e5e7eb;
188
+ color: #1f2937;
189
+ }
190
+
191
+ .grand-total {
192
+ color: #1d4ed8 !important;
193
+ font-size: 1rem;
194
+ }
195
+
196
+ @media (max-width: 768px) {
197
+ .cost-table th,
198
+ .cost-table td {
199
+ padding: 0.5rem;
200
+ font-size: 0.75rem;
201
+ }
202
+
203
+ .project-name {
204
+ max-width: 100px;
205
+ overflow: hidden;
206
+ text-overflow: ellipsis;
207
+ white-space: nowrap;
208
+ }
209
+ }
210
+ </style>