@gravito/zenith 1.0.0-beta.1 → 1.0.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 (35) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bin.js +436 -43
  3. package/dist/client/assets/index-C332gZ-J.css +1 -0
  4. package/dist/client/assets/{index-oXEse8ih.js → index-D4HibwTK.js} +88 -88
  5. package/dist/client/index.html +2 -2
  6. package/dist/server/index.js +436 -43
  7. package/docs/LARAVEL_ZENITH_ROADMAP.md +109 -0
  8. package/{QUASAR_MASTER_PLAN.md → docs/QUASAR_MASTER_PLAN.md} +6 -3
  9. package/package.json +1 -1
  10. package/scripts/debug_redis_keys.ts +24 -0
  11. package/src/client/App.tsx +1 -1
  12. package/src/client/Layout.tsx +11 -12
  13. package/src/client/WorkerStatus.tsx +97 -56
  14. package/src/client/components/BrandIcons.tsx +119 -44
  15. package/src/client/components/ConfirmDialog.tsx +0 -1
  16. package/src/client/components/JobInspector.tsx +18 -6
  17. package/src/client/components/PageHeader.tsx +32 -28
  18. package/src/client/pages/OverviewPage.tsx +0 -1
  19. package/src/client/pages/PulsePage.tsx +422 -340
  20. package/src/client/pages/SettingsPage.tsx +69 -15
  21. package/src/client/pages/WorkersPage.tsx +70 -2
  22. package/src/server/index.ts +171 -11
  23. package/src/server/services/QueueService.ts +6 -3
  24. package/src/shared/types.ts +2 -0
  25. package/ARCHITECTURE.md +0 -88
  26. package/BATCH_OPERATIONS_IMPLEMENTATION.md +0 -159
  27. package/EVOLUTION_BLUEPRINT.md +0 -112
  28. package/JOBINSPECTOR_SCROLL_FIX.md +0 -152
  29. package/TESTING_BATCH_OPERATIONS.md +0 -252
  30. package/dist/client/assets/index-BSTyMCFd.css +0 -1
  31. /package/{ALERTING_GUIDE.md → docs/ALERTING_GUIDE.md} +0 -0
  32. /package/{DEPLOYMENT.md → docs/DEPLOYMENT.md} +0 -0
  33. /package/{DOCS_INTERNAL.md → docs/DOCS_INTERNAL.md} +0 -0
  34. /package/{QUICK_TEST_GUIDE.md → docs/QUICK_TEST_GUIDE.md} +0 -0
  35. /package/{ROADMAP.md → docs/ROADMAP.md} +0 -0
