@littlebearapps/platform-admin-sdk 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +2 -2
  2. package/dist/templates.d.ts +1 -1
  3. package/dist/templates.js +86 -2
  4. package/package.json +1 -1
  5. package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
  6. package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
  7. package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
  8. package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
  9. package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
  10. package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
  11. package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
  12. package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
  13. package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
  14. package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
  15. package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
  16. package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
  17. package/templates/full/dashboard/src/lib/search/api.ts +258 -0
  18. package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
  19. package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
  20. package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
  21. package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
  22. package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
  23. package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
  24. package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
  25. package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
  26. package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
  27. package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
  28. package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
  29. package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
  30. package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
  31. package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
  32. package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
  33. package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
  34. package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
  35. package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
  36. package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
  37. package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
  38. package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
  39. package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
  40. package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
  41. package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
  42. package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
  43. package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
  44. package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
  45. package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
  46. package/templates/shared/tests/helpers/mock-storage.ts +166 -0
  47. package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
  48. package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
  49. package/templates/shared/tests/unit/billing.test.ts +331 -0
  50. package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
  51. package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
  52. package/templates/shared/tests/unit/control.test.ts +226 -0
  53. package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
  54. package/templates/shared/tests/unit/economics.test.ts +365 -0
  55. package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
  56. package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
  57. package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
  58. package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
  59. package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
  60. package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
  61. package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
  62. package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
  63. package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
  64. package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
  65. package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
  66. package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
  67. package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
  68. package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
  69. package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
  70. package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
  71. package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
  72. package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
  73. package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
  74. package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
