@gravito/zenith 1.1.2 → 1.1.3
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/CHANGELOG.md +15 -0
- package/README.md +77 -22
- package/README.zh-TW.md +88 -0
- package/dist/bin.js +64681 -15842
- package/dist/client/assets/index-C80c1frR.css +1 -0
- package/dist/client/assets/index-CrWem9u3.js +434 -0
- package/dist/server/index.js +64681 -15842
- package/package.json +9 -7
- package/postcss.config.js +4 -4
- package/src/client/Layout.tsx +36 -39
- package/src/client/Sidebar.tsx +7 -7
- package/src/client/ThroughputChart.tsx +31 -17
- package/src/client/WorkerStatus.tsx +56 -80
- package/src/client/components/ConfirmDialog.tsx +22 -14
- package/src/client/components/JobInspector.tsx +95 -162
- package/src/client/index.css +29 -31
- package/src/client/pages/LoginPage.tsx +33 -31
- package/src/client/pages/MetricsPage.tsx +65 -37
- package/src/client/pages/OverviewPage.tsx +30 -28
- package/src/client/pages/PulsePage.tsx +111 -190
- package/src/client/pages/QueuesPage.tsx +82 -83
- package/src/client/pages/SchedulesPage.tsx +56 -61
- package/src/client/pages/SettingsPage.tsx +118 -137
- package/src/client/pages/WorkersPage.tsx +101 -115
- package/src/server/services/CommandService.ts +8 -9
- package/src/server/services/PulseService.ts +61 -4
- package/src/server/services/QueueService.ts +293 -0
- package/src/shared/types.ts +38 -13
- package/tailwind.config.js +75 -68
- package/tsconfig.json +28 -37
- package/tsconfig.node.json +9 -11
- package/dist/client/assets/index-BSMp8oq_.js +0 -436
- package/dist/client/assets/index-BwxlHx-_.css +0 -1
- package/dist/client/index.html +0 -13
- package/src/client/index.html +0 -12
- /package/{ECOSYSTEM_EXPANSION_RFC.md → doc/ECOSYSTEM_EXPANSION_RFC.md} +0 -0
|
@@ -53,31 +53,35 @@ export function LoginPage() {
|
|
|
53
53
|
className="relative z-10 w-full max-w-md"
|
|
54
54
|
>
|
|
55
55
|
{/* Logo & Title */}
|
|
56
|
-
<div className="text-center mb-8">
|
|
56
|
+
<div className="text-center mb-8 font-heading">
|
|
57
57
|
<motion.div
|
|
58
58
|
initial={{ scale: 0 }}
|
|
59
59
|
animate={{ scale: 1 }}
|
|
60
60
|
transition={{ type: 'spring', delay: 0.2 }}
|
|
61
|
-
className="inline-flex items-center justify-center w-
|
|
61
|
+
className="inline-flex items-center justify-center w-24 h-24 bg-primary rounded-3xl shadow-[0_0_40px_rgba(0,240,255,0.2)] mb-8"
|
|
62
62
|
>
|
|
63
|
-
<Activity size={
|
|
63
|
+
<Activity size={48} className="text-black" />
|
|
64
64
|
</motion.div>
|
|
65
|
-
<h1 className="text-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
<h1 className="text-5xl font-black tracking-tighter mb-2 text-white italic uppercase">
|
|
66
|
+
ZENITH
|
|
67
|
+
</h1>
|
|
68
|
+
<p className="text-[10px] font-black text-primary uppercase tracking-[0.5em] ml-1">
|
|
69
|
+
Quantum Control Plane
|
|
68
70
|
</p>
|
|
69
71
|
</div>
|
|
70
72
|
|
|
71
73
|
{/* Login Card */}
|
|
72
|
-
<div className="card-premium p-
|
|
73
|
-
<div className="text-center mb-
|
|
74
|
-
<h2 className="text-xl font-
|
|
75
|
-
|
|
76
|
-
|
|
74
|
+
<div className="card-premium p-10 backdrop-blur-2xl bg-zinc-900/40 border-white/5">
|
|
75
|
+
<div className="text-center mb-10">
|
|
76
|
+
<h2 className="text-xl font-black text-white uppercase tracking-tight font-heading">
|
|
77
|
+
Secure Access
|
|
78
|
+
</h2>
|
|
79
|
+
<p className="text-xs text-muted-foreground mt-2 font-bold uppercase tracking-widest opacity-60">
|
|
80
|
+
Biometric or Password verification required
|
|
77
81
|
</p>
|
|
78
82
|
</div>
|
|
79
83
|
|
|
80
|
-
<form onSubmit={handleSubmit} className="space-y-
|
|
84
|
+
<form onSubmit={handleSubmit} className="space-y-8 font-heading">
|
|
81
85
|
{/* Error Message */}
|
|
82
86
|
{error && (
|
|
83
87
|
<motion.div
|
|
@@ -86,21 +90,21 @@ export function LoginPage() {
|
|
|
86
90
|
className="flex items-center gap-3 p-4 bg-red-500/10 border border-red-500/20 rounded-xl text-red-500"
|
|
87
91
|
>
|
|
88
92
|
<AlertCircle size={18} />
|
|
89
|
-
<span className="text-
|
|
93
|
+
<span className="text-[11px] font-black uppercase tracking-widest">{error}</span>
|
|
90
94
|
</motion.div>
|
|
91
95
|
)}
|
|
92
96
|
|
|
93
97
|
{/* Password Field */}
|
|
94
|
-
<div className="space-y-
|
|
98
|
+
<div className="space-y-3">
|
|
95
99
|
<label
|
|
96
100
|
htmlFor="password"
|
|
97
|
-
className="text-
|
|
101
|
+
className="text-[10px] font-black text-muted-foreground uppercase tracking-[0.2em] ml-1"
|
|
98
102
|
>
|
|
99
|
-
|
|
103
|
+
System Authentication
|
|
100
104
|
</label>
|
|
101
105
|
<div className="relative">
|
|
102
106
|
<Lock
|
|
103
|
-
className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground"
|
|
107
|
+
className="absolute left-4 top-1/2 -translate-y-1/2 text-muted-foreground/40"
|
|
104
108
|
size={18}
|
|
105
109
|
/>
|
|
106
110
|
<input
|
|
@@ -108,11 +112,11 @@ export function LoginPage() {
|
|
|
108
112
|
type={showPassword ? 'text' : 'password'}
|
|
109
113
|
value={password}
|
|
110
114
|
onChange={(e) => setPassword(e.target.value)}
|
|
111
|
-
placeholder="Enter
|
|
115
|
+
placeholder="Enter access code..."
|
|
112
116
|
className={cn(
|
|
113
|
-
'w-full bg-
|
|
114
|
-
'text-sm font-
|
|
115
|
-
'focus:outline-none focus:ring-
|
|
117
|
+
'w-full bg-black/40 border border-white/5 rounded-xl py-4 pl-12 pr-12 font-mono',
|
|
118
|
+
'text-sm font-bold placeholder:text-white/10 tracking-widest',
|
|
119
|
+
'focus:outline-none focus:ring-1 focus:ring-primary/40 focus:border-primary/40',
|
|
116
120
|
'transition-all'
|
|
117
121
|
)}
|
|
118
122
|
disabled={isLoading}
|
|
@@ -120,7 +124,7 @@ export function LoginPage() {
|
|
|
120
124
|
<button
|
|
121
125
|
type="button"
|
|
122
126
|
onClick={() => setShowPassword(!showPassword)}
|
|
123
|
-
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-
|
|
127
|
+
className="absolute right-4 top-1/2 -translate-y-1/2 text-muted-foreground/40 hover:text-primary transition-colors"
|
|
124
128
|
>
|
|
125
129
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
126
130
|
</button>
|
|
@@ -132,20 +136,18 @@ export function LoginPage() {
|
|
|
132
136
|
type="submit"
|
|
133
137
|
disabled={isLoading || !password}
|
|
134
138
|
className={cn(
|
|
135
|
-
'w-full py-4 rounded-xl font-
|
|
136
|
-
'bg-
|
|
137
|
-
'
|
|
138
|
-
'
|
|
139
|
-
'
|
|
140
|
-
'flex items-center justify-center gap-2'
|
|
139
|
+
'w-full py-4 rounded-xl font-black text-xs uppercase tracking-[0.3em]',
|
|
140
|
+
'bg-primary text-black font-heading shadow-[0_0_30px_rgba(0,240,255,0.2)] hover:shadow-[0_0_40px_rgba(0,240,255,0.4)]',
|
|
141
|
+
'transition-all duration-500 hover:scale-[1.02] active:scale-95',
|
|
142
|
+
'disabled:opacity-20 disabled:cursor-not-allowed disabled:hover:scale-100',
|
|
143
|
+
'flex items-center justify-center gap-3'
|
|
141
144
|
)}
|
|
142
145
|
>
|
|
143
146
|
{isLoading ? (
|
|
144
|
-
<div className="w-5 h-5 border-2 border-
|
|
147
|
+
<div className="w-5 h-5 border-2 border-black/30 border-t-black rounded-full animate-spin" />
|
|
145
148
|
) : (
|
|
146
149
|
<>
|
|
147
|
-
|
|
148
|
-
<ArrowRight size={18} />
|
|
150
|
+
Connect <ArrowRight size={18} />
|
|
149
151
|
</>
|
|
150
152
|
)}
|
|
151
153
|
</button>
|
|
@@ -213,7 +213,7 @@ export function MetricsPage() {
|
|
|
213
213
|
<AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
|
214
214
|
<defs>
|
|
215
215
|
<linearGradient id="colorThroughput" x1="0" y1="0" x2="0" y2="1">
|
|
216
|
-
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.
|
|
216
|
+
<stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.5} />
|
|
217
217
|
<stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
|
|
218
218
|
</linearGradient>
|
|
219
219
|
</defs>
|
|
@@ -221,25 +221,37 @@ export function MetricsPage() {
|
|
|
221
221
|
strokeDasharray="3 3"
|
|
222
222
|
vertical={false}
|
|
223
223
|
stroke="hsl(var(--border))"
|
|
224
|
-
opacity={0.
|
|
224
|
+
opacity={0.3}
|
|
225
225
|
/>
|
|
226
226
|
<XAxis
|
|
227
227
|
dataKey="time"
|
|
228
228
|
axisLine={false}
|
|
229
229
|
tickLine={false}
|
|
230
|
-
tick={{
|
|
230
|
+
tick={{
|
|
231
|
+
fontSize: 10,
|
|
232
|
+
fill: 'hsl(var(--muted-foreground))',
|
|
233
|
+
fontWeight: 700,
|
|
234
|
+
fontFamily: 'Fira Code',
|
|
235
|
+
}}
|
|
231
236
|
/>
|
|
232
237
|
<YAxis
|
|
233
238
|
axisLine={false}
|
|
234
239
|
tickLine={false}
|
|
235
|
-
tick={{
|
|
240
|
+
tick={{
|
|
241
|
+
fontSize: 10,
|
|
242
|
+
fill: 'hsl(var(--muted-foreground))',
|
|
243
|
+
fontWeight: 700,
|
|
244
|
+
fontFamily: 'Fira Code',
|
|
245
|
+
}}
|
|
236
246
|
/>
|
|
237
247
|
<Tooltip
|
|
238
248
|
contentStyle={{
|
|
239
|
-
backgroundColor: '
|
|
240
|
-
border: '1px solid
|
|
249
|
+
backgroundColor: 'rgba(9, 9, 11, 0.95)',
|
|
250
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
241
251
|
borderRadius: '12px',
|
|
242
|
-
fontSize: '
|
|
252
|
+
fontSize: '11px',
|
|
253
|
+
fontFamily: 'Fira Code',
|
|
254
|
+
backdropFilter: 'blur(8px)',
|
|
243
255
|
}}
|
|
244
256
|
/>
|
|
245
257
|
<Area
|
|
@@ -248,7 +260,7 @@ export function MetricsPage() {
|
|
|
248
260
|
stroke="hsl(var(--primary))"
|
|
249
261
|
fillOpacity={1}
|
|
250
262
|
fill="url(#colorThroughput)"
|
|
251
|
-
strokeWidth={
|
|
263
|
+
strokeWidth={3}
|
|
252
264
|
/>
|
|
253
265
|
</AreaChart>
|
|
254
266
|
</ResponsiveContainer>
|
|
@@ -279,37 +291,39 @@ export function MetricsPage() {
|
|
|
279
291
|
dataKey="name"
|
|
280
292
|
axisLine={false}
|
|
281
293
|
tickLine={false}
|
|
282
|
-
tick={{
|
|
294
|
+
tick={{
|
|
295
|
+
fontSize: 9,
|
|
296
|
+
fill: 'hsl(var(--muted-foreground))',
|
|
297
|
+
fontWeight: 700,
|
|
298
|
+
fontFamily: 'Fira Code',
|
|
299
|
+
}}
|
|
283
300
|
angle={-45}
|
|
284
301
|
textAnchor="end"
|
|
285
302
|
/>
|
|
286
303
|
<YAxis
|
|
287
304
|
axisLine={false}
|
|
288
305
|
tickLine={false}
|
|
289
|
-
tick={{
|
|
306
|
+
tick={{
|
|
307
|
+
fontSize: 10,
|
|
308
|
+
fill: 'hsl(var(--muted-foreground))',
|
|
309
|
+
fontWeight: 700,
|
|
310
|
+
fontFamily: 'Fira Code',
|
|
311
|
+
}}
|
|
290
312
|
/>
|
|
291
313
|
<Tooltip
|
|
292
314
|
contentStyle={{
|
|
293
|
-
backgroundColor: '
|
|
294
|
-
border: '1px solid
|
|
315
|
+
backgroundColor: 'rgba(9, 9, 11, 0.95)',
|
|
316
|
+
border: '1px solid rgba(255, 255, 255, 0.1)',
|
|
295
317
|
borderRadius: '12px',
|
|
296
|
-
fontSize: '
|
|
318
|
+
fontSize: '11px',
|
|
319
|
+
fontFamily: 'Fira Code',
|
|
320
|
+
backdropFilter: 'blur(8px)',
|
|
297
321
|
}}
|
|
298
322
|
/>
|
|
299
|
-
<Legend />
|
|
300
|
-
<Bar
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
name="Waiting"
|
|
304
|
-
radius={[4, 4, 0, 0]}
|
|
305
|
-
/>
|
|
306
|
-
<Bar
|
|
307
|
-
dataKey="delayed"
|
|
308
|
-
fill="hsl(217, 91%, 60%)"
|
|
309
|
-
name="Delayed"
|
|
310
|
-
radius={[4, 4, 0, 0]}
|
|
311
|
-
/>
|
|
312
|
-
<Bar dataKey="failed" fill="hsl(0, 84%, 60%)" name="Failed" radius={[4, 4, 0, 0]} />
|
|
323
|
+
<Legend iconType="circle" />
|
|
324
|
+
<Bar dataKey="waiting" fill="#F59E0B" name="Waiting" radius={[4, 4, 0, 0]} />
|
|
325
|
+
<Bar dataKey="delayed" fill="#3B82F6" name="Delayed" radius={[4, 4, 0, 0]} />
|
|
326
|
+
<Bar dataKey="failed" fill="#EF4444" name="Failed" radius={[4, 4, 0, 0]} />
|
|
313
327
|
</BarChart>
|
|
314
328
|
</ResponsiveContainer>
|
|
315
329
|
</div>
|
|
@@ -391,26 +405,40 @@ interface StatCardProps {
|
|
|
391
405
|
|
|
392
406
|
function StatCard({ icon: Icon, label, value, color }: StatCardProps) {
|
|
393
407
|
const colorClasses = {
|
|
394
|
-
amber:
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
408
|
+
amber:
|
|
409
|
+
'text-amber-500 bg-amber-500/10 border-amber-500/20 shadow-[0_0_15px_rgba(245,158,11,0.05)]',
|
|
410
|
+
blue: 'text-blue-500 bg-blue-500/10 border-blue-500/20 shadow-[0_0_15px_rgba(59,130,246,0.05)]',
|
|
411
|
+
green:
|
|
412
|
+
'text-emerald-500 bg-emerald-500/10 border-emerald-500/20 shadow-[0_0_15px_rgba(16,185,129,0.05)]',
|
|
413
|
+
red: 'text-red-500 bg-red-500/10 border-red-500/20 shadow-[0_0_15px_rgba(239,68,68,0.05)]',
|
|
414
|
+
indigo:
|
|
415
|
+
'text-indigo-400 bg-indigo-500/10 border-indigo-500/20 shadow-[0_0_15px_rgba(99,102,241,0.05)]',
|
|
416
|
+
primary: 'text-primary bg-primary/10 border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.05)]',
|
|
400
417
|
}
|
|
401
418
|
|
|
402
419
|
return (
|
|
403
|
-
<div
|
|
420
|
+
<div
|
|
421
|
+
className={cn(
|
|
422
|
+
'card-premium p-4 flex items-center gap-4 border-l-4',
|
|
423
|
+
colorClasses[color]
|
|
424
|
+
.split(' ')
|
|
425
|
+
.find((c) => c.startsWith('border-'))
|
|
426
|
+
?.replace('border-', 'border-l-')
|
|
427
|
+
)}
|
|
428
|
+
>
|
|
404
429
|
<div
|
|
405
|
-
className={cn(
|
|
430
|
+
className={cn(
|
|
431
|
+
'w-10 h-10 rounded-xl flex items-center justify-center border transition-transform group-hover:scale-110',
|
|
432
|
+
colorClasses[color]
|
|
433
|
+
)}
|
|
406
434
|
>
|
|
407
435
|
<Icon size={20} />
|
|
408
436
|
</div>
|
|
409
437
|
<div>
|
|
410
|
-
<p className="text-[9px] font-black text-muted-foreground/
|
|
438
|
+
<p className="text-[9px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] font-heading mb-0.5">
|
|
411
439
|
{label}
|
|
412
440
|
</p>
|
|
413
|
-
<p className="text-xl font-black">{value.toLocaleString()}</p>
|
|
441
|
+
<p className="text-xl font-black font-mono tracking-tighter">{value.toLocaleString()}</p>
|
|
414
442
|
</div>
|
|
415
443
|
</div>
|
|
416
444
|
)
|
|
@@ -76,6 +76,7 @@ function LiveLogs({
|
|
|
76
76
|
<button
|
|
77
77
|
type="button"
|
|
78
78
|
onClick={onSearchArchive}
|
|
79
|
+
aria-label="Search Logs Archive"
|
|
79
80
|
className="flex items-center gap-1.5 px-2 py-1 hover:bg-muted rounded-md text-[10px] font-black uppercase tracking-tighter text-muted-foreground transition-all"
|
|
80
81
|
>
|
|
81
82
|
<Search size={12} />
|
|
@@ -89,12 +90,12 @@ function LiveLogs({
|
|
|
89
90
|
</div>
|
|
90
91
|
<ul
|
|
91
92
|
ref={scrollRef}
|
|
92
|
-
className="flex-1 min-h-0 overflow-y-auto p-4 font-mono text-[
|
|
93
|
+
className="flex-1 min-h-0 overflow-y-auto p-4 font-mono text-[10px] space-y-2 scrollbar-thin scroll-smooth bg-black/20"
|
|
93
94
|
>
|
|
94
95
|
{logs.length === 0 ? (
|
|
95
|
-
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/
|
|
96
|
-
<Activity size={24} className="animate-pulse" />
|
|
97
|
-
<p className="font-bold uppercase tracking-
|
|
96
|
+
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/20 gap-2">
|
|
97
|
+
<Activity size={24} className="animate-pulse opacity-50" />
|
|
98
|
+
<p className="font-bold uppercase tracking-[0.3em] text-[8px]">Scanning spectrum...</p>
|
|
98
99
|
</div>
|
|
99
100
|
) : (
|
|
100
101
|
logs.map((log, i) => (
|
|
@@ -102,9 +103,9 @@ function LiveLogs({
|
|
|
102
103
|
key={i}
|
|
103
104
|
onMouseEnter={() => onWorkerHover?.(log.workerId)}
|
|
104
105
|
onMouseLeave={() => onWorkerHover?.(null)}
|
|
105
|
-
className="group flex gap-3 hover:bg-
|
|
106
|
+
className="group flex gap-3 hover:bg-white/[0.03] -mx-2 px-3 py-1 rounded transition-all animate-in fade-in slide-in-from-left-1 duration-200 cursor-default border-l-2 border-transparent hover:border-primary/40"
|
|
106
107
|
>
|
|
107
|
-
<span className="text-muted-foreground/
|
|
108
|
+
<span className="text-muted-foreground/60 shrink-0 tabular-nums select-none opacity-60 group-hover:opacity-100 transition-opacity">
|
|
108
109
|
{new Date(log.timestamp).toLocaleTimeString([], {
|
|
109
110
|
hour12: false,
|
|
110
111
|
hour: '2-digit',
|
|
@@ -116,23 +117,23 @@ function LiveLogs({
|
|
|
116
117
|
<div className="flex items-center gap-2 mb-0.5">
|
|
117
118
|
<span
|
|
118
119
|
className={cn(
|
|
119
|
-
'text-[
|
|
120
|
+
'text-[8px] font-black uppercase tracking-widest px-1 rounded',
|
|
120
121
|
log.level === 'error'
|
|
121
|
-
? 'text-red-500'
|
|
122
|
+
? 'bg-red-500/10 text-red-500'
|
|
122
123
|
: log.level === 'warn'
|
|
123
|
-
? 'text-amber-500'
|
|
124
|
+
? 'bg-amber-500/10 text-amber-500'
|
|
124
125
|
: log.level === 'success'
|
|
125
|
-
? 'text-green-500'
|
|
126
|
-
: 'text-
|
|
126
|
+
? 'bg-green-500/10 text-green-500'
|
|
127
|
+
: 'bg-primary/10 text-primary'
|
|
127
128
|
)}
|
|
128
129
|
>
|
|
129
|
-
|
|
130
|
+
{log.level}
|
|
130
131
|
</span>
|
|
131
|
-
<span className="text-[
|
|
132
|
-
{log.workerId}
|
|
132
|
+
<span className="text-[8px] font-bold text-muted-foreground/40 uppercase tracking-tighter opacity-0 group-hover:opacity-100 transition-all">
|
|
133
|
+
ID:{log.workerId}
|
|
133
134
|
</span>
|
|
134
135
|
</div>
|
|
135
|
-
<p className="text-foreground/
|
|
136
|
+
<p className="text-foreground/70 group-hover:text-foreground leading-relaxed whitespace-pre-wrap break-all">
|
|
136
137
|
{log.message}
|
|
137
138
|
</p>
|
|
138
139
|
</div>
|
|
@@ -219,13 +220,13 @@ function MetricCard({ title, value, icon, color, trend, data }: MetricCardProps)
|
|
|
219
220
|
const max = Math.max(...displayData, 10)
|
|
220
221
|
|
|
221
222
|
return (
|
|
222
|
-
<div className="card-premium p-
|
|
223
|
+
<div className="card-premium p-6 hover:shadow-2xl transform hover:-translate-y-1 group relative overflow-hidden flex flex-col justify-between">
|
|
223
224
|
<div className="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none scanline z-0"></div>
|
|
224
225
|
|
|
225
|
-
<div className="flex justify-between items-start mb-
|
|
226
|
+
<div className="flex justify-between items-start mb-4 z-10 relative">
|
|
226
227
|
<div
|
|
227
228
|
className={cn(
|
|
228
|
-
'p-
|
|
229
|
+
'p-3 rounded-xl bg-muted/20 transition-all group-hover:scale-110 duration-500 border border-transparent group-hover:border-primary/20 shadow-inner',
|
|
229
230
|
color
|
|
230
231
|
)}
|
|
231
232
|
>
|
|
@@ -233,14 +234,14 @@ function MetricCard({ title, value, icon, color, trend, data }: MetricCardProps)
|
|
|
233
234
|
</div>
|
|
234
235
|
{trend && (
|
|
235
236
|
<div className="flex flex-col items-end">
|
|
236
|
-
<span className=
|
|
237
|
+
<span className={cn('text-[9px] font-black uppercase tracking-widest', color)}>
|
|
237
238
|
{trend}
|
|
238
239
|
</span>
|
|
239
|
-
<div className="w-
|
|
240
|
+
<div className="w-10 h-1 bg-muted/30 rounded-full mt-1 overflow-hidden">
|
|
240
241
|
<motion.div
|
|
241
242
|
initial={{ width: 0 }}
|
|
242
243
|
animate={{ width: '100%' }}
|
|
243
|
-
transition={{ duration: 1, repeat: Infinity, repeatType: 'reverse' }}
|
|
244
|
+
transition={{ duration: 1.5, repeat: Infinity, repeatType: 'reverse' }}
|
|
244
245
|
className={cn('h-full', color.replace('text-', 'bg-'))}
|
|
245
246
|
/>
|
|
246
247
|
</div>
|
|
@@ -249,29 +250,29 @@ function MetricCard({ title, value, icon, color, trend, data }: MetricCardProps)
|
|
|
249
250
|
</div>
|
|
250
251
|
|
|
251
252
|
<div className="z-10 relative">
|
|
252
|
-
<p className="text-[
|
|
253
|
+
<p className="text-[10px] font-black text-muted-foreground/60 uppercase tracking-[0.2em] mb-1 font-heading">
|
|
253
254
|
{title}
|
|
254
255
|
</p>
|
|
255
|
-
<div className="text-
|
|
256
|
+
<div className="text-3xl font-black tracking-tight flex items-baseline gap-1 font-mono">
|
|
256
257
|
<AnimatedNumber value={value} />
|
|
257
258
|
{title === 'Waiting Jobs' && value > 100 && (
|
|
258
|
-
<span className="text-red-500 animate-pulse text-
|
|
259
|
+
<span className="text-red-500 animate-pulse text-sm ml-1">!</span>
|
|
259
260
|
)}
|
|
260
261
|
</div>
|
|
261
262
|
</div>
|
|
262
263
|
|
|
263
|
-
<div className="mt-
|
|
264
|
+
<div className="mt-6 flex items-end gap-1 h-12 opacity-10 group-hover:opacity-30 transition-all duration-700 absolute bottom-0 left-0 right-0 pointer-events-none">
|
|
264
265
|
{displayData.map((v, i) => (
|
|
265
266
|
<div
|
|
266
267
|
key={i}
|
|
267
268
|
className={cn(
|
|
268
|
-
'flex-1 rounded-t-
|
|
269
|
+
'flex-1 rounded-t-sm transition-all duration-1000',
|
|
269
270
|
color.replace('text-', 'bg-')
|
|
270
271
|
)}
|
|
271
272
|
style={{
|
|
272
273
|
height: `${(v / max) * 100}%`,
|
|
273
|
-
opacity: 0.
|
|
274
|
-
transitionDelay: `${i *
|
|
274
|
+
opacity: 0.2 + (i / displayData.length) * 0.8,
|
|
275
|
+
transitionDelay: `${i * 20}ms`,
|
|
275
276
|
}}
|
|
276
277
|
></div>
|
|
277
278
|
))}
|
|
@@ -333,6 +334,7 @@ function QueueList({
|
|
|
333
334
|
onClick={() => setSelectedQueue(queue.name)}
|
|
334
335
|
className="p-1.5 bg-muted hover:bg-primary/20 hover:text-primary rounded text-muted-foreground transition-all active:scale-90"
|
|
335
336
|
title="Inspect"
|
|
337
|
+
aria-label={`Inspect ${queue.name} queue`}
|
|
336
338
|
>
|
|
337
339
|
<ArrowRight size={14} />
|
|
338
340
|
</button>
|