@littlebearapps/platform-admin-sdk 2.0.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 (185) hide show
  1. package/README.md +4 -7
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +206 -4
  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/DigestStats.tsx +151 -0
  9. package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
  10. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  11. package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
  12. package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
  13. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  14. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  15. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  16. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  17. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  18. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  19. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  20. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  21. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  22. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  23. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  24. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  25. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  26. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  27. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  28. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  29. package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
  30. package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
  31. package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
  32. package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
  33. package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
  34. package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
  35. package/templates/full/dashboard/src/pages/feedback.astro +365 -0
  36. package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
  37. package/templates/full/dashboard/src/pages/map.astro +561 -0
  38. package/templates/full/dashboard/src/pages/revenue.astro +72 -0
  39. package/templates/full/dashboard/src/pages/tests.astro +431 -0
  40. package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
  41. package/templates/full/scripts/ops/verify-account-total.ts +256 -0
  42. package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
  43. package/templates/full/tests/integration/r2-archive.test.ts +108 -0
  44. package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
  45. package/templates/shared/.github/workflows/validate-controls.yml +27 -0
  46. package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
  47. package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
  48. package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
  49. package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
  50. package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
  51. package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
  52. package/templates/shared/dashboard/src/components/Toast.astro +170 -0
  53. package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
  54. package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
  55. package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
  56. package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
  57. package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
  58. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  59. package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
  60. package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
  61. package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
  62. package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
  63. package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
  64. package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
  65. package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
  66. package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
  67. package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
  68. package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
  69. package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
  70. package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
  71. package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
  72. package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
  73. package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
  74. package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
  75. package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
  76. package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
  77. package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
  78. package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
  79. package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
  80. package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
  81. package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
  82. package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
  83. package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
  84. package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
  85. package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
  86. package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
  87. package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
  88. package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
  89. package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
  90. package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
  91. package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
  92. package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
  93. package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
  94. package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
  95. package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
  96. package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
  97. package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
  98. package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
  99. package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
  100. package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
  101. package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
  102. package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
  103. package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
  104. package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
  105. package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
  106. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  107. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  108. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  109. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  110. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  111. package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
  112. package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
  113. package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
  114. package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
  115. package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
  116. package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
  117. package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
  118. package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
  119. package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
  120. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  121. package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
  122. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  123. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  124. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  125. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  126. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  127. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  128. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  129. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  130. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  131. package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
  132. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  133. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  134. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  135. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  136. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  137. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  138. package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
  139. package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
  140. package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
  141. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  142. package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
  143. package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
  144. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  145. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  146. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  147. package/templates/shared/tests/unit/billing.test.ts +331 -0
  148. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  149. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  150. package/templates/shared/tests/unit/control.test.ts +226 -0
  151. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  152. package/templates/shared/tests/unit/economics.test.ts +365 -0
  153. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  154. package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
  155. package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
  156. package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
  157. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  158. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  159. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  160. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  161. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  162. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  163. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  164. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  165. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  166. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  167. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  168. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  169. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  170. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  171. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  172. package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
  173. package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
  174. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
  175. package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
  176. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
  177. package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
  178. package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
  179. package/templates/standard/tests/integration/connectors.test.ts +241 -0
  180. package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
  181. package/templates/standard/tests/integration/ingestion.test.ts +211 -0
  182. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  183. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  184. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  185. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,401 @@
