@gravito/zenith 0.1.0-beta.1

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 (63) hide show
  1. package/ARCHITECTURE.md +88 -0
  2. package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
  3. package/DEMO.md +156 -0
  4. package/DEPLOYMENT.md +157 -0
  5. package/DOCS_INTERNAL.md +73 -0
  6. package/Dockerfile +46 -0
  7. package/Dockerfile.demo-worker +29 -0
  8. package/EVOLUTION_BLUEPRINT.md +112 -0
  9. package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
  10. package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
  11. package/QUICK_TEST_GUIDE.md +72 -0
  12. package/README.md +33 -0
  13. package/ROADMAP.md +85 -0
  14. package/TESTING_BATCH_OPERATIONS.md +252 -0
  15. package/bin/flux-console.ts +2 -0
  16. package/dist/bin.js +108196 -0
  17. package/dist/client/assets/index-DGYEwTDL.css +1 -0
  18. package/dist/client/assets/index-oyTdySX0.js +421 -0
  19. package/dist/client/index.html +13 -0
  20. package/dist/server/index.js +108191 -0
  21. package/docker-compose.yml +40 -0
  22. package/docs/integrations/LARAVEL.md +207 -0
  23. package/package.json +50 -0
  24. package/postcss.config.js +6 -0
  25. package/scripts/flood-logs.ts +21 -0
  26. package/scripts/seed.ts +213 -0
  27. package/scripts/verify-throttle.ts +45 -0
  28. package/scripts/worker.ts +123 -0
  29. package/src/bin.ts +6 -0
  30. package/src/client/App.tsx +70 -0
  31. package/src/client/Layout.tsx +644 -0
  32. package/src/client/Sidebar.tsx +102 -0
  33. package/src/client/ThroughputChart.tsx +135 -0
  34. package/src/client/WorkerStatus.tsx +170 -0
  35. package/src/client/components/ConfirmDialog.tsx +103 -0
  36. package/src/client/components/JobInspector.tsx +524 -0
  37. package/src/client/components/LogArchiveModal.tsx +383 -0
  38. package/src/client/components/NotificationBell.tsx +203 -0
  39. package/src/client/components/Toaster.tsx +80 -0
  40. package/src/client/components/UserProfileDropdown.tsx +177 -0
  41. package/src/client/contexts/AuthContext.tsx +93 -0
  42. package/src/client/contexts/NotificationContext.tsx +103 -0
  43. package/src/client/index.css +174 -0
  44. package/src/client/index.html +12 -0
  45. package/src/client/main.tsx +15 -0
  46. package/src/client/pages/LoginPage.tsx +153 -0
  47. package/src/client/pages/MetricsPage.tsx +408 -0
  48. package/src/client/pages/OverviewPage.tsx +511 -0
  49. package/src/client/pages/QueuesPage.tsx +372 -0
  50. package/src/client/pages/SchedulesPage.tsx +531 -0
  51. package/src/client/pages/SettingsPage.tsx +449 -0
  52. package/src/client/pages/WorkersPage.tsx +316 -0
  53. package/src/client/pages/index.ts +7 -0
  54. package/src/client/utils.ts +6 -0
  55. package/src/server/index.ts +556 -0
  56. package/src/server/middleware/auth.ts +127 -0
  57. package/src/server/services/AlertService.ts +160 -0
  58. package/src/server/services/QueueService.ts +828 -0
  59. package/tailwind.config.js +73 -0
  60. package/tests/placeholder.test.ts +7 -0
  61. package/tsconfig.json +38 -0
  62. package/tsconfig.node.json +12 -0
  63. package/vite.config.ts +27 -0