@@ -0,0 +1,109 @@
1
+ # 🚀 Project Zenith: Laravel Integration Roadmap
2
+
3
+ **Repository**: `gravito-framework/laravel-zenith`
4
+ **Target Audience**: Laravel 10/11 Applications
5
+ **Goal**: Provide deep, native introspection into Laravel applications for Gravito Zenith.
6
+
7
+ ---
8
+
9
+ ## 1. Vision & Architecture
10
+
11
+ Unlike the **Quasar Agent** (which is a sidecar daemon for OS/Infrastructure monitoring), **Laravel Zenith** is a native Composer package that lives *inside* the application.
12
+
13
+ * **Role**: " The Reporter". It sees what the OS cannot see.
14
+ * **Transport**: Direct Redis connection (utilizing `swarrot` or standard `predis`/`phpredis`).
15
+ * **Philosophy**: Zero-blocking. All reporting should be "fire-and-forget" or queued to avoid slowing down the user request lifecycle.
16
+
17
+ ---
18
+
19
+ ## 2. Core Features (The "Why")
20
+
21
+ ### A. Live Operational Logs (`logs`)
22
+ * **Feature**: A custom `Log Channel` driver.
23
+ * **Goal**: Stream logs (Info/Error/Debug) directly to Zenith's Live Log view.
24
+ * **Implementation**:
25
+ * `config/logging.php`: Add a `zenith` channel.
26
+ * Push JSON payloads to `flux_console:logs` Redis channel.
27
+
28
+ ### B. Queue Lifecycle Events (`queues`)
29
+ * **Feature**: Listen to Laravel Queue Events (`JobProcessing`, `JobProcessed`, `JobFailed`).
30
+ * **Goal**: Provide granular job insight that `quasar-go` cannot (e.g., "Job X failed with Exception Y", "Job Z took 45s").
31
+ * **Implementation**:
32
+ * Event Subscriber for `Illuminate\Queue\Events\*`.
33
+ * Capture `job->getRawBody()`, `exception->getMessage()`.
34
+
35
+ ### C. Request Performance (`http`)
36
+ * **Feature**: Global Middleware (`ZenithMonitorMiddleware`).
37
+ * **Goal**: Track "Slow Requests", 500 Errors, and Throughput.
38
+ * **Metrics**:
39
+ * Status Codes (2xx, 4xx, 5xx).
40
+ * Duration (ms).
41
+ * Route Name / Controller Action.
42
+
43
+ ### D. System Health Checks
44
+ * **Feature**: `php artisan zenith:check`
45
+ * **Goal**: Verify Redis connection and permissions.
46
+
47
+ ---
48
+
49
+ ## 3. Implementation Roadmap
50
+
51
+ ### Phase 1: The Foundation (Logs & Config)
52
+ **Goal**: Get the package installed and streaming basic logs.
53
+ - [ ] Initialize Repository `gravito-framework/laravel-zenith`.
54
+ - [ ] Create `ZenithServiceProvider`.
55
+ - [ ] Implement `ZenithLogger` (Monolog Handler).
56
+ - [ ] Publishing `config/zenith.php` (Redis connection settings).
57
+ - [ ] **Deliverable**: `Log::info('Hello Zenith')` appears in Zenith UI.
58
+
59
+ ### Phase 2: The Worker's Eye (Queues)
60
+ **Goal**: Deep visibility into Queue Jobs.
61
+ - [ ] Create `ZenithQueueSubscriber`.
62
+ - [ ] Handle `JobFailed`: Serialize exception and push to Zenith Alerting.
63
+ - [ ] Handle `JobProcessed`: Record metrics for "Jobs per minute".
64
+ - [ ] **Deliverable**: Seeing real-time "Job Completed" toasts and Error details in Zenith.
65
+
66
+ ### Phase 3: The Watchtower (HTTP & Exceptions)
67
+ **Goal**: Monitoring web requests.
68
+ - [ ] Create `RecordRequestMetrics` Middleware.
69
+ - [ ] Exception Handler integration (optional, for global error catching).
70
+ - [ ] Filter logic (ignore `/nova`, `/telescope`, etc.).
71
+ - [ ] **Deliverable**: HTTP Throughput graphs in Zenith.
72
+
73
+ ### Phase 4: The Bridge (Remote Control Hooks)
74
+ **Goal**: Allow Zenith to trigger Laravel actions safely.
75
+ - [ ] Expose internal hooks for `quasar-go` to call?
76
+ * *Note*: `quasar-go` already calls `artisan`. Phase 4 might be about ensuring `artisan zenith:run-job {id}` exists if we need advanced job re-running that `queue:retry` can't handle.
77
+
78
+ ---
79
+
80
+ ## 4. Technical Specifications
81
+
82
+ ### Redis Protocol
83
+ We will reuse the **Gravito Pulse Protocol (GPP)** used by `quasar-go`:
84
+ * **Logs**: `PUBLISH flux_console:logs`
85
+ * **Metrics**: `INCR flux_console:metrics:...`
86
+
87
+ ### Configuration (`zenith.php`)
88
+ ```php
89
+ return [
90
+ 'enabled' => env('ZENITH_ENABLED', true),
91
+
92
+ 'connection' => env('ZENITH_REDIS_CONNECTION', 'default'),
93
+
94
+ 'logging' => [
95
+ 'enabled' => true,
96
+ 'level' => 'debug',
97
+ ],
98
+
99
+ 'queues' => [
100
+ 'monitor_all' => true,
101
+ 'ignore_jobs' => [],
102
+ ],
103
+ ];
104
+ ```
105
+
106
+ ### Dependency Strategy
107
+ * **Support**: Laravel 10.x, 11.x
108
+ * **Php**: 8.1+
109
+ * **Driver**: `phpredis` (preferred) or `predis`.
@@ -22,6 +22,7 @@ We employ a "Right Tool for the Job" strategy for deployment:
22
22
  | :--- | :--- | :--- | :--- |
