@gravito/zenith 0.1.0-beta.1 → 1.0.0
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 +9 -0
- package/dist/bin.js +38846 -27303
- package/dist/client/assets/index-C332gZ-J.css +1 -0
- package/dist/client/assets/index-D4HibwTK.js +436 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +38846 -27303
- package/docs/ALERTING_GUIDE.md +71 -0
- package/docs/LARAVEL_ZENITH_ROADMAP.md +109 -0
- package/docs/QUASAR_MASTER_PLAN.md +140 -0
- package/package.json +52 -48
- package/scripts/debug_redis_keys.ts +24 -0
- package/specs/PULSE_SPEC.md +86 -0
- package/src/client/App.tsx +2 -0
- package/src/client/Layout.tsx +18 -0
- package/src/client/Sidebar.tsx +2 -1
- package/src/client/WorkerStatus.tsx +121 -76
- package/src/client/components/BrandIcons.tsx +138 -0
- package/src/client/components/ConfirmDialog.tsx +0 -1
- package/src/client/components/JobInspector.tsx +18 -6
- package/src/client/components/PageHeader.tsx +38 -0
- package/src/client/pages/OverviewPage.tsx +17 -20
- package/src/client/pages/PulsePage.tsx +478 -0
- package/src/client/pages/QueuesPage.tsx +1 -3
- package/src/client/pages/SettingsPage.tsx +640 -78
- package/src/client/pages/WorkersPage.tsx +71 -3
- package/src/client/pages/index.ts +1 -0
- package/src/server/index.ts +311 -11
- package/src/server/services/AlertService.ts +189 -41
- package/src/server/services/CommandService.ts +137 -0
- package/src/server/services/PulseService.ts +80 -0
- package/src/server/services/QueueService.ts +63 -6
- package/src/shared/types.ts +99 -0
- package/tsconfig.json +2 -2
- package/ARCHITECTURE.md +0 -88
- package/BATCH_OPERATIONS_IMPLEMENTATION.md +0 -159
- package/EVOLUTION_BLUEPRINT.md +0 -112
- package/JOBINSPECTOR_SCROLL_FIX.md +0 -152
- package/PULSE_IMPLEMENTATION_PLAN.md +0 -111
- package/TESTING_BATCH_OPERATIONS.md +0 -252
- package/dist/client/assets/index-DGYEwTDL.css +0 -1
- package/dist/client/assets/index-oyTdySX0.js +0 -421
- /package/{DEPLOYMENT.md → docs/DEPLOYMENT.md} +0 -0
- /package/{DOCS_INTERNAL.md → docs/DOCS_INTERNAL.md} +0 -0
- /package/{QUICK_TEST_GUIDE.md → docs/QUICK_TEST_GUIDE.md} +0 -0
- /package/{ROADMAP.md → docs/ROADMAP.md} +0 -0
|
@@ -1,13 +1,38 @@
|
|
|
1
1
|
import { type ClassValue, clsx } from 'clsx'
|
|
2
|
-
import { Activity, Cpu } from 'lucide-react'
|
|
2
|
+
import { Activity, Cpu, Terminal } from 'lucide-react'
|
|
3
3
|
import { twMerge } from 'tailwind-merge'
|
|
4
4
|
|
|
5
5
|
function cn(...inputs: ClassValue[]) {
|
|
6
6
|
return twMerge(clsx(inputs))
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
function formatBytes(bytes: number) {
|
|
10
|
+
if (bytes === 0) return '0 B'
|
|
11
|
+
const k = 1024
|
|
12
|
+
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
|
13
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
14
|
+
return parseFloat((bytes / k ** i).toFixed(1)) + ' ' + sizes[i]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getWorkerName(id: string, pid: number) {
|
|
18
|
+
// If ID contains Hostname+PID, try to simplify it
|
|
19
|
+
// Example: CarldeMacBook-Air.local-99401
|
|
20
|
+
const complexIdMatch = id.match(/^(.*)-(\d+)$/)
|
|
21
|
+
if (complexIdMatch && parseInt(complexIdMatch[2]) === pid) {
|
|
22
|
+
// Return just the hostname part, and maybe truncate if too long
|
|
23
|
+
let hostname = complexIdMatch[1]
|
|
24
|
+
if (hostname.endsWith('.local')) {
|
|
25
|
+
hostname = hostname.replace('.local', '')
|
|
26
|
+
}
|
|
27
|
+
return hostname
|
|
28
|
+
}
|
|
29
|
+
// Fallback
|
|
30
|
+
return id.replace('.local', '')
|
|
31
|
+
}
|
|
32
|
+
|
|
9
33
|
interface WorkerInfo {
|
|
10
34
|
id: string
|
|
35
|
+
service?: string
|
|
11
36
|
status: 'online' | 'offline'
|
|
12
37
|
pid: number
|
|
13
38
|
uptime: number
|
|
@@ -18,6 +43,12 @@ interface WorkerInfo {
|
|
|
18
43
|
rss: number
|
|
19
44
|
}
|
|
20
45
|
}
|
|
46
|
+
meta?: {
|
|
47
|
+
laravel?: {
|
|
48
|
+
workerCount: number
|
|
49
|
+
roots: string[]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
21
52
|
}
|
|
22
53
|
|
|
23
54
|
export function WorkerStatus({
|
|
@@ -27,32 +58,28 @@ export function WorkerStatus({
|
|
|
27
58
|
highlightedWorkerId?: string | null
|
|
28
59
|
workers?: WorkerInfo[]
|
|
29
60
|
}) {
|
|
30
|
-
// Legacy polling removed, now using passed props
|
|
31
|
-
// const { data: workerData } = useQuery<{ workers: any[] }>({ ... })
|
|
32
|
-
|
|
33
|
-
// Fallback if not passed (though it should be)
|
|
34
|
-
// const workers = workerData?.workers || []
|
|
35
|
-
|
|
36
61
|
const onlineCount = workers.filter((w) => w.status === 'online').length
|
|
37
62
|
|
|
38
63
|
return (
|
|
39
|
-
<div className="card-premium
|
|
40
|
-
<div className="
|
|
41
|
-
<div>
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
<div className="card-premium h-full flex flex-col overflow-hidden">
|
|
65
|
+
<div className="p-6 pb-0 flex-none">
|
|
66
|
+
<div className="flex justify-between items-center mb-6">
|
|
67
|
+
<div>
|
|
68
|
+
<h3 className="text-lg font-black flex items-center gap-2 tracking-tight">
|
|
69
|
+
<Cpu size={20} className="text-primary" />
|
|
70
|
+
Cluster Nodes
|
|
71
|
+
</h3>
|
|
72
|
+
<p className="text-[10px] text-muted-foreground uppercase font-black tracking-widest opacity-60">
|
|
73
|
+
Real-time load
|
|
74
|
+
</p>
|
|
75
|
+
</div>
|
|
76
|
+
<span className="text-[10px] font-black text-green-500 bg-green-500/10 px-3 py-1 rounded-full uppercase tracking-widest border border-green-500/20">
|
|
77
|
+
{onlineCount} ACTIVE
|
|
78
|
+
</span>
|
|
49
79
|
</div>
|
|
50
|
-
<span className="text-[10px] font-black text-green-500 bg-green-500/10 px-3 py-1 rounded-full uppercase tracking-widest border border-green-500/20">
|
|
51
|
-
{onlineCount} ACTIVE
|
|
52
|
-
</span>
|
|
53
80
|
</div>
|
|
54
81
|
|
|
55
|
-
<div className="space-y-3">
|
|
82
|
+
<div className="flex-1 overflow-y-auto min-h-0 px-6 space-y-3 scrollbar-thin pb-6">
|
|
56
83
|
{workers.length === 0 && (
|
|
57
84
|
<div className="py-12 text-center text-muted-foreground/30 flex flex-col items-center gap-2">
|
|
58
85
|
<Activity size={24} className="opacity-20 animate-pulse" />
|
|
@@ -64,10 +91,10 @@ export function WorkerStatus({
|
|
|
64
91
|
<div
|
|
65
92
|
key={worker.id}
|
|
66
93
|
className={cn(
|
|
67
|
-
'flex items-center
|
|
94
|
+
'relative flex items-center gap-4 p-4 rounded-2xl border transition-all group overflow-hidden shrink-0',
|
|
68
95
|
worker.id === highlightedWorkerId
|
|
69
|
-
? '
|
|
70
|
-
: '
|
|
96
|
+
? 'bg-primary/5 border-primary/50 shadow-[0_0_20px_rgba(var(--primary-rgb),0.1)] -translate-y-1 scale-[1.02] z-10'
|
|
97
|
+
: 'bg-card hover:bg-muted/10 border-border/50 hover:border-primary/20'
|
|
71
98
|
)}
|
|
72
99
|
>
|
|
73
100
|
{/* Status bar */}
|
|
@@ -78,80 +105,96 @@ export function WorkerStatus({
|
|
|
78
105
|
)}
|
|
79
106
|
/>
|
|
80
107
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
<
|
|
97
|
-
|
|
98
|
-
|
|
108
|
+
{/* Icon/Dot */}
|
|
109
|
+
<div className="relative shrink-0 ml-1">
|
|
110
|
+
<div
|
|
111
|
+
className={cn(
|
|
112
|
+
'w-3 h-3 rounded-full',
|
|
113
|
+
worker.status === 'online'
|
|
114
|
+
? 'bg-green-500 animate-pulse shadow-[0_0_12px_rgba(34,197,94,0.6)]'
|
|
115
|
+
: 'bg-muted-foreground/40'
|
|
116
|
+
)}
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
|
|
120
|
+
{/* Main Info */}
|
|
121
|
+
<div className="flex-1 min-w-0 flex flex-col justify-center mr-2">
|
|
122
|
+
{worker.service && (
|
|
123
|
+
<span className="text-[10px] font-black text-primary/80 uppercase tracking-widest mb-0.5 whitespace-nowrap">
|
|
124
|
+
{worker.service}
|
|
125
|
+
</span>
|
|
126
|
+
)}
|
|
127
|
+
<h4
|
|
128
|
+
className="text-sm font-black tracking-tight text-foreground truncate"
|
|
129
|
+
title={worker.id}
|
|
130
|
+
>
|
|
131
|
+
{getWorkerName(worker.id, worker.pid) || worker.id}
|
|
132
|
+
</h4>
|
|
133
|
+
<div className="flex items-center gap-2 mt-1">
|
|
134
|
+
<span className="text-[9px] font-bold text-muted-foreground uppercase tracking-wider whitespace-nowrap">
|
|
135
|
+
PID {worker.pid}
|
|
136
|
+
</span>
|
|
137
|
+
{worker.meta?.laravel && worker.meta.laravel.workerCount > 0 && (
|
|
138
|
+
<span className="inline-flex items-center gap-1 text-[9px] font-black text-white bg-red-500 px-1.5 py-0.5 rounded shadow-sm uppercase tracking-widest leading-none whitespace-nowrap">
|
|
139
|
+
<Terminal size={8} />
|
|
140
|
+
{worker.meta.laravel.workerCount} PHP
|
|
99
141
|
</span>
|
|
100
|
-
|
|
101
|
-
</div>
|
|
142
|
+
)}
|
|
102
143
|
</div>
|
|
103
144
|
</div>
|
|
104
145
|
|
|
105
|
-
|
|
146
|
+
{/* Metrics (Right Side) */}
|
|
147
|
+
<div className="flex items-center gap-3 text-right shrink-0">
|
|
106
148
|
{worker.metrics && (
|
|
107
|
-
|
|
108
|
-
<div className="space-y-1
|
|
149
|
+
<>
|
|
150
|
+
<div className="hidden sm:block space-y-1 w-12">
|
|
109
151
|
<div className="flex justify-between text-[8px] font-black text-muted-foreground uppercase tracking-tighter">
|
|
110
|
-
<span>
|
|
152
|
+
<span>CPU</span>
|
|
111
153
|
<span
|
|
112
154
|
className={cn(
|
|
113
|
-
worker.metrics.cpu > (worker.metrics.cores ||
|
|
155
|
+
worker.metrics.cpu > (worker.metrics.cores || 1) * 100 && 'text-red-500'
|
|
114
156
|
)}
|
|
115
157
|
>
|
|
116
|
-
{worker.metrics.cpu.toFixed(
|
|
158
|
+
{worker.metrics.cpu.toFixed(0)}%
|
|
117
159
|
</span>
|
|
118
160
|
</div>
|
|
119
161
|
<div className="h-1 w-full bg-muted rounded-full overflow-hidden">
|
|
120
162
|
<div
|
|
121
|
-
className=
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
? 'bg-red-500'
|
|
125
|
-
: worker.metrics.cpu > (worker.metrics.cores || 4) * 0.7
|
|
126
|
-
? 'bg-amber-500'
|
|
127
|
-
: 'bg-green-500'
|
|
128
|
-
)}
|
|
129
|
-
style={{
|
|
130
|
-
width: `${Math.min(100, (worker.metrics.cpu / (worker.metrics.cores || 1)) * 100)}%`,
|
|
131
|
-
}}
|
|
132
|
-
/>
|
|
163
|
+
className="h-full bg-foreground transition-all duration-700"
|
|
164
|
+
style={{ width: `${Math.min(100, worker.metrics.cpu)}%` }}
|
|
165
|
+
></div>
|
|
133
166
|
</div>
|
|
134
167
|
</div>
|
|
135
|
-
|
|
168
|
+
|
|
169
|
+
<div className="hidden sm:block space-y-1 w-12">
|
|
136
170
|
<div className="flex justify-between text-[8px] font-black text-muted-foreground uppercase tracking-tighter">
|
|
137
171
|
<span>RAM</span>
|
|
138
|
-
<span
|
|
172
|
+
<span className="truncate ml-1">
|
|
173
|
+
{formatBytes(worker.metrics.ram.rss).split(' ')[0]}
|
|
174
|
+
</span>
|
|
139
175
|
</div>
|
|
140
176
|
<div className="h-1 w-full bg-muted rounded-full overflow-hidden">
|
|
141
177
|
<div
|
|
142
178
|
className="h-full bg-indigo-500 transition-all duration-700"
|
|
143
179
|
style={{
|
|
144
|
-
width: `${Math.min(100, (worker.metrics.ram.rss /
|
|
180
|
+
width: `${Math.min(100, (worker.metrics.ram.rss / 2000000000) * 100)}%`,
|
|
145
181
|
}}
|
|
146
|
-
|
|
182
|
+
></div>
|
|
147
183
|
</div>
|
|
148
184
|
</div>
|
|
149
|
-
|
|
185
|
+
</>
|
|
150
186
|
)}
|
|
151
|
-
|
|
152
|
-
|
|
187
|
+
|
|
188
|
+
<div className="w-12">
|
|
189
|
+
<p className="text-xs font-black tracking-tighter tabular-nums text-foreground">
|
|
190
|
+
{worker.uptime > 3600
|
|
191
|
+
? `${(worker.uptime / 3600).toFixed(1)}h`
|
|
192
|
+
: worker.uptime > 60
|
|
193
|
+
? `${(worker.uptime / 60).toFixed(0)}m`
|
|
194
|
+
: `${worker.uptime.toFixed(0)}s`}
|
|
195
|
+
</p>
|
|
153
196
|
<p className="text-[8px] text-muted-foreground uppercase font-black tracking-widest opacity-50">
|
|
154
|
-
|
|
197
|
+
UP
|
|
155
198
|
</p>
|
|
156
199
|
</div>
|
|
157
200
|
</div>
|
|
@@ -159,12 +202,14 @@ export function WorkerStatus({
|
|
|
159
202
|
))}
|
|
160
203
|
</div>
|
|
161
204
|
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
205
|
+
<div className="p-6 pt-0 flex-none">
|
|
206
|
+
<button
|
|
207
|
+
type="button"
|
|
208
|
+
className="w-full py-3 bg-muted text-[10px] font-black rounded-xl hover:bg-primary hover:text-primary-foreground transition-all uppercase tracking-[0.2em] opacity-60 hover:opacity-100 active:scale-95 shadow-lg shadow-transparent hover:shadow-primary/20"
|
|
209
|
+
>
|
|
210
|
+
Manage Nodes
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
168
213
|
</div>
|
|
169
214
|
)
|
|
170
215
|
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
export function NodeIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg
|
|
6
|
+
viewBox="0 0 32 32"
|
|
7
|
+
fill="none"
|
|
8
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
9
|
+
role="img"
|
|
10
|
+
aria-label="Node.js"
|
|
11
|
+
{...props}
|
|
12
|
+
>
|
|
13
|
+
<path
|
|
14
|
+
d="M16 2L2.1 9.9v12.2L16 30l13.9-7.9V9.9L16 2zm11.9 19.1L16 27.8l-11.9-6.7V11.1L16 4.2l11.9 6.9v10z"
|
|
15
|
+
fill="#339933"
|
|
16
|
+
/>
|
|
17
|
+
<path d="M16 22.5l-6-3.4v-6.8l6-3.4 6 3.4v6.8l-6 3.4z" fill="#339933" />
|
|
18
|
+
</svg>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function BunIcon(props: SVGProps<SVGSVGElement>) {
|
|
23
|
+
return (
|
|
24
|
+
<svg
|
|
25
|
+
viewBox="0 0 32 32"
|
|
26
|
+
fill="none"
|
|
27
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
28
|
+
role="img"
|
|
29
|
+
aria-label="Bun"
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
{/* Outer Outline/Shadow for contrast on light bg */}
|
|
33
|
+
<path
|
|
34
|
+
d="M30 17.045a9.8 9.8 0 0 0-.32-2.306l-.004.034a11.2 11.2 0 0 0-5.762-6.786c-3.495-1.89-5.243-3.326-6.8-3.811h.003c-1.95-.695-3.949.82-5.825 1.927-4.52 2.481-9.573 5.45-9.28 11.417.008-.029.017-.052.026-.08a9.97 9.97 0 0 0 3.934 7.257l-.01-.006C13.747 31.473 30.05 27.292 30 17.045"
|
|
35
|
+
fill="#fbf0df"
|
|
36
|
+
stroke="#4a4a4a"
|
|
37
|
+
strokeWidth="1.5"
|
|
38
|
+
/>
|
|
39
|
+
|
|
40
|
+
<path
|
|
41
|
+
fill="#37474f"
|
|
42
|
+
d="M19.855 20.236A.8.8 0 0 0 19.26 20h-6.514a.8.8 0 0 0-.596.236.51.51 0 0 0-.137.463 4.37 4.37 0 0 0 1.641 2.339 4.2 4.2 0 0 0 2.349.926 4.2 4.2 0 0 0 2.343-.926 4.37 4.37 0 0 0 1.642-2.339.5.5 0 0 0-.132-.463Z"
|
|
43
|
+
/>
|
|
44
|
+
<ellipse cx="22.5" cy="18.5" fill="#f8bbd0" rx="2.5" ry="1.5" />
|
|
45
|
+
<ellipse cx="9.5" cy="18.5" fill="#f8bbd0" rx="2.5" ry="1.5" />
|
|
46
|
+
<circle cx="10" cy="16" r="2" fill="#37474f" />
|
|
47
|
+
<circle cx="22" cy="16" r="2" fill="#37474f" />
|
|
48
|
+
</svg>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function DenoIcon(props: SVGProps<SVGSVGElement>) {
|
|
53
|
+
return (
|
|
54
|
+
<svg
|
|
55
|
+
viewBox="0 0 32 32"
|
|
56
|
+
fill="none"
|
|
57
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
58
|
+
role="img"
|
|
59
|
+
aria-label="Deno"
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
<circle cx="16" cy="16" r="14" fill="currentColor" />
|
|
63
|
+
<path
|
|
64
|
+
d="M16 6C16 6 24 10 24 18C24 23.5228 19.5228 28 14 28C8.47715 28 4 23.5228 4 18C4 10 16 6 16 6Z"
|
|
65
|
+
fill="white"
|
|
66
|
+
/>
|
|
67
|
+
<circle cx="12" cy="18" r="2" fill="black" />
|
|
68
|
+
</svg>
|
|
69
|
+
)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function PhpIcon(props: SVGProps<SVGSVGElement>) {
|
|
73
|
+
return (
|
|
74
|
+
<svg
|
|
75
|
+
viewBox="0 0 32 32"
|
|
76
|
+
fill="none"
|
|
77
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
78
|
+
role="img"
|
|
79
|
+
aria-label="PHP"
|
|
80
|
+
{...props}
|
|
81
|
+
>
|
|
82
|
+
<ellipse cx="16" cy="16" rx="14" ry="10" fill="#777BB4" />
|
|
83
|
+
<text
|
|
84
|
+
x="50%"
|
|
85
|
+
y="54%"
|
|
86
|
+
dominantBaseline="middle"
|
|
87
|
+
textAnchor="middle"
|
|
88
|
+
fill="white"
|
|
89
|
+
fontSize="9"
|
|
90
|
+
fontWeight="bold"
|
|
91
|
+
>
|
|
92
|
+
PHP
|
|
93
|
+
</text>
|
|
94
|
+
</svg>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function GoIcon(props: SVGProps<SVGSVGElement>) {
|
|
99
|
+
return (
|
|
100
|
+
<svg
|
|
101
|
+
viewBox="0 0 32 32"
|
|
102
|
+
fill="none"
|
|
103
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
104
|
+
role="img"
|
|
105
|
+
aria-label="Go"
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
<path
|
|
109
|
+
d="M5 16C5 10 10 5 16 5H24V13H16C14.3431 13 13 14.3431 13 16C13 17.6569 14.3431 19 16 19H27V27H16C10 27 5 22 5 16Z"
|
|
110
|
+
fill="#00ADD8"
|
|
111
|
+
/>
|
|
112
|
+
<circle cx="9" cy="16" r="2" fill="white" />
|
|
113
|
+
<circle cx="23" cy="9" r="2" fill="white" />
|
|
114
|
+
</svg>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function PythonIcon(props: SVGProps<SVGSVGElement>) {
|
|
119
|
+
return (
|
|
120
|
+
<svg
|
|
121
|
+
viewBox="0 0 32 32"
|
|
122
|
+
fill="none"
|
|
123
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
124
|
+
role="img"
|
|
125
|
+
aria-label="Python"
|
|
126
|
+
{...props}
|
|
127
|
+
>
|
|
128
|
+
<path
|
|
129
|
+
d="M16 2C10 2 10 5 10 5L10 9L18 9L18 11L8 11L8 20L12 20L12 14L22 14C22 14 22 12 16 2Z"
|
|
130
|
+
fill="#3776AB"
|
|
131
|
+
/>
|
|
132
|
+
<path
|
|
133
|
+
d="M16 30C22 30 22 27 22 27L22 23L14 23L14 21L24 21L24 12L20 12L20 18L10 18C10 18 10 20 16 30Z"
|
|
134
|
+
fill="#FFD43B"
|
|
135
|
+
/>
|
|
136
|
+
</svg>
|
|
137
|
+
)
|
|
138
|
+
}
|
|
@@ -35,7 +35,6 @@ export function ConfirmDialog({
|
|
|
35
35
|
onClick={(e) => e.stopPropagation()}
|
|
36
36
|
onKeyDown={(e) => e.stopPropagation()}
|
|
37
37
|
>
|
|
38
|
-
{/* biome-ignore lint/a11y/noStaticElementInteractions: Modal content needs to stop propagation */}
|
|
39
38
|
<motion.div
|
|
40
39
|
initial={{ scale: 0.9, opacity: 0 }}
|
|
41
40
|
animate={{ scale: 1, opacity: 1 }}
|
|
@@ -83,14 +83,18 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
const toggleSelectAll = React.useCallback(() => {
|
|
86
|
-
if (!data?.jobs)
|
|
86
|
+
if (!data?.jobs) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
87
89
|
const availableCount = data.jobs.filter((j) => j._raw && !j._archived).length
|
|
88
90
|
if (selectedIndices.size === availableCount && availableCount > 0) {
|
|
89
91
|
setSelectedIndices(new Set())
|
|
90
92
|
} else {
|
|
91
93
|
const indices = new Set<number>()
|
|
92
94
|
data.jobs.forEach((j, i) => {
|
|
93
|
-
if (j._raw && !j._archived)
|
|
95
|
+
if (j._raw && !j._archived) {
|
|
96
|
+
indices.add(i)
|
|
97
|
+
}
|
|
94
98
|
})
|
|
95
99
|
setSelectedIndices(indices)
|
|
96
100
|
}
|
|
@@ -134,10 +138,14 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
134
138
|
}, [])
|
|
135
139
|
|
|
136
140
|
const handleAction = async (action: 'delete' | 'retry', job: Job) => {
|
|
137
|
-
if (!job._raw)
|
|
141
|
+
if (!job._raw) {
|
|
142
|
+
return
|
|
143
|
+
}
|
|
138
144
|
const endpoint = action === 'delete' ? 'delete' : 'retry'
|
|
139
145
|
const body: any = { raw: job._raw }
|
|
140
|
-
if (action === 'delete')
|
|
146
|
+
if (action === 'delete') {
|
|
147
|
+
body.type = view
|
|
148
|
+
}
|
|
141
149
|
|
|
142
150
|
await fetch(`/api/queues/${queueName}/jobs/${endpoint}`, {
|
|
143
151
|
method: 'POST',
|
|
@@ -150,7 +158,9 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
150
158
|
}
|
|
151
159
|
|
|
152
160
|
const handleBulkAction = async (action: 'delete' | 'retry') => {
|
|
153
|
-
if (selectedIndices.size === 0 || !data?.jobs)
|
|
161
|
+
if (selectedIndices.size === 0 || !data?.jobs) {
|
|
162
|
+
return
|
|
163
|
+
}
|
|
154
164
|
|
|
155
165
|
const count = selectedIndices.size
|
|
156
166
|
setConfirmDialog({
|
|
@@ -186,7 +196,9 @@ export function JobInspector({ queueName, onClose }: JobInspectorProps) {
|
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
const handleBulkActionAll = async (action: 'delete' | 'retry') => {
|
|
189
|
-
if (view === 'archive')
|
|
199
|
+
if (view === 'archive') {
|
|
200
|
+
return
|
|
201
|
+
}
|
|
190
202
|
|
|
191
203
|
setConfirmDialog({
|
|
192
204
|
open: true,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react'
|
|
2
|
+
import type { ReactNode } from 'react'
|
|
3
|
+
import { cn } from '../utils'
|
|
4
|
+
|
|
5
|
+
interface PageHeaderProps {
|
|
6
|
+
icon: LucideIcon
|
|
7
|
+
title: string
|
|
8
|
+
description?: string
|
|
9
|
+
children?: ReactNode
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PageHeader({
|
|
14
|
+
icon: Icon,
|
|
15
|
+
title,
|
|
16
|
+
description,
|
|
17
|
+
children,
|
|
18
|
+
className,
|
|
19
|
+
}: PageHeaderProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className={cn('flex justify-between items-end', className)}>
|
|
22
|
+
<div>
|
|
23
|
+
<h1 className="text-4xl font-black tracking-tighter flex items-center gap-3">
|
|
24
|
+
<div className="p-2 bg-primary/10 rounded-xl text-primary">
|
|
25
|
+
<Icon size={32} />
|
|
26
|
+
</div>
|
|
27
|
+
{title}
|
|
28
|
+
</h1>
|
|
29
|
+
{description && (
|
|
30
|
+
<p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest pl-[3.75rem]">
|
|
31
|
+
{description}
|
|
32
|
+
</p>
|
|
33
|
+
)}
|
|
34
|
+
</div>
|
|
35
|
+
<div>{children}</div>
|
|
36
|
+
</div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useQuery
|
|
1
|
+
import { useQuery } from '@tanstack/react-query'
|
|
2
2
|
import { AnimatePresence, animate, motion } from 'framer-motion'
|
|
3
3
|
import {
|
|
4
4
|
Activity,
|
|
@@ -9,13 +9,10 @@ import {
|
|
|
9
9
|
Cpu,
|
|
10
10
|
Hourglass,
|
|
11
11
|
ListTree,
|
|
12
|
-
RefreshCcw,
|
|
13
12
|
Search,
|
|
14
13
|
Terminal,
|
|
15
|
-
Trash2,
|
|
16
14
|
} from 'lucide-react'
|
|
17
15
|
import React from 'react'
|
|
18
|
-
import { useNavigate } from 'react-router-dom'
|
|
19
16
|
import { JobInspector } from '../components/JobInspector'
|
|
20
17
|
import { LogArchiveModal } from '../components/LogArchiveModal'
|
|
21
18
|
import { ThroughputChart } from '../ThroughputChart'
|
|
@@ -57,7 +54,7 @@ function LiveLogs({
|
|
|
57
54
|
onSearchArchive: () => void
|
|
58
55
|
onWorkerHover?: (id: string | null) => void
|
|
59
56
|
}) {
|
|
60
|
-
const scrollRef = React.useRef<
|
|
57
|
+
const scrollRef = React.useRef<HTMLUListElement>(null)
|
|
61
58
|
|
|
62
59
|
React.useEffect(() => {
|
|
63
60
|
// Access logs to satisfy dependency check (and trigger on update)
|
|
@@ -67,7 +64,7 @@ function LiveLogs({
|
|
|
67
64
|
}, [logs])
|
|
68
65
|
|
|
69
66
|
return (
|
|
70
|
-
<div className="card-premium
|
|
67
|
+
<div className="absolute inset-0 card-premium flex flex-col overflow-hidden group">
|
|
71
68
|
<div className="p-4 border-b bg-muted/5 flex justify-between items-center">
|
|
72
69
|
<div className="flex items-center gap-2">
|
|
73
70
|
<Terminal size={14} className="text-primary" />
|
|
@@ -92,7 +89,7 @@ function LiveLogs({
|
|
|
92
89
|
</div>
|
|
93
90
|
<ul
|
|
94
91
|
ref={scrollRef}
|
|
95
|
-
className="flex-1 overflow-y-auto p-4 font-mono text-[11px] space-y-2.5 scrollbar-thin scroll-smooth"
|
|
92
|
+
className="flex-1 min-h-0 overflow-y-auto p-4 font-mono text-[11px] space-y-2.5 scrollbar-thin scroll-smooth"
|
|
96
93
|
>
|
|
97
94
|
{logs.length === 0 ? (
|
|
98
95
|
<div className="h-full flex flex-col items-center justify-center text-muted-foreground/30 gap-2 opacity-50">
|
|
@@ -290,8 +287,6 @@ function QueueList({
|
|
|
290
287
|
queues: QueueStats[]
|
|
291
288
|
setSelectedQueue: (name: string | null) => void
|
|
292
289
|
}) {
|
|
293
|
-
const queryClient = useQueryClient()
|
|
294
|
-
|
|
295
290
|
return (
|
|
296
291
|
<div className="card-premium h-full flex flex-col overflow-hidden">
|
|
297
292
|
<div className="p-4 border-b bg-muted/5 flex justify-between items-center">
|
|
@@ -353,10 +348,8 @@ function QueueList({
|
|
|
353
348
|
}
|
|
354
349
|
|
|
355
350
|
export function OverviewPage() {
|
|
356
|
-
const navigate = useNavigate()
|
|
357
351
|
const [selectedQueue, setSelectedQueue] = React.useState<string | null>(null)
|
|
358
352
|
const [hoveredWorkerId, setHoveredWorkerId] = React.useState<string | null>(null)
|
|
359
|
-
const queryClient = useQueryClient()
|
|
360
353
|
|
|
361
354
|
const [logs, setLogs] = React.useState<SystemLog[]>([])
|
|
362
355
|
const [stats, setStats] = React.useState<FluxStats>(DEFAULT_STATS)
|
|
@@ -493,17 +486,21 @@ export function OverviewPage() {
|
|
|
493
486
|
|
|
494
487
|
<QueueHeatmap queues={queues} />
|
|
495
488
|
|
|
496
|
-
<div className="
|
|
497
|
-
<div className="lg:col-span-1 h-full">
|
|
489
|
+
<div className="flex flex-col lg:grid lg:grid-cols-3 gap-8 lg:h-[600px]">
|
|
490
|
+
<div className="lg:col-span-1 h-[400px] lg:h-full">
|
|
498
491
|
<WorkerStatus highlightedWorkerId={hoveredWorkerId} workers={workers} />
|
|
499
492
|
</div>
|
|
500
|
-
<div className="lg:col-span-2 grid grid-rows-2 gap-6 h-full">
|
|
501
|
-
<
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
493
|
+
<div className="lg:col-span-2 flex flex-col lg:grid lg:grid-rows-2 gap-6 lg:h-full">
|
|
494
|
+
<div className="relative h-[300px] lg:h-full min-h-0 overflow-hidden bg-card rounded-xl border border-border/50">
|
|
495
|
+
<LiveLogs
|
|
496
|
+
logs={logs}
|
|
497
|
+
onSearchArchive={() => setIsLogArchiveOpen(true)}
|
|
498
|
+
onWorkerHover={setHoveredWorkerId}
|
|
499
|
+
/>
|
|
500
|
+
</div>
|
|
501
|
+
<div className="h-[300px] lg:h-full min-h-0 overflow-hidden bg-card rounded-xl border border-border/50">
|
|
502
|
+
<QueueList queues={queues} setSelectedQueue={setSelectedQueue} />
|
|
503
|
+
</div>
|
|
507
504
|
</div>
|
|
508
505
|
</div>
|
|
509
506
|
</div>
|