@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,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CostChart — Stacked bar chart for daily costs by provider
|
|
3
|
+
*
|
|
4
|
+
* Pure CSS bars (no chart library). Shows daily cost breakdown across all
|
|
5
|
+
* providers, or filtered to a single provider when a card is selected.
|
|
6
|
+
* Matches existing HourlyUsageChart pattern.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// =============================================================================
|
|
10
|
+
// TYPES
|
|
11
|
+
// =============================================================================
|
|
12
|
+
|
|
13
|
+
export interface ChartDataPoint {
|
|
14
|
+
date: string;
|
|
15
|
+
providers: Record<string, number>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
data: ChartDataPoint[];
|
|
20
|
+
selectedProvider: string | null;
|
|
21
|
+
providerColours: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// CONSTANTS
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
const DEFAULT_COLOURS: Record<string, string> = {
|
|
29
|
+
cloudflare: '#f97316', // orange-500
|
|
30
|
+
github: '#6b7280', // gray-500
|
|
31
|
+
openai: '#10b981', // emerald-500
|
|
32
|
+
anthropic: '#f59e0b', // amber-500
|
|
33
|
+
gemini: '#6366f1', // indigo-500
|
|
34
|
+
apify: '#14b8a6', // teal-500
|
|
35
|
+
resend: '#8b5cf6', // violet-500
|
|
36
|
+
minimax: '#3b82f6', // blue-500
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// =============================================================================
|
|
40
|
+
// HELPERS
|
|
41
|
+
// =============================================================================
|
|
42
|
+
|
|
43
|
+
function formatDateLabel(dateStr: string): string {
|
|
44
|
+
const d = new Date(dateStr + 'T00:00:00Z');
|
|
45
|
+
return d.toLocaleDateString('en-AU', { day: 'numeric', month: 'short' });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// COMPONENT
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
export function CostChart({ data, selectedProvider, providerColours }: Props) {
|
|
53
|
+
const colours = { ...DEFAULT_COLOURS, ...providerColours };
|
|
54
|
+
|
|
55
|
+
if (data.length === 0) {
|
|
56
|
+
return (
|
|
57
|
+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6 text-center">
|
|
58
|
+
<p className="text-sm text-gray-500 dark:text-gray-400">No chart data available for this period.</p>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Compute max total for scaling
|
|
64
|
+
const barData = data.map(point => {
|
|
65
|
+
const entries = Object.entries(point.providers);
|
|
66
|
+
const filtered = selectedProvider
|
|
67
|
+
? entries.filter(([p]) => p === selectedProvider)
|
|
68
|
+
: entries;
|
|
69
|
+
const total = filtered.reduce((s, [, v]) => s + v, 0);
|
|
70
|
+
return { date: point.date, segments: filtered, total };
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const maxTotal = Math.max(...barData.map(d => d.total), 0.01);
|
|
74
|
+
|
|
75
|
+
// Determine label density (show every Nth label to avoid crowding)
|
|
76
|
+
const labelStep = data.length > 30 ? 7 : data.length > 14 ? 3 : data.length > 7 ? 2 : 1;
|
|
77
|
+
|
|
78
|
+
// Collect all providers for legend
|
|
79
|
+
const allProviders = new Set<string>();
|
|
80
|
+
for (const point of data) {
|
|
81
|
+
for (const p of Object.keys(point.providers)) {
|
|
82
|
+
if (!selectedProvider || p === selectedProvider) allProviders.add(p);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-4">
|
|
88
|
+
{/* Legend */}
|
|
89
|
+
<div className="flex flex-wrap gap-3 mb-3">
|
|
90
|
+
{Array.from(allProviders).map(p => (
|
|
91
|
+
<div key={p} className="flex items-center gap-1.5">
|
|
92
|
+
<div
|
|
93
|
+
className="w-2.5 h-2.5 rounded-sm flex-shrink-0"
|
|
94
|
+
style={{ backgroundColor: colours[p] ?? '#9ca3af' }}
|
|
95
|
+
/>
|
|
96
|
+
<span className="text-[10px] text-gray-600 dark:text-gray-400 capitalize">{p}</span>
|
|
97
|
+
</div>
|
|
98
|
+
))}
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
{/* Y-axis max label */}
|
|
102
|
+
<div className="flex items-end mb-1">
|
|
103
|
+
<span className="text-[9px] text-gray-400 dark:text-gray-500 w-12 text-right mr-2">
|
|
104
|
+
${maxTotal.toFixed(2)}
|
|
105
|
+
</span>
|
|
106
|
+
<div className="flex-1 border-b border-dashed border-gray-200 dark:border-gray-700" />
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
{/* Bars */}
|
|
110
|
+
<div className="flex items-end gap-px" style={{ height: '120px' }}>
|
|
111
|
+
{barData.map((bar, idx) => {
|
|
112
|
+
const heightPct = maxTotal > 0 ? (bar.total / maxTotal) * 100 : 0;
|
|
113
|
+
return (
|
|
114
|
+
<div key={bar.date} className="flex-1 flex flex-col justify-end h-full group relative">
|
|
115
|
+
{/* Stacked bar */}
|
|
116
|
+
<div
|
|
117
|
+
className="w-full rounded-t-sm overflow-hidden flex flex-col-reverse transition-all"
|
|
118
|
+
style={{ height: `${heightPct}%`, minHeight: bar.total > 0 ? '2px' : '0px' }}
|
|
119
|
+
>
|
|
120
|
+
{bar.segments.map(([provider, cost]) => {
|
|
121
|
+
const segPct = bar.total > 0 ? (cost / bar.total) * 100 : 0;
|
|
122
|
+
return (
|
|
123
|
+
<div
|
|
124
|
+
key={provider}
|
|
125
|
+
style={{
|
|
126
|
+
height: `${segPct}%`,
|
|
127
|
+
backgroundColor: colours[provider] ?? '#9ca3af',
|
|
128
|
+
minHeight: cost > 0 ? '1px' : '0px',
|
|
129
|
+
}}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Tooltip */}
|
|
136
|
+
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 hidden group-hover:block z-10 pointer-events-none">
|
|
137
|
+
<div className="bg-gray-900 text-white text-[10px] rounded px-2 py-1.5 whitespace-nowrap shadow-lg">
|
|
138
|
+
<div className="font-medium mb-0.5">{formatDateLabel(bar.date)}</div>
|
|
139
|
+
{bar.segments.map(([p, cost]) => (
|
|
140
|
+
<div key={p} className="flex items-center gap-1">
|
|
141
|
+
<div className="w-1.5 h-1.5 rounded-sm" style={{ backgroundColor: colours[p] }} />
|
|
142
|
+
<span className="capitalize">{p}:</span>
|
|
143
|
+
<span className="font-medium">${cost.toFixed(2)}</span>
|
|
144
|
+
</div>
|
|
145
|
+
))}
|
|
146
|
+
<div className="border-t border-gray-700 mt-0.5 pt-0.5 font-medium">
|
|
147
|
+
Total: ${bar.total.toFixed(2)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* X-axis label (sparse) */}
|
|
153
|
+
{idx % labelStep === 0 && (
|
|
154
|
+
<span className="text-[8px] text-gray-400 dark:text-gray-500 mt-1 text-center block truncate">
|
|
155
|
+
{formatDateLabel(bar.date)}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
})}
|
|
161
|
+
</div>
|
|
162
|
+
|
|
163
|
+
{/* Zero line */}
|
|
164
|
+
<div className="flex items-start mt-0.5">
|
|
165
|
+
<span className="text-[9px] text-gray-400 dark:text-gray-500 w-12 text-right mr-2">$0</span>
|
|
166
|
+
<div className="flex-1 border-t border-gray-200 dark:border-gray-700" />
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProviderCard — Compact cost card for a single provider
|
|
3
|
+
*
|
|
4
|
+
* Variants:
|
|
5
|
+
* - Allowance-based (Cloudflare, GitHub): progress bar + overage
|
|
6
|
+
* - Pay-as-you-go (OpenAI, Anthropic, Gemini, Apify, Resend): total spend + key metric
|
|
7
|
+
* - Subscription + PAYG (Minimax): subscription cost + API usage
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// TYPES (match /api/costs/overview response)
|
|
12
|
+
// =============================================================================
|
|
13
|
+
|
|
14
|
+
export interface ModelUsage {
|
|
15
|
+
model: string;
|
|
16
|
+
inputTokens: number;
|
|
17
|
+
outputTokens: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ProviderCostSummary {
|
|
21
|
+
provider: string;
|
|
22
|
+
displayName: string;
|
|
23
|
+
type: 'allowance' | 'pay-as-you-go' | 'subscription-payg' | 'balance';
|
|
24
|
+
totalCostUsd: number;
|
|
25
|
+
subscriptionCostUsd?: number;
|
|
26
|
+
allowanceUsagePct?: number;
|
|
27
|
+
keyMetric?: { label: string; value: string };
|
|
28
|
+
status: 'ok' | 'warning' | 'critical' | 'incomplete';
|
|
29
|
+
lastUpdated: string;
|
|
30
|
+
resources?: ProviderResource[];
|
|
31
|
+
modelBreakdown?: ModelUsage[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ProviderResource {
|
|
35
|
+
name: string;
|
|
36
|
+
label: string;
|
|
37
|
+
used: number;
|
|
38
|
+
allowance?: number;
|
|
39
|
+
pctUsed?: number;
|
|
40
|
+
overageCost?: number;
|
|
41
|
+
costUsd: number;
|
|
42
|
+
unit: string;
|
|
43
|
+
category?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// CONSTANTS
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
const PROVIDER_ICONS: Record<string, string> = {
|
|
51
|
+
cloudflare: '☁️',
|
|
52
|
+
github: '🐙',
|
|
53
|
+
openai: '🤖',
|
|
54
|
+
anthropic: '🧠',
|
|
55
|
+
apify: '🕷️',
|
|
56
|
+
resend: '✉️',
|
|
57
|
+
minimax: '🎯',
|
|
58
|
+
gemini: '💎',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const PROVIDER_GRADIENTS: Record<string, string> = {
|
|
62
|
+
cloudflare: 'from-orange-500 to-amber-600',
|
|
63
|
+
github: 'from-gray-700 to-gray-900',
|
|
64
|
+
openai: 'from-green-500 to-emerald-600',
|
|
65
|
+
anthropic: 'from-orange-500 to-amber-600',
|
|
66
|
+
apify: 'from-teal-500 to-green-600',
|
|
67
|
+
resend: 'from-violet-500 to-purple-600',
|
|
68
|
+
minimax: 'from-indigo-500 to-blue-600',
|
|
69
|
+
gemini: 'from-blue-400 to-indigo-500',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// =============================================================================
|
|
73
|
+
// HELPERS
|
|
74
|
+
// =============================================================================
|
|
75
|
+
|
|
76
|
+
function formatCompact(n: number): string {
|
|
77
|
+
if (n >= 1_000_000_000) return `${(n / 1_000_000_000).toFixed(1)}B`;
|
|
78
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
79
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
80
|
+
return n.toFixed(0);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function barColour(pct: number): string {
|
|
84
|
+
if (pct >= 100) return 'bg-red-500';
|
|
85
|
+
if (pct >= 75) return 'bg-amber-500';
|
|
86
|
+
if (pct >= 50) return 'bg-yellow-400';
|
|
87
|
+
return 'bg-emerald-500';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function statusBadge(status: string): { label: string; cls: string } | null {
|
|
91
|
+
if (status === 'critical') return { label: 'OVERAGE', cls: 'bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-300' };
|
|
92
|
+
if (status === 'warning') return { label: 'HIGH', cls: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300' };
|
|
93
|
+
if (status === 'incomplete') return { label: 'PARTIAL', cls: 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400' };
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// =============================================================================
|
|
98
|
+
// COMPONENT
|
|
99
|
+
// =============================================================================
|
|
100
|
+
|
|
101
|
+
interface Props {
|
|
102
|
+
provider: ProviderCostSummary;
|
|
103
|
+
selected: boolean;
|
|
104
|
+
onClick: () => void;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ProviderCard({ provider, selected, onClick }: Props) {
|
|
108
|
+
const icon = PROVIDER_ICONS[provider.provider] ?? '📦';
|
|
109
|
+
const gradient = PROVIDER_GRADIENTS[provider.provider] ?? 'from-gray-500 to-gray-600';
|
|
110
|
+
const badge = statusBadge(provider.status);
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<button
|
|
114
|
+
type="button"
|
|
115
|
+
onClick={onClick}
|
|
116
|
+
className={`
|
|
117
|
+
w-full text-left bg-white dark:bg-gray-800 rounded-lg border overflow-hidden
|
|
118
|
+
transition-all duration-150 cursor-pointer
|
|
119
|
+
${selected
|
|
120
|
+
? 'border-blue-500 ring-2 ring-blue-500/30 shadow-md'
|
|
121
|
+
: 'border-gray-200 dark:border-gray-700 hover:border-gray-300 dark:hover:border-gray-600 hover:shadow-sm'
|
|
122
|
+
}
|
|
123
|
+
`}
|
|
124
|
+
>
|
|
125
|
+
{/* Gradient header */}
|
|
126
|
+
<div className={`bg-gradient-to-r ${gradient} px-3 py-2.5 text-white`}>
|
|
127
|
+
<div className="flex items-center justify-between">
|
|
128
|
+
<div className="flex items-center gap-1.5 min-w-0">
|
|
129
|
+
<span className="text-lg flex-shrink-0">{icon}</span>
|
|
130
|
+
<span className="text-sm font-semibold truncate">{provider.displayName}</span>
|
|
131
|
+
{badge && (
|
|
132
|
+
<span className={`ml-1 inline-flex items-center px-1.5 py-0.5 rounded text-[9px] font-bold ${badge.cls}`}>
|
|
133
|
+
{badge.label}
|
|
134
|
+
</span>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<div className="text-right flex-shrink-0 ml-2">
|
|
138
|
+
{provider.type === 'allowance' && provider.subscriptionCostUsd != null && provider.subscriptionCostUsd > 0 ? (
|
|
139
|
+
<div>
|
|
140
|
+
<span className="text-base font-bold">${provider.subscriptionCostUsd.toFixed(2)}</span>
|
|
141
|
+
<span className="text-[9px] opacity-75 block">
|
|
142
|
+
{provider.totalCostUsd > 0 ? `+$${provider.totalCostUsd.toFixed(2)} overage` : '/mo'}
|
|
143
|
+
</span>
|
|
144
|
+
</div>
|
|
145
|
+
) : (
|
|
146
|
+
<span className="text-base font-bold">${provider.totalCostUsd.toFixed(2)}</span>
|
|
147
|
+
)}
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
{/* Card body */}
|
|
153
|
+
<div className="px-3 py-2.5 space-y-2">
|
|
154
|
+
{provider.type === 'allowance' && <AllowanceBody provider={provider} />}
|
|
155
|
+
{provider.type === 'pay-as-you-go' && <PayAsYouGoBody provider={provider} />}
|
|
156
|
+
{provider.type === 'subscription-payg' && <SubscriptionBody provider={provider} />}
|
|
157
|
+
{provider.type === 'balance' && <BalanceBody provider={provider} />}
|
|
158
|
+
|
|
159
|
+
{/* Footer */}
|
|
160
|
+
<div className="text-[9px] text-gray-400 dark:text-gray-500 pt-1">
|
|
161
|
+
Updated {provider.lastUpdated}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
</button>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// =============================================================================
|
|
169
|
+
// BODY VARIANTS
|
|
170
|
+
// =============================================================================
|
|
171
|
+
|
|
172
|
+
function AllowanceBody({ provider }: { provider: ProviderCostSummary }) {
|
|
173
|
+
const pct = provider.allowanceUsagePct ?? 0;
|
|
174
|
+
const clampedPct = Math.min(pct, 100);
|
|
175
|
+
|
|
176
|
+
return (
|
|
177
|
+
<>
|
|
178
|
+
{/* Subscription line */}
|
|
179
|
+
{provider.subscriptionCostUsd != null && provider.subscriptionCostUsd > 0 && (
|
|
180
|
+
<div className="flex justify-between text-[10px]">
|
|
181
|
+
<span className="text-gray-500 dark:text-gray-400">
|
|
182
|
+
{provider.provider === 'cloudflare' ? 'Workers Paid plan' : 'License'}
|
|
183
|
+
</span>
|
|
184
|
+
<span className="text-gray-600 dark:text-gray-300 font-medium">
|
|
185
|
+
${provider.subscriptionCostUsd.toFixed(2)}/mo
|
|
186
|
+
</span>
|
|
187
|
+
</div>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{/* Progress bar */}
|
|
191
|
+
<div>
|
|
192
|
+
<div className="flex justify-between text-[10px] mb-0.5">
|
|
193
|
+
<span className="text-gray-500 dark:text-gray-400">Allowance usage</span>
|
|
194
|
+
<span className="text-gray-700 dark:text-gray-300 font-medium">
|
|
195
|
+
{pct >= 1000 ? '999+%' : `${Math.round(pct)}%`}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
<div className="w-full h-1.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
199
|
+
<div
|
|
200
|
+
className={`h-full rounded-full transition-all ${barColour(pct)}`}
|
|
201
|
+
style={{ width: `${clampedPct}%` }}
|
|
202
|
+
/>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
{/* Key metric */}
|
|
207
|
+
{provider.keyMetric && (
|
|
208
|
+
<div className="flex justify-between text-[10px]">
|
|
209
|
+
<span className="text-gray-500 dark:text-gray-400">{provider.keyMetric.label}</span>
|
|
210
|
+
<span className="text-gray-700 dark:text-gray-300 font-medium">{provider.keyMetric.value}</span>
|
|
211
|
+
</div>
|
|
212
|
+
)}
|
|
213
|
+
</>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function PayAsYouGoBody({ provider }: { provider: ProviderCostSummary }) {
|
|
218
|
+
return (
|
|
219
|
+
<>
|
|
220
|
+
{provider.keyMetric && (
|
|
221
|
+
<div className="flex justify-between text-[10px]">
|
|
222
|
+
<span className="text-gray-500 dark:text-gray-400">{provider.keyMetric.label}</span>
|
|
223
|
+
<span className="text-gray-700 dark:text-gray-300 font-medium">{provider.keyMetric.value}</span>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
{/* Show top resources compactly */}
|
|
227
|
+
{provider.resources && provider.resources.length > 0 && (
|
|
228
|
+
<div className="space-y-0.5">
|
|
229
|
+
{provider.resources.slice(0, 3).map(r => (
|
|
230
|
+
<div key={r.name} className="flex justify-between text-[9px] text-gray-400 dark:text-gray-500">
|
|
231
|
+
<span className="truncate mr-1">{r.label}</span>
|
|
232
|
+
<span className="flex-shrink-0">{formatCompact(r.used)} {r.unit === 'dollars' ? '' : r.unit}</span>
|
|
233
|
+
</div>
|
|
234
|
+
))}
|
|
235
|
+
{provider.resources.length > 3 && (
|
|
236
|
+
<span className="text-[9px] text-gray-400">+{provider.resources.length - 3} more</span>
|
|
237
|
+
)}
|
|
238
|
+
</div>
|
|
239
|
+
)}
|
|
240
|
+
</>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function SubscriptionBody({ provider }: { provider: ProviderCostSummary }) {
|
|
245
|
+
return (
|
|
246
|
+
<>
|
|
247
|
+
{provider.keyMetric && (
|
|
248
|
+
<div className="flex justify-between text-[10px]">
|
|
249
|
+
<span className="text-gray-500 dark:text-gray-400">{provider.keyMetric.label}</span>
|
|
250
|
+
<span className="text-gray-700 dark:text-gray-300 font-medium">{provider.keyMetric.value}</span>
|
|
251
|
+
</div>
|
|
252
|
+
)}
|
|
253
|
+
<div className="flex justify-between text-[10px]">
|
|
254
|
+
<span className="text-gray-500 dark:text-gray-400">Type</span>
|
|
255
|
+
<span className="text-indigo-600 dark:text-indigo-400 font-medium text-[9px]">Subscription + API</span>
|
|
256
|
+
</div>
|
|
257
|
+
</>
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function BalanceBody({ provider }: { provider: ProviderCostSummary }) {
|
|
262
|
+
return (
|
|
263
|
+
<>
|
|
264
|
+
{provider.keyMetric && (
|
|
265
|
+
<div className="flex justify-between text-[10px]">
|
|
266
|
+
<span className="text-gray-500 dark:text-gray-400">{provider.keyMetric.label}</span>
|
|
267
|
+
<span className="text-gray-700 dark:text-gray-300 font-medium">{provider.keyMetric.value}</span>
|
|
268
|
+
</div>
|
|
269
|
+
)}
|
|
270
|
+
</>
|
|
271
|
+
);
|
|
272
|
+
}
|