@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/package.json CHANGED
@@ -1,50 +1,54 @@
1
1
  {
2
- "name": "@gravito/zenith",
3
- "version": "0.1.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
- "seed": "bun scripts/seed.ts",
19
- "worker": "bun scripts/worker.ts"
20
- },
21
- "dependencies": {
22
- "@gravito/atlas": "workspace:*",
23
- "@gravito/photon": "workspace:*",
24
- "@gravito/stream": "workspace:*",
25
- "@tanstack/react-query": "^5.0.0",
26
- "clsx": "^2.1.1",
27
- "date-fns": "^4.1.0",
28
- "framer-motion": "^12.23.26",
29
- "ioredis": "^5.0.0",
30
- "lucide-react": "^0.562.0",
31
- "react": "^19.0.0",
32
- "react-dom": "^19.0.0",
33
- "react-router-dom": "^7.11.0",
34
- "recharts": "^3.6.0",
35
- "tailwind-merge": "^3.4.0"
36
- },
37
- "devDependencies": {
38
- "@types/react": "^19.0.0",
39
- "@types/react-dom": "^19.0.0",
40
- "@vitejs/plugin-react": "^5.1.2",
41
- "autoprefixer": "^10.4.0",
42
- "postcss": "^8.4.0",
43
- "tailwindcss": "^3.4.0",
44
- "typescript": "^5.0.0",
45
- "vite": "^6.0.0"
46
- },
47
- "publishConfig": {
48
- "access": "public"
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.
@@ -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>
@@ -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
- id: 'sys-logout',
251
- title: 'Logout',
252
- description: 'Sign out from the console',
253
- icon: <LogOut size={18} />,
254
- category: 'System' as const,
255
- action: logout,
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',
@@ -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 p-6 h-full">
40
- <div className="flex justify-between items-center mb-8">
41
- <div>
42
- <h3 className="text-lg font-black flex items-center gap-2 tracking-tight">
43
- <Cpu size={20} className="text-primary" />
44
- Cluster Nodes
45
- </h3>
46
- <p className="text-[10px] text-muted-foreground uppercase font-black tracking-widest opacity-60">
47
- Real-time load
48
- </p>
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
- <button
163
- type="button"
164
- className="w-full mt-8 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"
165
- >
166
- Manage Nodes
167
- </button>
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, useQueryClient } from '@tanstack/react-query'
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<HTMLDivElement>(null)
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 h-full flex flex-col overflow-hidden group">
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="grid grid-cols-1 lg:grid-cols-3 gap-8 h-[600px]">
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
- <LiveLogs
502
- logs={logs}
503
- onSearchArchive={() => setIsLogArchiveOpen(true)}
504
- onWorkerHover={setHoveredWorkerId}
505
- />
506
- <QueueList queues={queues} setSelectedQueue={setSelectedQueue} />
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
+