@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,77 @@
1
+ /**
2
+ * StatusBadge Component
3
+ * Industrial Command Center aesthetic - sharp corners, pulse dot, monospace
4
+ */
5
+
6
+ import { type OperationalStatus, STATUS_COLORS } from './types';
7
+ import { clsx } from 'clsx';
8
+
9
+ interface StatusBadgeProps {
10
+ status: OperationalStatus;
11
+ size?: 'sm' | 'md';
12
+ showLabel?: boolean;
13
+ }
14
+
15
+ export function StatusBadge({ status, size = 'md', showLabel = true }: StatusBadgeProps) {
16
+ const colors = STATUS_COLORS[status];
17
+
18
+ const sizeClasses = {
19
+ sm: 'px-1.5 py-0.5 text-[10px] gap-1',
20
+ md: 'px-2 py-1 text-xs gap-1.5',
21
+ };
22
+
23
+ const dotSizes = {
24
+ sm: 'w-1.5 h-1.5',
25
+ md: 'w-2 h-2',
26
+ };
27
+
28
+ return (
29
+ <span
30
+ className={clsx(
31
+ // Base styles - sharp industrial corners
32
+ 'inline-flex items-center rounded-sm font-mono font-semibold uppercase tracking-wider',
33
+ // Size
34
+ sizeClasses[size],
35
+ // Colours
36
+ colors.bg,
37
+ colors.text,
38
+ // Subtle glow effect
39
+ 'shadow-sm',
40
+ colors.glow
41
+ )}
42
+ >
43
+ {/* Pulse dot indicator */}
44
+ <span className="relative flex">
45
+ <span
46
+ className={clsx(
47
+ dotSizes[size],
48
+ 'rounded-full',
49
+ status === 'RUN' ? 'bg-emerald-400' : status === 'WARN' ? 'bg-amber-400' : 'bg-rose-400'
50
+ )}
51
+ />
52
+ {/* Pulse animation for RUN status */}
53
+ {status === 'RUN' && (
54
+ <span
55
+ className={clsx(
56
+ 'absolute inset-0 rounded-full bg-emerald-400 animate-ping opacity-75',
57
+ dotSizes[size]
58
+ )}
59
+ />
60
+ )}
61
+ {/* Slower pulse for WARN status */}
62
+ {status === 'WARN' && (
63
+ <span
64
+ className={clsx(
65
+ 'absolute inset-0 rounded-full bg-amber-400 animate-pulse opacity-50',
66
+ dotSizes[size]
67
+ )}
68
+ />
69
+ )}
70
+ </span>
71
+
72
+ {showLabel && <span>{status}</span>}
73
+ </span>
74
+ );
75
+ }
76
+
77
+ export default StatusBadge;
@@ -0,0 +1,391 @@
1
+ /**
2
+ * UsageChart Component
3
+ * Industrial Command Center aesthetic - stacked bar/area chart with radar grid texture
4
+ */
5
+
6
+ import { useMemo } from 'react';
7
+ import {
8
+ AreaChart,
9
+ Area,
10
+ BarChart,
11
+ Bar,
12
+ XAxis,
13
+ YAxis,
14
+ CartesianGrid,
15
+ Tooltip,
16
+ Legend,
17
+ ResponsiveContainer,
18
+ type TooltipProps,
19
+ } from 'recharts';
20
+ import { format, parseISO } from 'date-fns';
21
+ import { type UsageRow, type ChartDataPoint, METRIC_COLORS } from './types';
22
+ import type { GranularResponse, ToolType } from '../unified/types';
23
+ import { TOOL_CONFIG } from '../unified/types';
24
+
25
+ interface UsageChartProps {
26
+ data: UsageRow[];
27
+ loading?: boolean;
28
+ period?: '24h' | '7d' | '30d';
29
+ /** When 'stacked', renders a BarChart with all 9 tools from granularData */
30
+ chartType?: 'area' | 'stacked';
31
+ /** Granular usage data for stacked bar chart (all 9 tools per day) */
32
+ granularData?: GranularResponse | null;
33
+ }
34
+
35
+ /**
36
+ * Format large numbers with K/M suffix
37
+ */
38
+ function formatNumber(value: number): string {
39
+ if (isNaN(value) || value === 0) return '0';
40
+ if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
41
+ if (value >= 1_000) return `${(value / 1_000).toFixed(1)}K`;
42
+ return value.toFixed(0);
43
+ }
44
+
45
+ /**
46
+ * Transform raw usage rows into chart data points
47
+ * Groups by time_bucket and aggregates metrics
48
+ */
49
+ function transformData(rows: UsageRow[]): ChartDataPoint[] {
50
+ const grouped = new Map<string, ChartDataPoint>();
51
+
52
+ for (const row of rows) {
53
+ const existing = grouped.get(row.time_bucket);
54
+ if (existing) {
55
+ existing.d1 += (row.d1_reads || 0) + (row.d1_writes || 0);
56
+ existing.kv += (row.kv_reads || 0) + (row.kv_writes || 0);
57
+ existing.workers += row.workers_requests || 0;
58
+ existing.total = existing.d1 + existing.kv + existing.workers;
59
+ } else {
60
+ const d1 = (row.d1_reads || 0) + (row.d1_writes || 0);
61
+ const kv = (row.kv_reads || 0) + (row.kv_writes || 0);
62
+ const workers = row.workers_requests || 0;
63
+ grouped.set(row.time_bucket, {
64
+ time: row.time_bucket,
65
+ timestamp: new Date(row.time_bucket).getTime(),
66
+ d1,
67
+ kv,
68
+ workers,
69
+ total: d1 + kv + workers,
70
+ });
71
+ }
72
+ }
73
+
74
+ return Array.from(grouped.values()).sort((a, b) => a.timestamp - b.timestamp);
75
+ }
76
+
77
+ /**
78
+ * Custom tooltip with dark industrial styling
79
+ */
80
+ function CustomTooltip({ active, payload, label }: TooltipProps<number, string>) {
81
+ if (!active || !payload || !payload.length) return null;
82
+
83
+ let formattedTime: string;
84
+ try {
85
+ formattedTime = format(parseISO(label), 'MMM d, HH:mm');
86
+ } catch {
87
+ formattedTime = label;
88
+ }
89
+
90
+ return (
91
+ <div className="bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-700 rounded-sm p-3 shadow-xl">
92
+ <p className="text-gray-600 dark:text-slate-400 text-xs font-mono mb-2">{formattedTime}</p>
93
+ <div className="space-y-1">
94
+ {payload.map((entry) => (
95
+ <div key={entry.name} className="flex items-center justify-between gap-4">
96
+ <span className="flex items-center gap-2">
97
+ <span className="w-2 h-2 rounded-sm" style={{ backgroundColor: entry.color }} />
98
+ <span className="text-gray-700 dark:text-slate-300 text-xs uppercase tracking-wide">
99
+ {entry.name}
100
+ </span>
101
+ </span>
102
+ <span className="text-gray-900 dark:text-slate-100 text-xs font-mono font-semibold">
103
+ {formatNumber(entry.value as number)}
104
+ </span>
105
+ </div>
106
+ ))}
107
+ </div>
108
+ </div>
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Custom tooltip for stacked bar chart with all 9 tools
114
+ */
115
+ function StackedTooltip({ active, payload, label }: TooltipProps<number, string>) {
116
+ if (!active || !payload || !payload.length) return null;
117
+
118
+ // Format date (YYYY-MM-DD format from granular API)
119
+ let formattedDate: string;
120
+ try {
121
+ formattedDate = format(parseISO(label), 'MMM d, yyyy');
122
+ } catch {
123
+ formattedDate = label;
124
+ }
125
+
126
+ // Calculate total
127
+ const total = payload.reduce((sum, entry) => sum + ((entry.value as number) || 0), 0);
128
+
129
+ // Sort by value descending for display
130
+ const sortedPayload = [...payload].sort(
131
+ (a, b) => ((b.value as number) || 0) - ((a.value as number) || 0)
132
+ );
133
+
134
+ return (
135
+ <div className="bg-white dark:bg-slate-900 border border-gray-300 dark:border-slate-700 rounded-sm p-3 shadow-xl min-w-[180px]">
136
+ <p className="text-gray-600 dark:text-slate-400 text-xs font-mono mb-2 border-b border-gray-200 dark:border-slate-700 pb-2">
137
+ {formattedDate}
138
+ </p>
139
+ <div className="space-y-1">
140
+ {sortedPayload
141
+ .filter((entry) => (entry.value as number) > 0)
142
+ .map((entry) => (
143
+ <div key={entry.name} className="flex items-center justify-between gap-4">
144
+ <span className="flex items-center gap-2">
145
+ <span className="w-2.5 h-2.5 rounded-sm" style={{ backgroundColor: entry.color }} />
146
+ <span className="text-gray-700 dark:text-slate-300 text-xs">{entry.name}</span>
147
+ </span>
148
+ <span className="text-gray-900 dark:text-slate-100 text-xs font-mono font-semibold">
149
+ {formatNumber(entry.value as number)}
150
+ </span>
151
+ </div>
152
+ ))}
153
+ </div>
154
+ <div className="border-t border-gray-200 dark:border-slate-700 mt-2 pt-2 flex justify-between">
155
+ <span className="text-gray-600 dark:text-slate-400 text-xs font-semibold">Total</span>
156
+ <span className="text-gray-900 dark:text-slate-100 text-xs font-mono font-bold">
157
+ {formatNumber(total)}
158
+ </span>
159
+ </div>
160
+ </div>
161
+ );
162
+ }
163
+
164
+ /**
165
+ * Loading skeleton with industrial styling
166
+ */
167
+ function ChartSkeleton() {
168
+ return (
169
+ <div className="h-64 bg-gray-50 dark:bg-slate-900/50 rounded-sm border border-gray-200 dark:border-slate-800 animate-pulse flex items-center justify-center">
170
+ <div className="text-gray-400 dark:text-slate-600 font-mono text-sm">
171
+ Loading activity data...
172
+ </div>
173
+ </div>
174
+ );
175
+ }
176
+
177
+ export function UsageChart({
178
+ data,
179
+ loading = false,
180
+ period = '24h',
181
+ chartType = 'area',
182
+ granularData,
183
+ }: UsageChartProps) {
184
+ const chartData = useMemo(() => transformData(data), [data]);
185
+
186
+ // Transform granular data for stacked bar chart (all 9 tools)
187
+ const stackedChartData = useMemo(() => {
188
+ if (!granularData?.byDate) return [];
189
+ return granularData.byDate.map((day) => ({
190
+ ...day,
191
+ // Ensure all tool values exist for stacking
192
+ workers: day.workers || 0,
193
+ d1: day.d1 || 0,
194
+ kv: day.kv || 0,
195
+ r2: day.r2 || 0,
196
+ vectorize: day.vectorize || 0,
197
+ durableObjects: day.durableObjects || 0,
198
+ queues: day.queues || 0,
199
+ workersAI: day.workersAI || 0,
200
+ pages: day.pages || 0,
201
+ }));
202
+ }, [granularData]);
203
+
204
+ // Sorted tool keys by order from TOOL_CONFIG
205
+ const sortedToolKeys = useMemo(() => {
206
+ return (Object.keys(TOOL_CONFIG) as ToolType[]).sort(
207
+ (a, b) => TOOL_CONFIG[a].order - TOOL_CONFIG[b].order
208
+ );
209
+ }, []);
210
+
211
+ // Format X-axis ticks based on period and chart type
212
+ const formatXAxisTick = (value: string) => {
213
+ try {
214
+ const date = parseISO(value);
215
+ // For stacked chart (daily data) or longer periods, show date (MMM d)
216
+ // For 24h area chart, show time (HH:mm)
217
+ if (chartType === 'stacked' || period !== '24h') {
218
+ return format(date, 'MMM d');
219
+ }
220
+ return format(date, 'HH:mm');
221
+ } catch {
222
+ return value;
223
+ }
224
+ };
225
+
226
+ if (loading) return <ChartSkeleton />;
227
+
228
+ // Stacked bar chart rendering for all 9 tools
229
+ if (chartType === 'stacked') {
230
+ if (stackedChartData.length === 0) {
231
+ return (
232
+ <div className="h-64 bg-gray-50 dark:bg-slate-900/50 rounded-sm border border-gray-200 dark:border-slate-800 flex items-center justify-center">
233
+ <div className="text-gray-500 dark:text-slate-500 font-mono text-sm">
234
+ No granular usage data available
235
+ </div>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ return (
241
+ <div className="h-64 bg-white dark:bg-slate-950 rounded-sm border border-gray-200 dark:border-slate-800 p-4 relative overflow-hidden">
242
+ {/* Subtle grid texture overlay for radar feel */}
243
+ <div
244
+ className="absolute inset-0 opacity-5 pointer-events-none"
245
+ style={{
246
+ backgroundImage: `
247
+ linear-gradient(to right, #475569 1px, transparent 1px),
248
+ linear-gradient(to bottom, #475569 1px, transparent 1px)
249
+ `,
250
+ backgroundSize: '20px 20px',
251
+ }}
252
+ />
253
+
254
+ <div className="w-full h-full min-h-[200px]">
255
+ <ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={180}>
256
+ <BarChart data={stackedChartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
257
+ <CartesianGrid strokeDasharray="3 3" stroke="#334155" strokeOpacity={0.5} />
258
+ <XAxis
259
+ dataKey="date"
260
+ stroke="#64748b"
261
+ fontSize={10}
262
+ fontFamily="ui-monospace, monospace"
263
+ tickFormatter={formatXAxisTick}
264
+ tick={{ fill: '#64748b' }}
265
+ axisLine={{ stroke: '#475569' }}
266
+ tickLine={{ stroke: '#475569' }}
267
+ />
268
+ <YAxis
269
+ stroke="#64748b"
270
+ fontSize={10}
271
+ fontFamily="ui-monospace, monospace"
272
+ tickFormatter={formatNumber}
273
+ tick={{ fill: '#64748b' }}
274
+ axisLine={{ stroke: '#475569' }}
275
+ tickLine={{ stroke: '#475569' }}
276
+ width={45}
277
+ />
278
+ <Tooltip content={<StackedTooltip />} />
279
+ <Legend
280
+ wrapperStyle={{ fontSize: '10px', fontFamily: 'ui-monospace, monospace' }}
281
+ iconType="square"
282
+ iconSize={8}
283
+ />
284
+ {sortedToolKeys.map((toolKey) => (
285
+ <Bar
286
+ key={toolKey}
287
+ dataKey={toolKey}
288
+ name={TOOL_CONFIG[toolKey].label}
289
+ stackId="usage"
290
+ fill={TOOL_CONFIG[toolKey].color}
291
+ />
292
+ ))}
293
+ </BarChart>
294
+ </ResponsiveContainer>
295
+ </div>
296
+ </div>
297
+ );
298
+ }
299
+
300
+ if (chartData.length === 0) {
301
+ return (
302
+ <div className="h-64 bg-gray-50 dark:bg-slate-900/50 rounded-sm border border-gray-200 dark:border-slate-800 flex items-center justify-center">
303
+ <div className="text-gray-500 dark:text-slate-500 font-mono text-sm">
304
+ No activity data available
305
+ </div>
306
+ </div>
307
+ );
308
+ }
309
+
310
+ return (
311
+ <div className="h-64 bg-white dark:bg-slate-950 rounded-sm border border-gray-200 dark:border-slate-800 p-4 relative overflow-hidden">
312
+ {/* Subtle grid texture overlay for radar feel */}
313
+ <div
314
+ className="absolute inset-0 opacity-5 pointer-events-none"
315
+ style={{
316
+ backgroundImage: `
317
+ linear-gradient(to right, #475569 1px, transparent 1px),
318
+ linear-gradient(to bottom, #475569 1px, transparent 1px)
319
+ `,
320
+ backgroundSize: '20px 20px',
321
+ }}
322
+ />
323
+
324
+ <div className="w-full h-full min-h-[200px]">
325
+ <ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={180}>
326
+ <AreaChart data={chartData} margin={{ top: 10, right: 10, left: 0, bottom: 0 }}>
327
+ {/* Grid with industrial styling */}
328
+ <CartesianGrid strokeDasharray="3 3" stroke="#334155" strokeOpacity={0.5} />
329
+
330
+ {/* X-Axis - Time */}
331
+ <XAxis
332
+ dataKey="time"
333
+ stroke="#64748b"
334
+ fontSize={10}
335
+ fontFamily="ui-monospace, monospace"
336
+ tickFormatter={formatXAxisTick}
337
+ tick={{ fill: '#64748b' }}
338
+ axisLine={{ stroke: '#475569' }}
339
+ tickLine={{ stroke: '#475569' }}
340
+ />
341
+
342
+ {/* Y-Axis - Count */}
343
+ <YAxis
344
+ stroke="#64748b"
345
+ fontSize={10}
346
+ fontFamily="ui-monospace, monospace"
347
+ tickFormatter={formatNumber}
348
+ tick={{ fill: '#64748b' }}
349
+ axisLine={{ stroke: '#475569' }}
350
+ tickLine={{ stroke: '#475569' }}
351
+ width={45}
352
+ />
353
+
354
+ <Tooltip content={<CustomTooltip />} />
355
+
356
+ {/* Stacked areas - Workers (bottom), KV (middle), D1 (top) */}
357
+ <Area
358
+ type="monotone"
359
+ dataKey="workers"
360
+ name="Workers"
361
+ stackId="1"
362
+ stroke={METRIC_COLORS.workers.stroke}
363
+ fill={METRIC_COLORS.workers.fill}
364
+ strokeWidth={2}
365
+ />
366
+ <Area
367
+ type="monotone"
368
+ dataKey="kv"
369
+ name="KV"
370
+ stackId="1"
371
+ stroke={METRIC_COLORS.kv.stroke}
372
+ fill={METRIC_COLORS.kv.fill}
373
+ strokeWidth={2}
374
+ />
375
+ <Area
376
+ type="monotone"
377
+ dataKey="d1"
378
+ name="D1"
379
+ stackId="1"
380
+ stroke={METRIC_COLORS.d1.stroke}
381
+ fill={METRIC_COLORS.d1.fill}
382
+ strokeWidth={2}
383
+ />
384
+ </AreaChart>
385
+ </ResponsiveContainer>
386
+ </div>
387
+ </div>
388
+ );
389
+ }
390
+
391
+ export default UsageChart;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Interactive Usage Dashboard - React Components
3
+ * Industrial Command Center aesthetic
4
+ */
5
+
6
+ // Types
7
+ export type {
8
+ OperationalStatus,
9
+ Period,
10
+ CircuitBreakerState,
11
+ ProjectStatus,
12
+ UsageRow,
13
+ ChartDataPoint,
14
+ ProjectTableRow,
15
+ StatusResponse,
16
+ QueryResponse,
17
+ MetricType,
18
+ } from './types';
19
+
20
+ export { METRIC_COLORS, STATUS_COLORS, CB_COLORS } from './types';
21
+
22
+ // Components
23
+ export { StatusBadge } from './StatusBadge';
24
+ export { UsageChart } from './UsageChart';
25
+ export { DashboardShell } from './DashboardShell';
26
+
27
+ {{#if isStandard}}
28
+ // Standard tier components
29
+ export { UsageTable } from './UsageTable';
30
+ {{/if}}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Interactive Usage Dashboard Types
3
+ * Industrial Command Center aesthetic - data-dense, monospace metrics
4
+ */
5
+
6
+ export type OperationalStatus = 'RUN' | 'WARN' | 'STOP';
7
+ export type Period = '24h' | '7d';
8
+ export type CircuitBreakerState = 'active' | 'tripped';
9
+
10
+ /**
11
+ * Project status summary for table display
12
+ */
13
+ export interface ProjectStatus {
14
+ status: OperationalStatus;
15
+ spend: number;
16
+ cap: number;
17
+ percentage: number;
18
+ circuitBreaker: CircuitBreakerState;
19
+ lastSeen?: string; // ISO timestamp from heartbeat
20
+ }
21
+
22
+ /**
23
+ * Raw usage row from the API - time-series data
24
+ */
25
+ export interface UsageRow {
26
+ time_bucket: string;
27
+ project_id: string;
28
+ d1_writes: number;
29
+ d1_reads: number;
30
+ kv_reads: number;
31
+ kv_writes: number;
32
+ workers_requests: number;
33
+ interaction_count: number;
34
+ }
35
+
36
+ /**
37
+ * Aggregated data point for chart display
38
+ */
39
+ export interface ChartDataPoint {
40
+ time: string;
41
+ timestamp: number;
42
+ d1: number;
43
+ kv: number;
44
+ workers: number;
45
+ total: number;
46
+ }
47
+
48
+ /**
49
+ * Table row with project summary
50
+ */
51
+ export interface ProjectTableRow {
52
+ id: string;
53
+ name: string;
54
+ status: OperationalStatus;
55
+ activity: number;
56
+ activityTrend: number[]; // Last 6 data points for sparkline
57
+ spend: number;
58
+ cap: number;
59
+ percentage: number;
60
+ circuitBreaker: CircuitBreakerState;
61
+ lastSeen?: string; // ISO timestamp from heartbeat
62
+ }
63
+
64
+ /**
65
+ * API response: project status endpoint
66
+ */
67
+ export interface StatusResponse {
68
+ success: true;
69
+ projects: Record<string, ProjectStatus>;
70
+ timestamp: string;
71
+ }
72
+
73
+ /**
74
+ * API response: usage query endpoint
75
+ */
76
+ export interface QueryResponse {
77
+ success: true;
78
+ data: UsageRow[];
79
+ }
80
+
81
+ /**
82
+ * Metric type for chart colour mapping
83
+ */
84
+ export type MetricType = 'd1' | 'kv' | 'workers';
85
+
86
+ /**
87
+ * Chart colour configuration
88
+ */
89
+ export const METRIC_COLORS: Record<MetricType, { fill: string; stroke: string }> = {
90
+ d1: { fill: 'rgba(16, 185, 129, 0.3)', stroke: '#10b981' }, // emerald
91
+ kv: { fill: 'rgba(245, 158, 11, 0.3)', stroke: '#f59e0b' }, // amber
92
+ workers: { fill: 'rgba(59, 130, 246, 0.3)', stroke: '#3b82f6' }, // blue
93
+ };
94
+
95
+ /**
96
+ * Status colour configuration
97
+ */
98
+ export const STATUS_COLORS: Record<OperationalStatus, { bg: string; text: string; glow: string }> =
99
+ {
100
+ RUN: {
101
+ bg: 'bg-emerald-500/20',
102
+ text: 'text-emerald-400',
103
+ glow: 'shadow-emerald-500/20',
104
+ },
105
+ WARN: {
106
+ bg: 'bg-amber-500/20',
107
+ text: 'text-amber-400',
108
+ glow: 'shadow-amber-500/20',
109
+ },
110
+ STOP: {
111
+ bg: 'bg-rose-500/20',
112
+ text: 'text-rose-400',
113
+ glow: 'shadow-rose-500/20',
114
+ },
115
+ };
116
+
117
+ /**
118
+ * Circuit breaker colour configuration
119
+ */
120
+ export const CB_COLORS: Record<CircuitBreakerState, string> = {
121
+ active: 'bg-emerald-500',
122
+ tripped: 'bg-rose-500',
123
+ };
124
+
125
+ /**
126
+ * Health status based on heartbeat recency
127
+ */
128
+ export type HealthStatus = 'online' | 'idle' | 'unknown';
129
+
130
+ /**
131
+ * Health indicator colour configuration
132
+ */
133
+ export const HEALTH_COLORS: Record<HealthStatus, { dot: string; text: string }> = {
134
+ online: { dot: '🟢', text: 'text-emerald-400' },
135
+ idle: { dot: '🟡', text: 'text-amber-400' },
136
+ unknown: { dot: '⚪', text: 'text-slate-500' },
137
+ };