@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,293 @@
1
+ /**
2
+ * ProviderDetail — Expandable detail panel for a selected provider
3
+ *
4
+ * Shows per-service/resource breakdown with:
5
+ * - Cloudflare: allowance bars per service (D1, KV, R2, DO, etc.)
6
+ * - GitHub: subscription vs usage overage breakdown
7
+ * - Others: resource-level detail with usage values and costs
8
+ */
9
+ import type { ProviderCostSummary, ProviderResource, ModelUsage } from './ProviderCard';
10
+
11
+ // =============================================================================
12
+ // HELPERS
13
+ // =============================================================================
14
+
15
+ function formatCompact(n: number): string {
16
+ if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
17
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
18
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
19
+ return n.toFixed(0);
20
+ }
21
+
22
+ function barColour(pct: number): string {
23
+ if (pct >= 100) return 'bg-red-500';
24
+ if (pct >= 75) return 'bg-amber-500';
25
+ if (pct >= 50) return 'bg-yellow-400';
26
+ return 'bg-emerald-500';
27
+ }
28
+
29
+ // =============================================================================
30
+ // COMPONENT
31
+ // =============================================================================
32
+
33
+ interface Props {
34
+ provider: ProviderCostSummary;
35
+ }
36
+
37
+ export function ProviderDetail({ provider }: Props) {
38
+ if (!provider.resources || provider.resources.length === 0) {
39
+ return (
40
+ <div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
41
+ <p className="text-sm text-gray-500 dark:text-gray-400">
42
+ No detailed resource data available for {provider.displayName}.
43
+ </p>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 animate-in slide-in-from-top-2">
50
+ <div className="flex items-center justify-between mb-3">
51
+ <h3 className="text-sm font-semibold text-gray-900 dark:text-gray-100">
52
+ {provider.displayName} — Service Breakdown
53
+ </h3>
54
+ <div className="flex items-center gap-3 text-xs font-medium text-gray-500 dark:text-gray-400">
55
+ {provider.subscriptionCostUsd != null && provider.subscriptionCostUsd > 0 && (
56
+ <span>License: ${provider.subscriptionCostUsd.toFixed(2)}/mo</span>
57
+ )}
58
+ {provider.totalCostUsd > 0 && (
59
+ <span className="text-red-600 dark:text-red-400">Overage: ${provider.totalCostUsd.toFixed(2)}</span>
60
+ )}
61
+ {provider.totalCostUsd === 0 && !provider.subscriptionCostUsd && (
62
+ <span>$0.00</span>
63
+ )}
64
+ </div>
65
+ </div>
66
+
67
+ {provider.type === 'allowance' && provider.provider === 'cloudflare' && (
68
+ <CloudflareDetail resources={provider.resources} />
69
+ )}
70
+ {provider.provider === 'github' && (
71
+ <GitHubDetail resources={provider.resources} subscriptionCost={provider.subscriptionCostUsd} />
72
+ )}
73
+ {provider.type === 'pay-as-you-go' && provider.provider !== 'github' && (
74
+ <GenericDetail resources={provider.resources} modelBreakdown={provider.modelBreakdown} />
75
+ )}
76
+ {provider.type === 'subscription-payg' && (
77
+ <GenericDetail resources={provider.resources} modelBreakdown={provider.modelBreakdown} />
78
+ )}
79
+ </div>
80
+ );
81
+ }
82
+
83
+ // =============================================================================
84
+ // CLOUDFLARE DETAIL — Allowance bars per service
85
+ // =============================================================================
86
+
87
+ function CloudflareDetail({ resources }: { resources: ProviderResource[] }) {
88
+ return (
89
+ <div className="space-y-3">
90
+ {resources.map(r => {
91
+ const hasAllowance = r.allowance != null && r.allowance > 0;
92
+ const pct = r.pctUsed ?? 0;
93
+ const clampedPct = Math.min(pct, 100);
94
+
95
+ return (
96
+ <div key={r.name}>
97
+ <div className="flex items-center justify-between mb-1">
98
+ <span className="text-xs font-medium text-gray-700 dark:text-gray-300">{r.label}</span>
99
+ <div className="flex items-center gap-2">
100
+ {hasAllowance && (
101
+ <span className="text-[10px] text-gray-500 dark:text-gray-400">
102
+ {formatCompact(r.used)} / {formatCompact(r.allowance!)}
103
+ </span>
104
+ )}
105
+ {r.overageCost != null && r.overageCost > 0 ? (
106
+ <span className="text-[10px] font-medium text-red-600 dark:text-red-400">
107
+ +${r.overageCost.toFixed(2)}
108
+ </span>
109
+ ) : (
110
+ <span className="text-[10px] text-emerald-500">$0.00</span>
111
+ )}
112
+ </div>
113
+ </div>
114
+ {hasAllowance && (
115
+ <div className="w-full h-1.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
116
+ <div
117
+ className={`h-full rounded-full transition-all ${barColour(pct)}`}
118
+ style={{ width: `${clampedPct}%` }}
119
+ />
120
+ </div>
121
+ )}
122
+ </div>
123
+ );
124
+ })}
125
+ </div>
126
+ );
127
+ }
128
+
129
+ // =============================================================================
130
+ // GITHUB DETAIL — Subscription vs usage
131
+ // =============================================================================
132
+
133
+ function GitHubDetail({ resources, subscriptionCost }: { resources: ProviderResource[]; subscriptionCost?: number }) {
134
+ const subs = resources.filter(r => r.category === 'subscription');
135
+ const usage = resources.filter(r => r.category === 'usage');
136
+
137
+ return (
138
+ <div className="space-y-4">
139
+ {/* Usage vs allowance section (like Cloudflare) */}
140
+ {usage.length > 0 && (
141
+ <div>
142
+ <p className="text-[10px] font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-1.5">
143
+ Usage vs Enterprise Allowances
144
+ </p>
145
+ <div className="space-y-3">
146
+ {usage.map(r => {
147
+ const hasAllowance = r.allowance != null && r.allowance > 0;
148
+ const pct = r.pctUsed ?? 0;
149
+ const clampedPct = Math.min(pct, 100);
150
+
151
+ return (
152
+ <div key={r.name}>
153
+ <div className="flex items-center justify-between mb-1">
154
+ <span className="text-xs font-medium text-gray-700 dark:text-gray-300">{r.label}</span>
155
+ <div className="flex items-center gap-2">
156
+ {hasAllowance && (
157
+ <span className="text-[10px] text-gray-500 dark:text-gray-400">
158
+ {formatCompact(r.used)} / {formatCompact(r.allowance!)} {r.unit}
159
+ </span>
160
+ )}
161
+ {r.overageCost != null && r.overageCost > 0 ? (
162
+ <span className="text-[10px] font-medium text-red-600 dark:text-red-400">
163
+ +${r.overageCost.toFixed(2)}
164
+ </span>
165
+ ) : (
166
+ <span className="text-[10px] text-emerald-500">Within allowance</span>
167
+ )}
168
+ </div>
169
+ </div>
170
+ {hasAllowance && (
171
+ <div className="w-full h-1.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
172
+ <div
173
+ className={`h-full rounded-full transition-all ${barColour(pct)}`}
174
+ style={{ width: `${clampedPct}%` }}
175
+ />
176
+ </div>
177
+ )}
178
+ </div>
179
+ );
180
+ })}
181
+ </div>
182
+ </div>
183
+ )}
184
+
185
+ {/* Subscription section */}
186
+ {subs.length > 0 && (
187
+ <div className="pt-2 border-t border-gray-200 dark:border-gray-700">
188
+ <div className="flex justify-between items-center mb-1.5">
189
+ <p className="text-[10px] font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
190
+ Subscriptions (fixed monthly)
191
+ </p>
192
+ {subscriptionCost != null && subscriptionCost > 0 && (
193
+ <span className="text-[10px] text-gray-500 dark:text-gray-400 font-medium">
194
+ ${subscriptionCost.toFixed(2)}/mo
195
+ </span>
196
+ )}
197
+ </div>
198
+ <div className="space-y-1.5">
199
+ {subs.map(r => (
200
+ <ResourceRow key={r.name} resource={r} showLicenseBadge />
201
+ ))}
202
+ </div>
203
+ </div>
204
+ )}
205
+ </div>
206
+ );
207
+ }
208
+
209
+ // =============================================================================
210
+ // GENERIC DETAIL — Simple resource list
211
+ // =============================================================================
212
+
213
+ function GenericDetail({ resources, modelBreakdown }: { resources: ProviderResource[]; modelBreakdown?: ModelUsage[] }) {
214
+ return (
215
+ <div className="space-y-1.5">
216
+ {resources.map(r => (
217
+ <ResourceRow key={r.name} resource={r} />
218
+ ))}
219
+ {modelBreakdown && modelBreakdown.length > 0 && (
220
+ <ModelBreakdownSection models={modelBreakdown} />
221
+ )}
222
+ </div>
223
+ );
224
+ }
225
+
226
+ // =============================================================================
227
+ // MODEL BREAKDOWN — Per-model token usage table
228
+ // =============================================================================
229
+
230
+ function ModelBreakdownSection({ models }: { models: ModelUsage[] }) {
231
+ return (
232
+ <div className="pt-2 mt-1 border-t border-gray-200 dark:border-gray-700">
233
+ <p className="text-[10px] font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-1.5">
234
+ Model Breakdown
235
+ </p>
236
+ <div className="space-y-1">
237
+ {/* Header */}
238
+ <div className="flex text-[9px] text-gray-400 dark:text-gray-500 font-medium">
239
+ <span className="flex-1">Model</span>
240
+ <span className="w-16 text-right">Input</span>
241
+ <span className="w-16 text-right">Output</span>
242
+ <span className="w-16 text-right">Total</span>
243
+ </div>
244
+ {/* Rows */}
245
+ {models.map(m => (
246
+ <div key={m.model} className="flex text-[10px] items-center">
247
+ <span className="flex-1 text-gray-700 dark:text-gray-300 font-medium truncate mr-1">
248
+ {m.model}
249
+ </span>
250
+ <span className="w-16 text-right text-gray-500 dark:text-gray-400">
251
+ {formatCompact(m.inputTokens)}
252
+ </span>
253
+ <span className="w-16 text-right text-gray-500 dark:text-gray-400">
254
+ {formatCompact(m.outputTokens)}
255
+ </span>
256
+ <span className="w-16 text-right text-gray-700 dark:text-gray-300 font-medium">
257
+ {formatCompact(m.inputTokens + m.outputTokens)}
258
+ </span>
259
+ </div>
260
+ ))}
261
+ </div>
262
+ </div>
263
+ );
264
+ }
265
+
266
+ // =============================================================================
267
+ // RESOURCE ROW
268
+ // =============================================================================
269
+
270
+ function ResourceRow({ resource, showLicenseBadge }: { resource: ProviderResource; showLicenseBadge?: boolean }) {
271
+ return (
272
+ <div className="flex justify-between items-center text-xs py-1">
273
+ <span className="text-gray-600 dark:text-gray-400 truncate mr-2 flex items-center gap-1.5">
274
+ {showLicenseBadge && (
275
+ <span className="inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-medium bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300">
276
+ LICENSE
277
+ </span>
278
+ )}
279
+ {resource.label}
280
+ </span>
281
+ <div className="flex items-center gap-2 whitespace-nowrap">
282
+ <span className="text-gray-900 dark:text-white font-medium">
283
+ {formatCompact(resource.used)} {resource.unit !== 'dollars' ? resource.unit : ''}
284
+ </span>
285
+ {resource.costUsd > 0 && (
286
+ <span className="text-gray-400 dark:text-gray-500 text-[10px]">
287
+ (${resource.costUsd.toFixed(2)})
288
+ </span>
289
+ )}
290
+ </div>
291
+ </div>
292
+ );
293
+ }
@@ -0,0 +1,102 @@
1
+ ---
2
+ /**
3
+ * SettingsCard.astro
4
+ * Reusable card component for settings sections
5
+ *
6
+ * @created 2026-02-03
7
+ * @task task-303.4
8
+ */
9
+
10
+ interface Props {
11
+ title: string;
12
+ description: string;
13
+ icon: string;
14
+ href: string;
15
+ badge?: string;
16
+ badgeColor?: 'blue' | 'green' | 'yellow' | 'red' | 'gray' | 'purple';
17
+ external?: boolean;
18
+ }
19
+
20
+ const {
21
+ title,
22
+ description,
23
+ icon,
24
+ href,
25
+ badge,
26
+ badgeColor = 'gray',
27
+ external = false,
28
+ } = Astro.props;
29
+
30
+ const badgeColors = {
31
+ blue: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400',
32
+ green: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400',
33
+ yellow: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400',
34
+ red: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400',
35
+ gray: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-400',
36
+ purple: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400',
37
+ };
38
+ ---
39
+
40
+ <a
41
+ href={href}
42
+ class="group block p-6 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-md transition-all"
43
+ target={external ? '_blank' : undefined}
44
+ rel={external ? 'noopener noreferrer' : undefined}
45
+ >
46
+ <div class="flex items-start gap-4">
47
+ <!-- Icon -->
48
+ <div
49
+ class="flex-shrink-0 w-12 h-12 rounded-lg bg-gray-100 dark:bg-gray-700 flex items-center justify-center text-2xl"
50
+ >
51
+ {icon}
52
+ </div>
53
+
54
+ <!-- Content -->
55
+ <div class="flex-1 min-w-0">
56
+ <div class="flex items-center gap-2 mb-1">
57
+ <h3
58
+ class="text-lg font-semibold text-gray-900 dark:text-white group-hover:text-blue-600 dark:group-hover:text-blue-400 transition-colors"
59
+ >
60
+ {title}
61
+ </h3>
62
+ {
63
+ badge && (
64
+ <span class={`text-xs font-medium px-2 py-0.5 rounded-full ${badgeColors[badgeColor]}`}>
65
+ {badge}
66
+ </span>
67
+ )
68
+ }
69
+ {
70
+ external && (
71
+ <svg
72
+ class="w-4 h-4 text-gray-400"
73
+ fill="none"
74
+ stroke="currentColor"
75
+ viewBox="0 0 24 24"
76
+ >
77
+ <path
78
+ stroke-linecap="round"
79
+ stroke-linejoin="round"
80
+ stroke-width="2"
81
+ d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
82
+ />
83
+ </svg>
84
+ )
85
+ }
86
+ </div>
87
+ <p class="text-sm text-gray-500 dark:text-gray-400">
88
+ {description}
89
+ </p>
90
+ </div>
91
+
92
+ <!-- Arrow -->
93
+ <div
94
+ class="flex-shrink-0 text-gray-400 group-hover:text-gray-600 dark:group-hover:text-gray-300 transition-colors"
95
+ >
96
+ <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
97
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"
98
+ ></path>
99
+ </svg>
100
+ </div>
101
+ </div>
102
+ </a>
@@ -0,0 +1,170 @@
1
+ ---
2
+ /**
3
+ * AllowanceGauge - Circular gauge showing allowance utilisation
4
+ *
5
+ * Displays "X% of Free Tier Used" with a circular progress gauge.
6
+ * Supports prorated allowances for sub-monthly query periods.
7
+ *
8
+ * @created 2026-01-22
9
+ */
10
+
11
+ interface Props {
12
+ /** Resource name (e.g., "Workers Requests", "D1 Reads") */
13
+ resourceName?: string;
14
+ /** ID for JavaScript updates */
15
+ id?: string;
16
+ }
17
+
18
+ const { resourceName = 'Allowance', id = 'allowance-gauge' } = Astro.props;
19
+ ---
20
+
21
+ <div
22
+ id={id}
23
+ class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4 shadow-sm"
24
+ >
25
+ <!-- Header -->
26
+ <div class="flex items-center justify-between mb-3">
27
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">{resourceName}</h3>
28
+ <span
29
+ id={`${id}-badge`}
30
+ class="px-2 py-0.5 text-xs font-medium rounded-full bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
31
+ >
32
+ Within Limit
33
+ </span>
34
+ </div>
35
+
36
+ <!-- Circular Gauge -->
37
+ <div class="flex items-center justify-center mb-3">
38
+ <div class="relative w-28 h-28">
39
+ <!-- Background circle -->
40
+ <svg class="w-28 h-28 transform -rotate-90" viewBox="0 0 120 120">
41
+ <circle
42
+ cx="60"
43
+ cy="60"
44
+ r="50"
45
+ stroke="currentColor"
46
+ stroke-width="10"
47
+ fill="none"
48
+ class="text-gray-200 dark:text-gray-700"></circle>
49
+ <!-- Progress circle -->
50
+ <circle
51
+ id={`${id}-progress`}
52
+ cx="60"
53
+ cy="60"
54
+ r="50"
55
+ stroke="currentColor"
56
+ stroke-width="10"
57
+ fill="none"
58
+ stroke-linecap="round"
59
+ class="text-green-500 transition-all duration-700 ease-out"
60
+ stroke-dasharray="314"
61
+ stroke-dashoffset="314"></circle>
62
+ </svg>
63
+ <!-- Center text -->
64
+ <div class="absolute inset-0 flex flex-col items-center justify-center">
65
+ <span id={`${id}-pct`} class="text-2xl font-bold text-gray-900 dark:text-gray-100">
66
+ --%
67
+ </span>
68
+ <span class="text-xs text-gray-500 dark:text-gray-400">used</span>
69
+ </div>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- Usage details -->
74
+ <div class="text-center text-sm">
75
+ <p id={`${id}-usage`} class="text-gray-600 dark:text-gray-300">-- / -- used</p>
76
+ <p id={`${id}-period`} class="text-xs text-gray-400 dark:text-gray-500 mt-1">
77
+ (prorated for period)
78
+ </p>
79
+ </div>
80
+ </div>
81
+
82
+ <script>
83
+ interface GaugeData {
84
+ used: number;
85
+ monthlyAllowance: number;
86
+ proratedAllowance: number;
87
+ pctUsed: number;
88
+ unit?: string;
89
+ periodLabel?: string;
90
+ }
91
+
92
+ function formatNumber(num: number): string {
93
+ if (num >= 1_000_000_000) return `${(num / 1_000_000_000).toFixed(1)}B`;
94
+ if (num >= 1_000_000) return `${(num / 1_000_000).toFixed(1)}M`;
95
+ if (num >= 1_000) return `${(num / 1_000).toFixed(1)}K`;
96
+ return num.toFixed(0);
97
+ }
98
+
99
+ function getGaugeColor(pct: number): string {
100
+ if (pct >= 100) return 'text-red-500';
101
+ if (pct >= 90) return 'text-orange-500';
102
+ if (pct >= 70) return 'text-yellow-500';
103
+ return 'text-green-500';
104
+ }
105
+
106
+ function getBadgeClasses(pct: number): string {
107
+ if (pct >= 100) {
108
+ return 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400';
109
+ }
110
+ if (pct >= 90) {
111
+ return 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400';
112
+ }
113
+ if (pct >= 70) {
114
+ return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400';
115
+ }
116
+ return 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400';
117
+ }
118
+
119
+ function getBadgeText(pct: number): string {
120
+ if (pct >= 100) return 'Over Limit!';
121
+ if (pct >= 90) return 'Critical';
122
+ if (pct >= 70) return 'High Usage';
123
+ return 'Within Limit';
124
+ }
125
+
126
+ function updateAllowanceGauge(gaugeId: string, data: GaugeData) {
127
+ const pctEl = document.getElementById(`${gaugeId}-pct`);
128
+ const progressEl = document.getElementById(`${gaugeId}-progress`);
129
+ const usageEl = document.getElementById(`${gaugeId}-usage`);
130
+ const periodEl = document.getElementById(`${gaugeId}-period`);
131
+ const badgeEl = document.getElementById(`${gaugeId}-badge`);
132
+
133
+ const displayPct = Math.min(data.pctUsed, 999);
134
+
135
+ // Update percentage display
136
+ if (pctEl) {
137
+ pctEl.textContent = `${displayPct.toFixed(0)}%`;
138
+ }
139
+
140
+ // Update circular progress (circumference = 2 * PI * r = 2 * 3.14159 * 50 = 314)
141
+ if (progressEl) {
142
+ const circumference = 314;
143
+ const offset = circumference - (Math.min(data.pctUsed, 100) / 100) * circumference;
144
+ progressEl.style.strokeDashoffset = String(offset);
145
+ progressEl.className = `${getGaugeColor(data.pctUsed)} transition-all duration-700 ease-out`;
146
+ }
147
+
148
+ // Update usage text
149
+ if (usageEl) {
150
+ const unit = data.unit ?? '';
151
+ usageEl.textContent = `${formatNumber(data.used)} / ${formatNumber(data.proratedAllowance)} ${unit}`;
152
+ }
153
+
154
+ // Update period label
155
+ if (periodEl && data.periodLabel) {
156
+ periodEl.textContent = `(${data.periodLabel} allowance)`;
157
+ }
158
+
159
+ // Update badge
160
+ if (badgeEl) {
161
+ badgeEl.className = `px-2 py-0.5 text-xs font-medium rounded-full ${getBadgeClasses(data.pctUsed)}`;
162
+ badgeEl.textContent = getBadgeText(data.pctUsed);
163
+ }
164
+ }
165
+
166
+ // Expose globally for page scripts
167
+ (
168
+ window as unknown as { updateAllowanceGauge: typeof updateAllowanceGauge }
169
+ ).updateAllowanceGauge = updateAllowanceGauge;
170
+ </script>