@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +77 -22
  3. package/README.zh-TW.md +88 -0
  4. package/dist/bin.js +64681 -15842
  5. package/dist/client/assets/index-C80c1frR.css +1 -0
  6. package/dist/client/assets/index-CrWem9u3.js +434 -0
  7. package/dist/server/index.js +64681 -15842
  8. package/package.json +9 -7
  9. package/postcss.config.js +4 -4
  10. package/src/client/Layout.tsx +36 -39
  11. package/src/client/Sidebar.tsx +7 -7
  12. package/src/client/ThroughputChart.tsx +31 -17
  13. package/src/client/WorkerStatus.tsx +56 -80
  14. package/src/client/components/ConfirmDialog.tsx +22 -14
  15. package/src/client/components/JobInspector.tsx +95 -162
  16. package/src/client/index.css +29 -31
  17. package/src/client/pages/LoginPage.tsx +33 -31
  18. package/src/client/pages/MetricsPage.tsx +65 -37
  19. package/src/client/pages/OverviewPage.tsx +30 -28
  20. package/src/client/pages/PulsePage.tsx +111 -190
  21. package/src/client/pages/QueuesPage.tsx +82 -83
  22. package/src/client/pages/SchedulesPage.tsx +56 -61
  23. package/src/client/pages/SettingsPage.tsx +118 -137
  24. package/src/client/pages/WorkersPage.tsx +101 -115
  25. package/src/server/services/CommandService.ts +8 -9
  26. package/src/server/services/PulseService.ts +61 -4
  27. package/src/server/services/QueueService.ts +293 -0
  28. package/src/shared/types.ts +38 -13
  29. package/tailwind.config.js +75 -68
  30. package/tsconfig.json +28 -37
  31. package/tsconfig.node.json +9 -11
  32. package/dist/client/assets/index-BSMp8oq_.js +0 -436
  33. package/dist/client/assets/index-BwxlHx-_.css +0 -1
  34. package/dist/client/index.html +0 -13
  35. package/src/client/index.html +0 -12
  36. /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, Trash2 } from 'lucide-react'
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.95 }}
84
+ initial={{ opacity: 0, scale: 0.98 }}
85
85
  animate={{ opacity: 1, scale: 1 }}
86
- className="bg-card border border-border/50 rounded-xl p-4 shadow-sm relative overflow-hidden group"
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-24 h-24 bg-gradient-to-bl opacity-10 rounded-bl-full transition-all duration-500',
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-4 relative z-10">
101
- <div className="flex items-center gap-3">
102
- <div className="w-10 h-10 rounded-lg flex items-center justify-center bg-white dark:bg-card border border-border/20 shadow-sm shrink-0">
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-bold text-foreground text-sm flex items-center gap-2">
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-[10px] px-1.5 py-0.5 rounded-full bg-muted text-muted-foreground uppercase tracking-wider">
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-xs text-muted-foreground flex items-center gap-1 mt-0.5">
113
- <Laptop size={10} /> {node.hostname} <span className="opacity-30">|</span> PID:{' '}
114
- {node.pid}
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-2.5 h-2.5 rounded-full shadow-[0_0_10px_currentColor]',
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-3">
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-lg p-2.5 border border-amber-500/20">
135
- <div className="flex items-center justify-between text-xs mb-2">
136
- <div className="flex items-center gap-2 font-bold text-amber-600 dark:text-amber-400">
137
- <PhpIcon className="w-3 h-3" />
138
- Laravel Workers ({laravel.workerCount})
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-600 dark:text-amber-400 px-2 py-1 rounded text-[10px] font-black uppercase flex items-center gap-1 transition-all border border-amber-500/10"
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={10} /> Retry All
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-amber-500/10 hover:bg-amber-500/20 text-amber-600 dark:text-amber-400 px-2 py-1 rounded text-[10px] font-black uppercase flex items-center gap-1 transition-all border border-amber-500/10"
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={10} /> Restart Workers
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-muted/30 rounded-lg p-2.5 border border-border/50">
182
- <div className="flex items-center justify-between text-xs text-muted-foreground mb-2">
183
- <div className="flex items-center gap-2 font-bold text-foreground">
184
- <span
185
- className={cn(
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-2">
188
+ <div className="space-y-3">
196
189
  {node.queues.map((q) => (
197
- <div key={q.name} className="flex flex-col gap-1 text-xs">
198
- <div className="flex justify-between items-center">
199
- <span className="font-mono text-muted-foreground">{q.name}</span>
200
- <div className="flex gap-2 font-mono items-center">
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-bold">{q.size.failed} fail</span>
204
- {/* Action Buttons for Failed Jobs */}
205
- <div className="flex gap-1 ml-1">
206
- <button
207
- type="button"
208
- onClick={async () => {
209
- if (q.driver === 'redis' && node.language === 'php') {
210
- // For PHP + Redis, provide option to use artisan
211
- if (confirm('Use php artisan queue:retry for precision?')) {
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
- {/* Mini Progress bar for Queue Health (Failed vs Total) */}
255
- {q.size.waiting + q.size.active + q.size.failed > 0 && (
256
- <div className="h-1 w-full bg-muted rounded-full overflow-hidden flex">
257
- <div
258
- className="bg-red-500 h-full transition-all"
259
- style={{
260
- width: `${(q.size.failed / (q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
261
- }}
262
- />
263
- <div
264
- className="bg-yellow-500 h-full transition-all"
265
- style={{
266
- width: `${(q.size.waiting / (q.size.waiting + q.size.active + q.size.failed)) * 100}%`,
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
- {/* CPU */}
284
- <div className="bg-muted/30 rounded-lg p-2.5 border border-border/50">
285
- <div className="flex items-center justify-between text-xs text-muted-foreground mb-1">
286
- <div className="flex items-center gap-2">
287
- <Cpu size={12} /> CPU Usage
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
- <span className="text-[10px]">{node.cpu.cores} cores</span>
290
- </div>
291
- <div className="flex items-baseline gap-1">
292
- <span className="text-xl font-bold text-foreground">{node.cpu.process}%</span>
293
- <span className="text-xs text-muted-foreground ml-1">proc</span>
294
- </div>
295
- {/* Mini Bar */}
296
- <div className="h-2 w-full bg-muted rounded-full mt-2 overflow-hidden relative">
297
- {/* Process Usage */}
298
- <div
299
- className={cn(
300
- 'h-full rounded-full transition-all duration-500 absolute top-0 left-0 z-20 shadow-sm',
301
- node.cpu.process > 80 ? 'bg-red-500' : 'bg-primary'
302
- )}
303
- style={{ width: `${Math.min(node.cpu.process, 100)}%` }}
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="grid grid-cols-2 gap-2 text-[10px] text-muted-foreground mt-3 border-t border-border/30 pt-2 font-mono">
350
- <div className="flex flex-col">
351
- <span className="opacity-70">Heap</span>
352
- <span className="font-bold">{formatBytes(node.memory.process.heapUsed)}</span>
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="flex flex-col text-right">
355
- <span className="opacity-70">Sys Free</span>
356
- <span className="">{formatBytes(node.memory.system.free)}</span>
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-3 pt-3 border-t border-border/50 flex items-center justify-between text-[10px] text-muted-foreground">
363
- <span className="flex items-center gap-1.5">
364
- <Server size={10} /> {node.runtime.framework}
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>Ups: {Math.floor(node.runtime.uptime / 60)}m</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
  )