@gravito/zenith 1.0.0-beta.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/bin.js +436 -43
- package/dist/client/assets/index-C332gZ-J.css +1 -0
- package/dist/client/assets/{index-oXEse8ih.js → index-D4HibwTK.js} +88 -88
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +436 -43
- package/docs/LARAVEL_ZENITH_ROADMAP.md +109 -0
- package/{QUASAR_MASTER_PLAN.md → docs/QUASAR_MASTER_PLAN.md} +6 -3
- package/package.json +1 -1
- package/scripts/debug_redis_keys.ts +24 -0
- package/src/client/App.tsx +1 -1
- package/src/client/Layout.tsx +11 -12
- package/src/client/WorkerStatus.tsx +97 -56
- package/src/client/components/BrandIcons.tsx +119 -44
- package/src/client/components/ConfirmDialog.tsx +0 -1
- package/src/client/components/JobInspector.tsx +18 -6
- package/src/client/components/PageHeader.tsx +32 -28
- package/src/client/pages/OverviewPage.tsx +0 -1
- package/src/client/pages/PulsePage.tsx +422 -340
- package/src/client/pages/SettingsPage.tsx +69 -15
- package/src/client/pages/WorkersPage.tsx +70 -2
- package/src/server/index.ts +171 -11
- package/src/server/services/QueueService.ts +6 -3
- package/src/shared/types.ts +2 -0
- package/ARCHITECTURE.md +0 -88
- package/BATCH_OPERATIONS_IMPLEMENTATION.md +0 -159
- package/EVOLUTION_BLUEPRINT.md +0 -112
- package/JOBINSPECTOR_SCROLL_FIX.md +0 -152
- package/TESTING_BATCH_OPERATIONS.md +0 -252
- package/dist/client/assets/index-BSTyMCFd.css +0 -1
- /package/{ALERTING_GUIDE.md → docs/ALERTING_GUIDE.md} +0 -0
- /package/{DEPLOYMENT.md → docs/DEPLOYMENT.md} +0 -0
- /package/{DOCS_INTERNAL.md → docs/DOCS_INTERNAL.md} +0 -0
- /package/{QUICK_TEST_GUIDE.md → docs/QUICK_TEST_GUIDE.md} +0 -0
- /package/{ROADMAP.md → docs/ROADMAP.md} +0 -0
|
@@ -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) - **
|
|
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 - **
|
|
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
|
@@ -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()
|
package/src/client/App.tsx
CHANGED
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
|
|
@@ -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
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
|
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
|
-
? '
|
|
72
|
-
: '
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
103
|
-
</div>
|
|
142
|
+
)}
|
|
104
143
|
</div>
|
|
105
144
|
</div>
|
|
106
145
|
|
|
107
|
-
|
|
146
|
+
{/* Metrics (Right Side) */}
|
|
147
|
+
<div className="flex items-center gap-3 text-right shrink-0">
|
|
108
148
|
{worker.metrics && (
|
|
109
|
-
|
|
110
|
-
<div className="space-y-1
|
|
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>
|
|
152
|
+
<span>CPU</span>
|
|
113
153
|
<span
|
|
114
154
|
className={cn(
|
|
115
|
-
worker.metrics.cpu > (worker.metrics.cores ||
|
|
155
|
+
worker.metrics.cpu > (worker.metrics.cores || 1) * 100 && 'text-red-500'
|
|
116
156
|
)}
|
|
117
157
|
>
|
|
118
|
-
{worker.metrics.cpu.toFixed(
|
|
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=
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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
|
|
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 /
|
|
180
|
+
width: `${Math.min(100, (worker.metrics.ram.rss / 2000000000) * 100)}%`,
|
|
147
181
|
}}
|
|
148
|
-
|
|
182
|
+
></div>
|
|
149
183
|
</div>
|
|
150
184
|
</div>
|
|
151
|
-
|
|
185
|
+
</>
|
|
152
186
|
)}
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
}
|