23
23
  | **Node.js / Bun** | **SDK** | `@gravito/quasar` | **In-App Integration**. Directly imports into the app. Captures Event Loop, Heap, and Queues. |
24
24
  | **Legacy / Polyglot** | **Agent** | `gravito/quasar-agent` | **Sidecar / Daemon**. Standalone Go binary. Captures OS-level metrics and external Queue states via Redis/API. |
25
+ | **PHP / Laravel** | **Package** | `gravito/laravel-zenith` | **Native Integration**. Laravel Service Provider. Captures Jobs, Logs, and Exceptions. |
25
26
 
26
27
  ### 🚀 Deployment Methods (Zero Friction)
27
28
  1. **NPM**: `npm install @gravito/quasar` (For Node developers)
@@ -77,14 +78,14 @@ All agents/SDKs report to Redis using this unified schema.
77
78
 
78
79
  ---
79
80
 
80
- ### Phase 2: Architecture Evolution - "The Brain-Hand Model" 🧠 🖐️
81
+ ### Phase 2: Architecture Evolution - "The Brain-Hand Model" 🧠 🖐️ - **Completed** ✅
81
82
  To support advanced features like **Queue Insights** (Phase 2) and **Remote Control** (Phase 3), we are adopting a bidirectional architecture.
82
83
 
83
84
  * **Metric Transport (The Mouth)**: Agent sends metrics to Zenith (via shared Redis).
84
85
  * **Local Insight (The Eyes)**: Agent inspects *its own* environment (Local Redis, Local Queue) to gather data. Zenith doesn't need to connect to the App DB directly.
85
86
  * **Command execution (The Hand)**: Zenith publishes commands (Retry/Delete), and Agent listens and executes them locally.
86
87
 
87
- #### Revised Phase 2: Application Insights (Queues) - **In Progress** 🟡
88
+ #### Revised Phase 2: Application Insights (Queues) - **Completed**
88
89
  **Goal**: Enable Quasar Agent to "see" local queues and report their status.
89
90
 
90
91
  - [x] **SDK Architecture**: Update `QuasarAgent` to handle **Dual Connections**:
@@ -110,7 +111,7 @@ To support advanced features like **Queue Insights** (Phase 2) and **Remote Cont
110
111
  - [x] **UI**: Add "Retry/Delete" buttons in Zenith `PulsePage` for failed queue jobs.
111
112
  - [x] **Documentation**: Created `ALERTING_GUIDE.md` for configuration best practices.
112
113
 
113
- ### Phase 4: Polyglot Agent - **In Progress** 🟡
114
+ ### Phase 4: Polyglot Agent - **Completed**
114
115
  * [x] Create `gravito-framework/quasar` repo (`quasar-go`).
115
116
  * [x] Develop Go Agent core (utilizing `gopsutil`).
116
117
  * [x] System Probe (CPU/RAM)
@@ -125,6 +126,8 @@ To support advanced features like **Queue Insights** (Phase 2) and **Remote Cont
125
126
  * [x] **Laravel Deep Integration**:
126
127
  * [x] `LARAVEL_ACTION` Executor (runs `artisan` safely).
127
128
  * [x] Auto-discovery of Laravel project root via process inspection.
129
+ * [x] **Advanced Process Introspection**: Captures real-time CPU/RAM usage per Laravel Worker process.
130
+ * [x] **Virtual Node Mapping**: Visualizes individual Laravel Workers as distinct nodes in Zenith UI.
128
131
  * [x] Support for `retry-all`, `retry {id}`, and `restart` (graceful worker reload).
129
132
  * [x] Docker & Makefile setup.
130
133
  * [x] Binary Release pipeline (GitHub Actions).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravito/zenith",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.1",
4
4
  "description": "Gravito Zenith: Zero-config control plane for Gravito Flux & Stream",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,24 @@
