@gravito/zenith 0.1.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.
Files changed (63) hide show
  1. package/ARCHITECTURE.md +88 -0
  2. package/BATCH_OPERATIONS_IMPLEMENTATION.md +159 -0
  3. package/DEMO.md +156 -0
  4. package/DEPLOYMENT.md +157 -0
  5. package/DOCS_INTERNAL.md +73 -0
  6. package/Dockerfile +46 -0
  7. package/Dockerfile.demo-worker +29 -0
  8. package/EVOLUTION_BLUEPRINT.md +112 -0
  9. package/JOBINSPECTOR_SCROLL_FIX.md +152 -0
  10. package/PULSE_IMPLEMENTATION_PLAN.md +111 -0
  11. package/QUICK_TEST_GUIDE.md +72 -0
  12. package/README.md +33 -0
  13. package/ROADMAP.md +85 -0
  14. package/TESTING_BATCH_OPERATIONS.md +252 -0
  15. package/bin/flux-console.ts +2 -0
  16. package/dist/bin.js +108196 -0
  17. package/dist/client/assets/index-DGYEwTDL.css +1 -0
  18. package/dist/client/assets/index-oyTdySX0.js +421 -0
  19. package/dist/client/index.html +13 -0
  20. package/dist/server/index.js +108191 -0
  21. package/docker-compose.yml +40 -0
  22. package/docs/integrations/LARAVEL.md +207 -0
  23. package/package.json +50 -0
  24. package/postcss.config.js +6 -0
  25. package/scripts/flood-logs.ts +21 -0
  26. package/scripts/seed.ts +213 -0
  27. package/scripts/verify-throttle.ts +45 -0
  28. package/scripts/worker.ts +123 -0
  29. package/src/bin.ts +6 -0
  30. package/src/client/App.tsx +70 -0
  31. package/src/client/Layout.tsx +644 -0
  32. package/src/client/Sidebar.tsx +102 -0
  33. package/src/client/ThroughputChart.tsx +135 -0
  34. package/src/client/WorkerStatus.tsx +170 -0
  35. package/src/client/components/ConfirmDialog.tsx +103 -0
  36. package/src/client/components/JobInspector.tsx +524 -0
  37. package/src/client/components/LogArchiveModal.tsx +383 -0
  38. package/src/client/components/NotificationBell.tsx +203 -0
  39. package/src/client/components/Toaster.tsx +80 -0
  40. package/src/client/components/UserProfileDropdown.tsx +177 -0
  41. package/src/client/contexts/AuthContext.tsx +93 -0
  42. package/src/client/contexts/NotificationContext.tsx +103 -0
  43. package/src/client/index.css +174 -0
  44. package/src/client/index.html +12 -0
  45. package/src/client/main.tsx +15 -0
  46. package/src/client/pages/LoginPage.tsx +153 -0
  47. package/src/client/pages/MetricsPage.tsx +408 -0
  48. package/src/client/pages/OverviewPage.tsx +511 -0
  49. package/src/client/pages/QueuesPage.tsx +372 -0
  50. package/src/client/pages/SchedulesPage.tsx +531 -0
  51. package/src/client/pages/SettingsPage.tsx +449 -0
  52. package/src/client/pages/WorkersPage.tsx +316 -0
  53. package/src/client/pages/index.ts +7 -0
  54. package/src/client/utils.ts +6 -0
  55. package/src/server/index.ts +556 -0
  56. package/src/server/middleware/auth.ts +127 -0
  57. package/src/server/services/AlertService.ts +160 -0
  58. package/src/server/services/QueueService.ts +828 -0
  59. package/tailwind.config.js +73 -0
  60. package/tests/placeholder.test.ts +7 -0
  61. package/tsconfig.json +38 -0
  62. package/tsconfig.node.json +12 -0
  63. package/vite.config.ts +27 -0