@@ -0,0 +1,236 @@
1
+ /**
2
+ * ResourceBreakdown Component
3
+ *
4
+ * Displays resource metrics for an expanded project row.
5
+ * Shows D1, KV, R2, Vectorize, Workers, etc. with sparklines.
6
+ * Highlights top 3 cost drivers with flame icons.
7
+ */
8
+
9
+ import { clsx } from 'clsx';
10
+ import { Flame } from 'lucide-react';
11
+ import type { ProjectBreakdown, ResourceMetric, ResourceType } from './types';
12
+ import { RESOURCE_COLORS, RESOURCE_LABELS } from './types';
13
+ import { Sparkline } from './Sparkline';
14
+
15
+ interface ResourceBreakdownProps {
16
+ projectId: string;
17
+ breakdown: ProjectBreakdown | null;
18
+ isLoading: boolean;
19
+ }
20
+
21
+ function formatCost(cost: number): string {
22
+ if (cost >= 1000) return `$${(cost / 1000).toFixed(1)}K`;
23
+ if (cost >= 1) return `$${cost.toFixed(2)}`;
24
+ if (cost >= 0.01) return `$${cost.toFixed(3)}`;
25
+ return `$${cost.toFixed(4)}`;
26
+ }
27
+
28
+ /**
29
+ * Loading skeleton for resource breakdown
30
+ */
31
+ function LoadingSkeleton() {
32
+ return (
33
+ <div className="space-y-2 animate-pulse">
34
+ {[1, 2, 3].map((i) => (
35
+ <div
36
+ key={i}
37
+ className="flex items-center gap-4 p-2 bg-gray-100/50 dark:bg-slate-800/50 rounded"
38
+ >
39
+ <div className="w-3 h-3 rounded bg-slate-700" />
40
+ <div className="w-20 h-4 bg-gray-200 dark:bg-slate-700 rounded" />
41
+ <div className="flex-1" />
42
+ <div className="w-16 h-4 bg-gray-200 dark:bg-slate-700 rounded" />
43
+ <div className="w-24 h-4 bg-gray-200 dark:bg-slate-700 rounded" />
44
+ <div className="w-40 h-3 bg-gray-200 dark:bg-slate-700 rounded" />
45
+ </div>
46
+ ))}
47
+ </div>
48
+ );
49
+ }
50
+
51
+ /**
52
+ * Empty state when no resources found
53
+ */
54
+ function EmptyState() {
55
+ return (
56
+ <div className="py-4 text-center">
57
+ <p className="text-gray-500 dark:text-slate-500 text-xs font-mono">
58
+ No resource data available
59
+ </p>
60
+ </div>
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Resource type icon/indicator
66
+ */
67
+ function ResourceIcon({ type }: { type: ResourceType }) {
68
+ const colors = RESOURCE_COLORS[type] || { stroke: '#64748b', bg: 'rgba(100, 116, 139, 0.3)' };
69
+
70
+ return (
71
+ <div
72
+ className="w-3 h-3 rounded-sm"
73
+ style={{ backgroundColor: colors.stroke }}
74
+ title={RESOURCE_LABELS[type] || type}
75
+ />
76
+ );
77
+ }
78
+
79
+ /**
80
+ * Progress bar for limit percentage
81
+ */
82
+ function LimitBar({ percentage }: { percentage: number }) {
83
+ const pct = Math.min(percentage, 100);
84
+ const barColor =
85
+ percentage >= 90
86
+ ? 'bg-rose-500'
87
+ : percentage >= 75
88
+ ? 'bg-orange-500'
89
+ : percentage >= 50
90
+ ? 'bg-amber-500'
91
+ : 'bg-emerald-500';
92
+
93
+ return (
94
+ <div className="w-24 flex items-center gap-2">
95
+ <div className="flex-1 h-1 bg-gray-200 dark:bg-slate-700 rounded-full overflow-hidden">
96
+ <div
97
+ className={clsx('h-full rounded-full transition-all', barColor)}
98
+ style={{ width: `${pct}%` }}
99
+ />
100
+ </div>
101
+ <span className="text-xs font-mono text-gray-500 dark:text-slate-500 w-10 text-right">
102
+ {percentage > 999 ? '>999' : percentage.toFixed(0)}%
103
+ </span>
104
+ </div>
105
+ );
106
+ }
107
+
108
+ /**
109
+ * Single resource row
110
+ */
111
+ function ResourceRow({
112
+ resource,
113
+ isCostDriver,
114
+ }: {
115
+ resource: ResourceMetric;
116
+ isCostDriver: boolean;
117
+ }) {
118
+ const colors = RESOURCE_COLORS[resource.type] || {
119
+ stroke: '#64748b',
120
+ bg: 'rgba(100, 116, 139, 0.3)',
121
+ };
122
+
123
+ return (
124
+ <div
125
+ className={clsx(
126
+ 'flex items-center gap-4 px-3 py-2 rounded transition-colors',
127
+ isCostDriver
128
+ ? 'bg-amber-50/80 dark:bg-amber-900/20 hover:bg-amber-100/80 dark:hover:bg-amber-900/30'
129
+ : 'hover:bg-gray-100/50 dark:hover:bg-slate-800/50'
130
+ )}
131
+ >
132
+ {/* Type indicator */}
133
+ <ResourceIcon type={resource.type} />
134
+
135
+ {/* Label with flame icon for cost drivers */}
136
+ <span
137
+ className={clsx(
138
+ 'w-28 text-xs font-medium truncate flex items-center gap-1',
139
+ isCostDriver
140
+ ? 'text-amber-700 dark:text-amber-400 font-semibold'
141
+ : 'text-gray-600 dark:text-slate-400'
142
+ )}
143
+ title={isCostDriver ? `${resource.label} - Top cost driver this period` : resource.label}
144
+ >
145
+ {isCostDriver && <Flame className="w-3 h-3 text-amber-500" />}
146
+ {RESOURCE_LABELS[resource.type] || resource.label}
147
+ </span>
148
+
149
+ {/* Cost */}
150
+ <span
151
+ className={clsx(
152
+ 'w-16 text-xs font-mono text-right',
153
+ isCostDriver
154
+ ? 'text-amber-800 dark:text-amber-300 font-semibold'
155
+ : 'text-gray-700 dark:text-slate-300'
156
+ )}
157
+ >
158
+ {formatCost(resource.cost)}
159
+ </span>
160
+
161
+ {/* Usage */}
162
+ <span className="w-28 text-xs font-mono text-gray-500 dark:text-slate-500">
163
+ {resource.usageFormatted}
164
+ </span>
165
+
166
+ {/* Limit bar (if applicable) */}
167
+ {resource.limitPct !== undefined ? (
168
+ <LimitBar percentage={resource.limitPct} />
169
+ ) : (
170
+ <div className="w-24" /> // Spacer
171
+ )}
172
+
173
+ {/* Sparkline */}
174
+ {resource.trend && resource.trend.length > 1 ? (
175
+ <Sparkline data={resource.trend} width={60} height={16} color={colors.stroke} />
176
+ ) : (
177
+ <div className="w-[60px]" /> // Spacer
178
+ )}
179
+ </div>
180
+ );
181
+ }
182
+
183
+ export function ResourceBreakdown({ projectId, breakdown, isLoading }: ResourceBreakdownProps) {
184
+ if (isLoading) {
185
+ return <LoadingSkeleton />;
186
+ }
187
+
188
+ if (!breakdown || breakdown.resources.length === 0) {
189
+ return <EmptyState />;
190
+ }
191
+
192
+ // Sort resources by cost (descending)
193
+ const sortedResources = [...breakdown.resources].sort((a, b) => b.cost - a.cost);
194
+
195
+ // Identify top 3 cost drivers (only if they have non-zero cost)
196
+ const costDriverTypes = new Set(
197
+ sortedResources
198
+ .filter((r) => r.cost > 0)
199
+ .slice(0, 3)
200
+ .map((r) => r.type)
201
+ );
202
+
203
+ return (
204
+ <div className="space-y-1">
205
+ {/* Header */}
206
+ <div className="flex items-center gap-4 px-3 py-1 text-xs font-mono text-gray-400 dark:text-slate-600 uppercase tracking-wider">
207
+ <span className="w-3" /> {/* Icon spacer */}
208
+ <span className="w-28">Resource</span>
209
+ <span className="w-16 text-right">Cost</span>
210
+ <span className="w-28">Usage</span>
211
+ <span className="w-24">Limit</span>
212
+ <span className="w-[60px]">Trend</span>
213
+ </div>
214
+
215
+ {/* Resource rows */}
216
+ {sortedResources.map((resource) => (
217
+ <ResourceRow
218
+ key={`${projectId}-${resource.type}`}
219
+ resource={resource}
220
+ isCostDriver={costDriverTypes.has(resource.type)}
221
+ />
222
+ ))}
223
+
224
+ {/* Total row */}
225
+ <div className="flex items-center gap-4 px-3 py-2 mt-2 border-t border-gray-200 dark:border-slate-800">
226
+ <span className="w-3" />
227
+ <span className="w-28 text-xs font-semibold text-gray-600 dark:text-slate-400">Total</span>
228
+ <span className="w-16 text-xs font-mono font-semibold text-gray-800 dark:text-slate-200 text-right">
229
+ {formatCost(breakdown.totalCost)}
230
+ </span>
231
+ </div>
232
+ </div>
233
+ );
234
+ }
235
+
236
+ export default ResourceBreakdown;
@@ -0,0 +1,127 @@
1
+ /**
2
+ * Sparkline Component
3
+ *
4
+ * Inline SVG sparkline for activity trends.
5
+ * Minimal, industrial aesthetic.
6
+ */
7
+
8
+ import { useMemo } from 'react';
9
+
10
+ interface SparklineProps {
11
+ data: number[];
12
+ width?: number;
13
+ height?: number;
14
+ color?: string;
15
+ strokeWidth?: number;
16
+ showFill?: boolean;
17
+ className?: string;
18
+ }
19
+
20
+ export function Sparkline({
21
+ data,
22
+ width = 60,
23
+ height = 20,
24
+ color = '#3b82f6',
25
+ strokeWidth = 1.5,
26
+ showFill = true,
27
+ className = '',
28
+ }: SparklineProps) {
29
+ const pathData = useMemo(() => {
30
+ if (data.length < 2) return { line: '', fill: '' };
31
+
32
+ const min = Math.min(...data);
33
+ const max = Math.max(...data);
34
+ const range = max - min || 1;
35
+
36
+ // Padding to prevent clipping
37
+ const padding = 2;
38
+ const chartWidth = width - padding * 2;
39
+ const chartHeight = height - padding * 2;
40
+
41
+ const points = data.map((value, index) => {
42
+ const x = padding + (index / (data.length - 1)) * chartWidth;
43
+ const y = padding + chartHeight - ((value - min) / range) * chartHeight;
44
+ return { x, y };
45
+ });
46
+
47
+ // Build line path
48
+ const linePath = points
49
+ .map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`))
50
+ .join(' ');
51
+
52
+ // Build fill path (close at bottom)
53
+ const fillPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${padding} ${height - padding} Z`;
54
+
55
+ return { line: linePath, fill: fillPath };
56
+ }, [data, width, height]);
57
+
58
+ if (data.length < 2) {
59
+ return (
60
+ <svg
61
+ width={width}
62
+ height={height}
63
+ className={className}
64
+ viewBox={`0 0 ${width} ${height}`}
65
+ aria-hidden="true"
66
+ >
67
+ <line
68
+ x1={2}
69
+ y1={height / 2}
70
+ x2={width - 2}
71
+ y2={height / 2}
72
+ stroke={color}
73
+ strokeWidth={strokeWidth}
74
+ strokeOpacity={0.3}
75
+ strokeDasharray="2 2"
76
+ />
77
+ </svg>
78
+ );
79
+ }
80
+
81
+ return (
82
+ <svg
83
+ width={width}
84
+ height={height}
85
+ className={className}
86
+ viewBox={`0 0 ${width} ${height}`}
87
+ aria-hidden="true"
88
+ >
89
+ {/* Fill gradient */}
90
+ {showFill && (
91
+ <>
92
+ <defs>
93
+ <linearGradient
94
+ id={`sparkline-fill-${color.replace('#', '')}`}
95
+ x1="0"
96
+ y1="0"
97
+ x2="0"
98
+ y2="1"
99
+ >
100
+ <stop offset="0%" stopColor={color} stopOpacity={0.3} />
101
+ <stop offset="100%" stopColor={color} stopOpacity={0} />
102
+ </linearGradient>
103
+ </defs>
104
+ <path d={pathData.fill} fill={`url(#sparkline-fill-${color.replace('#', '')})`} />
105
+ </>
106
+ )}
107
+ {/* Line */}
108
+ <path
109
+ d={pathData.line}
110
+ fill="none"
111
+ stroke={color}
112
+ strokeWidth={strokeWidth}
113
+ strokeLinecap="round"
114
+ strokeLinejoin="round"
115
+ />
116
+ {/* End dot */}
117
+ <circle
118
+ cx={width - 2}
119
+ cy={pathData.line ? parseFloat(pathData.line.split(' ').slice(-1)[0]) : height / 2}
120
+ r={2}
121
+ fill={color}
122
+ />
123
+ </svg>
124
+ );
125
+ }
126
+
127
+ export default Sparkline;