@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.
- package/README.md +4 -7
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +206 -4
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/notifications/NotificationDropdown.tsx +130 -0
- package/templates/full/dashboard/src/components/notifications/NotificationItem.tsx +264 -0
- package/templates/full/dashboard/src/components/patterns/PatternInfoButton.tsx +60 -0
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/FeatureUsageReport.tsx +339 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/search/SearchResultGroup.tsx +46 -0
- package/templates/full/dashboard/src/components/search/SearchResultItem.tsx +212 -0
- package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/approve.ts +49 -0
- package/templates/full/dashboard/src/pages/api/patterns/[id]/reject.ts +50 -0
- package/templates/full/dashboard/src/pages/api/reports/digests/stats.ts +38 -0
- package/templates/full/dashboard/src/pages/api/reports/digests.ts +39 -0
- package/templates/full/dashboard/src/pages/api/search/reindex/[type].ts +56 -0
- package/templates/full/dashboard/src/pages/api/test-reports/[id].ts +102 -0
- package/templates/full/dashboard/src/pages/feedback.astro +365 -0
- package/templates/full/dashboard/src/pages/kiosk.astro +206 -0
- package/templates/full/dashboard/src/pages/map.astro +561 -0
- package/templates/full/dashboard/src/pages/revenue.astro +72 -0
- package/templates/full/dashboard/src/pages/tests.astro +431 -0
- package/templates/full/scripts/ops/audit-cost-anomaly.ts +430 -0
- package/templates/full/scripts/ops/verify-account-total.ts +256 -0
- package/templates/full/tests/integration/feedback-schema.test.ts +361 -0
- package/templates/full/tests/integration/r2-archive.test.ts +108 -0
- package/templates/shared/.github/workflows/dependabot-automerge.yml +41 -0
- package/templates/shared/.github/workflows/validate-controls.yml +27 -0
- package/templates/shared/dashboard/src/components/Breadcrumbs.astro +101 -0
- package/templates/shared/dashboard/src/components/EmptyState.astro +46 -0
- package/templates/shared/dashboard/src/components/ErrorBoundary.astro +79 -0
- package/templates/shared/dashboard/src/components/LoadingSkeleton.astro +105 -0
- package/templates/shared/dashboard/src/components/PageShell.astro +72 -0
- package/templates/shared/dashboard/src/components/SkipLinks.astro +22 -0
- package/templates/shared/dashboard/src/components/Toast.astro +170 -0
- package/templates/shared/dashboard/src/components/ToastContainer.astro +156 -0
- package/templates/shared/dashboard/src/components/costs/ProviderCostsGrid.tsx +401 -0
- package/templates/shared/dashboard/src/components/costs/index.ts +4 -0
- package/templates/shared/dashboard/src/components/overview/AlertBanner.tsx +94 -0
- package/templates/shared/dashboard/src/components/overview/index.ts +9 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/resources/CostChart.tsx +170 -0
- package/templates/shared/dashboard/src/components/resources/ProviderCard.tsx +272 -0
- package/templates/shared/dashboard/src/components/resources/ProviderDetail.tsx +293 -0
- package/templates/shared/dashboard/src/components/settings/SettingsCard.astro +102 -0
- package/templates/shared/dashboard/src/components/usage/AllowanceGauge.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/AnomalyAlerts.astro +633 -0
- package/templates/shared/dashboard/src/components/usage/BillingCycleCountdown.astro +192 -0
- package/templates/shared/dashboard/src/components/usage/BurnRateHero.astro +539 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerEventLog.astro +542 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerPanel.tsx +292 -0
- package/templates/shared/dashboard/src/components/usage/CircuitBreakerStatus.astro +669 -0
- package/templates/shared/dashboard/src/components/usage/CompactThresholdBanner.astro +531 -0
- package/templates/shared/dashboard/src/components/usage/ComparisonModeSelector.astro +651 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownChart.astro +381 -0
- package/templates/shared/dashboard/src/components/usage/CostBreakdownTable.astro +210 -0
- package/templates/shared/dashboard/src/components/usage/CostDataTable.astro +0 -0
- package/templates/shared/dashboard/src/components/usage/CostDonutChart.astro +311 -0
- package/templates/shared/dashboard/src/components/usage/DailyCostChart.astro +632 -0
- package/templates/shared/dashboard/src/components/usage/ExportButton.astro +114 -0
- package/templates/shared/dashboard/src/components/usage/FeatureBudgetsTable.astro +872 -0
- package/templates/shared/dashboard/src/components/usage/FilterBar.astro +190 -0
- package/templates/shared/dashboard/src/components/usage/FilterToggles.astro +175 -0
- package/templates/shared/dashboard/src/components/usage/GitHubUsageCard.astro +537 -0
- package/templates/shared/dashboard/src/components/usage/OverageCostCard.astro +212 -0
- package/templates/shared/dashboard/src/components/usage/PlanUtilizationCard.astro +193 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCard.astro +640 -0
- package/templates/shared/dashboard/src/components/usage/ProjectCardsGrid.astro +272 -0
- package/templates/shared/dashboard/src/components/usage/ResourceSearch.astro +279 -0
- package/templates/shared/dashboard/src/components/usage/ServiceUtilizationList.astro +604 -0
- package/templates/shared/dashboard/src/components/usage/SparklineCard.astro +399 -0
- package/templates/shared/dashboard/src/components/usage/StatsHero.astro +600 -0
- package/templates/shared/dashboard/src/components/usage/TableFilters.astro +1033 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdAlert.astro +271 -0
- package/templates/shared/dashboard/src/components/usage/ThresholdSettings.astro +618 -0
- package/templates/shared/dashboard/src/components/usage/TopSpenderCard.astro +170 -0
- package/templates/shared/dashboard/src/components/usage/UnifiedResourceTable.astro +1737 -0
- package/templates/shared/dashboard/src/components/usage/UsageCard.astro +135 -0
- package/templates/shared/dashboard/src/components/usage/UsageHealthBanner.astro +387 -0
- package/templates/shared/dashboard/src/components/usage/UtilizationBar.astro +159 -0
- package/templates/shared/dashboard/src/components/usage/WorkersBreakdownTable.astro +659 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostChart.astro +461 -0
- package/templates/shared/dashboard/src/components/usage/daily/CostTable.astro +946 -0
- package/templates/shared/dashboard/src/components/usage/daily/DailyOverview.astro +1079 -0
- package/templates/shared/dashboard/src/components/usage/design-tokens.ts +187 -0
- package/templates/shared/dashboard/src/components/usage/filters/InlineDateRange.astro +285 -0
- package/templates/shared/dashboard/src/components/usage/filters/PeriodButtons.astro +157 -0
- package/templates/shared/dashboard/src/components/usage/filters/ProjectSelect.astro +284 -0
- package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/scripts/ai-tab-controller.ts +419 -0
- package/templates/shared/dashboard/src/components/usage/scripts/constants.ts +60 -0
- package/templates/shared/dashboard/src/components/usage/scripts/formatters.ts +62 -0
- package/templates/shared/dashboard/src/components/usage/scripts/overview-controller.ts +1633 -0
- package/templates/shared/dashboard/src/components/usage/scripts/resource-table-builder.ts +294 -0
- package/templates/shared/dashboard/src/components/usage/scripts/tabs-filters-controller.ts +464 -0
- package/templates/shared/dashboard/src/components/usage/state/index.ts +55 -0
- package/templates/shared/dashboard/src/components/usage/state/usageActions.ts +439 -0
- package/templates/shared/dashboard/src/components/usage/state/usageStore.ts +376 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/types.ts +283 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/components/usage/usage-colors.ts +292 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
- package/templates/shared/dashboard/src/pages/api/usage/ai-models.ts +235 -0
- package/templates/shared/dashboard/src/pages/api/usage/billing-context.ts +296 -0
- package/templates/shared/scripts/test-telemetry-flow.ts +464 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/e2e/usage-export.test.ts +784 -0
- package/templates/shared/tests/e2e/usage-mobile.test.ts +531 -0
- package/templates/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
- package/templates/standard/dashboard/src/components/errors/PriorityBadge.astro +27 -0
- package/templates/standard/dashboard/src/components/infrastructure/HealthchecksStatus.tsx +293 -0
- package/templates/standard/dashboard/src/components/infrastructure/InfrastructureTabs.tsx +268 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
- package/templates/standard/dashboard/src/pages/analytics.astro +64 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/alerts.ts +85 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks/[id]/flips.ts +110 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/healthchecks.ts +101 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime/[id]/response-times.ts +121 -0
- package/templates/standard/dashboard/src/pages/api/infrastructure/uptime.ts +89 -0
- package/templates/standard/dashboard/src/pages/api/test/service-auth.ts +178 -0
- package/templates/standard/tests/integration/connectors.test.ts +241 -0
- package/templates/standard/tests/integration/github-monitor.test.ts +143 -0
- package/templates/standard/tests/integration/ingestion.test.ts +211 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- 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>
|