1
+ import { Redis } from 'ioredis'
2
+
3
+ const redis = new Redis('redis://localhost:6379')
4
+
5
+ async function check() {
6
+ console.log('Connecting to Redis...')
7
+ try {
8
+ const keys = await redis.keys('gravito:quasar:node:*')
9
+ console.log('Keys found count:', keys.length)
10
+ console.log('Keys:', keys)
11
+
12
+ if (keys.length > 0) {
13
+ const val = await redis.get(keys[0])
14
+ console.log('--- Value of first key ---')
15
+ console.log(val)
16
+ console.log('--- End Value ---')
17
+ }
18
+ } catch (err) {
19
+ console.error('Redis Error:', err)
20
+ }
21
+ process.exit(0)
22
+ }
23
+
24
+ check()
@@ -8,11 +8,11 @@ import {
8
8
  LoginPage,
9
9
  MetricsPage,
10
10
  OverviewPage,
11
+ PulsePage,
11
12
  QueuesPage,
12
13
  SchedulesPage,
13
14
  SettingsPage,
14
15
  WorkersPage,
15
- PulsePage,
16
16
  } from './pages'
17
17
 
18
18
  const queryClient = new QueryClient()
@@ -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
@@ -116,7 +116,7 @@ export function Layout({ children }: LayoutProps) {
116
116
  fetch('/api/queues')
117
117
  .then((res) => res.json())
118
118
  .then(setQueueData)
119
- .catch(() => { })
119
+ .catch(() => {})
120
120
 
121
121
  // Optional: Listen to global stats if available (from OverviewPage) to keep queue stats fresh in command palette
122
122
  const handler = (e: Event) => {
@@ -255,15 +255,15 @@ export function Layout({ children }: LayoutProps) {
255
255
  },
256
256
  ...(isAuthEnabled
257
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
+ {
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
+ ]
267
267
  : []),
268
268
  ]
269
269
 
@@ -537,7 +537,6 @@ export function Layout({ children }: LayoutProps) {
537
537
  <div className="p-6 border-b flex items-center gap-4 bg-muted/5">
538
538
  <Command className="text-primary animate-pulse" size={24} />
539
539
  <input
540
- autoFocus
541
540
  type="text"
542
541
  placeholder="Execute command or navigate..."
543
542
  className="flex-1 bg-transparent border-none outline-none text-lg font-bold placeholder:text-muted-foreground/30"
@@ -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,12 +58,6 @@ 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 (
@@ -66,10 +91,10 @@ export function WorkerStatus({
66
91
  <div
67
92
  key={worker.id}
68
93
  className={cn(
69
- 'flex items-center justify-between p-4 rounded-2xl bg-muted/10 border transition-all group overflow-hidden relative shrink-0',
94
+ 'relative flex items-center gap-4 p-4 rounded-2xl border transition-all group overflow-hidden shrink-0',
70
95
  worker.id === highlightedWorkerId
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'
72
- : 'border-transparent hover:border-primary/20 hover:bg-muted/20'
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'
73
98
  )}
74
99
  >
75
100
  {/* Status bar */}
@@ -80,80 +105,96 @@ export function WorkerStatus({
80
105
  )}
81
106
  />
82
107
 
83
- <div className="flex items-center gap-4">
84
- <div className="relative">
85
- <div
86
- className={cn(
87
- 'w-3 h-3 rounded-full',
88
- worker.status === 'online'
89
- ? 'bg-green-500 animate-pulse shadow-[0_0_12px_rgba(34,197,94,0.6)]'
90
- : 'bg-muted-foreground/40'
91
- )}
92
- ></div>
93
- </div>
94
- <div>
95
- <p className="text-sm font-black tracking-tight group-hover:text-primary transition-colors">
96
- {worker.id}
97
- </p>
98
- <div className="flex items-center gap-2">
99
- <span className="text-[9px] font-black uppercase tracking-tighter opacity-50">
100
- {worker.status}
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
101
141
  </span>
102
- <span className="text-[9px] font-black text-primary/60">PID {worker.pid}</span>
103
- </div>
142
+ )}
104
143
  </div>
105
144
  </div>
106
145
 
107
- <div className="flex items-center gap-6">
146
+ {/* Metrics (Right Side) */}
147
+ <div className="flex items-center gap-3 text-right shrink-0">
108
148
  {worker.metrics && (
109
- <div className="flex gap-4">
110
- <div className="space-y-1.5 w-20">
149
+ <>
150
+ <div className="hidden sm:block space-y-1 w-12">
111
151
  <div className="flex justify-between text-[8px] font-black text-muted-foreground uppercase tracking-tighter">
112
- <span>LOAD ({worker.metrics.cores || '-'})</span>
152
+ <span>CPU</span>
113
153
  <span
114
154
  className={cn(
115
- worker.metrics.cpu > (worker.metrics.cores || 4) && 'text-red-500'
155
+ worker.metrics.cpu > (worker.metrics.cores || 1) * 100 && 'text-red-500'
116
156
  )}
117
157
  >
118
- {worker.metrics.cpu.toFixed(2)}
158
+ {worker.metrics.cpu.toFixed(0)}%
119
159
  </span>
120
160
  </div>
121
161
  <div className="h-1 w-full bg-muted rounded-full overflow-hidden">
122
162
  <div
123
- className={cn(
124
- 'h-full transition-all duration-700',
125
- worker.metrics.cpu > (worker.metrics.cores || 4)
126
- ? 'bg-red-500'
127
- : worker.metrics.cpu > (worker.metrics.cores || 4) * 0.7
128
- ? 'bg-amber-500'
129
- : 'bg-green-500'
130
- )}
131
- style={{
132
- width: `${Math.min(100, (worker.metrics.cpu / (worker.metrics.cores || 1)) * 100)}%`,
133
- }}
134
- />
163
+ className="h-full bg-foreground transition-all duration-700"
164
+ style={{ width: `${Math.min(100, worker.metrics.cpu)}%` }}
165
+ ></div>
135
166
  </div>
136
167
  </div>
137
- <div className="space-y-1.5 w-16">
168
+
169
+ <div className="hidden sm:block space-y-1 w-12">
138
170
  <div className="flex justify-between text-[8px] font-black text-muted-foreground uppercase tracking-tighter">
139
171
  <span>RAM</span>
140
- <span>{Math.round(worker.metrics.ram.rss / 1024)}G</span>
172
+ <span className="truncate ml-1">
173
+ {formatBytes(worker.metrics.ram.rss).split(' ')[0]}
174
+ </span>
141
175
  </div>
142
176
  <div className="h-1 w-full bg-muted rounded-full overflow-hidden">
143
177
  <div
144
178
  className="h-full bg-indigo-500 transition-all duration-700"
145
179
  style={{
146
- width: `${Math.min(100, (worker.metrics.ram.rss / 2048) * 100)}%`,
180
+ width: `${Math.min(100, (worker.metrics.ram.rss / 2000000000) * 100)}%`,
147
181
  }}
148
- />
182
+ ></div>
149
183
  </div>
150
184
  </div>
151
- </div>
185
+ </>
152
186
  )}
153
- <div className="text-right whitespace-nowrap hidden sm:block">
154
- <p className="text-sm font-black tracking-tighter">{worker.uptime}s</p>
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>
155
196
  <p className="text-[8px] text-muted-foreground uppercase font-black tracking-widest opacity-50">
156
- UPTIME
197
+ UP
157
198
  </p>
158
199
  </div>
159
200
  </div>
@@ -1,63 +1,138 @@
1
- import { SVGProps } from 'react'
1
+ import type { SVGProps } from 'react'
2
2
 
3
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
- )
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
+ )
10
20
  }
11
21
 
12
22
  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" />
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
+ />
17
39
 
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
- )
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
+ )
25
50
  }
26
51
 
27
52
  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
- )
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
+ )
35
70
  }
36
71
 
37
72
  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
- )
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
+ )
44
96
  }
45
97
 
46
98
  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
- )
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
+ )
54
116
  }
55
117
 
56
118
  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
- )
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
+ )
63
138
  }