@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, useQueryClient } from '@tanstack/react-query'
|
|
2
2
|
import { motion } from 'framer-motion'
|
|
3
|
-
import { AlertCircle,
|
|
3
|
+
import { AlertCircle, CheckCircle2, Clock, RefreshCcw, Search } from 'lucide-react'
|
|
4
4
|
import React from 'react'
|
|
5
5
|
import { createPortal } from 'react-dom'
|
|
6
6
|
import { cn } from '../utils'
|
|
@@ -48,7 +48,6 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
48
48
|
const [view, setView] = React.useState<'waiting' | 'delayed' | 'failed' | 'archive'>('waiting')
|
|
49
49
|
const [page, setPage] = React.useState(1)
|
|
50
50
|
const [selectedIndices, setSelectedIndices] = React.useState<Set<number>>(new Set())
|
|
51
|
-
const [totalCount, setTotalCount] = React.useState<number>(0)
|
|
52
51
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
|
53
52
|
const [confirmDialog, setConfirmDialog] = React.useState<{
|
|
54
53
|
open: boolean
|
|
@@ -71,18 +70,6 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
71
70
|
},
|
|
72
71
|
})
|
|
73
72
|
|
|
74
|
-
// Fetch total count for non-archive views
|
|
75
|
-
React.useEffect(() => {
|
|
76
|
-
if (view !== 'archive') {
|
|
77
|
-
fetch(`/api/queues/${queueName}/jobs/count?type=${view}`)
|
|
78
|
-
.then((res) => res.json())
|
|
79
|
-
.then((data) => setTotalCount(data.count))
|
|
80
|
-
.catch(() => setTotalCount(0))
|
|
81
|
-
} else {
|
|
82
|
-
setTotalCount(data?.total || 0)
|
|
83
|
-
}
|
|
84
|
-
}, [queueName, view, data?.total])
|
|
85
|
-
|
|
86
73
|
// Reset selection when view changes
|
|
87
74
|
// biome-ignore lint/correctness/useExhaustiveDependencies: We want to reset when view changes
|
|
88
75
|
React.useEffect(() => {
|
|
@@ -213,39 +200,6 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
213
200
|
})
|
|
214
201
|
}
|
|
215
202
|
|
|
216
|
-
const handleBulkActionAll = async (action: 'delete' | 'retry') => {
|
|
217
|
-
if (view === 'archive') {
|
|
218
|
-
return
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
setConfirmDialog({
|
|
222
|
-
open: true,
|
|
223
|
-
title: `${action === 'delete' ? 'Delete' : 'Retry'} ALL ${totalCount} Jobs?`,
|
|
224
|
-
message: `⚠️ WARNING: This will ${action} ALL ${totalCount} ${view} jobs in "${queueName}".\n\nThis is a destructive operation that cannot be undone.`,
|
|
225
|
-
variant: 'danger',
|
|
226
|
-
action: async () => {
|
|
227
|
-
setIsProcessing(true)
|
|
228
|
-
try {
|
|
229
|
-
const endpoint = action === 'delete' ? 'bulk-delete-all' : 'bulk-retry-all'
|
|
230
|
-
await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
|
|
231
|
-
method: 'POST',
|
|
232
|
-
headers: { 'Content-Type': 'application/json' },
|
|
233
|
-
body: JSON.stringify({ type: view }),
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
setSelectedIndices(new Set())
|
|
237
|
-
queryClient.invalidateQueries({ queryKey: ['jobs', queueName] })
|
|
238
|
-
queryClient.invalidateQueries({ queryKey: ['queues'] })
|
|
239
|
-
setConfirmDialog(null)
|
|
240
|
-
} catch (err) {
|
|
241
|
-
console.error(`Failed to ${action} all jobs:`, err)
|
|
242
|
-
} finally {
|
|
243
|
-
setIsProcessing(false)
|
|
244
|
-
}
|
|
245
|
-
},
|
|
246
|
-
})
|
|
247
|
-
}
|
|
248
|
-
|
|
249
203
|
return createPortal(
|
|
250
204
|
<div className="fixed inset-0 z-[1001] flex items-center justify-end p-4 sm:p-6 outline-none pointer-events-none">
|
|
251
205
|
<motion.div
|
|
@@ -261,32 +215,32 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
261
215
|
animate={{ x: 0, opacity: 1 }}
|
|
262
216
|
exit={{ x: '100%', opacity: 0 }}
|
|
263
217
|
transition={{ type: 'spring', damping: 25, stiffness: 200 }}
|
|
264
|
-
className="bg-
|
|
218
|
+
className="bg-zinc-950 border-l border-white/10 h-screen w-full max-w-2xl shadow-2xl flex flex-col overflow-hidden relative z-[1002] pointer-events-auto"
|
|
265
219
|
onClick={(e) => e.stopPropagation()}
|
|
266
220
|
>
|
|
267
|
-
<div className="p-
|
|
221
|
+
<div className="p-8 border-b border-white/5 bg-black/40 flex justify-between items-center flex-shrink-0">
|
|
268
222
|
<div>
|
|
269
|
-
<h2 className="text-
|
|
270
|
-
<Search className="text-primary" size={
|
|
271
|
-
|
|
223
|
+
<h2 className="text-2xl font-black flex items-center gap-3 font-heading tracking-tight italic uppercase">
|
|
224
|
+
<Search className="text-primary" size={24} />
|
|
225
|
+
Inspector <span className="text-primary/60">/</span> {queueName}
|
|
272
226
|
</h2>
|
|
273
|
-
<div className="flex items-center gap-
|
|
227
|
+
<div className="flex items-center gap-3 mt-4">
|
|
274
228
|
{(['waiting', 'delayed', 'failed', 'archive'] as const).map((v) => (
|
|
275
229
|
<button
|
|
276
230
|
type="button"
|
|
277
231
|
key={v}
|
|
278
232
|
onClick={() => setView(v)}
|
|
279
233
|
className={cn(
|
|
280
|
-
'text-[9px] font-black px-3 py-1 rounded-
|
|
234
|
+
'text-[9px] font-black px-3 py-1.5 rounded-lg transition-all border shrink-0 uppercase tracking-[0.2em] font-mono',
|
|
281
235
|
view === v
|
|
282
236
|
? v === 'failed'
|
|
283
|
-
? 'bg-red-500 text-
|
|
237
|
+
? 'bg-red-500 text-black border-red-500 shadow-[0_0_20px_rgba(239,68,68,0.2)]'
|
|
284
238
|
: v === 'delayed'
|
|
285
|
-
? 'bg-amber-500 text-
|
|
239
|
+
? 'bg-amber-500 text-black border-amber-500 shadow-[0_0_20px_rgba(245,158,11,0.2)]'
|
|
286
240
|
: v === 'archive'
|
|
287
|
-
? 'bg-indigo-500 text-
|
|
288
|
-
: 'bg-primary text-
|
|
289
|
-
: 'bg-
|
|
241
|
+
? 'bg-indigo-500 text-black border-indigo-500 shadow-[0_0_20px_rgba(99,102,241,0.2)]'
|
|
242
|
+
: 'bg-primary text-black border-primary shadow-[0_0_20px_rgba(0,240,255,0.2)]'
|
|
243
|
+
: 'bg-zinc-900 text-muted-foreground border-white/5 hover:bg-zinc-800'
|
|
290
244
|
)}
|
|
291
245
|
>
|
|
292
246
|
{v}
|
|
@@ -297,158 +251,135 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
297
251
|
<button
|
|
298
252
|
type="button"
|
|
299
253
|
onClick={onClose}
|
|
300
|
-
|
|
254
|
+
aria-label="Close"
|
|
255
|
+
className="w-12 h-12 rounded-2xl bg-white/5 border border-white/5 hover:bg-white/10 flex items-center justify-center transition-all text-white/40 hover:text-white"
|
|
301
256
|
>
|
|
302
257
|
✕
|
|
303
258
|
</button>
|
|
304
259
|
</div>
|
|
305
260
|
|
|
306
|
-
<div className="flex-1 overflow-y-auto bg-
|
|
261
|
+
<div className="flex-1 overflow-y-auto bg-black/20 min-h-0 scrollbar-thin">
|
|
307
262
|
{isPending && (
|
|
308
|
-
<div className="p-
|
|
309
|
-
|
|
263
|
+
<div className="p-20 text-center flex flex-col items-center gap-4">
|
|
264
|
+
<RefreshCcw className="animate-spin text-primary opacity-40" size={32} />
|
|
265
|
+
<p className="text-[10px] font-black uppercase tracking-[0.3em] text-muted-foreground animate-pulse">
|
|
266
|
+
Syncing jobs...
|
|
267
|
+
</p>
|
|
310
268
|
</div>
|
|
311
269
|
)}
|
|
312
270
|
{error && (
|
|
313
|
-
<div className="p-
|
|
271
|
+
<div className="p-20 text-center">
|
|
272
|
+
<div className="bg-red-500/10 text-red-500 p-8 rounded-2xl border border-red-500/20 font-black uppercase text-xs tracking-widest italic">
|
|
273
|
+
Connection Fault: {error.message}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
314
276
|
)}
|
|
315
277
|
|
|
316
278
|
{data?.jobs && data.jobs.length > 0 && (
|
|
317
|
-
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
<span className="text-[
|
|
334
|
-
{
|
|
279
|
+
<div className="px-8 py-4 border-b border-white/5 bg-white/[0.02] flex items-center gap-4">
|
|
280
|
+
<input
|
|
281
|
+
type="checkbox"
|
|
282
|
+
aria-label="Select all jobs"
|
|
283
|
+
className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
|
|
284
|
+
checked={
|
|
285
|
+
selectedIndices.size === data.jobs.filter((j) => j._raw && !j._archived).length &&
|
|
286
|
+
selectedIndices.size > 0
|
|
287
|
+
}
|
|
288
|
+
onChange={toggleSelectAll}
|
|
289
|
+
/>
|
|
290
|
+
<span className="text-[10px] font-black uppercase tracking-[0.2em] text-muted-foreground/60 font-heading">
|
|
291
|
+
Batch Operations
|
|
292
|
+
</span>
|
|
293
|
+
{selectedIndices.size > 0 && (
|
|
294
|
+
<div className="ml-auto flex items-center gap-3">
|
|
295
|
+
<span className="text-[10px] font-black uppercase text-primary font-mono bg-primary/10 px-2 py-0.5 rounded border border-primary/20">
|
|
296
|
+
{selectedIndices.size} Selected
|
|
335
297
|
</span>
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
onClick={() => handleBulkAction('delete')}
|
|
345
|
-
className="px-3 py-1 bg-red-500/10 text-red-500 rounded-md text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all"
|
|
346
|
-
>
|
|
347
|
-
Delete Selected
|
|
348
|
-
</button>
|
|
349
|
-
{(view === 'delayed' || view === 'failed') && (
|
|
350
|
-
<button
|
|
351
|
-
type="button"
|
|
352
|
-
onClick={() => handleBulkAction('retry')}
|
|
353
|
-
className="px-3 py-1 bg-primary/10 text-primary rounded-md text-[10px] font-black uppercase hover:bg-primary hover:text-primary-foreground transition-all"
|
|
354
|
-
>
|
|
355
|
-
Retry Selected
|
|
356
|
-
</button>
|
|
357
|
-
)}
|
|
358
|
-
</div>
|
|
359
|
-
)}
|
|
360
|
-
</div>
|
|
361
|
-
{view !== 'archive' && totalCount > data.jobs.filter((j) => !j._archived).length && (
|
|
362
|
-
<div className="px-6 py-3 border-b bg-amber-500/5 flex items-center justify-between">
|
|
363
|
-
<span className="text-xs font-bold text-amber-600 flex items-center gap-2">
|
|
364
|
-
<AlertCircle size={14} />
|
|
365
|
-
Showing {data.jobs.filter((j) => !j._archived).length} of {totalCount} total{' '}
|
|
366
|
-
{view} jobs
|
|
367
|
-
</span>
|
|
368
|
-
<div className="flex items-center gap-2">
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
onClick={() => handleBulkAction('delete')}
|
|
301
|
+
className="px-3 py-1.5 bg-red-500/10 text-red-500 rounded-lg text-[10px] font-black uppercase hover:bg-red-500 hover:text-white transition-all border border-red-500/20"
|
|
302
|
+
>
|
|
303
|
+
Delete
|
|
304
|
+
</button>
|
|
305
|
+
{(view === 'delayed' || view === 'failed') && (
|
|
369
306
|
<button
|
|
370
307
|
type="button"
|
|
371
|
-
onClick={() =>
|
|
372
|
-
className="px-3 py-1.5 bg-
|
|
308
|
+
onClick={() => handleBulkAction('retry')}
|
|
309
|
+
className="px-3 py-1.5 bg-primary/10 text-primary rounded-lg text-[10px] font-black uppercase hover:bg-primary hover:text-black transition-all border border-primary/20"
|
|
373
310
|
>
|
|
374
|
-
|
|
311
|
+
Retry
|
|
375
312
|
</button>
|
|
376
|
-
|
|
377
|
-
<button
|
|
378
|
-
type="button"
|
|
379
|
-
onClick={() => handleBulkActionAll('retry')}
|
|
380
|
-
className="px-3 py-1.5 bg-amber-500/10 text-amber-600 rounded-md text-[10px] font-black uppercase hover:bg-amber-500 hover:text-white transition-all"
|
|
381
|
-
>
|
|
382
|
-
Retry All {totalCount}
|
|
383
|
-
</button>
|
|
384
|
-
)}
|
|
385
|
-
</div>
|
|
313
|
+
)}
|
|
386
314
|
</div>
|
|
387
315
|
)}
|
|
388
|
-
|
|
316
|
+
</div>
|
|
389
317
|
)}
|
|
390
318
|
|
|
391
319
|
{data?.jobs && data.jobs.length === 0 && (
|
|
392
|
-
<div className="p-
|
|
393
|
-
<div className="w-
|
|
394
|
-
<CheckCircle2 size={
|
|
320
|
+
<div className="p-20 text-center text-muted-foreground flex flex-col items-center gap-6 opacity-40">
|
|
321
|
+
<div className="w-20 h-20 bg-white/5 rounded-full flex items-center justify-center text-primary/40 border border-white/5">
|
|
322
|
+
<CheckCircle2 size={40} />
|
|
323
|
+
</div>
|
|
324
|
+
<div className="space-y-2">
|
|
325
|
+
<p className="text-xl font-black font-heading uppercase italic tracking-widest">
|
|
326
|
+
Pipeline Clear
|
|
327
|
+
</p>
|
|
328
|
+
<p className="text-[10px] font-black uppercase tracking-[0.2em]">
|
|
329
|
+
Zero incidents detected in spectrum
|
|
330
|
+
</p>
|
|
395
331
|
</div>
|
|
396
|
-
<p className="text-lg font-bold">Clear Sky!</p>
|
|
397
|
-
<p className="text-sm opacity-60">No jobs found in this queue.</p>
|
|
398
332
|
</div>
|
|
399
333
|
)}
|
|
334
|
+
|
|
400
335
|
{data?.jobs && (
|
|
401
|
-
<div className="p-
|
|
336
|
+
<div className="p-8 space-y-6">
|
|
402
337
|
{data.jobs.map((job, i) => (
|
|
403
338
|
<div
|
|
404
339
|
key={i}
|
|
405
340
|
className={cn(
|
|
406
|
-
'bg-
|
|
407
|
-
selectedIndices.has(i) && 'ring-2 ring-primary border-primary'
|
|
341
|
+
'bg-zinc-900/40 border rounded-2xl overflow-hidden transition-all group border-white/5',
|
|
342
|
+
selectedIndices.has(i) && 'ring-2 ring-primary border-primary bg-primary/5'
|
|
408
343
|
)}
|
|
409
344
|
>
|
|
410
|
-
<div className="p-4 border-b bg-
|
|
411
|
-
<div className="flex items-center gap-
|
|
345
|
+
<div className="p-4 border-b border-white/5 bg-black/40 flex justify-between items-center text-[10px] font-mono">
|
|
346
|
+
<div className="flex items-center gap-4">
|
|
412
347
|
{job._raw && !job._archived && (
|
|
413
348
|
<input
|
|
414
349
|
type="checkbox"
|
|
415
|
-
|
|
350
|
+
aria-label={`Select job ${job.id || i}`}
|
|
351
|
+
className="w-4 h-4 rounded border-white/10 bg-black/40 text-primary focus:ring-primary/20"
|
|
416
352
|
checked={selectedIndices.has(i)}
|
|
417
353
|
onChange={() => toggleSelection(i)}
|
|
418
354
|
/>
|
|
419
355
|
)}
|
|
420
|
-
<span className="
|
|
421
|
-
ID:
|
|
356
|
+
<span className="bg-primary/10 text-primary px-2 py-1 rounded-md font-black uppercase tracking-widest flex items-center gap-2 border border-primary/20 shadow-[0_0_15px_rgba(0,240,255,0.05)]">
|
|
357
|
+
ID:{job.id || 'N/A'}
|
|
422
358
|
{job._archived && (
|
|
423
359
|
<span
|
|
424
360
|
className={cn(
|
|
425
|
-
'px-1.5 py-0.5 rounded text-[8px] border',
|
|
361
|
+
'px-1.5 py-0.5 rounded text-[8px] border ml-1',
|
|
426
362
|
job._status === 'completed'
|
|
427
363
|
? 'bg-green-500/20 text-green-500 border-green-500/20'
|
|
428
364
|
: 'bg-red-500/20 text-red-500 border-red-500/20'
|
|
429
365
|
)}
|
|
430
366
|
>
|
|
431
|
-
|
|
367
|
+
{job._status}
|
|
432
368
|
</span>
|
|
433
369
|
)}
|
|
434
370
|
</span>
|
|
435
371
|
</div>
|
|
436
|
-
<span className="text-
|
|
372
|
+
<span className="text-white/20 font-black flex items-center gap-4 uppercase tracking-tighter">
|
|
437
373
|
{view === 'delayed' && job.scheduledAt && (
|
|
438
|
-
<span className="text-amber-500 flex items-center gap-1
|
|
374
|
+
<span className="text-amber-500 flex items-center gap-1.5">
|
|
439
375
|
<Clock size={12} /> {new Date(job.scheduledAt).toLocaleString()}
|
|
440
376
|
</span>
|
|
441
377
|
)}
|
|
442
378
|
{view === 'failed' && job.failedAt && (
|
|
443
|
-
<span className="text-red-500 flex items-center gap-1
|
|
379
|
+
<span className="text-red-500 flex items-center gap-1.5">
|
|
444
380
|
<AlertCircle size={12} /> {new Date(job.failedAt).toLocaleString()}
|
|
445
381
|
</span>
|
|
446
382
|
)}
|
|
447
|
-
{job._archivedAt && (
|
|
448
|
-
<span className="text-indigo-400 flex items-center gap-1 font-bold">
|
|
449
|
-
<ArrowRight size={12} /> {new Date(job._archivedAt).toLocaleString()}
|
|
450
|
-
</span>
|
|
451
|
-
)}
|
|
452
383
|
{job.timestamp &&
|
|
453
384
|
!job._archivedAt &&
|
|
454
385
|
new Date(job.timestamp).toLocaleString()}
|
|
@@ -457,24 +388,24 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
457
388
|
<button
|
|
458
389
|
type="button"
|
|
459
390
|
onClick={() => job._raw && !job._archived && toggleSelection(i)}
|
|
460
|
-
className="w-full text-left cursor-pointer focus:outline-none focus:ring-
|
|
391
|
+
className="w-full text-left cursor-pointer focus:outline-none focus:ring-inset"
|
|
461
392
|
>
|
|
462
393
|
{job.error && (
|
|
463
|
-
<div className="p-
|
|
464
|
-
<AlertCircle size={
|
|
394
|
+
<div className="p-5 bg-red-500/10 text-red-500 text-xs font-black border-b border-red-500/10 flex items-start gap-3 uppercase tracking-tight">
|
|
395
|
+
<AlertCircle size={16} className="mt-0.5 shrink-0" />
|
|
465
396
|
<p>{job.error}</p>
|
|
466
397
|
</div>
|
|
467
398
|
)}
|
|
468
|
-
<pre className="text-[11px] font-mono p-
|
|
399
|
+
<pre className="text-[11px] font-mono p-6 overflow-x-auto text-white/60 leading-relaxed bg-black/40">
|
|
469
400
|
{JSON.stringify(job, null, 2)}
|
|
470
401
|
</pre>
|
|
471
402
|
</button>
|
|
472
|
-
<div className="p-
|
|
403
|
+
<div className="p-4 bg-black/20 border-t border-white/5 flex justify-end gap-3">
|
|
473
404
|
{!job._archived && (
|
|
474
405
|
<button
|
|
475
406
|
type="button"
|
|
476
407
|
onClick={() => handleAction('delete', job)}
|
|
477
|
-
className="text-[10px] font-
|
|
408
|
+
className="text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl hover:bg-red-500/10 text-red-500/60 hover:text-red-500 transition-all font-heading border border-transparent hover:border-red-500/20"
|
|
478
409
|
>
|
|
479
410
|
Terminate
|
|
480
411
|
</button>
|
|
@@ -484,13 +415,13 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
484
415
|
type="button"
|
|
485
416
|
onClick={() => handleAction('retry', job)}
|
|
486
417
|
className={cn(
|
|
487
|
-
'text-[10px] font-
|
|
418
|
+
'text-[10px] font-black uppercase tracking-[0.2em] px-5 py-2.5 rounded-xl text-black shadow-lg transition-all font-heading',
|
|
488
419
|
view === 'delayed'
|
|
489
|
-
? 'bg-amber-500 hover:bg-amber-
|
|
490
|
-
: 'bg-
|
|
420
|
+
? 'bg-amber-500 shadow-amber-500/20 hover:bg-amber-400'
|
|
421
|
+
: 'bg-primary shadow-primary/20 hover:bg-primary/80'
|
|
491
422
|
)}
|
|
492
423
|
>
|
|
493
|
-
{view === 'delayed' ? '
|
|
424
|
+
{view === 'delayed' ? 'Execute Now' : 'Re-Run Cycle'}
|
|
494
425
|
</button>
|
|
495
426
|
)}
|
|
496
427
|
</div>
|
|
@@ -507,6 +438,7 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
507
438
|
type="button"
|
|
508
439
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
|
509
440
|
disabled={page === 1}
|
|
441
|
+
aria-label="Previous page"
|
|
510
442
|
className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
|
|
511
443
|
>
|
|
512
444
|
←
|
|
@@ -516,6 +448,7 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
516
448
|
type="button"
|
|
517
449
|
onClick={() => setPage((p) => p + 1)}
|
|
518
450
|
disabled={page * 50 >= (data.total || 0)}
|
|
451
|
+
aria-label="Next page"
|
|
519
452
|
className="p-2 rounded-lg bg-muted text-muted-foreground disabled:opacity-30 hover:bg-primary hover:text-white transition-all"
|
|
520
453
|
>
|
|
521
454
|
→
|
package/src/client/index.css
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
@tailwind components;
|
|
3
3
|
@tailwind utilities;
|
|
4
4
|
|
|
5
|
+
@import url("https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600;700&family=Poppins:wght@400;500;600;700;800;900&display=swap");
|
|
6
|
+
|
|
5
7
|
@layer base {
|
|
6
8
|
:root {
|
|
7
9
|
--background: 0 0% 100%;
|
|
@@ -24,29 +26,29 @@
|
|
|
24
26
|
--border: 220 13% 91%;
|
|
25
27
|
--input: 220 13% 91%;
|
|
26
28
|
--ring: 238.7 83.5% 66.7%;
|
|
27
|
-
--radius:
|
|
29
|
+
--radius: 1.25rem;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
.dark {
|
|
31
|
-
--background:
|
|
32
|
-
--foreground:
|
|
33
|
-
--card:
|
|
34
|
-
--card-foreground:
|
|
35
|
-
--popover:
|
|
36
|
-
--popover-foreground:
|
|
37
|
-
--primary:
|
|
38
|
-
--primary-foreground:
|
|
39
|
-
--secondary:
|
|
40
|
-
--secondary-foreground:
|
|
41
|
-
--muted:
|
|
42
|
-
--muted-foreground:
|
|
43
|
-
--accent:
|
|
44
|
-
--accent-foreground:
|
|
33
|
+
--background: 240 10% 3.9%;
|
|
34
|
+
--foreground: 0 0% 98%;
|
|
35
|
+
--card: 240 10% 3.9%;
|
|
36
|
+
--card-foreground: 0 0% 98%;
|
|
37
|
+
--popover: 240 10% 3.9%;
|
|
38
|
+
--popover-foreground: 0 0% 98%;
|
|
39
|
+
--primary: 180 100% 50%; /* Electric Cyan */
|
|
40
|
+
--primary-foreground: 240 5.9% 10%;
|
|
41
|
+
--secondary: 240 3.7% 15.9%;
|
|
42
|
+
--secondary-foreground: 0 0% 98%;
|
|
43
|
+
--muted: 240 3.7% 10%;
|
|
44
|
+
--muted-foreground: 240 5% 64.9%;
|
|
45
|
+
--accent: 240 3.7% 15.9%;
|
|
46
|
+
--accent-foreground: 0 0% 98%;
|
|
45
47
|
--destructive: 0 62.8% 30.6%;
|
|
46
|
-
--destructive-foreground:
|
|
47
|
-
--border:
|
|
48
|
-
--input:
|
|
49
|
-
--ring:
|
|
48
|
+
--destructive-foreground: 0 0% 98%;
|
|
49
|
+
--border: 240 3.7% 15.9%;
|
|
50
|
+
--input: 240 3.7% 15.9%;
|
|
51
|
+
--ring: 180 100% 50%;
|
|
50
52
|
}
|
|
51
53
|
}
|
|
52
54
|
|
|
@@ -61,7 +63,8 @@
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
.dark body {
|
|
64
|
-
background-image:
|
|
66
|
+
background-image:
|
|
67
|
+
radial-gradient(at 50% 0%, hsla(238, 83%, 66%, 0.05) 0%, transparent 50%),
|
|
65
68
|
radial-gradient(at 100% 100%, hsla(238, 83%, 66%, 0.02) 0%, transparent 50%);
|
|
66
69
|
background-attachment: fixed;
|
|
67
70
|
}
|
|
@@ -98,9 +101,7 @@
|
|
|
98
101
|
left: 0;
|
|
99
102
|
right: 0;
|
|
100
103
|
bottom: 0;
|
|
101
|
-
background: linear-gradient(to bottom,
|
|
102
|
-
transparent 50%,
|
|
103
|
-
rgba(var(--primary), 0.02) 50%);
|
|
104
|
+
background: linear-gradient(to bottom, transparent 50%, rgba(var(--primary), 0.02) 50%);
|
|
104
105
|
background-size: 100% 4px;
|
|
105
106
|
z-index: 2;
|
|
106
107
|
pointer-events: none;
|
|
@@ -122,7 +123,6 @@
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
@keyframes glow-pulse {
|
|
125
|
-
|
|
126
126
|
0%,
|
|
127
127
|
100% {
|
|
128
128
|
opacity: 0.3;
|
|
@@ -142,15 +142,13 @@
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
.dark .card-premium {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
border-color: rgba(255, 255, 255, 0.05);
|
|
148
|
-
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
|
|
145
|
+
@apply bg-zinc-900/40 backdrop-blur-xl border-white/5;
|
|
146
|
+
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.8);
|
|
149
147
|
}
|
|
150
148
|
|
|
151
149
|
.dark .card-premium:hover {
|
|
152
|
-
border-
|
|
153
|
-
box-shadow: 0 8px
|
|
150
|
+
@apply border-primary/20 bg-zinc-900/60;
|
|
151
|
+
box-shadow: 0 8px 40px rgba(0, 240, 255, 0.05);
|
|
154
152
|
}
|
|
155
153
|
}
|
|
156
154
|
|
|
@@ -171,4 +169,4 @@
|
|
|
171
169
|
|
|
172
170
|
.animate-toast-progress {
|
|
173
171
|
animation: toast-progress 5s linear forwards;
|
|
174
|
-
}
|
|
172
|
+
}
|