@gravito/zenith 0.1.0-beta.1 → 1.0.0-beta.1
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/ALERTING_GUIDE.md +71 -0
- package/QUASAR_MASTER_PLAN.md +137 -0
- package/dist/bin.js +38061 -26911
- package/dist/client/assets/index-BSTyMCFd.css +1 -0
- package/dist/client/assets/index-oXEse8ih.js +436 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +38061 -26911
- package/package.json +52 -48
- package/specs/PULSE_SPEC.md +86 -0
- package/src/client/App.tsx +2 -0
- package/src/client/Layout.tsx +30 -11
- package/src/client/Sidebar.tsx +2 -1
- package/src/client/WorkerStatus.tsx +25 -21
- package/src/client/components/BrandIcons.tsx +63 -0
- package/src/client/components/PageHeader.tsx +34 -0
- package/src/client/pages/OverviewPage.tsx +18 -20
- package/src/client/pages/PulsePage.tsx +396 -0
- package/src/client/pages/QueuesPage.tsx +1 -3
- package/src/client/pages/SettingsPage.tsx +586 -78
- package/src/client/pages/WorkersPage.tsx +1 -1
- package/src/client/pages/index.ts +1 -0
- package/src/server/index.ts +148 -8
- 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 +58 -4
- package/src/shared/types.ts +97 -0
- package/tsconfig.json +2 -2
- package/PULSE_IMPLEMENTATION_PLAN.md +0 -111
- package/dist/client/assets/index-DGYEwTDL.css +0 -1
- package/dist/client/assets/index-oyTdySX0.js +0 -421
package/package.json
CHANGED
|
@@ -1,50 +1,54 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
2
|
+
"name": "@gravito/zenith",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "Gravito Zenith: Zero-config control plane for Gravito Flux & Stream",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"zenith": "dist/bin.js",
|
|
8
|
+
"flux-console": "dist/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"scripts": {
|
|
13
|
+
"dev:server": "bun run --watch src/server/index.ts",
|
|
14
|
+
"dev:client": "vite",
|
|
15
|
+
"build": "vite build && bun build ./src/server/index.ts ./src/bin.ts --outdir ./dist --target bun",
|
|
16
|
+
"start": "bun ./dist/bin.js",
|
|
17
|
+
"test": "bun test",
|
|
18
|
+
"typecheck": "bun tsc -p tsconfig.json --noEmit --skipLibCheck",
|
|
19
|
+
"seed": "bun scripts/seed.ts",
|
|
20
|
+
"worker": "bun scripts/worker.ts"
|
|
21
|
+
},
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@gravito/atlas": "workspace:*",
|
|
24
|
+
"@gravito/photon": "workspace:*",
|
|
25
|
+
"@gravito/quasar": "workspace:*",
|
|
26
|
+
"@gravito/stream": "workspace:*",
|
|
27
|
+
"@tanstack/react-query": "^5.0.0",
|
|
28
|
+
"clsx": "^2.1.1",
|
|
29
|
+
"date-fns": "^4.1.0",
|
|
30
|
+
"framer-motion": "^12.23.26",
|
|
31
|
+
"ioredis": "^5.0.0",
|
|
32
|
+
"lucide-react": "^0.562.0",
|
|
33
|
+
"nodemailer": "^7.0.12",
|
|
34
|
+
"react": "^19.0.0",
|
|
35
|
+
"react-dom": "^19.0.0",
|
|
36
|
+
"react-router-dom": "^7.11.0",
|
|
37
|
+
"recharts": "^3.6.0",
|
|
38
|
+
"tailwind-merge": "^3.4.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/nodemailer": "^7.0.4",
|
|
42
|
+
"@types/react": "^19.0.0",
|
|
43
|
+
"@types/react-dom": "^19.0.0",
|
|
44
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
45
|
+
"autoprefixer": "^10.4.0",
|
|
46
|
+
"postcss": "^8.4.0",
|
|
47
|
+
"tailwindcss": "^3.4.0",
|
|
48
|
+
"typescript": "^5.9.3",
|
|
49
|
+
"vite": "^6.0.0"
|
|
50
|
+
},
|
|
51
|
+
"publishConfig": {
|
|
52
|
+
"access": "public"
|
|
53
|
+
}
|
|
50
54
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Gravito Pulse Implementation Spec
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
Gravito Pulse is a lightweight APM (Application Performance Monitoring) system integrated into Zenith. It follows the philosophy: *"If you can connect to Redis, you are monitored."*
|
|
5
|
+
|
|
6
|
+
## 1. Gravito Pulse Protocol (GPP)
|
|
7
|
+
|
|
8
|
+
### Data Structure
|
|
9
|
+
Pulse uses Redis keys with specific TTLs to represent live services.
|
|
10
|
+
|
|
11
|
+
- **Key Pattern**: `gravito:quasar:node:{service}:{node_id}`
|
|
12
|
+
- **TTL**: 30 seconds (Agents should heartbeat every 10-15s).
|
|
13
|
+
- **Data Type**: String (JSON)
|
|
14
|
+
|
|
15
|
+
### Payload Schema
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"id": "string", // Unique Instance ID (e.g., UUID or Hostname-PID)
|
|
19
|
+
"service": "string", // Group name (e.g., "worker-billing", "api-gateway")
|
|
20
|
+
"language": "string", // "node" | "bun" | "deno" | "php" | "go" | "python" | "other"
|
|
21
|
+
"version": "string", // Language/Runtime Version
|
|
22
|
+
"pid": "number", // Process ID
|
|
23
|
+
"hostname": "string", // Machine Hostname or Custom Name
|
|
24
|
+
"platform": "string", // OS Platform (linux, darwin, win32)
|
|
25
|
+
"cpu": {
|
|
26
|
+
"system": "number", // System Load % (0-100)
|
|
27
|
+
"process": "number", // Process Usage % (0-100)
|
|
28
|
+
"cores": "number" // Core count
|
|
29
|
+
},
|
|
30
|
+
"memory": {
|
|
31
|
+
"system": {
|
|
32
|
+
"total": "number", // System Total Memory (bytes)
|
|
33
|
+
"free": "number", // System Free Memory (bytes)
|
|
34
|
+
"used": "number" // System Used Memory (bytes)
|
|
35
|
+
},
|
|
36
|
+
"process": {
|
|
37
|
+
"rss": "number", // Resident Set Size (bytes)
|
|
38
|
+
"heapTotal": "number",// Heap Total (bytes)
|
|
39
|
+
"heapUsed": "number" // Heap Used (bytes)
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"runtime": {
|
|
43
|
+
"uptime": "number", // Process uptime in seconds
|
|
44
|
+
"framework": "string" // Optional framework info
|
|
45
|
+
},
|
|
46
|
+
"timestamp": "number" // Unix Ms Timestamp
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 2. Implementation Modules
|
|
51
|
+
|
|
52
|
+
### A. Client SDK (`@gravito/pulse-node`)
|
|
53
|
+
A lightweight agent to collect metrics and publish to Redis.
|
|
54
|
+
- **Dependencies**: `ioredis`, `pidusage` (optional, or use native `os`/`process`).
|
|
55
|
+
- **Functionality**:
|
|
56
|
+
- `startPulse({ service: string })`: Starts the heartbeat loop.
|
|
57
|
+
- Collects CPU/RAM usage.
|
|
58
|
+
- Publishes to Redis.
|
|
59
|
+
|
|
60
|
+
### B. Server Collector (Zenith Console)
|
|
61
|
+
- **Service**: `PulseService`
|
|
62
|
+
- **Method**: `getNodes()`
|
|
63
|
+
- Performs `SCAN 0 MATCH pulse:* COUNT 100`.
|
|
64
|
+
- Returns grouped nodes by `service`.
|
|
65
|
+
- **API**: `GET /api/pulse/nodes`
|
|
66
|
+
|
|
67
|
+
### C. Frontend Dashboard (Zenith UI)
|
|
68
|
+
- **Route**: `/pulse`
|
|
69
|
+
- **Components**:
|
|
70
|
+
- `ServiceGroup`: A container for nodes of a specific service.
|
|
71
|
+
- `NodeCard`: Displays CPU/RAM sparklines (optional) and current health.
|
|
72
|
+
- `HealthBadge`: Green (Fresh), Yellow (>15s ago), Red (Dead/Gone - though Redis TTL handles removal, frontend can handle stale UI).
|
|
73
|
+
|
|
74
|
+
## 3. Alerts (Phase 2)
|
|
75
|
+
- Server-side checker that monitors values from `PulseService`.
|
|
76
|
+
- Triggers `AlertService` if:
|
|
77
|
+
- CPU > 90% for 2 mins.
|
|
78
|
+
- Memory > 90% for 5 mins.
|
|
79
|
+
- Disk < 10% free.
|
|
80
|
+
|
|
81
|
+
## 4. Work Plan
|
|
82
|
+
1. **Define Types**: Add `PulseNode` interface to `@gravito/custom-types` or `flux-console` shared types.
|
|
83
|
+
2. **Implement Server Collector**: Create `PulseService` in `packages/flux-console/server/services`.
|
|
84
|
+
3. **Implement API**: Add route in `packages/flux-console/server/routes.ts`.
|
|
85
|
+
4. **Implement UI**: Create `PulsePage` and components.
|
|
86
|
+
5. **Implement Node Client**: Add `startPulse` to `packages/stream` (or separate package) to verify "dogfooding" by having the server monitor itself.
|
package/src/client/App.tsx
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
SchedulesPage,
|
|
13
13
|
SettingsPage,
|
|
14
14
|
WorkersPage,
|
|
15
|
+
PulsePage,
|
|
15
16
|
} from './pages'
|
|
16
17
|
|
|
17
18
|
const queryClient = new QueryClient()
|
|
@@ -48,6 +49,7 @@ function AuthenticatedRoutes() {
|
|
|
48
49
|
<Route path="/schedules" element={<SchedulesPage />} />
|
|
49
50
|
<Route path="/workers" element={<WorkersPage />} />
|
|
50
51
|
<Route path="/metrics" element={<MetricsPage />} />
|
|
52
|
+
<Route path="/pulse" element={<PulsePage />} />
|
|
51
53
|
<Route path="/settings" element={<SettingsPage />} />
|
|
52
54
|
</Routes>
|
|
53
55
|
</Layout>
|
package/src/client/Layout.tsx
CHANGED
|
@@ -64,7 +64,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
64
64
|
fetch('/api/system/status')
|
|
65
65
|
.then((res) => res.json())
|
|
66
66
|
.then(setSystemStatus)
|
|
67
|
-
.catch(() => {})
|
|
67
|
+
.catch(() => { })
|
|
68
68
|
}, [])
|
|
69
69
|
|
|
70
70
|
// Global SSE Stream Manager
|
|
@@ -90,6 +90,15 @@ export function Layout({ children }: LayoutProps) {
|
|
|
90
90
|
}
|
|
91
91
|
})
|
|
92
92
|
|
|
93
|
+
ev.addEventListener('pulse', (e) => {
|
|
94
|
+
try {
|
|
95
|
+
const data = JSON.parse(e.data)
|
|
96
|
+
window.dispatchEvent(new CustomEvent('flux-pulse-update', { detail: data }))
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error('SSE Pulse Error', err)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
|
|
93
102
|
ev.onerror = (err) => {
|
|
94
103
|
console.error('[Zenith] SSE Connection Error', err)
|
|
95
104
|
ev.close()
|
|
@@ -107,7 +116,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
107
116
|
fetch('/api/queues')
|
|
108
117
|
.then((res) => res.json())
|
|
109
118
|
.then(setQueueData)
|
|
110
|
-
.catch(() => {})
|
|
119
|
+
.catch(() => { })
|
|
111
120
|
|
|
112
121
|
// Optional: Listen to global stats if available (from OverviewPage) to keep queue stats fresh in command palette
|
|
113
122
|
const handler = (e: Event) => {
|
|
@@ -246,15 +255,15 @@ export function Layout({ children }: LayoutProps) {
|
|
|
246
255
|
},
|
|
247
256
|
...(isAuthEnabled
|
|
248
257
|
? [
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
{
|
|
259
|
+
id: 'sys-logout',
|
|
260
|
+
title: 'Logout',
|
|
261
|
+
description: 'Sign out from the console',
|
|
262
|
+
icon: <LogOut size={18} />,
|
|
263
|
+
category: 'System' as const,
|
|
264
|
+
action: logout,
|
|
265
|
+
},
|
|
266
|
+
]
|
|
258
267
|
: []),
|
|
259
268
|
]
|
|
260
269
|
|
|
@@ -339,6 +348,14 @@ export function Layout({ children }: LayoutProps) {
|
|
|
339
348
|
return () => window.removeEventListener('keydown', handleKeyDown)
|
|
340
349
|
}, [])
|
|
341
350
|
|
|
351
|
+
// Auto-scroll to selected item
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
const el = document.getElementById(`command-item-${selectedIndex}`)
|
|
354
|
+
if (el) {
|
|
355
|
+
el.scrollIntoView({ block: 'nearest', behavior: 'smooth' })
|
|
356
|
+
}
|
|
357
|
+
}, [selectedIndex])
|
|
358
|
+
|
|
342
359
|
const handleSelect = (cmd: CommandItem) => {
|
|
343
360
|
cmd.action()
|
|
344
361
|
setIsCommandPaletteOpen(false)
|
|
@@ -520,6 +537,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
520
537
|
<div className="p-6 border-b flex items-center gap-4 bg-muted/5">
|
|
521
538
|
<Command className="text-primary animate-pulse" size={24} />
|
|
522
539
|
<input
|
|
540
|
+
autoFocus
|
|
523
541
|
type="text"
|
|
524
542
|
placeholder="Execute command or navigate..."
|
|
525
543
|
className="flex-1 bg-transparent border-none outline-none text-lg font-bold placeholder:text-muted-foreground/30"
|
|
@@ -559,6 +577,7 @@ export function Layout({ children }: LayoutProps) {
|
|
|
559
577
|
{filteredCommands.map((cmd, i) => (
|
|
560
578
|
<button
|
|
561
579
|
type="button"
|
|
580
|
+
id={`command-item-${i}`}
|
|
562
581
|
key={cmd.id}
|
|
563
582
|
className={cn(
|
|
564
583
|
'w-full flex items-center justify-between p-4 rounded-2xl transition-all cursor-pointer group/cmd outline-none',
|
package/src/client/Sidebar.tsx
CHANGED
|
@@ -23,10 +23,11 @@ export function Sidebar({ className, collapsed, toggleCollapse }: SidebarProps)
|
|
|
23
23
|
|
|
24
24
|
const navItems = [
|
|
25
25
|
{ icon: LayoutDashboard, label: 'Overview', path: '/' },
|
|
26
|
+
{ icon: Activity, label: 'Pulse', path: '/pulse' },
|
|
26
27
|
{ icon: ListTree, label: 'Queues', path: '/queues' },
|
|
27
28
|
{ icon: Clock, label: 'Schedules', path: '/schedules' },
|
|
28
29
|
{ icon: HardDrive, label: 'Workers', path: '/workers' },
|
|
29
|
-
{ icon: Activity, label: 'Metrics', path: '/metrics' },
|
|
30
|
+
// { icon: Activity, label: 'Metrics', path: '/metrics' },
|
|
30
31
|
{ icon: Settings, label: 'Settings', path: '/settings' },
|
|
31
32
|
]
|
|
32
33
|
|
|
@@ -36,23 +36,25 @@ export function WorkerStatus({
|
|
|
36
36
|
const onlineCount = workers.filter((w) => w.status === 'online').length
|
|
37
37
|
|
|
38
38
|
return (
|
|
39
|
-
<div className="card-premium
|
|
40
|
-
<div className="
|
|
41
|
-
<div>
|
|
42
|
-
<
|
|
43
|
-
<
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
<div className="card-premium h-full flex flex-col overflow-hidden">
|
|
40
|
+
<div className="p-6 pb-0 flex-none">
|
|
41
|
+
<div className="flex justify-between items-center mb-6">
|
|
42
|
+
<div>
|
|
43
|
+
<h3 className="text-lg font-black flex items-center gap-2 tracking-tight">
|
|
44
|
+
<Cpu size={20} className="text-primary" />
|
|
45
|
+
Cluster Nodes
|
|
46
|
+
</h3>
|
|
47
|
+
<p className="text-[10px] text-muted-foreground uppercase font-black tracking-widest opacity-60">
|
|
48
|
+
Real-time load
|
|
49
|
+
</p>
|
|
50
|
+
</div>
|
|
51
|
+
<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">
|
|
52
|
+
{onlineCount} ACTIVE
|
|
53
|
+
</span>
|
|
49
54
|
</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
55
|
</div>
|
|
54
56
|
|
|
55
|
-
<div className="space-y-3">
|
|
57
|
+
<div className="flex-1 overflow-y-auto min-h-0 px-6 space-y-3 scrollbar-thin pb-6">
|
|
56
58
|
{workers.length === 0 && (
|
|
57
59
|
<div className="py-12 text-center text-muted-foreground/30 flex flex-col items-center gap-2">
|
|
58
60
|
<Activity size={24} className="opacity-20 animate-pulse" />
|
|
@@ -64,7 +66,7 @@ export function WorkerStatus({
|
|
|
64
66
|
<div
|
|
65
67
|
key={worker.id}
|
|
66
68
|
className={cn(
|
|
67
|
-
'flex items-center justify-between p-4 rounded-2xl bg-muted/10 border transition-all group overflow-hidden relative',
|
|
69
|
+
'flex items-center justify-between p-4 rounded-2xl bg-muted/10 border transition-all group overflow-hidden relative shrink-0',
|
|
68
70
|
worker.id === highlightedWorkerId
|
|
69
71
|
? 'border-primary/50 bg-primary/5 shadow-[0_0_20px_rgba(var(--primary-rgb),0.1)] -translate-y-1 scale-[1.02] z-10'
|
|
70
72
|
: 'border-transparent hover:border-primary/20 hover:bg-muted/20'
|
|
@@ -159,12 +161,14 @@ export function WorkerStatus({
|
|
|
159
161
|
))}
|
|
160
162
|
</div>
|
|
161
163
|
|
|
162
|
-
<
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
164
|
+
<div className="p-6 pt-0 flex-none">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
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"
|
|
168
|
+
>
|
|
169
|
+
Manage Nodes
|
|
170
|
+
</button>
|
|
171
|
+
</div>
|
|
168
172
|
</div>
|
|
169
173
|
)
|
|
170
174
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { SVGProps } from 'react'
|
|
2
|
+
|
|
3
|
+
export function NodeIcon(props: SVGProps<SVGSVGElement>) {
|
|
4
|
+
return (
|
|
5
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
6
|
+
<path 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" fill="#339933" />
|
|
7
|
+
<path d="M16 22.5l-6-3.4v-6.8l6-3.4 6 3.4v6.8l-6 3.4z" fill="#339933" />
|
|
8
|
+
</svg>
|
|
9
|
+
)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function BunIcon(props: SVGProps<SVGSVGElement>) {
|
|
13
|
+
return (
|
|
14
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
15
|
+
{/* Outer Outline/Shadow for contrast on light bg */}
|
|
16
|
+
<path 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" fill="#fbf0df" stroke="#4a4a4a" strokeWidth="1.5" />
|
|
17
|
+
|
|
18
|
+
<path fill="#37474f" 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" />
|
|
19
|
+
<ellipse cx="22.5" cy="18.5" fill="#f8bbd0" rx="2.5" ry="1.5" />
|
|
20
|
+
<ellipse cx="9.5" cy="18.5" fill="#f8bbd0" rx="2.5" ry="1.5" />
|
|
21
|
+
<circle cx="10" cy="16" r="2" fill="#37474f" />
|
|
22
|
+
<circle cx="22" cy="16" r="2" fill="#37474f" />
|
|
23
|
+
</svg>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function DenoIcon(props: SVGProps<SVGSVGElement>) {
|
|
28
|
+
return (
|
|
29
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
30
|
+
<circle cx="16" cy="16" r="14" fill="currentColor" />
|
|
31
|
+
<path 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" fill="white" />
|
|
32
|
+
<circle cx="12" cy="18" r="2" fill="black" />
|
|
33
|
+
</svg>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function PhpIcon(props: SVGProps<SVGSVGElement>) {
|
|
38
|
+
return (
|
|
39
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
40
|
+
<ellipse cx="16" cy="16" rx="14" ry="10" fill="#777BB4" />
|
|
41
|
+
<text x="50%" y="54%" dominantBaseline="middle" textAnchor="middle" fill="white" fontSize="9" fontWeight="bold">PHP</text>
|
|
42
|
+
</svg>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function GoIcon(props: SVGProps<SVGSVGElement>) {
|
|
47
|
+
return (
|
|
48
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
49
|
+
<path 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" fill="#00ADD8" />
|
|
50
|
+
<circle cx="9" cy="16" r="2" fill="white" />
|
|
51
|
+
<circle cx="23" cy="9" r="2" fill="white" />
|
|
52
|
+
</svg>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function PythonIcon(props: SVGProps<SVGSVGElement>) {
|
|
57
|
+
return (
|
|
58
|
+
<svg viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
|
|
59
|
+
<path d="M16 2C10 2 10 5 10 5L10 9L18 9L18 11L8 11L8 20L12 20L12 14L22 14C22 14 22 12 16 2Z" fill="#3776AB" />
|
|
60
|
+
<path d="M16 30C22 30 22 27 22 27L22 23L14 23L14 21L24 21L24 12L20 12L20 18L10 18C10 18 10 20 16 30Z" fill="#FFD43B" />
|
|
61
|
+
</svg>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { LucideIcon } from 'lucide-react'
|
|
2
|
+
import { 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({ icon: Icon, title, description, children, className }: PageHeaderProps) {
|
|
14
|
+
return (
|
|
15
|
+
<div className={cn("flex justify-between items-end", className)}>
|
|
16
|
+
<div>
|
|
17
|
+
<h1 className="text-4xl font-black tracking-tighter flex items-center gap-3">
|
|
18
|
+
<div className="p-2 bg-primary/10 rounded-xl text-primary">
|
|
19
|
+
<Icon size={32} />
|
|
20
|
+
</div>
|
|
21
|
+
{title}
|
|
22
|
+
</h1>
|
|
23
|
+
{description && (
|
|
24
|
+
<p className="text-muted-foreground mt-2 text-sm font-bold opacity-60 uppercase tracking-widest pl-[3.75rem]">
|
|
25
|
+
{description}
|
|
26
|
+
</p>
|
|
27
|
+
)}
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
{children}
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
)
|
|
34
|
+
}
|
|
@@ -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,19 +486,24 @@ 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>
|
|
510
507
|
)
|
|
511
508
|
}
|
|
509
|
+
|