@@ -0,0 +1,153 @@
1
+ import { motion } from 'framer-motion'
2
+ import { Activity, AlertCircle, ArrowRight, Eye, EyeOff, Lock } from 'lucide-react'
3
+ import { useState } from 'react'
4
+ import { useAuth } from '../contexts/AuthContext'
5
+ import { cn } from '../utils'
6
+
7
+ export function LoginPage() {
8
+ const { login } = useAuth()
9
+ const [password, setPassword] = useState('')
10
+ const [showPassword, setShowPassword] = useState(false)
11
+ const [error, setError] = useState('')
12
+ const [isLoading, setIsLoading] = useState(false)
13
+
14
+ const handleSubmit = async (e: React.FormEvent) => {
15
+ e.preventDefault()
16
+ setError('')
17
+ setIsLoading(true)
18
+
19
+ const result = await login(password)
20
+
21
+ if (!result.success) {
22
+ setError(result.error || 'Authentication failed')
23
+ setIsLoading(false)
24
+ }
25
+ }
26
+
27
+ return (
28
+ <div className="min-h-screen bg-background flex items-center justify-center p-4 relative overflow-hidden">
29
+ {/* Background Effects */}
30
+ <div className="absolute inset-0 bg-gradient-to-br from-primary/5 via-transparent to-indigo-500/5" />
31
+ <div className="absolute top-1/4 -left-1/4 w-96 h-96 bg-primary/10 rounded-full blur-3xl animate-pulse" />
32
+ <div
33
+ className="absolute bottom-1/4 -right-1/4 w-96 h-96 bg-indigo-500/10 rounded-full blur-3xl animate-pulse"
34
+ style={{ animationDelay: '1s' }}
35
+ />
36
+
37
+ {/* Grid Pattern */}
38
+ <div className="absolute inset-0 bg-[linear-gradient(rgba(var(--primary-rgb),0.03)_1px,transparent_1px),linear-gradient(90deg,rgba(var(--primary-rgb),0.03)_1px,transparent_1px)] bg-[size:64px_64px]" />
39
+
40
+ <motion.div
41
+ initial={{ opacity: 0, y: 20 }}
42
+ animate={{ opacity: 1, y: 0 }}
43
+ transition={{ duration: 0.5 }}
44
+ className="relative z-10 w-full max-w-md"
45
+ >
46
+ {/* Logo & Title */}
47
+ <div className="text-center mb-8">
48
+ <motion.div
49
+ initial={{ scale: 0 }}
50
+ animate={{ scale: 1 }}
51
+ transition={{ type: 'spring', delay: 0.2 }}
52
+ className="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-primary to-indigo-600 rounded-3xl shadow-2xl shadow-primary/30 mb-6"
53
+ >
54
+ <Activity size={40} className="text-primary-foreground" />
55
+ </motion.div>
56
+ <h1 className="text-4xl font-black tracking-tighter mb-2">GRAVITO</h1>
57
+ <p className="text-[10px] font-bold text-primary uppercase tracking-[0.4em]">
58
+ Flux Console
59
+ </p>
60
+ </div>
61
+
62
+ {/* Login Card */}
63
+ <div className="card-premium p-8 backdrop-blur-xl">
64
+ <div className="text-center mb-8">
65
+ <h2 className="text-xl font-bold mb-2">Welcome Back</h2>
66
+ <p className="text-sm text-muted-foreground">
67
+ Enter your password to access the console
68
+ </p>
69
+ </div>
70
+
71
+ <form onSubmit={handleSubmit} className="space-y-6">
72
+ {/* Error Message */}
73
+ {error && (
74
+ <motion.div
75
+ initial={{ opacity: 0, y: -10 }}
76
+ animate={{ opacity: 1, y: 0 }}
77
+ className="flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500"
78
+ >
79
+ <AlertCircle size={18} />
80
+ <span className="text-sm font-medium">{error}</span>
81
+ </motion.div>
82
+ )}
83
+
84
+ {/* Password Field */}
85
+ <div className="space-y-2">
86
+ <label
87
+ htmlFor="password"
88
+ className="text-sm font-bold text-muted-foreground uppercase tracking-widest"
89
+ >
90
+ Password
91
+ </label>
92
+ <div className="relative">
93
+ <Lock
94
+ className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground"
95
+ size={18}
96
+ />
97
+ <input
98
+ id="password"
99
+ type={showPassword ? 'text' : 'password'}
100
+ value={password}
101
+ onChange={(e) => setPassword(e.target.value)}
102
+ placeholder="Enter console password"
103
+ className={cn(
104
+ 'w-full bg-muted/40 border border-border/50 rounded-xl py-4 pl-12 pr-12',
105
+ 'text-sm font-medium placeholder:text-muted-foreground/40',
106
+ 'focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary/50',
107
+ 'transition-all'
108
+ )}
109
+ disabled={isLoading}
110
+ />
111
+ <button
112
+ type="button"
113
+ onClick={() => setShowPassword(!showPassword)}
114
+ className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors"
115
+ >
116
+ {showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
117
+ </button>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Submit Button */}
122
+ <button
123
+ type="submit"
124
+ disabled={isLoading || !password}
125
+ className={cn(
126
+ 'w-full py-4 rounded-xl font-bold text-sm uppercase tracking-widest',
127
+ 'bg-gradient-to-r from-primary to-indigo-600 text-primary-foreground',
128
+ 'shadow-lg shadow-primary/30 hover:shadow-xl hover:shadow-primary/40',
129
+ 'transition-all duration-300 hover:scale-[1.02] active:scale-[0.98]',
130
+ 'disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100',
131
+ 'flex items-center justify-center gap-2'
132
+ )}
133
+ >
134
+ {isLoading ? (
135
+ <div className="w-5 h-5 border-2 border-primary-foreground/30 border-t-primary-foreground rounded-full animate-spin" />
136
+ ) : (
137
+ <>
138
+ Access Console
139
+ <ArrowRight size={18} />
140
+ </>
141
+ )}
142
+ </button>
143
+ </form>
144
+ </div>
145
+
146
+ {/* Footer */}
147
+ <p className="text-center mt-6 text-xs text-muted-foreground/50">
148
+ Protected by Gravito Security Layer
149
+ </p>
150
+ </motion.div>
151
+ </div>
152
+ )
153
+ }
@@ -0,0 +1,408 @@
1
+ import { useQuery, useQueryClient } from '@tanstack/react-query'
2
+ import {
3
+ Activity,
4
+ BarChart3,
5
+ CheckCircle,
6
+ Clock,
7
+ Hourglass,
8
+ LineChart,
9
+ RefreshCcw,
10
+ TrendingUp,
11
+ XCircle,
12
+ } from 'lucide-react'
13
+ import React from 'react'
14
+ import {
15
+ Area,
16
+ AreaChart,
17
+ Bar,
18
+ BarChart,
19
+ CartesianGrid,
20
+ Legend,
21
+ ResponsiveContainer,
22
+ Tooltip,
23
+ XAxis,
24
+ YAxis,
25
+ } from 'recharts'
26
+ import { cn } from '../utils'
27
+
28
+ export function MetricsPage() {
29
+ const [timeRange, setTimeRange] = React.useState<'15m' | '1h' | '6h' | '24h'>('15m')
30
+
31
+ const { data: throughputData } = useQuery({
32
+ queryKey: ['throughput'],
33
+ queryFn: async () => {
34
+ const res = await fetch('/api/throughput')
35
+ const json = await res.json()
36
+ return json.data || []
37
+ },
38
+ staleTime: Infinity,
39
+ })
40
+
41
+ const { data: historyData } = useQuery<{ history: Record<string, number[]> }>({
42
+ queryKey: ['metrics-history'],
43
+ queryFn: () => fetch('/api/metrics/history').then((res) => res.json()),
44
+ refetchInterval: 30000,
45
+ })
46
+
47
+ const { isPending, data: queueData } = useQuery<{ queues: any[] }>({
48
+ queryKey: ['queues'],
49
+ queryFn: () => fetch('/api/queues').then((res) => res.json()),
50
+ staleTime: Infinity,
51
+ })
52
+
53
+ const { data: workerData } = useQuery<{ workers: any[] }>({
54
+ queryKey: ['workers'],
55
+ queryFn: () => fetch('/api/workers').then((res) => res.json()),
56
+ staleTime: Infinity,
57
+ })
58
+
59
+ const queryClient = useQueryClient()
60
+ // Live update from global stream
61
+ React.useEffect(() => {
62
+ const handler = (e: CustomEvent) => {
63
+ const stats = e.detail
64
+ if (!stats) {
65
+ return
66
+ }
67
+ if (stats.queues) {
68
+ queryClient.setQueryData(['queues'], { queues: stats.queues })
69
+ }
70
+ if (stats.workers) {
71
+ queryClient.setQueryData(['workers'], { workers: stats.workers })
72
+ }
73
+ if (stats.throughput) {
74
+ queryClient.setQueryData(['throughput'], stats.throughput)
75
+ }
76
+ }
77
+ window.addEventListener('flux-stats-update', handler as EventListener)
78
+ return () => window.removeEventListener('flux-stats-update', handler as EventListener)
79
+ }, [queryClient])
80
+
81
+ const history = historyData?.history || {}
82
+ const queues = queueData?.queues || []
83
+ const workers = workerData?.workers || []
84
+
85
+ const chartData =
86
+ throughputData?.map((d: any) => ({
87
+ time: d.timestamp,
88
+ value: d.count,
89
+ })) || []
90
+
91
+ // Calculate totals
92
+ const totalWaiting = queues.reduce((acc, q) => acc + q.waiting, 0)
93
+ const totalDelayed = queues.reduce((acc, q) => acc + q.delayed, 0)
94
+ const totalFailed = queues.reduce((acc, q) => acc + q.failed, 0)
95
+ const totalActive = queues.reduce((acc, q) => acc + q.active, 0)
96
+
97
+ // Calculate throughput stats
98
+ const currentThroughput = chartData[chartData.length - 1]?.value || 0
99
+ const avgThroughput =
100
+ chartData.length > 0
101
+ ? Math.round(chartData.reduce((acc: number, d: any) => acc + d.value, 0) / chartData.length)
102
+ : 0
103
+ const maxThroughput = chartData.length > 0 ? Math.max(...chartData.map((d: any) => d.value)) : 0
104
+
105
+ // Prepare queue distribution data
106
+ const queueDistribution = queues.slice(0, 10).map((q) => ({
107
+ name: q.name.length > 12 ? `${q.name.slice(0, 12)}...` : q.name,
108
+ waiting: q.waiting,
109
+ delayed: q.delayed,
110
+ failed: q.failed,
111
+ }))
112
+
113
+ // Prepare historical sparkline data
114
+ const historyLabels = Array.from({ length: 15 }, (_, i) => `${14 - i}m ago`)
115
+ const sparklineData = historyLabels.map((label, i) => ({
116
+ time: label,
117
+ waiting: history.waiting?.[i] || 0,
118
+ delayed: history.delayed?.[i] || 0,
119
+ failed: history.failed?.[i] || 0,
120
+ workers: history.workers?.[i] || 0,
121
+ }))
122
+
123
+ if (isPending) {
124
+ return (
125
+ <div className="flex flex-col items-center justify-center p-20 space-y-6">
126
+ <RefreshCcw className="animate-spin text-primary" size={48} />
127
+ <p className="text-muted-foreground font-bold uppercase tracking-[0.3em] text-xs">
128
+ Loading metrics...
129
+ </p>
130
+ </div>
131
+ )
132
+ }
133
+
134
+ return (
135
+ <div className="space-y-8">
136
+ {/* Header */}
137
+ <div className="flex justify-between items-end">
138
+ <div>
139
+ <h1 className="text-4xl font-black tracking-tighter">System Metrics</h1>
140
+ <p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest">
141
+ Real-time performance analytics and trends.
142
+ </p>
143
+ </div>
144
+ <div className="flex items-center gap-2">
145
+ {(['15m', '1h', '6h', '24h'] as const).map((range) => (
146
+ <button
147
+ type="button"
148
+ key={range}
149
+ onClick={() => setTimeRange(range)}
150
+ className={cn(
151
+ 'px-3 py-1.5 rounded-lg text-[10px] font-black uppercase tracking-widest transition-all',
152
+ timeRange === range
153
+ ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20'
154
+ : 'hover:bg-muted text-muted-foreground'
155
+ )}
156
+ >
157
+ {range}
158
+ </button>
159
+ ))}
160
+ </div>
161
+ </div>
162
+
163
+ {/* Quick Stats */}
164
+ <div className="grid grid-cols-2 lg:grid-cols-6 gap-4">
165
+ <StatCard icon={Hourglass} label="Waiting" value={totalWaiting} color="amber" />
166
+ <StatCard icon={Clock} label="Delayed" value={totalDelayed} color="blue" />
167
+ <StatCard icon={Activity} label="Active" value={totalActive} color="green" />
168
+ <StatCard icon={XCircle} label="Failed" value={totalFailed} color="red" />
169
+ <StatCard icon={CheckCircle} label="Workers" value={workers.length} color="indigo" />
170
+ <StatCard icon={TrendingUp} label="Jobs/min" value={currentThroughput} color="primary" />
171
+ </div>
172
+
173
+ {/* Throughput Chart */}
174
+ <div className="card-premium p-6">
175
+ <div className="flex justify-between items-start mb-6">
176
+ <div>
177
+ <div className="flex items-center gap-2">
178
+ <LineChart size={20} className="text-primary" />
179
+ <h3 className="text-xl font-bold tracking-tight">Throughput Over Time</h3>
180
+ </div>
181
+ <p className="text-[10px] text-muted-foreground uppercase tracking-[0.2em] font-bold mt-1">
182
+ Jobs processed per minute
183
+ </p>
184
+ </div>
185
+ <div className="text-right">
186
+ <div className="flex gap-6">
187
+ <div>
188
+ <p className="text-[9px] font-black text-muted-foreground/50 uppercase">Current</p>
189
+ <p className="text-2xl font-black text-primary">{currentThroughput}</p>
190
+ </div>
191
+ <div>
192
+ <p className="text-[9px] font-black text-muted-foreground/50 uppercase">Average</p>
193
+ <p className="text-2xl font-black">{avgThroughput}</p>
194
+ </div>
195
+ <div>
196
+ <p className="text-[9px] font-black text-muted-foreground/50 uppercase">Peak</p>
197
+ <p className="text-2xl font-black text-green-500">{maxThroughput}</p>
198
+ </div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ <div className="h-[300px]">
203
+ <ResponsiveContainer width="100%" height="100%">
204
+ <AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
205
+ <defs>
206
+ <linearGradient id="colorThroughput" x1="0" y1="0" x2="0" y2="1">
207
+ <stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.4} />
208
+ <stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
209
+ </linearGradient>
210
+ </defs>
211
+ <CartesianGrid
212
+ strokeDasharray="3 3"
213
+ vertical={false}
214
+ stroke="hsl(var(--border))"
215
+ opacity={0.5}
216
+ />
217
+ <XAxis
218
+ dataKey="time"
219
+ axisLine={false}
220
+ tickLine={false}
221
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
222
+ />
223
+ <YAxis
224
+ axisLine={false}
225
+ tickLine={false}
226
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
227
+ />
228
+ <Tooltip
229
+ contentStyle={{
230
+ backgroundColor: 'hsl(var(--card))',
231
+ border: '1px solid hsl(var(--border))',
232
+ borderRadius: '12px',
233
+ fontSize: '12px',
234
+ }}
235
+ />
236
+ <Area
237
+ type="monotone"
238
+ dataKey="value"
239
+ stroke="hsl(var(--primary))"
240
+ fillOpacity={1}
241
+ fill="url(#colorThroughput)"
242
+ strokeWidth={2}
243
+ />
244
+ </AreaChart>
245
+ </ResponsiveContainer>
246
+ </div>
247
+ </div>
248
+
249
+ {/* Two Column Layout */}
250
+ <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
251
+ {/* Queue Distribution */}
252
+ <div className="card-premium p-6">
253
+ <div className="flex items-center gap-2 mb-6">
254
+ <BarChart3 size={20} className="text-primary" />
255
+ <h3 className="text-lg font-bold tracking-tight">Queue Distribution</h3>
256
+ </div>
257
+ <div className="h-[300px]">
258
+ <ResponsiveContainer width="100%" height="100%">
259
+ <BarChart
260
+ data={queueDistribution}
261
+ margin={{ top: 10, right: 10, left: -20, bottom: 40 }}
262
+ >
263
+ <CartesianGrid
264
+ strokeDasharray="3 3"
265
+ vertical={false}
266
+ stroke="hsl(var(--border))"
267
+ opacity={0.5}
268
+ />
269
+ <XAxis
270
+ dataKey="name"
271
+ axisLine={false}
272
+ tickLine={false}
273
+ tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
274
+ angle={-45}
275
+ textAnchor="end"
276
+ />
277
+ <YAxis
278
+ axisLine={false}
279
+ tickLine={false}
280
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
281
+ />
282
+ <Tooltip
283
+ contentStyle={{
284
+ backgroundColor: 'hsl(var(--card))',
285
+ border: '1px solid hsl(var(--border))',
286
+ borderRadius: '12px',
287
+ fontSize: '12px',
288
+ }}
289
+ />
290
+ <Legend />
291
+ <Bar
292
+ dataKey="waiting"
293
+ fill="hsl(45, 93%, 47%)"
294
+ name="Waiting"
295
+ radius={[4, 4, 0, 0]}
296
+ />
297
+ <Bar
298
+ dataKey="delayed"
299
+ fill="hsl(217, 91%, 60%)"
300
+ name="Delayed"
301
+ radius={[4, 4, 0, 0]}
302
+ />
303
+ <Bar dataKey="failed" fill="hsl(0, 84%, 60%)" name="Failed" radius={[4, 4, 0, 0]} />
304
+ </BarChart>
305
+ </ResponsiveContainer>
306
+ </div>
307
+ </div>
308
+
309
+ {/* Historical Trends */}
310
+ <div className="card-premium p-6">
311
+ <div className="flex items-center gap-2 mb-6">
312
+ <TrendingUp size={20} className="text-primary" />
313
+ <h3 className="text-lg font-bold tracking-tight">15-Minute Trends</h3>
314
+ </div>
315
+ <div className="h-[300px]">
316
+ <ResponsiveContainer width="100%" height="100%">
317
+ <AreaChart data={sparklineData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
318
+ <defs>
319
+ <linearGradient id="colorWaiting" x1="0" y1="0" x2="0" y2="1">
320
+ <stop offset="5%" stopColor="hsl(45, 93%, 47%)" stopOpacity={0.3} />
321
+ <stop offset="95%" stopColor="hsl(45, 93%, 47%)" stopOpacity={0} />
322
+ </linearGradient>
323
+ <linearGradient id="colorFailed" x1="0" y1="0" x2="0" y2="1">
324
+ <stop offset="5%" stopColor="hsl(0, 84%, 60%)" stopOpacity={0.3} />
325
+ <stop offset="95%" stopColor="hsl(0, 84%, 60%)" stopOpacity={0} />
326
+ </linearGradient>
327
+ </defs>
328
+ <CartesianGrid
329
+ strokeDasharray="3 3"
330
+ vertical={false}
331
+ stroke="hsl(var(--border))"
332
+ opacity={0.5}
333
+ />
334
+ <XAxis
335
+ dataKey="time"
336
+ axisLine={false}
337
+ tickLine={false}
338
+ tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
339
+ />
340
+ <YAxis
341
+ axisLine={false}
342
+ tickLine={false}
343
+ tick={{ fontSize: 10, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
344
+ />
345
+ <Tooltip
346
+ contentStyle={{
347
+ backgroundColor: 'hsl(var(--card))',
348
+ border: '1px solid hsl(var(--border))',
349
+ borderRadius: '12px',
350
+ fontSize: '12px',
351
+ }}
352
+ />
353
+ <Area
354
+ type="monotone"
355
+ dataKey="waiting"
356
+ stroke="hsl(45, 93%, 47%)"
357
+ fill="url(#colorWaiting)"
358
+ strokeWidth={2}
359
+ />
360
+ <Area
361
+ type="monotone"
362
+ dataKey="failed"
363
+ stroke="hsl(0, 84%, 60%)"
364
+ fill="url(#colorFailed)"
365
+ strokeWidth={2}
366
+ />
367
+ </AreaChart>
368
+ </ResponsiveContainer>
369
+ </div>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ )
374
+ }
375
+
376
+ interface StatCardProps {
377
+ icon: React.ComponentType<{ size?: number; className?: string }>
378
+ label: string
379
+ value: number
380
+ color: 'amber' | 'blue' | 'green' | 'red' | 'indigo' | 'primary'
381
+ }
382
+
383
+ function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
384
+ const colorClasses = {
385
+ amber: 'text-amber-500 bg-amber-500/10',
386
+ blue: 'text-blue-500 bg-blue-500/10',
387
+ green: 'text-green-500 bg-green-500/10',
388
+ red: 'text-red-500 bg-red-500/10',
389
+ indigo: 'text-indigo-500 bg-indigo-500/10',
390
+ primary: 'text-primary bg-primary/10',
391
+ }
392
+
393
+ return (
394
+ <div className="card-premium p-4 flex items-center gap-3">
395
+ <div
396
+ className={cn('w-10 h-10 rounded-xl flex items-center justify-center', colorClasses[color])}
397
+ >
398
+ <Icon size={20} />
399
+ </div>
400
+ <div>
401
+ <p className="text-[9px] font-black text-muted-foreground/50 uppercase tracking-widest">
402
+ {label}
403
+ </p>
404
+ <p className="text-xl font-black">{value.toLocaleString()}</p>
405
+ </div>
406
+ </div>
407
+ )
408
+ }