@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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useQuery } from '@tanstack/react-query'
|
|
2
2
|
import { motion } from 'framer-motion'
|
|
3
|
-
import { Activity, Cpu, Database, HelpCircle, Laptop, RotateCw, Server
|
|
3
|
+
import { Activity, Cpu, Database, HelpCircle, Laptop, RotateCw, Server } from 'lucide-react'
|
|
4
4
|
import { useEffect, useState } from 'react'
|
|
5
5
|
import type { PulseNode } from '../../shared/types'
|
|
6
6
|
import { BunIcon, DenoIcon, GoIcon, NodeIcon, PhpIcon, PythonIcon } from '../components/BrandIcons'
|
|
@@ -81,14 +81,17 @@ function NodeCard({ node }: { node: PulseNode }) {
|
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<motion.div
|
|
84
|
-
initial={{ opacity: 0, scale: 0.
|
|
84
|
+
initial={{ opacity: 0, scale: 0.98 }}
|
|
85
85
|
animate={{ opacity: 1, scale: 1 }}
|
|
86
|
-
className="
|
|
86
|
+
className="card-premium p-5 relative overflow-hidden group border-l-4"
|
|
87
|
+
style={{
|
|
88
|
+
borderLeftColor: isHealthy ? '#10B981' : isWarning ? '#F59E0B' : '#EF4444',
|
|
89
|
+
}}
|
|
87
90
|
>
|
|
88
91
|
{/* Background Pulse Effect */}
|
|
89
92
|
<div
|
|
90
93
|
className={cn(
|
|
91
|
-
'absolute top-0 right-0 w-
|
|
94
|
+
'absolute top-0 right-0 w-32 h-32 bg-gradient-to-bl opacity-5 rounded-bl-full transition-all duration-700',
|
|
92
95
|
isHealthy
|
|
93
96
|
? 'from-emerald-500 to-transparent'
|
|
94
97
|
: isWarning
|
|
@@ -97,27 +100,28 @@ function NodeCard({ node }: { node: PulseNode }) {
|
|
|
97
100
|
)}
|
|
98
101
|
/>
|
|
99
102
|
|
|
100
|
-
<div className="flex items-start justify-between mb-
|
|
101
|
-
<div className="flex items-center gap-
|
|
102
|
-
<div className="w-
|
|
103
|
+
<div className="flex items-start justify-between mb-5 relative z-10">
|
|
104
|
+
<div className="flex items-center gap-4">
|
|
105
|
+
<div className="w-12 h-12 rounded-xl flex items-center justify-center bg-white dark:bg-zinc-800/50 border border-white/10 shadow-xl shrink-0">
|
|
103
106
|
{renderIcon()}
|
|
104
107
|
</div>
|
|
105
108
|
<div>
|
|
106
|
-
<h3 className="font-
|
|
109
|
+
<h3 className="font-black text-foreground text-base flex items-center gap-2 font-heading tracking-tight">
|
|
107
110
|
{node.id}
|
|
108
|
-
<span className="text-[
|
|
111
|
+
<span className="text-[9px] px-2 py-0.5 rounded-md bg-primary/10 text-primary uppercase font-mono border border-primary/20">
|
|
109
112
|
{node.platform}
|
|
110
113
|
</span>
|
|
111
114
|
</h3>
|
|
112
|
-
<div className="text-
|
|
113
|
-
<Laptop size={
|
|
114
|
-
{
|
|
115
|
+
<div className="text-[10px] text-muted-foreground font-bold flex items-center gap-1.5 mt-1 uppercase tracking-wider opacity-60">
|
|
116
|
+
<Laptop size={12} className="opacity-40" /> {node.hostname}{' '}
|
|
117
|
+
<span className="opacity-20">/</span> PID:{' '}
|
|
118
|
+
<span className="font-mono">{node.pid}</span>
|
|
115
119
|
</div>
|
|
116
120
|
</div>
|
|
117
121
|
</div>
|
|
118
122
|
<div
|
|
119
123
|
className={cn(
|
|
120
|
-
'w-
|
|
124
|
+
'w-3 h-3 rounded-full glow-pulse',
|
|
121
125
|
isHealthy
|
|
122
126
|
? 'bg-emerald-500 text-emerald-500'
|
|
123
127
|
: isWarning
|
|
@@ -128,14 +132,14 @@ function NodeCard({ node }: { node: PulseNode }) {
|
|
|
128
132
|
</div>
|
|
129
133
|
|
|
130
134
|
{/* Metrics Grid - Vertical Stack */}
|
|
131
|
-
<div className="space-y-
|
|
135
|
+
<div className="space-y-4 font-mono">
|
|
132
136
|
{/* Laravel Specific Tools (if detected) */}
|
|
133
137
|
{laravel && laravel.workerCount > 0 && (
|
|
134
|
-
<div className="bg-amber-500/5 rounded-
|
|
135
|
-
<div className="flex items-center justify-between text-
|
|
136
|
-
<div className="flex items-center gap-2 font-
|
|
137
|
-
<PhpIcon className="w-
|
|
138
|
-
Laravel
|
|
138
|
+
<div className="bg-amber-500/5 rounded-xl p-3 border border-amber-500/10">
|
|
139
|
+
<div className="flex items-center justify-between text-[10px] mb-3">
|
|
140
|
+
<div className="flex items-center gap-2 font-black text-amber-500 uppercase tracking-widest">
|
|
141
|
+
<PhpIcon className="w-4 h-4" />
|
|
142
|
+
Laravel Ecosystem ({laravel.workerCount})
|
|
139
143
|
</div>
|
|
140
144
|
</div>
|
|
141
145
|
<div className="flex flex-wrap gap-2">
|
|
@@ -148,9 +152,9 @@ function NodeCard({ node }: { node: PulseNode }) {
|
|
|
148
152
|
sendCommand(node.service, node.id, 'LARAVEL_ACTION', 'default', 'retry-all')
|
|
149
153
|
}
|
|
150
154
|
}}
|
|
151
|
-
className="bg-amber-500/10 hover:bg-amber-500/20 text-amber-
|
|
155
|
+
className="bg-amber-500/10 hover:bg-amber-500/20 text-amber-500 px-3 py-1.5 rounded-lg text-[9px] font-black uppercase flex items-center gap-2 transition-all border border-amber-500/20"
|
|
152
156
|
>
|
|
153
|
-
<RotateCw size={
|
|
157
|
+
<RotateCw size={12} /> Retry All
|
|
154
158
|
</button>
|
|
155
159
|
<button
|
|
156
160
|
type="button"
|
|
@@ -163,207 +167,124 @@ function NodeCard({ node }: { node: PulseNode }) {
|
|
|
163
167
|
sendCommand(node.service, node.id, 'LARAVEL_ACTION', 'default', 'restart')
|
|
164
168
|
}
|
|
165
169
|
}}
|
|
166
|
-
className="bg-
|
|
170
|
+
className="bg-white/5 hover:bg-white/10 text-foreground/80 px-3 py-1.5 rounded-lg text-[9px] font-black uppercase flex items-center gap-2 transition-all border border-white/5"
|
|
167
171
|
>
|
|
168
|
-
<RotateCw size={
|
|
172
|
+
<RotateCw size={12} /> Restart
|
|
169
173
|
</button>
|
|
170
174
|
</div>
|
|
171
|
-
{laravel.roots?.length > 0 && (
|
|
172
|
-
<div className="mt-2 text-[9px] text-muted-foreground font-mono opacity-60 truncate">
|
|
173
|
-
Root: {laravel.roots[0]}
|
|
174
|
-
</div>
|
|
175
|
-
)}
|
|
176
175
|
</div>
|
|
177
176
|
)}
|
|
178
177
|
|
|
179
178
|
{/* Queues Section (if present) */}
|
|
180
179
|
{node.queues && node.queues.length > 0 && (
|
|
181
|
-
<div className="bg-
|
|
182
|
-
<div className="flex items-center justify-between text-
|
|
183
|
-
<div className="flex items-center gap-2
|
|
184
|
-
<
|
|
185
|
-
|
|
186
|
-
'w-1.5 h-1.5 rounded-full',
|
|
187
|
-
node.queues.some((q) => q.size.failed > 0)
|
|
188
|
-
? 'bg-red-500 animate-pulse'
|
|
189
|
-
: 'bg-emerald-500'
|
|
190
|
-
)}
|
|
191
|
-
/>
|
|
192
|
-
Queues ({node.queues.length})
|
|
180
|
+
<div className="bg-zinc-900/50 rounded-xl p-3 border border-white/5">
|
|
181
|
+
<div className="flex items-center justify-between text-[9px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 mb-3">
|
|
182
|
+
<div className="flex items-center gap-2">
|
|
183
|
+
<Database size={12} />
|
|
184
|
+
Monitored Pipelines
|
|
193
185
|
</div>
|
|
186
|
+
<span className="bg-white/5 px-1.5 rounded">{node.queues.length} ACTIVE</span>
|
|
194
187
|
</div>
|
|
195
|
-
<div className="space-y-
|
|
188
|
+
<div className="space-y-3">
|
|
196
189
|
{node.queues.map((q) => (
|
|
197
|
-
<div key={q.name} className="flex flex-col gap-
|
|
198
|
-
<div className="flex justify-between items-center">
|
|
199
|
-
<span className="font-
|
|
200
|
-
<div className="flex gap-
|
|
190
|
+
<div key={q.name} className="flex flex-col gap-2">
|
|
191
|
+
<div className="flex justify-between items-center text-[11px]">
|
|
192
|
+
<span className="font-bold text-foreground/80 tracking-tighter">{q.name}</span>
|
|
193
|
+
<div className="flex gap-3 items-center">
|
|
201
194
|
{q.size.failed > 0 && (
|
|
202
|
-
|
|
203
|
-
<span className="text-red-500 font-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
await sendCommand(
|
|
213
|
-
node.service,
|
|
214
|
-
node.id,
|
|
215
|
-
'LARAVEL_ACTION',
|
|
216
|
-
q.name,
|
|
217
|
-
'retry-all'
|
|
218
|
-
)
|
|
219
|
-
return
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
sendCommand(node.service, node.id, 'RETRY_JOB', q.name)
|
|
223
|
-
}}
|
|
224
|
-
className="p-1 rounded hover:bg-emerald-500/20 text-emerald-500 transition-colors"
|
|
225
|
-
title="Retry all failed jobs"
|
|
226
|
-
>
|
|
227
|
-
<RotateCw size={12} />
|
|
228
|
-
</button>
|
|
229
|
-
<button
|
|
230
|
-
type="button"
|
|
231
|
-
onClick={() =>
|
|
232
|
-
sendCommand(node.service, node.id, 'DELETE_JOB', q.name)
|
|
233
|
-
}
|
|
234
|
-
className="p-1 rounded hover:bg-red-500/20 text-red-400 transition-colors"
|
|
235
|
-
title="Delete all failed jobs"
|
|
236
|
-
>
|
|
237
|
-
<Trash2 size={12} />
|
|
238
|
-
</button>
|
|
239
|
-
</div>
|
|
240
|
-
</>
|
|
241
|
-
)}
|
|
242
|
-
{q.size.active > 0 && (
|
|
243
|
-
<span className="text-emerald-500">{q.size.active} act</span>
|
|
195
|
+
<div className="flex items-center gap-1">
|
|
196
|
+
<span className="text-red-500 font-black">{q.size.failed} FAIL</span>
|
|
197
|
+
<button
|
|
198
|
+
type="button"
|
|
199
|
+
onClick={() => sendCommand(node.service, node.id, 'RETRY_JOB', q.name)}
|
|
200
|
+
className="p-1 rounded bg-red-500/10 text-red-500 hover:bg-red-500 hover:text-white transition-all"
|
|
201
|
+
>
|
|
202
|
+
<RotateCw size={10} />
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
244
205
|
)}
|
|
245
|
-
<span
|
|
246
|
-
className={cn(
|
|
247
|
-
q.size.waiting > 100 ? 'text-yellow-500' : 'text-muted-foreground'
|
|
248
|
-
)}
|
|
249
|
-
>
|
|
250
|
-
{q.size.waiting} wait
|
|
251
|
-
</span>
|
|
206
|
+
<span className="text-muted-foreground/60">{q.size.waiting} WAIT</span>
|
|
252
207
|
</div>
|
|
253
208
|
</div>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
/>
|
|
269
|
-
<div
|
|
270
|
-
className="bg-emerald-500 h-full transition-all"
|
|
271
|
-
style={{
|
|
272
|
-
width: `${(q.size.active / (q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
|
|
273
|
-
}}
|
|
274
|
-
/>
|
|
275
|
-
</div>
|
|
276
|
-
)}
|
|
209
|
+
<div className="h-1.5 w-full bg-black/40 rounded-full overflow-hidden flex border border-white/5">
|
|
210
|
+
<div
|
|
211
|
+
className="bg-red-500 h-full transition-all"
|
|
212
|
+
style={{
|
|
213
|
+
width: `${(q.size.failed / Math.max(1, q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
|
|
214
|
+
}}
|
|
215
|
+
/>
|
|
216
|
+
<div
|
|
217
|
+
className="bg-emerald-500 h-full transition-all shadow-[0_0_10px_#10B981]"
|
|
218
|
+
style={{
|
|
219
|
+
width: `${(q.size.active / Math.max(1, q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
|
|
220
|
+
}}
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
277
223
|
</div>
|
|
278
224
|
))}
|
|
279
225
|
</div>
|
|
280
226
|
</div>
|
|
281
227
|
)}
|
|
282
228
|
|
|
283
|
-
{/*
|
|
284
|
-
<div className="
|
|
285
|
-
<div className="
|
|
286
|
-
<div className="flex items-center
|
|
287
|
-
<
|
|
229
|
+
{/* System Load */}
|
|
230
|
+
<div className="grid grid-cols-2 gap-3">
|
|
231
|
+
<div className="bg-zinc-900/50 rounded-xl p-3 border border-white/5 flex flex-col justify-between">
|
|
232
|
+
<div className="flex justify-between items-center text-[9px] font-black uppercase text-muted-foreground/40 mb-2">
|
|
233
|
+
<span className="flex items-center gap-1.5">
|
|
234
|
+
<Cpu size={10} /> CPU
|
|
235
|
+
</span>
|
|
236
|
+
<span>{node.cpu.cores}C</span>
|
|
288
237
|
</div>
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
{/* System Load Background (Darker) */}
|
|
306
|
-
<div
|
|
307
|
-
className="h-full rounded-full transition-all duration-500 absolute top-0 left-0 bg-muted-foreground/30 z-10"
|
|
308
|
-
style={{ width: `${Math.min(node.cpu.system, 100)}%` }}
|
|
309
|
-
/>
|
|
310
|
-
</div>
|
|
311
|
-
<div className="flex justify-between text-[9px] text-muted-foreground mt-1 font-mono">
|
|
312
|
-
<span className="text-primary font-bold">Proc: {node.cpu.process}%</span>
|
|
313
|
-
<span>Sys: {node.cpu.system}%</span>
|
|
314
|
-
</div>
|
|
315
|
-
</div>
|
|
316
|
-
|
|
317
|
-
{/* Memory */}
|
|
318
|
-
<div className="bg-muted/30 rounded-lg p-2.5 border border-border/50">
|
|
319
|
-
<div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
|
|
320
|
-
<div className="flex items-center gap-2">
|
|
321
|
-
<Database size={12} /> RAM Usage
|
|
238
|
+
<div className="flex items-baseline gap-1">
|
|
239
|
+
<span className="text-2xl font-black text-primary tracking-tighter">
|
|
240
|
+
{node.cpu.process.toFixed(0)}%
|
|
241
|
+
</span>
|
|
242
|
+
<span className="text-[10px] font-bold opacity-40 uppercase">Load</span>
|
|
243
|
+
</div>
|
|
244
|
+
<div className="h-1 w-full bg-black/40 rounded-full mt-3 overflow-hidden">
|
|
245
|
+
<div
|
|
246
|
+
className={cn(
|
|
247
|
+
'h-full transition-all duration-1000',
|
|
248
|
+
node.cpu.process > 80
|
|
249
|
+
? 'bg-red-500 shadow-[0_0_10px_#EF4444]'
|
|
250
|
+
: 'bg-primary shadow-[0_0_10px_#00F0FF]'
|
|
251
|
+
)}
|
|
252
|
+
style={{ width: `${node.cpu.process}%` }}
|
|
253
|
+
/>
|
|
322
254
|
</div>
|
|
323
|
-
</div>
|
|
324
|
-
<div className="flex items-baseline gap-1">
|
|
325
|
-
<span className="text-xl font-bold text-foreground">
|
|
326
|
-
{formatBytes(node.memory.process.rss)}
|
|
327
|
-
</span>
|
|
328
|
-
<span className="text-xs text-muted-foreground ml-1">RSS</span>
|
|
329
|
-
</div>
|
|
330
|
-
|
|
331
|
-
{/* RAM Bar */}
|
|
332
|
-
<div className="h-2 w-full bg-muted rounded-full mt-2 overflow-hidden relative">
|
|
333
|
-
{/* Process Usage */}
|
|
334
|
-
<div
|
|
335
|
-
className="h-full rounded-full transition-all duration-500 absolute top-0 left-0 z-20 shadow-sm bg-indigo-500"
|
|
336
|
-
style={{
|
|
337
|
-
width: `${Math.min((node.memory.process.rss / node.memory.system.total) * 100, 100)}%`,
|
|
338
|
-
}}
|
|
339
|
-
/>
|
|
340
|
-
{/* System Usage */}
|
|
341
|
-
<div
|
|
342
|
-
className="h-full rounded-full transition-all duration-500 absolute top-0 left-0 bg-muted-foreground/30 z-10"
|
|
343
|
-
style={{
|
|
344
|
-
width: `${Math.min((node.memory.system.used / node.memory.system.total) * 100, 100)}%`,
|
|
345
|
-
}}
|
|
346
|
-
/>
|
|
347
255
|
</div>
|
|
348
256
|
|
|
349
|
-
<div className="
|
|
350
|
-
<div className="flex
|
|
351
|
-
<span className="
|
|
352
|
-
|
|
257
|
+
<div className="bg-zinc-900/50 rounded-xl p-3 border border-white/5 flex flex-col justify-between">
|
|
258
|
+
<div className="flex justify-between items-center text-[9px] font-black uppercase text-muted-foreground/40 mb-2">
|
|
259
|
+
<span className="flex items-center gap-1.5">
|
|
260
|
+
<Database size={10} /> RAM
|
|
261
|
+
</span>
|
|
262
|
+
<span>RSS</span>
|
|
263
|
+
</div>
|
|
264
|
+
<div className="flex items-baseline gap-1">
|
|
265
|
+
<span className="text-2xl font-black text-white tracking-tighter">
|
|
266
|
+
{formatBytes(node.memory.process.rss).split(' ')[0]}
|
|
267
|
+
</span>
|
|
268
|
+
<span className="text-[10px] font-bold opacity-40 uppercase">
|
|
269
|
+
{formatBytes(node.memory.process.rss).split(' ')[1]}
|
|
270
|
+
</span>
|
|
353
271
|
</div>
|
|
354
|
-
<div className="
|
|
355
|
-
<
|
|
356
|
-
|
|
272
|
+
<div className="h-1 w-full bg-black/40 rounded-full mt-3 overflow-hidden">
|
|
273
|
+
<div
|
|
274
|
+
className="bg-indigo-500 h-full transition-all duration-1000 shadow-[0_0_10px_#6366F1]"
|
|
275
|
+
style={{ width: `${(node.memory.process.rss / node.memory.system.total) * 100}%` }}
|
|
276
|
+
/>
|
|
357
277
|
</div>
|
|
358
278
|
</div>
|
|
359
279
|
</div>
|
|
360
280
|
</div>
|
|
361
281
|
|
|
362
|
-
<div className="mt-
|
|
363
|
-
<span className="flex items-center gap-
|
|
364
|
-
<Server size={
|
|
282
|
+
<div className="mt-5 pt-4 border-t border-white/5 flex items-center justify-between text-[10px] font-black uppercase tracking-widest text-muted-foreground/40">
|
|
283
|
+
<span className="flex items-center gap-2">
|
|
284
|
+
<Server size={12} className="text-primary/40" />
|
|
285
|
+
{node.runtime.framework} <span className="opacity-20">•</span> v{node.version}
|
|
365
286
|
</span>
|
|
366
|
-
<span>
|
|
287
|
+
<span className="font-mono tabular-nums">UP: {Math.floor(node.runtime.uptime / 60)}M</span>
|
|
367
288
|
</div>
|
|
368
289
|
</motion.div>
|
|
369
290
|
)
|