@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,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Trends Report Component
|
|
3
|
+
* Shows cost breakdown by resource type and project
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
interface DailyCost {
|
|
8
|
+
date: string;
|
|
9
|
+
workers_cost: number;
|
|
10
|
+
d1_cost: number;
|
|
11
|
+
kv_cost: number;
|
|
12
|
+
r2_cost: number;
|
|
13
|
+
ai_cost: number;
|
|
14
|
+
queue_cost: number;
|
|
15
|
+
total_cost: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function StatCard({ label, value, colour, subValue }: { label: string; value: number | string; colour: string; subValue?: string }) {
|
|
19
|
+
return (
|
|
20
|
+
<div className={`bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 ${colour}`}>
|
|
21
|
+
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{label}</p>
|
|
22
|
+
<p className="text-2xl font-bold mt-1">{value}</p>
|
|
23
|
+
{subValue && <p className="text-xs text-gray-500 mt-0.5">{subValue}</p>}
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatCost(n: number): string {
|
|
29
|
+
if (n === 0) return '$0.00';
|
|
30
|
+
if (n < 0.01) return '<$0.01';
|
|
31
|
+
return `$${n.toFixed(2)}`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function CostBar({ label, value, maxValue, colour }: { label: string; value: number; maxValue: number; colour: string }) {
|
|
35
|
+
const percentage = maxValue > 0 ? (value / maxValue) * 100 : 0;
|
|
36
|
+
return (
|
|
37
|
+
<div className="flex items-center gap-3">
|
|
38
|
+
<span className="w-20 text-sm text-gray-600 dark:text-gray-400">{label}</span>
|
|
39
|
+
<div className="flex-1 h-4 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
40
|
+
<div className={`h-full ${colour} transition-all duration-300`} style={{ width: `${percentage}%` }} />
|
|
41
|
+
</div>
|
|
42
|
+
<span className="w-16 text-sm font-medium text-gray-900 dark:text-white text-right">{formatCost(value)}</span>
|
|
43
|
+
</div>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function CostTrendsReport() {
|
|
48
|
+
const [dailyCosts, setDailyCosts] = useState<DailyCost[]>([]);
|
|
49
|
+
const [loading, setLoading] = useState(true);
|
|
50
|
+
const [error, setError] = useState<string | null>(null);
|
|
51
|
+
const [days, setDays] = useState(30);
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
async function fetchData() {
|
|
55
|
+
try {
|
|
56
|
+
setLoading(true);
|
|
57
|
+
// Use /api/usage/daily which returns daily cost breakdown
|
|
58
|
+
const response = await fetch(`/api/usage/daily?period=${days}d`);
|
|
59
|
+
|
|
60
|
+
if (!response.ok) throw new Error('Failed to fetch daily costs');
|
|
61
|
+
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
|
|
64
|
+
// API returns { success, data: { days: DailyCostBreakdown[], totals, period } }
|
|
65
|
+
// DailyCostBreakdown has: date, workers, d1, kv, r2, aiGateway, workersAI, queues, total, etc.
|
|
66
|
+
const responseData = data.data || data;
|
|
67
|
+
const costs: DailyCost[] = (responseData.days || []).map((d: Record<string, unknown>) => ({
|
|
68
|
+
date: (d.date as string) || '',
|
|
69
|
+
workers_cost: (d.workers as number) || 0,
|
|
70
|
+
d1_cost: (d.d1 as number) || 0,
|
|
71
|
+
kv_cost: (d.kv as number) || 0,
|
|
72
|
+
r2_cost: (d.r2 as number) || 0,
|
|
73
|
+
ai_cost: ((d.aiGateway as number) || 0) + ((d.workersAI as number) || 0),
|
|
74
|
+
queue_cost: (d.queues as number) || 0,
|
|
75
|
+
total_cost: (d.total as number) || 0,
|
|
76
|
+
}));
|
|
77
|
+
|
|
78
|
+
setDailyCosts(costs);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
setError(e instanceof Error ? e.message : 'Unknown error');
|
|
81
|
+
} finally {
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
fetchData();
|
|
86
|
+
}, [days]);
|
|
87
|
+
|
|
88
|
+
if (loading) {
|
|
89
|
+
return (
|
|
90
|
+
<div className="space-y-4">
|
|
91
|
+
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
92
|
+
{[...Array(6)].map((_, i) => (
|
|
93
|
+
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg border p-4 animate-pulse">
|
|
94
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2"></div>
|
|
95
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-12"></div>
|
|
96
|
+
</div>
|
|
97
|
+
))}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (error) {
|
|
104
|
+
return (
|
|
105
|
+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
|
|
106
|
+
Error loading cost data: {error}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Calculate totals
|
|
112
|
+
const totals = dailyCosts.reduce(
|
|
113
|
+
(acc, d) => ({
|
|
114
|
+
workers: acc.workers + d.workers_cost,
|
|
115
|
+
d1: acc.d1 + d.d1_cost,
|
|
116
|
+
kv: acc.kv + d.kv_cost,
|
|
117
|
+
r2: acc.r2 + d.r2_cost,
|
|
118
|
+
ai: acc.ai + d.ai_cost,
|
|
119
|
+
queue: acc.queue + d.queue_cost,
|
|
120
|
+
total: acc.total + d.total_cost,
|
|
121
|
+
}),
|
|
122
|
+
{ workers: 0, d1: 0, kv: 0, r2: 0, ai: 0, queue: 0, total: 0 }
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const maxCostType = Math.max(totals.workers, totals.d1, totals.kv, totals.r2, totals.ai, totals.queue);
|
|
126
|
+
const avgDaily = dailyCosts.length > 0 ? totals.total / dailyCosts.length : 0;
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<div className="space-y-6">
|
|
130
|
+
{/* Filter */}
|
|
131
|
+
<div className="flex items-center gap-4">
|
|
132
|
+
<label className="text-sm text-gray-600 dark:text-gray-400">Time range:</label>
|
|
133
|
+
<select
|
|
134
|
+
value={days}
|
|
135
|
+
onChange={(e) => setDays(Number(e.target.value))}
|
|
136
|
+
className="px-3 py-1.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-800 text-sm"
|
|
137
|
+
>
|
|
138
|
+
<option value={7}>Last 7 days</option>
|
|
139
|
+
<option value={30}>Last 30 days</option>
|
|
140
|
+
<option value={90}>Last 90 days</option>
|
|
141
|
+
</select>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
{/* Summary Stats */}
|
|
145
|
+
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
146
|
+
<StatCard label="Total Cost" value={formatCost(totals.total)} colour="border-l-4 border-l-purple-500" />
|
|
147
|
+
<StatCard label="Avg Daily" value={formatCost(avgDaily)} colour="border-l-4 border-l-blue-500" />
|
|
148
|
+
<StatCard label="Workers" value={formatCost(totals.workers)} colour="border-l-4 border-l-orange-500" />
|
|
149
|
+
<StatCard label="D1 Database" value={formatCost(totals.d1)} colour="border-l-4 border-l-green-500" />
|
|
150
|
+
<StatCard label="AI Gateway" value={formatCost(totals.ai)} colour="border-l-4 border-l-pink-500" />
|
|
151
|
+
<StatCard label="KV + R2 + Queue" value={formatCost(totals.kv + totals.r2 + totals.queue)} colour="border-l-4 border-l-teal-500" />
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
155
|
+
{/* Cost by Type */}
|
|
156
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
157
|
+
<h3 className="font-semibold text-gray-900 dark:text-white mb-4">Cost by Resource Type</h3>
|
|
158
|
+
<div className="space-y-3">
|
|
159
|
+
<CostBar label="Workers" value={totals.workers} maxValue={maxCostType} colour="bg-orange-500" />
|
|
160
|
+
<CostBar label="D1" value={totals.d1} maxValue={maxCostType} colour="bg-green-500" />
|
|
161
|
+
<CostBar label="AI" value={totals.ai} maxValue={maxCostType} colour="bg-pink-500" />
|
|
162
|
+
<CostBar label="KV" value={totals.kv} maxValue={maxCostType} colour="bg-blue-500" />
|
|
163
|
+
<CostBar label="R2" value={totals.r2} maxValue={maxCostType} colour="bg-purple-500" />
|
|
164
|
+
<CostBar label="Queue" value={totals.queue} maxValue={maxCostType} colour="bg-teal-500" />
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Additional Cost Details */}
|
|
169
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
|
|
170
|
+
<h3 className="font-semibold text-gray-900 dark:text-white mb-4">Cost Summary</h3>
|
|
171
|
+
<div className="space-y-3">
|
|
172
|
+
{[
|
|
173
|
+
{ label: 'Workers', value: totals.workers, colour: 'text-orange-600' },
|
|
174
|
+
{ label: 'D1 Database', value: totals.d1, colour: 'text-green-600' },
|
|
175
|
+
{ label: 'AI (Gateway + Workers AI)', value: totals.ai, colour: 'text-pink-600' },
|
|
176
|
+
{ label: 'KV Store', value: totals.kv, colour: 'text-blue-600' },
|
|
177
|
+
{ label: 'R2 Storage', value: totals.r2, colour: 'text-purple-600' },
|
|
178
|
+
{ label: 'Queues', value: totals.queue, colour: 'text-teal-600' },
|
|
179
|
+
]
|
|
180
|
+
.sort((a, b) => b.value - a.value)
|
|
181
|
+
.map(({ label, value, colour }) => (
|
|
182
|
+
<div key={label} className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700/50 rounded">
|
|
183
|
+
<span className="text-sm text-gray-700 dark:text-gray-300">{label}</span>
|
|
184
|
+
<span className={`font-bold ${colour}`}>{formatCost(value)}</span>
|
|
185
|
+
</div>
|
|
186
|
+
))}
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Daily Cost Table */}
|
|
192
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
|
|
193
|
+
<div className="px-4 py-3 border-b border-gray-200 dark:border-gray-700">
|
|
194
|
+
<h3 className="font-semibold text-gray-900 dark:text-white">Daily Cost Breakdown</h3>
|
|
195
|
+
</div>
|
|
196
|
+
{dailyCosts.length === 0 ? (
|
|
197
|
+
<div className="p-8 text-center text-gray-500">No daily cost data available</div>
|
|
198
|
+
) : (
|
|
199
|
+
<div className="overflow-x-auto">
|
|
200
|
+
<table className="w-full">
|
|
201
|
+
<thead className="bg-gray-50 dark:bg-gray-900/50">
|
|
202
|
+
<tr>
|
|
203
|
+
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
|
204
|
+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Workers</th>
|
|
205
|
+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">D1</th>
|
|
206
|
+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">AI</th>
|
|
207
|
+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Other</th>
|
|
208
|
+
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Total</th>
|
|
209
|
+
</tr>
|
|
210
|
+
</thead>
|
|
211
|
+
<tbody>
|
|
212
|
+
{dailyCosts.slice(0, 30).map((d) => (
|
|
213
|
+
<tr key={d.date} className="border-b border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800/50">
|
|
214
|
+
<td className="px-4 py-3 font-medium text-gray-900 dark:text-white">{d.date}</td>
|
|
215
|
+
<td className="px-4 py-3 text-right text-gray-600 dark:text-gray-400">{formatCost(d.workers_cost)}</td>
|
|
216
|
+
<td className="px-4 py-3 text-right text-gray-600 dark:text-gray-400">{formatCost(d.d1_cost)}</td>
|
|
217
|
+
<td className="px-4 py-3 text-right text-gray-600 dark:text-gray-400">{formatCost(d.ai_cost)}</td>
|
|
218
|
+
<td className="px-4 py-3 text-right text-gray-600 dark:text-gray-400">{formatCost(d.kv_cost + d.r2_cost + d.queue_cost)}</td>
|
|
219
|
+
<td className="px-4 py-3 text-right font-bold text-gray-900 dark:text-white">{formatCost(d.total_cost)}</td>
|
|
220
|
+
</tr>
|
|
221
|
+
))}
|
|
222
|
+
</tbody>
|
|
223
|
+
</table>
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error Trends Report Component
|
|
3
|
+
* Shows error occurrence trends by priority, with clickable drilldowns to /errors
|
|
4
|
+
*/
|
|
5
|
+
import { useState, useEffect } from 'react';
|
|
6
|
+
|
|
7
|
+
interface ErrorStats {
|
|
8
|
+
byPriority: Record<string, number>;
|
|
9
|
+
byStatus: Record<string, number>;
|
|
10
|
+
byType: Record<string, number>;
|
|
11
|
+
byProject: Record<string, number>;
|
|
12
|
+
recentCount: number;
|
|
13
|
+
todayOccurrences: number;
|
|
14
|
+
totalOpen: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ClickableStatCard({ label, value, colour, href, subValue }: {
|
|
18
|
+
label: string;
|
|
19
|
+
value: number | string;
|
|
20
|
+
colour: string;
|
|
21
|
+
href: string;
|
|
22
|
+
subValue?: string;
|
|
23
|
+
}) {
|
|
24
|
+
return (
|
|
25
|
+
<a
|
|
26
|
+
href={href}
|
|
27
|
+
className={`bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4 ${colour} block hover:ring-2 ring-blue-300 dark:ring-blue-600 transition-all cursor-pointer`}
|
|
28
|
+
>
|
|
29
|
+
<p className="text-sm font-medium text-gray-600 dark:text-gray-400">{label}</p>
|
|
30
|
+
<p className="text-2xl font-bold mt-1">{value}</p>
|
|
31
|
+
{subValue && <p className="text-xs text-gray-500 mt-0.5">{subValue}</p>}
|
|
32
|
+
</a>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function PriorityBar({ priority, count, maxCount }: { priority: string; count: number; maxCount: number }) {
|
|
37
|
+
const colours: Record<string, string> = {
|
|
38
|
+
P0: 'bg-red-500',
|
|
39
|
+
P1: 'bg-orange-500',
|
|
40
|
+
P2: 'bg-yellow-500',
|
|
41
|
+
P3: 'bg-blue-500',
|
|
42
|
+
P4: 'bg-gray-400',
|
|
43
|
+
};
|
|
44
|
+
const percentage = maxCount > 0 ? (count / maxCount) * 100 : 0;
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<a
|
|
48
|
+
href={`/errors?priority=${priority}`}
|
|
49
|
+
className="flex items-center gap-3 hover:bg-gray-100 dark:hover:bg-gray-700/50 rounded p-1 -m-1 transition-colors"
|
|
50
|
+
>
|
|
51
|
+
<span className="w-8 text-sm font-medium text-gray-700 dark:text-gray-300">{priority}</span>
|
|
52
|
+
<div className="flex-1 h-6 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
|
|
53
|
+
<div
|
|
54
|
+
className={`h-full ${colours[priority] || colours.P4} transition-all duration-300`}
|
|
55
|
+
style={{ width: `${percentage}%` }}
|
|
56
|
+
/>
|
|
57
|
+
</div>
|
|
58
|
+
<span className="w-12 text-sm font-medium text-gray-900 dark:text-white text-right">{count}</span>
|
|
59
|
+
</a>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ErrorTrendsReport() {
|
|
64
|
+
const [stats, setStats] = useState<ErrorStats | null>(null);
|
|
65
|
+
const [loading, setLoading] = useState(true);
|
|
66
|
+
const [error, setError] = useState<string | null>(null);
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
async function fetchData() {
|
|
70
|
+
try {
|
|
71
|
+
const response = await fetch('/api/errors/stats');
|
|
72
|
+
if (!response.ok) throw new Error('Failed to fetch error stats');
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
setStats(data);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
setError(e instanceof Error ? e.message : 'Unknown error');
|
|
77
|
+
} finally {
|
|
78
|
+
setLoading(false);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
fetchData();
|
|
82
|
+
}, []);
|
|
83
|
+
|
|
84
|
+
if (loading) {
|
|
85
|
+
return (
|
|
86
|
+
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
87
|
+
{[...Array(6)].map((_, i) => (
|
|
88
|
+
<div key={i} className="bg-white dark:bg-gray-800 rounded-lg border p-4 animate-pulse">
|
|
89
|
+
<div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2"></div>
|
|
90
|
+
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-12"></div>
|
|
91
|
+
</div>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (error) {
|
|
98
|
+
return (
|
|
99
|
+
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 text-red-700 dark:text-red-300">
|
|
100
|
+
Error loading stats: {error}
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (!stats) return null;
|
|
106
|
+
|
|
107
|
+
const priorityCounts = stats.byPriority || {};
|
|
108
|
+
const maxPriorityCount = Math.max(...Object.values(priorityCounts), 1);
|
|
109
|
+
const statusCounts = stats.byStatus || {};
|
|
110
|
+
const typeCounts = stats.byType || {};
|
|
111
|
+
const projectCounts = stats.byProject || {};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div className="space-y-6">
|
|
115
|
+
{/* Summary Stats — all clickable */}
|
|
116
|
+
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
117
|
+
<ClickableStatCard
|
|
118
|
+
label="P0 Critical"
|
|
119
|
+
value={priorityCounts.P0 || 0}
|
|
120
|
+
colour={`border-l-4 ${(priorityCounts.P0 || 0) > 0 ? 'border-l-red-500' : 'border-l-green-500'}`}
|
|
121
|
+
href="/errors?priority=P0"
|
|
122
|
+
/>
|
|
123
|
+
<ClickableStatCard
|
|
124
|
+
label="P1 High"
|
|
125
|
+
value={priorityCounts.P1 || 0}
|
|
126
|
+
colour="border-l-4 border-l-orange-500"
|
|
127
|
+
href="/errors?priority=P1"
|
|
128
|
+
/>
|
|
129
|
+
<ClickableStatCard
|
|
130
|
+
label="P2 Medium"
|
|
131
|
+
value={priorityCounts.P2 || 0}
|
|
132
|
+
colour="border-l-4 border-l-yellow-500"
|
|
133
|
+
href="/errors?priority=P2"
|
|
134
|
+
/>
|
|
135
|
+
<ClickableStatCard
|
|
136
|
+
label="P3 Low"
|
|
137
|
+
value={priorityCounts.P3 || 0}
|
|
138
|
+
colour="border-l-4 border-l-blue-500"
|
|
139
|
+
href="/errors?priority=P3"
|
|
140
|
+
/>
|
|
141
|
+
<ClickableStatCard
|
|
142
|
+
label="Total Open"
|
|
143
|
+
value={stats.totalOpen}
|
|
144
|
+
colour="border-l-4 border-l-purple-500"
|
|
145
|
+
href="/errors?status=open"
|
|
146
|
+
/>
|
|
147
|
+
<ClickableStatCard
|
|
148
|
+
label="Last 24h"
|
|
149
|
+
value={stats.recentCount}
|
|
150
|
+
colour="border-l-4 border-l-teal-500"
|
|
151
|
+
href="/errors?recent=24h"
|
|
152
|
+
/>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
156
|
+
{/* Priority Distribution */}
|
|
157
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border p-4">
|
|
158
|
+
<h3 className="font-semibold text-gray-900 dark:text-white mb-4">Priority Distribution</h3>
|
|
159
|
+
<div className="space-y-3">
|
|
160
|
+
{['P0', 'P1', 'P2', 'P3', 'P4'].map(p => (
|
|
161
|
+
<PriorityBar key={p} priority={p} count={priorityCounts[p] || 0} maxCount={maxPriorityCount} />
|
|
162
|
+
))}
|
|
163
|
+
</div>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
{/* By Status — clickable */}
|
|
167
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border p-4">
|
|
168
|
+
<h3 className="font-semibold text-gray-900 dark:text-white mb-4">By Status</h3>
|
|
169
|
+
<div className="space-y-2">
|
|
170
|
+
{Object.entries(statusCounts).map(([status, count]) => (
|
|
171
|
+
<a
|
|
172
|
+
key={status}
|
|
173
|
+
href={`/errors?status=${status}`}
|
|
174
|
+
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700/50 rounded hover:ring-2 ring-blue-300 dark:ring-blue-600 transition-all block"
|
|
175
|
+
>
|
|
176
|
+
<span className="capitalize text-gray-700 dark:text-gray-300">{status.replace('_', ' ')}</span>
|
|
177
|
+
<span className="font-bold text-gray-900 dark:text-white">{count}</span>
|
|
178
|
+
</a>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
{/* By Error Type — clickable, with "View all" link */}
|
|
184
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border p-4">
|
|
185
|
+
<h3 className="font-semibold text-gray-900 dark:text-white mb-4">By Error Type</h3>
|
|
186
|
+
<div className="space-y-2">
|
|
187
|
+
{Object.entries(typeCounts).map(([type, count]) => (
|
|
188
|
+
<a
|
|
189
|
+
key={type}
|
|
190
|
+
href={`/errors?error_type=${type}`}
|
|
191
|
+
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700/50 rounded hover:ring-2 ring-blue-300 dark:ring-blue-600 transition-all block"
|
|
192
|
+
>
|
|
193
|
+
<span className="text-gray-700 dark:text-gray-300">{type.replace(/_/g, ' ')}</span>
|
|
194
|
+
<span className="font-bold text-gray-900 dark:text-white">{count}</span>
|
|
195
|
+
</a>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
<a href="/errors" className="text-blue-600 dark:text-blue-400 text-sm mt-3 block hover:underline">
|
|
199
|
+
View all errors →
|
|
200
|
+
</a>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
{/* By Project — clickable, no slice limit */}
|
|
204
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border p-4">
|
|
205
|
+
<h3 className="font-semibold text-gray-900 dark:text-white mb-4">By Project</h3>
|
|
206
|
+
<div className="space-y-2">
|
|
207
|
+
{Object.entries(projectCounts)
|
|
208
|
+
.sort(([, a], [, b]) => b - a)
|
|
209
|
+
.map(([project, count]) => (
|
|
210
|
+
<a
|
|
211
|
+
key={project}
|
|
212
|
+
href={`/errors?project=${project}`}
|
|
213
|
+
className="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-700/50 rounded hover:ring-2 ring-blue-300 dark:ring-blue-600 transition-all block"
|
|
214
|
+
>
|
|
215
|
+
<span className="text-gray-700 dark:text-gray-300">{project}</span>
|
|
216
|
+
<span className="font-bold text-gray-900 dark:text-white">{count}</span>
|
|
217
|
+
</a>
|
|
218
|
+
))}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
{/* Today's Activity with explainer */}
|
|
224
|
+
<div className="bg-white dark:bg-gray-800 rounded-lg border p-4">
|
|
225
|
+
<div className="flex items-center gap-2 mb-2">
|
|
226
|
+
<h3 className="font-semibold text-gray-900 dark:text-white">Today's Activity</h3>
|
|
227
|
+
<span
|
|
228
|
+
className="inline-flex items-center justify-center w-4 h-4 rounded-full bg-gray-200 dark:bg-gray-600 text-gray-600 dark:text-gray-300 text-xs cursor-help"
|
|
229
|
+
title="Total Occurrences = sum of occurrence_count across all errors updated today. One error recurring 100 times counts as 100 occurrences. Total Open = distinct error records with status 'open'. Last 24h = distinct error records updated in the last 24 hours."
|
|
230
|
+
>
|
|
231
|
+
?
|
|
232
|
+
</span>
|
|
233
|
+
</div>
|
|
234
|
+
<p className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
235
|
+
{stats.todayOccurrences.toLocaleString()}
|
|
236
|
+
<span className="text-sm font-normal text-gray-500 ml-2">total occurrences</span>
|
|
237
|
+
</p>
|
|
238
|
+
<p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
|
|
239
|
+
Each occurrence is one invocation of an error. A single error issue can have many occurrences.
|
|
240
|
+
</p>
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
);
|
|
244
|
+
}
|