@@ -0,0 +1,102 @@
1
+ import { motion } from 'framer-motion'
2
+ import {
3
+ Activity,
4
+ ChevronLeft,
5
+ ChevronRight,
6
+ Clock,
7
+ HardDrive,
8
+ LayoutDashboard,
9
+ ListTree,
10
+ Settings,
11
+ } from 'lucide-react'
12
+ import { NavLink, useLocation } from 'react-router-dom'
13
+ import { cn } from './utils'
14
+
15
+ interface SidebarProps {
16
+ className?: string
17
+ collapsed: boolean
18
+ toggleCollapse: () => void
19
+ }
20
+
21
+ export function Sidebar({ className, collapsed, toggleCollapse }: SidebarProps) {
22
+ const location = useLocation()
23
+
24
+ const navItems = [
25
+ { icon: LayoutDashboard, label: 'Overview', path: '/' },
26
+ { icon: ListTree, label: 'Queues', path: '/queues' },
27
+ { icon: Clock, label: 'Schedules', path: '/schedules' },
28
+ { icon: HardDrive, label: 'Workers', path: '/workers' },
29
+ { icon: Activity, label: 'Metrics', path: '/metrics' },
30
+ { icon: Settings, label: 'Settings', path: '/settings' },
31
+ ]
32
+
33
+ return (
34
+ <div
35
+ className={cn(
36
+ 'flex-1 flex flex-col justify-between transition-all duration-300 ease-in-out relative group z-20 overflow-hidden',
37
+ className
38
+ )}
39
+ >
40
+ {/* Nav Items */}
41
+ <nav className="flex-1 px-4 py-6 space-y-2">
42
+ {navItems.map((item, i) => {
43
+ const isActive = location.pathname === item.path
44
+ return (
45
+ <NavLink
46
+ key={i}
47
+ to={item.path}
48
+ className={cn(
49
+ 'w-full flex items-center gap-4 px-4 py-3 rounded-xl transition-all text-muted-foreground group/item relative overflow-hidden',
50
+ isActive
51
+ ? 'bg-primary text-primary-foreground shadow-lg shadow-primary/20 hover:scale-[1.02]'
52
+ : 'hover:bg-muted font-medium hover:text-foreground active:scale-95'
53
+ )}
54
+ >
55
+ <item.icon
56
+ size={22}
57
+ className={cn(
58
+ 'transition-all shrink-0',
59
+ isActive ? 'scale-110' : 'group-hover/item:scale-110'
60
+ )}
61
+ />
62
+ <motion.span
63
+ initial={false}
64
+ animate={{
65
+ opacity: collapsed ? 0 : 1,
66
+ display: collapsed ? 'none' : 'block',
67
+ }}
68
+ className="font-semibold whitespace-nowrap tracking-tight"
69
+ >
70
+ {item.label}
71
+ </motion.span>
72
+ {isActive && (
73
+ <motion.div
74
+ layoutId="active-pill"
75
+ className="absolute left-0 w-1 h-6 bg-primary-foreground rounded-r-full"
76
+ />
77
+ )}
78
+ </NavLink>
79
+ )
80
+ })}
81
+ </nav>
82
+
83
+ {/* Footer / Toggle */}
84
+ <div className="p-4 border-t border-border/50">
85
+ <button
86
+ type="button"
87
+ onClick={toggleCollapse}
88
+ className="w-full flex items-center justify-center h-10 rounded-xl hover:bg-muted text-muted-foreground hover:text-foreground transition-all active:scale-90"
89
+ >
90
+ {collapsed ? (
91
+ <ChevronRight size={20} />
92
+ ) : (
93
+ <div className="flex items-center gap-2 px-2">
94
+ <ChevronLeft size={18} />{' '}
95
+ <span className="text-xs font-black uppercase tracking-widest">Collapse Sidebar</span>
96
+ </div>
97
+ )}
98
+ </button>
99
+ </div>
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,135 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import React from 'react'
3
+ import {
4
+ Area,
5
+ AreaChart,
6
+ CartesianGrid,
7
+ ResponsiveContainer,
8
+ Tooltip,
9
+ XAxis,
10
+ YAxis,
11
+ } from 'recharts'
12
+
13
+ interface ThroughputPoint {
14
+ timestamp: string
15
+ count: number
16
+ }
17
+
18
+ export function ThroughputChart() {
19
+ // Initial fetch via React Query
20
+ const { data: initialData } = useQuery({
21
+ queryKey: ['throughput'],
22
+ queryFn: async () => {
23
+ const res = await fetch('/api/throughput')
24
+ const json = await res.json()
25
+ return json.data || []
26
+ },
27
+ staleTime: Infinity, // Don't refetch automatically
28
+ })
29
+
30
+ const [throughputData, setThroughputData] = React.useState<ThroughputPoint[]>([])
31
+
32
+ // Sync with initial data
33
+ React.useEffect(() => {
34
+ if (initialData) {
35
+ setThroughputData(initialData)
36
+ }
37
+ }, [initialData])
38
+
39
+ // Listen for live updates
40
+ React.useEffect(() => {
41
+ const handler = (e: Event) => {
42
+ const customEvent = e as CustomEvent
43
+ if (customEvent.detail?.throughput) {
44
+ setThroughputData(customEvent.detail.throughput)
45
+ }
46
+ }
47
+ window.addEventListener('flux-stats-update', handler)
48
+ return () => window.removeEventListener('flux-stats-update', handler)
49
+ }, [])
50
+
51
+ const chartData =
52
+ throughputData?.map((d: ThroughputPoint) => ({
53
+ time: d.timestamp,
54
+ value: d.count,
55
+ })) || []
56
+ return (
57
+ <div className="card-premium h-[350px] w-full p-6 flex flex-col relative overflow-hidden group">
58
+ {/* Background Accent */}
59
+ <div className="absolute top-0 right-0 w-64 h-64 bg-primary/5 rounded-full -translate-y-1/2 translate-x-1/2 blur-3xl" />
60
+
61
+ <div className="flex justify-between items-start mb-6 z-10">
62
+ <div>
63
+ <div className="flex items-center gap-2">
64
+ <h3 className="text-xl font-bold tracking-tight">System Throughput</h3>
65
+ <div className="flex items-center gap-1.5 px-2 py-0.5 bg-green-500/10 text-green-500 text-[8px] font-black uppercase tracking-widest rounded-full border border-green-500/20">
66
+ <span className="w-1 h-1 bg-green-500 rounded-full animate-ping"></span>
67
+ Live
68
+ </div>
69
+ </div>
70
+ <p className="text-[10px] text-muted-foreground uppercase tracking-[0.2em] font-bold mt-1">
71
+ Jobs processed per minute
72
+ </p>
73
+ </div>
74
+ <div className="text-right">
75
+ <p className="text-2xl font-black text-foreground">
76
+ {chartData[chartData.length - 1]?.value || 0}
77
+ </p>
78
+ <p className="text-[8px] text-muted-foreground uppercase font-bold">Current Rate</p>
79
+ </div>
80
+ </div>
81
+
82
+ <div className="flex-1 w-full min-h-0">
83
+ <ResponsiveContainer width="100%" height="100%">
84
+ <AreaChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
85
+ <defs>
86
+ <linearGradient id="colorValue" x1="0" y1="0" x2="0" y2="1">
87
+ <stop offset="5%" stopColor="hsl(var(--primary))" stopOpacity={0.4} />
88
+ <stop offset="50%" stopColor="hsl(var(--primary))" stopOpacity={0.1} />
89
+ <stop offset="95%" stopColor="hsl(var(--primary))" stopOpacity={0} />
90
+ </linearGradient>
91
+ </defs>
92
+ <CartesianGrid
93
+ strokeDasharray="3 3"
94
+ vertical={false}
95
+ stroke="hsl(var(--border))"
96
+ opacity={0.5}
97
+ />
98
+ <XAxis
99
+ dataKey="time"
100
+ axisLine={false}
101
+ tickLine={false}
102
+ tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
103
+ dy={10}
104
+ />
105
+ <YAxis
106
+ axisLine={false}
107
+ tickLine={false}
108
+ tick={{ fontSize: 9, fill: 'hsl(var(--muted-foreground))', fontWeight: 600 }}
109
+ />
110
+ <Tooltip
111
+ cursor={{ stroke: 'hsl(var(--primary))', strokeWidth: 1, strokeDasharray: '4 4' }}
112
+ contentStyle={{
113
+ backgroundColor: 'hsl(var(--card))',
114
+ border: '1px solid hsl(var(--border))',
115
+ borderRadius: '16px',
116
+ fontSize: '12px',
117
+ boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1)',
118
+ }}
119
+ itemStyle={{ fontWeight: 'bold', color: 'hsl(var(--primary))' }}
120
+ />
121
+ <Area
122
+ type="stepAfter"
123
+ dataKey="value"
124
+ stroke="hsl(var(--primary))"
125
+ fillOpacity={1}
126
+ fill="url(#colorValue)"
127
+ strokeWidth={2.5}
128
+ animationDuration={1500}
129
+ />
130
+ </AreaChart>
131
+ </ResponsiveContainer>
132
+ </div>
133
+ </div>
134
+ )
135
+ }
@@ -0,0 +1,170 @@
1
+ import { type ClassValue, clsx } from 'clsx'
2
+ import { Activity, Cpu } from 'lucide-react'
3
+ import { twMerge } from 'tailwind-merge'
4
+
5
+ function cn(...inputs: ClassValue[]) {
6
+ return twMerge(clsx(inputs))
7
+ }
8
+
9
+ interface WorkerInfo {
10
+ id: string
11
+ status: 'online' | 'offline'
12
+ pid: number
13
+ uptime: number
14
+ metrics?: {
15
+ cpu: number
16
+ cores: number
17
+ ram: {
18
+ rss: number
19
+ }
20
+ }
21
+ }
22
+
23
+ export function WorkerStatus({
24
+ highlightedWorkerId,
25
+ workers = [],
26
+ }: {
27
+ highlightedWorkerId?: string | null
28
+ workers?: WorkerInfo[]
29
+ }) {
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
+ const onlineCount = workers.filter((w) => w.status === 'online').length
37
+
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>
49
+ </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
+ </div>
54
+
55
+ <div className="space-y-3">
56
+ {workers.length === 0 && (
57
+ <div className="py-12 text-center text-muted-foreground/30 flex flex-col items-center gap-2">
58
+ <Activity size={24} className="opacity-20 animate-pulse" />
59
+ <p className="text-[10px] font-black uppercase tracking-widest">No nodes connected</p>
60
+ </div>
61
+ )}
62
+
63
+ {workers.map((worker) => (
64
+ <div
65
+ key={worker.id}
66
+ className={cn(
67
+ 'flex items-center justify-between p-4 rounded-2xl bg-muted/10 border transition-all group overflow-hidden relative',
68
+ worker.id === highlightedWorkerId
69
+ ? '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
+ : 'border-transparent hover:border-primary/20 hover:bg-muted/20'
71
+ )}
72
+ >
73
+ {/* Status bar */}
74
+ <div
75
+ className={cn(
76
+ 'absolute left-0 top-0 bottom-0 w-1 transition-all',
77
+ worker.status === 'online' ? 'bg-green-500' : 'bg-muted-foreground/30'
78
+ )}
79
+ />
80
+
81
+ <div className="flex items-center gap-4">
82
+ <div className="relative">
83
+ <div
84
+ className={cn(
85
+ 'w-3 h-3 rounded-full',
86
+ worker.status === 'online'
87
+ ? 'bg-green-500 animate-pulse shadow-[0_0_12px_rgba(34,197,94,0.6)]'
88
+ : 'bg-muted-foreground/40'
89
+ )}
90
+ ></div>
91
+ </div>
92
+ <div>
93
+ <p className="text-sm font-black tracking-tight group-hover:text-primary transition-colors">
94
+ {worker.id}
95
+ </p>
96
+ <div className="flex items-center gap-2">
97
+ <span className="text-[9px] font-black uppercase tracking-tighter opacity-50">
98
+ {worker.status}
99
+ </span>
100
+ <span className="text-[9px] font-black text-primary/60">PID {worker.pid}</span>
101
+ </div>
102
+ </div>
103
+ </div>
104
+
105
+ <div className="flex items-center gap-6">
106
+ {worker.metrics && (
107
+ <div className="flex gap-4">
108
+ <div className="space-y-1.5 w-20">
109
+ <div className="flex justify-between text-[8px] font-black text-muted-foreground uppercase tracking-tighter">
110
+ <span>LOAD ({worker.metrics.cores || '-'})</span>
111
+ <span
112
+ className={cn(
113
+ worker.metrics.cpu > (worker.metrics.cores || 4) && 'text-red-500'
114
+ )}
115
+ >
116
+ {worker.metrics.cpu.toFixed(2)}
117
+ </span>
118
+ </div>
119
+ <div className="h-1 w-full bg-muted rounded-full overflow-hidden">
120
+ <div
121
+ className={cn(
122
+ 'h-full transition-all duration-700',
123
+ worker.metrics.cpu > (worker.metrics.cores || 4)
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
+ />
133
+ </div>
134
+ </div>
135
+ <div className="space-y-1.5 w-16">
136
+ <div className="flex justify-between text-[8px] font-black text-muted-foreground uppercase tracking-tighter">
137
+ <span>RAM</span>
138
+ <span>{Math.round(worker.metrics.ram.rss / 1024)}G</span>
139
+ </div>
140
+ <div className="h-1 w-full bg-muted rounded-full overflow-hidden">
141
+ <div
142
+ className="h-full bg-indigo-500 transition-all duration-700"
143
+ style={{
144
+ width: `${Math.min(100, (worker.metrics.ram.rss / 2048) * 100)}%`,
145
+ }}
146
+ />
147
+ </div>
148
+ </div>
149
+ </div>
150
+ )}
151
+ <div className="text-right whitespace-nowrap hidden sm:block">
152
+ <p className="text-sm font-black tracking-tighter">{worker.uptime}s</p>
153
+ <p className="text-[8px] text-muted-foreground uppercase font-black tracking-widest opacity-50">
154
+ UPTIME
155
+ </p>
156
+ </div>
157
+ </div>
158
+ </div>
159
+ ))}
160
+ </div>
161
+
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>
168
+ </div>
169
+ )
170
+ }
@@ -0,0 +1,103 @@
1
+ import { AnimatePresence, motion } from 'framer-motion'
2
+ import { createPortal } from 'react-dom'
3
+ import { cn } from '../utils'
4
+
5
+ export interface ConfirmDialogProps {
6
+ open: boolean
7
+ title: string
8
+ message: string
9
+ confirmText?: string
10
+ cancelText?: string
11
+ onConfirm: () => void
12
+ onCancel: () => void
13
+ variant?: 'danger' | 'warning' | 'info'
14
+ isProcessing?: boolean
15
+ }
16
+
17
+ export function ConfirmDialog({
18
+ open,
19
+ title,
20
+ message,
21
+ confirmText = 'Confirm',
22
+ cancelText = 'Cancel',
23
+ onConfirm,
24
+ onCancel,
25
+ variant = 'danger',
26
+ isProcessing = false,
27
+ }: ConfirmDialogProps) {
28
+ return createPortal(
29
+ <AnimatePresence>
30
+ {open && (
31
+ // biome-ignore lint/a11y/noStaticElementInteractions: Backdrop needs click handler to stop propagation
32
+ <div
33
+ role="presentation"
34
+ className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-[5000] pointer-events-auto"
35
+ onClick={(e) => e.stopPropagation()}
36
+ onKeyDown={(e) => e.stopPropagation()}
37
+ >
38
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: Modal content needs to stop propagation */}
39
+ <motion.div
40
+ initial={{ scale: 0.9, opacity: 0 }}
41
+ animate={{ scale: 1, opacity: 1 }}
42
+ exit={{ scale: 0.9, opacity: 0 }}
43
+ transition={{ type: 'spring', damping: 25, stiffness: 200 }}
44
+ className="bg-card border rounded-2xl p-6 max-w-md shadow-2xl"
45
+ onClick={(e) => e.stopPropagation()}
46
+ >
47
+ <h3 className="text-xl font-black mb-2">{title}</h3>
48
+ <p className="text-sm text-muted-foreground mb-6 whitespace-pre-line">{message}</p>
49
+ <div className="flex gap-3 justify-end">
50
+ <button
51
+ type="button"
52
+ onClick={(e) => {
53
+ e.stopPropagation()
54
+ onCancel()
55
+ }}
56
+ disabled={isProcessing}
57
+ className="px-4 py-2 bg-muted text-foreground rounded-lg hover:bg-muted/80 transition-all disabled:opacity-50 disabled:cursor-not-allowed"
58
+ >
59
+ {cancelText}
60
+ </button>
61
+ <button
62
+ type="button"
63
+ onClick={(e) => {
64
+ e.stopPropagation()
65
+ onConfirm()
66
+ }}
67
+ disabled={isProcessing}
68
+ className={cn(
69
+ 'px-4 py-2 rounded-lg text-white transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2',
70
+ variant === 'danger' && 'bg-red-500 hover:bg-red-600',
71
+ variant === 'warning' && 'bg-amber-500 hover:bg-amber-600',
72
+ variant === 'info' && 'bg-blue-500 hover:bg-blue-600'
73
+ )}
74
+ >
75
+ {isProcessing && (
76
+ <svg className="animate-spin h-4 w-4" viewBox="0 0 24 24" aria-label="Loading">
77
+ <title>Loading</title>
78
+ <circle
79
+ className="opacity-25"
80
+ cx="12"
81
+ cy="12"
82
+ r="10"
83
+ stroke="currentColor"
84
+ strokeWidth="4"
85
+ fill="none"
86
+ />
87
+ <path
88
+ className="opacity-75"
89
+ fill="currentColor"
90
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
91
+ />
92
+ </svg>
93
+ )}
94
+ {isProcessing ? 'Processing...' : confirmText}
95
+ </button>
96
+ </div>
97
+ </motion.div>
98
+ </div>
99
+ )}
100
+ </AnimatePresence>,
101
+ document.body
102
+ )
103
+ }