1
+ /**
2
+ * Provider Costs Grid Component
3
+ *
4
+ * Displays third-party provider usage and costs in a card grid.
5
+ * Fetches data from /api/costs/providers endpoint.
6
+ *
7
+ * GitHub gets special treatment: subscription fees (GHEC, GHAS) shown
8
+ * separately from usage overage (Actions, Storage).
9
+ */
10
+ import { useState, useEffect } from 'react';
11
+
12
+ // =============================================================================
13
+ // TYPES
14
+ // =============================================================================
15
+
16
+ interface ProviderResource {
17
+ provider: string;
18
+ resourceType: string;
19
+ resourceName?: string;
20
+ usageValue: number;
21
+ usageUnit: string;
22
+ costUsd: number;
23
+ snapshotDate: string;
24
+ label?: string;
25
+ category?: string;
26
+ }
27
+
28
+ interface ProviderSummary {
29
+ provider: string;
30
+ displayName: string;
31
+ totalCostUsd: number;
32
+ resources: ProviderResource[];
33
+ latestSnapshot: string;
34
+ subscriptionCostUsd?: number;
35
+ }
36
+
37
+ interface ApiResponse {
38
+ success: boolean;
39
+ providers?: ProviderSummary[];
40
+ totalCostUsd?: number;
41
+ period?: { startDate: string; endDate: string };
42
+ error?: string;
43
+ }
44
+
45
+ interface Props {
46
+ period: string;
47
+ }
48
+
49
+ // =============================================================================
50
+ // CONSTANTS
51
+ // =============================================================================
52
+
53
+ const PROVIDER_ICONS: Record<string, string> = {
54
+ github: '🐙',
55
+ openai: '🤖',
56
+ anthropic: '🧠',
57
+ apify: '🕷️',
58
+ resend: '✉️',
59
+ minimax: '🎯',
60
+ gemini: '💎',
61
+ };
62
+
63
+ const PROVIDER_GRADIENTS: Record<string, string> = {
64
+ github: 'from-gray-700 to-gray-900',
65
+ openai: 'from-green-500 to-emerald-600',
66
+ anthropic: 'from-orange-500 to-amber-600',
67
+ apify: 'from-teal-500 to-green-600',
68
+ resend: 'from-violet-500 to-purple-600',
69
+ minimax: 'from-indigo-500 to-blue-600',
70
+ gemini: 'from-blue-400 to-indigo-500',
71
+ };
72
+
73
+ const PERIOD_LABELS: Record<string, string> = {
74
+ '24h': 'Today',
75
+ '7d': 'Last 7 days',
76
+ '30d': 'Last 30 days',
77
+ };
78
+
79
+ // =============================================================================
80
+ // HELPERS
81
+ // =============================================================================
82
+
83
+ function formatResourceLabel(resource: ProviderResource): string {
84
+ if (resource.label) return resource.label;
85
+ // Fallback: convert snake_case to Title Case
86
+ return resource.resourceType.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
87
+ }
88
+
89
+ function formatValue(value: number, unit: string): string {
90
+ if (unit === 'dollars' || unit === 'usd') {
91
+ return `$${value.toFixed(2)}`;
92
+ }
93
+ if (unit === 'tokens') {
94
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M tokens`;
95
+ if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K tokens`;
96
+ return `${value.toLocaleString()} tokens`;
97
+ }
98
+ if (unit === 'minutes') {
99
+ return `${value.toLocaleString()} min`;
100
+ }
101
+ if (unit === 'requests') {
102
+ return `${value.toLocaleString()} req`;
103
+ }
104
+ if (unit === 'user_months') {
105
+ return `${value.toFixed(1)} user-mo`;
106
+ }
107
+ if (unit === 'seats') {
108
+ return `${value.toLocaleString()} seats`;
109
+ }
110
+ if (unit === 'gb_hours') {
111
+ return `${value.toFixed(1)} GB-hr`;
112
+ }
113
+ if (unit === 'percent') {
114
+ return `${value.toFixed(1)}%`;
115
+ }
116
+ if (unit === 'bytes') {
117
+ if (value > 1024 * 1024 * 1024) return `${(value / (1024 * 1024 * 1024)).toFixed(2)} GB`;
118
+ if (value > 1024 * 1024) return `${(value / (1024 * 1024)).toFixed(2)} MB`;
119
+ return `${(value / 1024).toFixed(2)} KB`;
120
+ }
121
+ if (unit === 'gb') {
122
+ return `${value.toFixed(2)} GB`;
123
+ }
124
+ if (unit === 'units') {
125
+ return `${value.toFixed(2)} units`;
126
+ }
127
+ return `${value.toLocaleString()}${unit ? ` ${unit}` : ''}`;
128
+ }
129
+
130
+ function formatDate(dateStr: string): string {
131
+ const date = new Date(dateStr);
132
+ return date.toLocaleDateString('en-AU', {
133
+ day: 'numeric',
134
+ month: 'short',
135
+ year: 'numeric',
136
+ });
137
+ }
138
+
139
+ // =============================================================================
140
+ // COMPONENTS
141
+ // =============================================================================
142
+
143
+ function LoadingState() {
144
+ return (
145
+ <div className="space-y-4">
146
+ <div className="animate-pulse">
147
+ <div className="h-24 bg-gray-200 dark:bg-gray-700 rounded-lg mb-4" />
148
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
149
+ <div className="h-48 bg-gray-200 dark:bg-gray-700 rounded-lg" />
150
+ <div className="h-48 bg-gray-200 dark:bg-gray-700 rounded-lg" />
151
+ <div className="h-48 bg-gray-200 dark:bg-gray-700 rounded-lg" />
152
+ </div>
153
+ </div>
154
+ </div>
155
+ );
156
+ }
157
+
158
+ function ErrorState({ message }: { message: string }) {
159
+ return (
160
+ <div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
161
+ <div className="flex items-center gap-3">
162
+ <span className="text-xl">⚠️</span>
163
+ <div>
164
+ <strong className="text-red-800 dark:text-red-200">Error loading data</strong>
165
+ <p className="text-sm text-red-600 dark:text-red-300 mt-1">{message}</p>
166
+ </div>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ function EmptyState() {
173
+ return (
174
+ <div className="text-center py-12">
175
+ <div className="text-6xl mb-4">📊</div>
176
+ <h3 className="text-lg font-medium text-gray-900 dark:text-white">No provider data</h3>
177
+ <p className="text-gray-500 dark:text-gray-400 mt-1">
178
+ Third-party usage data will appear here once collected.
179
+ </p>
180
+ </div>
181
+ );
182
+ }
183
+
184
+ function TotalCostHero({ totalCost, period }: { totalCost: number; period: string }) {
185
+ return (
186
+ <div className="bg-gradient-to-r from-purple-600 to-indigo-600 rounded-xl p-6 text-white">
187
+ <div className="flex items-center justify-between">
188
+ <div>
189
+ <p className="text-purple-100 text-sm font-medium">Total Third-Party Usage Costs</p>
190
+ <p className="text-4xl font-bold mt-1">${totalCost.toFixed(2)}</p>
191
+ <p className="text-purple-200 text-sm mt-2">
192
+ {PERIOD_LABELS[period] || 'Last 30 days'} — excludes fixed subscriptions
193
+ </p>
194
+ </div>
195
+ <div className="text-6xl opacity-20">💳</div>
196
+ </div>
197
+ </div>
198
+ );
199
+ }
200
+
201
+ function ResourceRow({ resource }: { resource: ProviderResource }) {
202
+ const isSubscription = resource.category === 'subscription';
203
+
204
+ return (
205
+ <div className="flex justify-between items-center text-sm py-1.5">
206
+ <span className="text-gray-600 dark:text-gray-400 truncate mr-2 flex items-center gap-1.5">
207
+ {isSubscription && (
208
+ <span className="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
209
+ LICENSE
210
+ </span>
211
+ )}
212
+ {formatResourceLabel(resource)}
213
+ </span>
214
+ <div className="flex items-center gap-2 whitespace-nowrap">
215
+ <span className="text-gray-900 dark:text-white font-medium">
216
+ {formatValue(resource.usageValue, resource.usageUnit)}
217
+ </span>
218
+ {resource.costUsd > 0 && (
219
+ <span className="text-gray-400 dark:text-gray-500 text-xs">
220
+ (${resource.costUsd.toFixed(2)})
221
+ </span>
222
+ )}
223
+ </div>
224
+ </div>
225
+ );
226
+ }
227
+
228
+ function GitHubCard({ provider }: { provider: ProviderSummary }) {
229
+ const icon = PROVIDER_ICONS.github;
230
+ const gradient = PROVIDER_GRADIENTS.github;
231
+ const subscriptionResources = provider.resources.filter((r) => r.category === 'subscription');
232
+ const usageResources = provider.resources.filter((r) => r.category !== 'subscription');
233
+
234
+ return (
235
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
236
+ {/* Header */}
237
+ <div className={`bg-gradient-to-r ${gradient} p-4 text-white`}>
238
+ <div className="flex items-center justify-between">
239
+ <div className="flex items-center gap-2">
240
+ <span className="text-2xl">{icon}</span>
241
+ <span className="font-semibold">{provider.displayName}</span>
242
+ </div>
243
+ <div className="text-right">
244
+ <span className="text-xl font-bold">${provider.totalCostUsd.toFixed(2)}</span>
245
+ <p className="text-xs text-gray-300 mt-0.5">usage overage</p>
246
+ </div>
247
+ </div>
248
+ </div>
249
+
250
+ <div className="p-4 space-y-3">
251
+ {/* Usage section */}
252
+ {usageResources.length > 0 && (
253
+ <div>
254
+ <p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-1">
255
+ Usage (above allowances)
256
+ </p>
257
+ <div className="divide-y divide-gray-100 dark:divide-gray-700">
258
+ {usageResources.map((r, idx) => (
259
+ <ResourceRow key={idx} resource={r} />
260
+ ))}
261
+ </div>
262
+ </div>
263
+ )}
264
+
265
+ {/* Subscription section */}
266
+ {subscriptionResources.length > 0 && (
267
+ <div className="pt-2 border-t border-gray-200 dark:border-gray-700">
268
+ <div className="flex justify-between items-center mb-1">
269
+ <p className="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
270
+ Subscriptions (fixed monthly)
271
+ </p>
272
+ {provider.subscriptionCostUsd != null && provider.subscriptionCostUsd > 0 && (
273
+ <span className="text-xs text-gray-500 dark:text-gray-400 font-medium">
274
+ ${provider.subscriptionCostUsd.toFixed(2)}/mo
275
+ </span>
276
+ )}
277
+ </div>
278
+ <div className="divide-y divide-gray-100 dark:divide-gray-700">
279
+ {subscriptionResources.map((r, idx) => (
280
+ <ResourceRow key={idx} resource={r} />
281
+ ))}
282
+ </div>
283
+ </div>
284
+ )}
285
+
286
+ <p className="text-xs text-gray-400 mt-2">
287
+ Last updated: {formatDate(provider.latestSnapshot)}
288
+ </p>
289
+ </div>
290
+ </div>
291
+ );
292
+ }
293
+
294
+ function StandardCard({ provider }: { provider: ProviderSummary }) {
295
+ const icon = PROVIDER_ICONS[provider.provider] || '📦';
296
+ const gradient = PROVIDER_GRADIENTS[provider.provider] || 'from-gray-500 to-gray-600';
297
+ const displayResources = provider.resources.slice(0, 6);
298
+ const moreCount = provider.resources.length - 6;
299
+
300
+ return (
301
+ <div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
302
+ {/* Header with gradient */}
303
+ <div className={`bg-gradient-to-r ${gradient} p-4 text-white`}>
304
+ <div className="flex items-center justify-between">
305
+ <div className="flex items-center gap-2">
306
+ <span className="text-2xl">{icon}</span>
307
+ <span className="font-semibold">{provider.displayName}</span>
308
+ </div>
309
+ <span className="text-xl font-bold">${provider.totalCostUsd.toFixed(2)}</span>
310
+ </div>
311
+ </div>
312
+
313
+ {/* Resources */}
314
+ <div className="p-4">
315
+ <div className="divide-y divide-gray-100 dark:divide-gray-700">
316
+ {displayResources.map((r, idx) => (
317
+ <ResourceRow key={idx} resource={r} />
318
+ ))}
319
+ </div>
320
+ {moreCount > 0 && (
321
+ <p className="text-xs text-gray-400 mt-1">+{moreCount} more resources</p>
322
+ )}
323
+ <p className="text-xs text-gray-400 mt-3">
324
+ Last updated: {formatDate(provider.latestSnapshot)}
325
+ </p>
326
+ </div>
327
+ </div>
328
+ );
329
+ }
330
+
331
+ function ProviderCard({ provider }: { provider: ProviderSummary }) {
332
+ if (provider.provider === 'github') {
333
+ return <GitHubCard provider={provider} />;
334
+ }
335
+ return <StandardCard provider={provider} />;
336
+ }
337
+
338
+ // =============================================================================
339
+ // MAIN COMPONENT
340
+ // =============================================================================
341
+
342
+ export default function ProviderCostsGrid({ period }: Props) {
343
+ const [loading, setLoading] = useState(true);
344
+ const [error, setError] = useState<string | null>(null);
345
+ const [providers, setProviders] = useState<ProviderSummary[]>([]);
346
+ const [totalCost, setTotalCost] = useState(0);
347
+
348
+ useEffect(() => {
349
+ async function fetchData() {
350
+ setLoading(true);
351
+ setError(null);
352
+
353
+ try {
354
+ const response = await fetch(`/api/costs/providers?period=${period}`);
355
+ const data: ApiResponse = await response.json();
356
+
357
+ if (!data.success) {
358
+ throw new Error(data.error || 'Failed to load data');
359
+ }
360
+
361
+ setProviders(data.providers || []);
362
+ setTotalCost(data.totalCostUsd || 0);
363
+ } catch (err) {
364
+ setError(err instanceof Error ? err.message : 'Unknown error');
365
+ } finally {
366
+ setLoading(false);
367
+ }
368
+ }
369
+
370
+ fetchData();
371
+ }, [period]);
372
+
373
+ if (loading) {
374
+ return <LoadingState />;
375
+ }
376
+
377
+ if (error) {
378
+ return <ErrorState message={error} />;
379
+ }
380
+
381
+ if (providers.length === 0) {
382
+ return (
383
+ <div className="space-y-6">
384
+ <TotalCostHero totalCost={0} period={period} />
385
+ <EmptyState />
386
+ </div>
387
+ );
388
+ }
389
+
390
+ return (
391
+ <div className="space-y-6">
392
+ <TotalCostHero totalCost={totalCost} period={period} />
393
+
394
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
395
+ {providers.map((provider) => (
396
+ <ProviderCard key={provider.provider} provider={provider} />
397
+ ))}
398
+ </div>
399
+ </div>
400
+ );
401
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Costs Components Export
3
+ */
4
+ export { default as ProviderCostsGrid } from './ProviderCostsGrid';
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Global Alert Banner
3
+ * Displays at the top of every page when there are critical issues:
4
+ * - P0/P1 open errors
5
+ * - Tripped circuit breakers
6
+ * - Services down (Gatus)
7
+ */
8
+ import { useState, useEffect } from 'react';
9
+
10
+ interface AlertData {
11
+ hasP0P1: boolean;
12
+ trippedBreakers: number;
13
+ servicesDown: number;
14
+ p0Count?: number;
15
+ p1Count?: number;
16
+ }
17
+
18
+ export function AlertBanner() {
19
+ const [alerts, setAlerts] = useState<AlertData | null>(null);
20
+
21
+ useEffect(() => {
22
+ fetch('/api/overview/summary')
23
+ .then(res => res.json())
24
+ .then(data => {
25
+ if (data.alerts) {
26
+ setAlerts({
27
+ ...data.alerts,
28
+ p0Count: data.errors?.p0Count ?? 0,
29
+ p1Count: data.errors?.p1Count ?? 0,
30
+ });
31
+ }
32
+ })
33
+ .catch(() => {
34
+ // Silently fail - banner just won't show
35
+ });
36
+ }, []);
37
+
38
+ if (!alerts) return null;
39
+
40
+ const hasIssues = alerts.hasP0P1 || alerts.trippedBreakers > 0 || alerts.servicesDown > 0;
41
+ if (!hasIssues) return null;
42
+
43
+ const items: Array<{ label: string; count: number; href: string; colour: string }> = [];
44
+
45
+ if (alerts.p0Count && alerts.p0Count > 0) {
46
+ items.push({
47
+ label: 'P0 error',
48
+ count: alerts.p0Count,
49
+ href: '/health?tab=errors',
50
+ colour: 'bg-red-500',
51
+ });
52
+ }
53
+ if (alerts.p1Count && alerts.p1Count > 0) {
54
+ items.push({
55
+ label: 'P1 error',
56
+ count: alerts.p1Count,
57
+ href: '/health?tab=errors',
58
+ colour: 'bg-orange-500',
59
+ });
60
+ }
61
+ if (alerts.trippedBreakers > 0) {
62
+ items.push({
63
+ label: 'tripped breaker',
64
+ count: alerts.trippedBreakers,
65
+ href: '/resources?tab=features',
66
+ colour: 'bg-yellow-500',
67
+ });
68
+ }
69
+ if (alerts.servicesDown > 0) {
70
+ items.push({
71
+ label: 'service down',
72
+ count: alerts.servicesDown,
73
+ href: '/health?tab=infrastructure',
74
+ colour: 'bg-red-500',
75
+ });
76
+ }
77
+
78
+ return (
79
+ <div className="bg-red-600 dark:bg-red-900 text-white px-4 py-2 text-sm flex items-center justify-center gap-4 flex-wrap">
80
+ <span className="font-semibold">Attention Required</span>
81
+ {items.map((item, i) => (
82
+ <a
83
+ key={i}
84
+ href={item.href}
85
+ className="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-white/20 hover:bg-white/30 transition-colors"
86
+ >
87
+ <span className={`w-2 h-2 rounded-full ${item.colour} animate-pulse`} />
88
+ {item.count} {item.label}{item.count !== 1 ? 's' : ''}
89
+ <span className="opacity-75">→</span>
90
+ </a>
91
+ ))}
92
+ </div>
93
+ );
94
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Overview Components Barrel Export
3
+ */
4
+ export { MissionControl } from './MissionControl';
5
+ export { AlertBanner } from './AlertBanner';
6
+ export { HealthQuadrant } from './HealthQuadrant';
7
+ export { ErrorsQuadrant } from './ErrorsQuadrant';
8
+ export { CostQuadrant } from './CostQuadrant';
9
+ export { ActivityFeed } from './ActivityFeed';
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ReportInfoButton Component
3
+ * Reusable info icon button that toggles a floating panel with report explanation.
4
+ * Based on PatternInfoButton pattern.
5
+ */
6
+ import { useState, useRef, useEffect, type ReactNode } from 'react';
7
+
8
+ interface ReportInfoButtonProps {
9
+ title: string;
10
+ children: ReactNode;
11
+ /** Colour theme: matches the original info panel colours per report */
12
+ theme?: 'blue' | 'yellow' | 'orange' | 'red' | 'purple' | 'green' | 'teal';
13
+ }
14
+
15
+ const themeClasses: Record<string, { button: string; panel: string; heading: string }> = {
16
+ blue: {
17
+ button: 'bg-blue-100 hover:bg-blue-200 dark:bg-blue-900/40 dark:hover:bg-blue-900/60 text-blue-600 dark:text-blue-400',
18
+ panel: 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800',
19
+ heading: 'text-blue-800 dark:text-blue-300',
20
+ },
21
+ yellow: {
22
+ button: 'bg-yellow-100 hover:bg-yellow-200 dark:bg-yellow-900/40 dark:hover:bg-yellow-900/60 text-yellow-600 dark:text-yellow-400',
23
+ panel: 'bg-yellow-50 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-800',
24
+ heading: 'text-yellow-800 dark:text-yellow-300',
25
+ },
26
+ orange: {
27
+ button: 'bg-orange-100 hover:bg-orange-200 dark:bg-orange-900/40 dark:hover:bg-orange-900/60 text-orange-600 dark:text-orange-400',
28
+ panel: 'bg-orange-50 dark:bg-orange-900/30 border-orange-200 dark:border-orange-800',
29
+ heading: 'text-orange-800 dark:text-orange-300',
30
+ },
31
+ red: {
32
+ button: 'bg-red-100 hover:bg-red-200 dark:bg-red-900/40 dark:hover:bg-red-900/60 text-red-600 dark:text-red-400',
33
+ panel: 'bg-red-50 dark:bg-red-900/30 border-red-200 dark:border-red-800',
34
+ heading: 'text-red-800 dark:text-red-300',
35
+ },
36
+ purple: {
37
+ button: 'bg-purple-100 hover:bg-purple-200 dark:bg-purple-900/40 dark:hover:bg-purple-900/60 text-purple-600 dark:text-purple-400',
38
+ panel: 'bg-purple-50 dark:bg-purple-900/30 border-purple-200 dark:border-purple-800',
39
+ heading: 'text-purple-800 dark:text-purple-300',
40
+ },
41
+ green: {
42
+ button: 'bg-green-100 hover:bg-green-200 dark:bg-green-900/40 dark:hover:bg-green-900/60 text-green-600 dark:text-green-400',
43
+ panel: 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800',
44
+ heading: 'text-green-800 dark:text-green-300',
45
+ },
46
+ teal: {
47
+ button: 'bg-teal-100 hover:bg-teal-200 dark:bg-teal-900/40 dark:hover:bg-teal-900/60 text-teal-600 dark:text-teal-400',
48
+ panel: 'bg-teal-50 dark:bg-teal-900/30 border-teal-200 dark:border-teal-800',
49
+ heading: 'text-teal-800 dark:text-teal-300',
50
+ },
51
+ };
52
+
53
+ export function ReportInfoButton({ title, children, theme = 'blue' }: ReportInfoButtonProps) {
54
+ const [open, setOpen] = useState(false);
55
+ const panelRef = useRef<HTMLDivElement>(null);
56
+ const colours = themeClasses[theme];
57
+
58
+ useEffect(() => {
59
+ if (!open) return;
60
+
61
+ function handleClickOutside(event: MouseEvent) {
62
+ if (panelRef.current && !panelRef.current.contains(event.target as Node)) {
63
+ setOpen(false);
64
+ }
65
+ }
66
+
67
+ document.addEventListener('mousedown', handleClickOutside);
68
+ return () => document.removeEventListener('mousedown', handleClickOutside);
69
+ }, [open]);
70
+
71
+ return (
72
+ <div className="relative" ref={panelRef}>
73
+ <button
74
+ onClick={() => setOpen(!open)}
75
+ className={`inline-flex items-center justify-center w-9 h-9 rounded-full ${colours.button} transition-colors`}
76
+ title={title}
77
+ aria-label={title}
78
+ >
79
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
80
+ <path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
81
+ </svg>
82
+ </button>
83
+
84
+ {open && (
85
+ <div className={`absolute right-0 top-12 z-50 w-96 border rounded-lg p-4 shadow-lg ${colours.panel}`}>
86
+ <h3 className={`font-semibold ${colours.heading} mb-2`}>{title}</h3>
87
+ {children}
88
+ <button
89
+ onClick={() => setOpen(false)}
90
+ className={`mt-3 text-xs ${colours.heading} hover:underline`}
91
+ >
92
+ Close
93
+ </button>
94
+ </div>
95
+ )}
96
+ </div>
97
+ );
98
+ }