@gravito/zenith 1.1.2 → 1.1.6
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/README.md +95 -22
- package/README.zh-TW.md +88 -0
- package/dist/bin.js +54699 -39316
- package/dist/client/assets/index-C80c1frR.css +1 -0
- package/dist/client/assets/index-CrWem9u3.js +434 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +54699 -39316
- package/package.json +20 -9
- package/CHANGELOG.md +0 -47
- package/Dockerfile +0 -46
- package/Dockerfile.demo-worker +0 -29
- package/ECOSYSTEM_EXPANSION_RFC.md +0 -130
- package/bin/flux-console.ts +0 -2
- package/dist/client/assets/index-BSMp8oq_.js +0 -436
- package/dist/client/assets/index-BwxlHx-_.css +0 -1
- package/docker-compose.yml +0 -40
- package/docs/ALERTING_GUIDE.md +0 -71
- package/docs/DEPLOYMENT.md +0 -157
- package/docs/DOCS_INTERNAL.md +0 -73
- package/docs/LARAVEL_ZENITH_ROADMAP.md +0 -109
- package/docs/QUASAR_MASTER_PLAN.md +0 -140
- package/docs/QUICK_TEST_GUIDE.md +0 -72
- package/docs/ROADMAP.md +0 -85
- package/docs/integrations/LARAVEL.md +0 -207
- package/postcss.config.js +0 -6
- package/scripts/debug_redis_keys.ts +0 -24
- package/scripts/flood-logs.ts +0 -21
- package/scripts/seed.ts +0 -213
- package/scripts/verify-throttle.ts +0 -49
- package/scripts/worker.ts +0 -124
- package/specs/PULSE_SPEC.md +0 -86
- package/src/bin.ts +0 -6
- package/src/client/App.tsx +0 -72
- package/src/client/Layout.tsx +0 -672
- package/src/client/Sidebar.tsx +0 -112
- package/src/client/ThroughputChart.tsx +0 -144
- package/src/client/WorkerStatus.tsx +0 -226
- package/src/client/components/BrandIcons.tsx +0 -168
- package/src/client/components/ConfirmDialog.tsx +0 -126
- package/src/client/components/JobInspector.tsx +0 -554
- package/src/client/components/LogArchiveModal.tsx +0 -432
- package/src/client/components/NotificationBell.tsx +0 -212
- package/src/client/components/PageHeader.tsx +0 -47
- package/src/client/components/Toaster.tsx +0 -90
- package/src/client/components/UserProfileDropdown.tsx +0 -186
- package/src/client/contexts/AuthContext.tsx +0 -105
- package/src/client/contexts/NotificationContext.tsx +0 -128
- package/src/client/index.css +0 -174
- package/src/client/index.html +0 -12
- package/src/client/main.tsx +0 -15
- package/src/client/pages/LoginPage.tsx +0 -162
- package/src/client/pages/MetricsPage.tsx +0 -417
- package/src/client/pages/OverviewPage.tsx +0 -517
- package/src/client/pages/PulsePage.tsx +0 -488
- package/src/client/pages/QueuesPage.tsx +0 -379
- package/src/client/pages/SchedulesPage.tsx +0 -540
- package/src/client/pages/SettingsPage.tsx +0 -1020
- package/src/client/pages/WorkersPage.tsx +0 -394
- package/src/client/pages/index.ts +0 -8
- package/src/client/utils.ts +0 -15
- package/src/server/config/ServerConfigManager.ts +0 -90
- package/src/server/index.ts +0 -860
- package/src/server/middleware/auth.ts +0 -127
- package/src/server/services/AlertService.ts +0 -321
- package/src/server/services/CommandService.ts +0 -137
- package/src/server/services/LogStreamProcessor.ts +0 -93
- package/src/server/services/MaintenanceScheduler.ts +0 -78
- package/src/server/services/PulseService.ts +0 -91
- package/src/server/services/QueueMetricsCollector.ts +0 -138
- package/src/server/services/QueueService.ts +0 -631
- package/src/shared/types.ts +0 -198
- package/tailwind.config.js +0 -73
- package/tests/placeholder.test.ts +0 -7
- package/tsconfig.json +0 -38
- package/tsconfig.node.json +0 -12
- package/vite.config.ts +0 -27
package/scripts/seed.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Job, QueueManager } from '@gravito/stream'
|
|
3
|
-
import Redis from 'ioredis'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Flux Console Unified Seed Script
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* bun scripts/seed.ts [mode]
|
|
10
|
-
*
|
|
11
|
-
* Modes:
|
|
12
|
-
* standard - Small set of diverse jobs (Waiting, Delayed, Failed)
|
|
13
|
-
* stress - Many queues and many jobs for performance testing
|
|
14
|
-
* batch - Setup for batch operation testing (100+ jobs)
|
|
15
|
-
* cron - Register recurring schedules
|
|
16
|
-
* cleanup - Flush Redis and clear logs
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const mode = process.argv[2] || 'standard'
|
|
20
|
-
const redis = new Redis('redis://localhost:6379')
|
|
21
|
-
const prefix = 'queue:'
|
|
22
|
-
|
|
23
|
-
// Simple Job class for testing
|
|
24
|
-
class GenericJob extends Job {
|
|
25
|
-
constructor(
|
|
26
|
-
id: any = null,
|
|
27
|
-
public data: any = {}
|
|
28
|
-
) {
|
|
29
|
-
super()
|
|
30
|
-
this.id = id
|
|
31
|
-
}
|
|
32
|
-
async handle() {}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const manager = new QueueManager({
|
|
36
|
-
default: 'redis',
|
|
37
|
-
connections: {
|
|
38
|
-
redis: {
|
|
39
|
-
driver: 'redis',
|
|
40
|
-
client: redis,
|
|
41
|
-
prefix,
|
|
42
|
-
},
|
|
43
|
-
},
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
manager.registerJobClasses([GenericJob])
|
|
47
|
-
|
|
48
|
-
async function cleanup() {
|
|
49
|
-
console.log('🧹 Cleaning up Redis...')
|
|
50
|
-
const keys = await redis.keys(`${prefix}*`)
|
|
51
|
-
const internalKeys = await redis.keys('flux_console:*')
|
|
52
|
-
const allKeys = [...keys, ...internalKeys]
|
|
53
|
-
|
|
54
|
-
if (allKeys.length > 0) {
|
|
55
|
-
await redis.del(...allKeys)
|
|
56
|
-
}
|
|
57
|
-
console.log(`✅ Removed ${allKeys.length} keys.`)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function seedStandard() {
|
|
61
|
-
console.log('🚀 Seeding standard data...')
|
|
62
|
-
|
|
63
|
-
// Orders Queue
|
|
64
|
-
for (let i = 1; i <= 5; i++) {
|
|
65
|
-
const job = new GenericJob(`ORD-${1000 + i}`, { amount: Math.random() * 100 })
|
|
66
|
-
job.queueName = 'orders'
|
|
67
|
-
await manager.push(job)
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Failed Jobs
|
|
71
|
-
for (let i = 1; i <= 3; i++) {
|
|
72
|
-
const jobInstance = new GenericJob(`FAIL-${i}`, { error: 'Payment Timeout' })
|
|
73
|
-
jobInstance.queueName = 'orders'
|
|
74
|
-
const serialized = manager.getSerializer().serialize(jobInstance)
|
|
75
|
-
|
|
76
|
-
await redis.lpush(
|
|
77
|
-
`${prefix}orders:failed`,
|
|
78
|
-
JSON.stringify({
|
|
79
|
-
...serialized,
|
|
80
|
-
status: 'failed',
|
|
81
|
-
failedReason: 'Payment Timeout',
|
|
82
|
-
failedAt: Date.now(),
|
|
83
|
-
})
|
|
84
|
-
)
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Delayed Jobs
|
|
88
|
-
for (let i = 1; i <= 3; i++) {
|
|
89
|
-
const job = new GenericJob(`DLY-${i}`, { type: 'reminder' })
|
|
90
|
-
job.queueName = 'notifications'
|
|
91
|
-
job.delay(3600 * 1000) // 1 hour
|
|
92
|
-
await manager.push(job)
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
async function seedStress() {
|
|
97
|
-
console.log('🔥 Stress Mode: Creating 15 queues with jobs...')
|
|
98
|
-
const queues = [
|
|
99
|
-
'billing',
|
|
100
|
-
'shipping',
|
|
101
|
-
'inventory',
|
|
102
|
-
'marketing',
|
|
103
|
-
'crm',
|
|
104
|
-
'auth',
|
|
105
|
-
'logs',
|
|
106
|
-
'backups',
|
|
107
|
-
'indexing',
|
|
108
|
-
'cache',
|
|
109
|
-
'sync',
|
|
110
|
-
'webhooks',
|
|
111
|
-
'api',
|
|
112
|
-
'metrics',
|
|
113
|
-
'events',
|
|
114
|
-
]
|
|
115
|
-
|
|
116
|
-
for (const q of queues) {
|
|
117
|
-
const count = 10 + Math.floor(Math.random() * 40)
|
|
118
|
-
for (let i = 0; i < count; i++) {
|
|
119
|
-
const job = new GenericJob(`JOB-${q}-${i}`, { timestamp: Date.now() })
|
|
120
|
-
job.queueName = q
|
|
121
|
-
await manager.push(job)
|
|
122
|
-
}
|
|
123
|
-
console.log(` - ${q}: ${count} jobs`)
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function seedBatch() {
|
|
128
|
-
console.log('📦 Batch Mode: Setting up specialized data for batch testing...')
|
|
129
|
-
|
|
130
|
-
// 100 Waiting jobs
|
|
131
|
-
for (let i = 1; i <= 100; i++) {
|
|
132
|
-
const job = new GenericJob(`BATCH-WAIT-${i}`)
|
|
133
|
-
job.queueName = 'test-batch'
|
|
134
|
-
await manager.push(job)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 50 Failed jobs
|
|
138
|
-
for (let i = 1; i <= 50; i++) {
|
|
139
|
-
const jobInstance = new GenericJob(`BATCH-FAIL-${i}`, { error: 'Database Connection Lost' })
|
|
140
|
-
jobInstance.queueName = 'test-batch-fail'
|
|
141
|
-
const serialized = manager.getSerializer().serialize(jobInstance)
|
|
142
|
-
|
|
143
|
-
await redis.lpush(
|
|
144
|
-
`${prefix}test-batch-fail:failed`,
|
|
145
|
-
JSON.stringify({
|
|
146
|
-
...serialized,
|
|
147
|
-
status: 'failed',
|
|
148
|
-
attempts: 3,
|
|
149
|
-
failedAt: Date.now(),
|
|
150
|
-
})
|
|
151
|
-
)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
async function seedCron() {
|
|
156
|
-
console.log('⏰ Cron Mode: Registering recurring schedules...')
|
|
157
|
-
const scheduler = manager.getScheduler()
|
|
158
|
-
const serializer = manager.getSerializer()
|
|
159
|
-
|
|
160
|
-
const rawSchedules = [
|
|
161
|
-
{ id: 'cleanup-tmp', cron: '*/1 * * * *', queue: 'system', name: 'CleanupTmp' },
|
|
162
|
-
{ id: 'daily-report', cron: '0 0 * * *', queue: 'reports', name: 'DailyReport' },
|
|
163
|
-
{ id: 'health-check', cron: '*/5 * * * *', queue: 'monitoring', name: 'HealthCheck' },
|
|
164
|
-
{ id: 'high-frequency', cron: '*/1 * * * *', queue: 'fast', name: 'Pulse' },
|
|
165
|
-
]
|
|
166
|
-
|
|
167
|
-
for (const s of rawSchedules) {
|
|
168
|
-
const jobInstance = new GenericJob(s.id, { task: s.name })
|
|
169
|
-
jobInstance.queueName = s.queue
|
|
170
|
-
const serialized = serializer.serialize(jobInstance)
|
|
171
|
-
|
|
172
|
-
await scheduler.register({
|
|
173
|
-
id: s.id,
|
|
174
|
-
cron: s.cron,
|
|
175
|
-
queue: s.queue,
|
|
176
|
-
job: serialized,
|
|
177
|
-
})
|
|
178
|
-
console.log(` - Registered: ${s.id} (${s.cron})`)
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
async function main() {
|
|
183
|
-
try {
|
|
184
|
-
if (mode === 'cleanup') {
|
|
185
|
-
await cleanup()
|
|
186
|
-
} else if (mode === 'standard') {
|
|
187
|
-
await cleanup()
|
|
188
|
-
await seedStandard()
|
|
189
|
-
} else if (mode === 'stress') {
|
|
190
|
-
await seedStress()
|
|
191
|
-
} else if (mode === 'batch') {
|
|
192
|
-
await seedBatch()
|
|
193
|
-
} else if (mode === 'cron') {
|
|
194
|
-
await seedCron()
|
|
195
|
-
} else if (mode === 'all') {
|
|
196
|
-
await cleanup()
|
|
197
|
-
await seedStandard()
|
|
198
|
-
await seedStress()
|
|
199
|
-
await seedBatch()
|
|
200
|
-
await seedCron()
|
|
201
|
-
} else {
|
|
202
|
-
console.log('❌ Unknown mode. Try: standard, stress, batch, cron, cleanup, all')
|
|
203
|
-
}
|
|
204
|
-
} catch (err) {
|
|
205
|
-
console.error('💥 Error:', err)
|
|
206
|
-
} finally {
|
|
207
|
-
redis.disconnect()
|
|
208
|
-
console.log('\n🏁 Done.')
|
|
209
|
-
process.exit(0)
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
main()
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
console.log('🎧 Connecting to log stream...')
|
|
2
|
-
const req = await fetch('http://localhost:3000/api/logs/stream')
|
|
3
|
-
if (!req.body) {
|
|
4
|
-
throw new Error('No body')
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
const reader = req.body.getReader()
|
|
8
|
-
const decoder = new TextDecoder()
|
|
9
|
-
|
|
10
|
-
let logCount = 0
|
|
11
|
-
const start = Date.now()
|
|
12
|
-
|
|
13
|
-
// Count for 2 seconds
|
|
14
|
-
const DURATION = 2000
|
|
15
|
-
|
|
16
|
-
console.log(`⏳ Measuring received logs for ${DURATION}ms...`)
|
|
17
|
-
|
|
18
|
-
async function readStream() {
|
|
19
|
-
while (true) {
|
|
20
|
-
const { done, value } = await reader.read()
|
|
21
|
-
if (done) {
|
|
22
|
-
break
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const chunk = decoder.decode(value)
|
|
26
|
-
// SSE format: event: log\ndata: ...\n\n
|
|
27
|
-
const matches = chunk.match(/event: log/g)
|
|
28
|
-
if (matches) {
|
|
29
|
-
logCount += matches.length
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (Date.now() - start > DURATION) {
|
|
33
|
-
break
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
console.log(`📊 Result: Received ${logCount} logs in ${(Date.now() - start) / 1000}s`)
|
|
38
|
-
console.log(`ℹ️ Expected max: ~50-60 logs (50/sec limit + potential buffer/history)`)
|
|
39
|
-
|
|
40
|
-
if (logCount > 150) {
|
|
41
|
-
console.error('❌ Throttling FAILED! Too many logs received.')
|
|
42
|
-
process.exit(1)
|
|
43
|
-
} else {
|
|
44
|
-
console.log('✅ Throttling PASSED!')
|
|
45
|
-
process.exit(0)
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
readStream()
|
package/scripts/worker.ts
DELETED
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
import { Consumer, Job, QueueManager } from '@gravito/stream'
|
|
3
|
-
import Redis from 'ioredis'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Flux Console Unified Worker Script
|
|
7
|
-
*
|
|
8
|
-
* Usage:
|
|
9
|
-
* bun scripts/worker.ts [queues] [--fail=rate] [--delay=ms]
|
|
10
|
-
*
|
|
11
|
-
* Example:
|
|
12
|
-
* bun scripts/worker.ts orders,reports --fail=0.1 --delay=200
|
|
13
|
-
* bun scripts/worker.ts all
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const redis = new Redis('redis://localhost:6379')
|
|
17
|
-
const prefix = 'queue:'
|
|
18
|
-
|
|
19
|
-
const queuesRaw = process.argv[2] || 'orders,notifications,test-batch,billing,analytics'
|
|
20
|
-
let queues: string[] = []
|
|
21
|
-
|
|
22
|
-
if (queuesRaw === 'all') {
|
|
23
|
-
console.log('🔍 Discovering all queues...')
|
|
24
|
-
const keys = await redis.keys(`${prefix}*`)
|
|
25
|
-
const set = new Set<string>()
|
|
26
|
-
for (const k of keys) {
|
|
27
|
-
const name = k.slice(prefix.length).split(':')[0]
|
|
28
|
-
// Exclude internal management keys
|
|
29
|
-
if (
|
|
30
|
-
name &&
|
|
31
|
-
![
|
|
32
|
-
'active',
|
|
33
|
-
'schedules',
|
|
34
|
-
'schedule',
|
|
35
|
-
'worker',
|
|
36
|
-
'lock',
|
|
37
|
-
'logs',
|
|
38
|
-
'metrics',
|
|
39
|
-
'throughput',
|
|
40
|
-
].includes(name)
|
|
41
|
-
) {
|
|
42
|
-
set.add(name)
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
queues = Array.from(set)
|
|
46
|
-
if (queues.length === 0) {
|
|
47
|
-
console.log('⚠️ No active queues found. Defaulting to standard set...')
|
|
48
|
-
queues = ['orders', 'notifications', 'test-batch', 'billing', 'analytics']
|
|
49
|
-
}
|
|
50
|
-
} else {
|
|
51
|
-
queues = queuesRaw.split(',')
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const failRate = parseFloat(process.argv.find((a) => a.startsWith('--fail='))?.split('=')[1] || '0')
|
|
55
|
-
const processDelay = parseInt(
|
|
56
|
-
process.argv.find((a) => a.startsWith('--delay='))?.split('=')[1] || '100',
|
|
57
|
-
10
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
// Simple Job class for testing
|
|
61
|
-
class GenericJob extends Job {
|
|
62
|
-
constructor(
|
|
63
|
-
id: any = null,
|
|
64
|
-
public data: any = {}
|
|
65
|
-
) {
|
|
66
|
-
super()
|
|
67
|
-
this.id = id
|
|
68
|
-
}
|
|
69
|
-
async handle() {
|
|
70
|
-
// Simulated work
|
|
71
|
-
if (processDelay > 0) {
|
|
72
|
-
await new Promise((r) => setTimeout(r, processDelay))
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Simulated failure
|
|
76
|
-
if (Math.random() < failRate) {
|
|
77
|
-
throw new Error('Simulated random failure')
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const manager = new QueueManager({
|
|
83
|
-
default: 'redis',
|
|
84
|
-
connections: {
|
|
85
|
-
redis: {
|
|
86
|
-
driver: 'redis',
|
|
87
|
-
client: redis,
|
|
88
|
-
prefix,
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
manager.registerJobClasses([GenericJob])
|
|
94
|
-
|
|
95
|
-
console.log('🚀 Starting Flux Unified Worker...')
|
|
96
|
-
console.log(`📡 Queues: ${queues.join(', ')}`)
|
|
97
|
-
console.log(`⚠️ Fail Rate: ${(failRate * 100).toFixed(0)}%`)
|
|
98
|
-
console.log(`⏱️ Sim Delay: ${processDelay}ms\n`)
|
|
99
|
-
|
|
100
|
-
const consumer = new Consumer(manager, {
|
|
101
|
-
queues,
|
|
102
|
-
connection: 'redis',
|
|
103
|
-
pollInterval: 100,
|
|
104
|
-
monitor: {
|
|
105
|
-
prefix: 'flux_console:', // Critical: match the prefix the Console looks for
|
|
106
|
-
},
|
|
107
|
-
workerOptions: {
|
|
108
|
-
maxAttempts: 3,
|
|
109
|
-
},
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
console.log('✅ Worker started. Monitoring enabled (check the Workers page in Console).')
|
|
113
|
-
|
|
114
|
-
consumer.start().catch((err) => {
|
|
115
|
-
console.error('💥 Consumer Error:', err)
|
|
116
|
-
process.exit(1)
|
|
117
|
-
})
|
|
118
|
-
|
|
119
|
-
process.on('SIGINT', async () => {
|
|
120
|
-
console.log('\n🛑 Shutting down worker...')
|
|
121
|
-
await consumer.stop()
|
|
122
|
-
redis.disconnect()
|
|
123
|
-
process.exit(0)
|
|
124
|
-
})
|
package/specs/PULSE_SPEC.md
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# Gravito Pulse Implementation Spec
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
Gravito Pulse is a lightweight APM (Application Performance Monitoring) system integrated into Zenith. It follows the philosophy: *"If you can connect to Redis, you are monitored."*
|
|
5
|
-
|
|
6
|
-
## 1. Gravito Pulse Protocol (GPP)
|
|
7
|
-
|
|
8
|
-
### Data Structure
|
|
9
|
-
Pulse uses Redis keys with specific TTLs to represent live services.
|
|
10
|
-
|
|
11
|
-
- **Key Pattern**: `gravito:quasar:node:{service}:{node_id}`
|
|
12
|
-
- **TTL**: 30 seconds (Agents should heartbeat every 10-15s).
|
|
13
|
-
- **Data Type**: String (JSON)
|
|
14
|
-
|
|
15
|
-
### Payload Schema
|
|
16
|
-
```json
|
|
17
|
-
{
|
|
18
|
-
"id": "string", // Unique Instance ID (e.g., UUID or Hostname-PID)
|
|
19
|
-
"service": "string", // Group name (e.g., "worker-billing", "api-gateway")
|
|
20
|
-
"language": "string", // "node" | "bun" | "deno" | "php" | "go" | "python" | "other"
|
|
21
|
-
"version": "string", // Language/Runtime Version
|
|
22
|
-
"pid": "number", // Process ID
|
|
23
|
-
"hostname": "string", // Machine Hostname or Custom Name
|
|
24
|
-
"platform": "string", // OS Platform (linux, darwin, win32)
|
|
25
|
-
"cpu": {
|
|
26
|
-
"system": "number", // System Load % (0-100)
|
|
27
|
-
"process": "number", // Process Usage % (0-100)
|
|
28
|
-
"cores": "number" // Core count
|
|
29
|
-
},
|
|
30
|
-
"memory": {
|
|
31
|
-
"system": {
|
|
32
|
-
"total": "number", // System Total Memory (bytes)
|
|
33
|
-
"free": "number", // System Free Memory (bytes)
|
|
34
|
-
"used": "number" // System Used Memory (bytes)
|
|
35
|
-
},
|
|
36
|
-
"process": {
|
|
37
|
-
"rss": "number", // Resident Set Size (bytes)
|
|
38
|
-
"heapTotal": "number",// Heap Total (bytes)
|
|
39
|
-
"heapUsed": "number" // Heap Used (bytes)
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"runtime": {
|
|
43
|
-
"uptime": "number", // Process uptime in seconds
|
|
44
|
-
"framework": "string" // Optional framework info
|
|
45
|
-
},
|
|
46
|
-
"timestamp": "number" // Unix Ms Timestamp
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## 2. Implementation Modules
|
|
51
|
-
|
|
52
|
-
### A. Client SDK (`@gravito/pulse-node`)
|
|
53
|
-
A lightweight agent to collect metrics and publish to Redis.
|
|
54
|
-
- **Dependencies**: `ioredis`, `pidusage` (optional, or use native `os`/`process`).
|
|
55
|
-
- **Functionality**:
|
|
56
|
-
- `startPulse({ service: string })`: Starts the heartbeat loop.
|
|
57
|
-
- Collects CPU/RAM usage.
|
|
58
|
-
- Publishes to Redis.
|
|
59
|
-
|
|
60
|
-
### B. Server Collector (Zenith Console)
|
|
61
|
-
- **Service**: `PulseService`
|
|
62
|
-
- **Method**: `getNodes()`
|
|
63
|
-
- Performs `SCAN 0 MATCH pulse:* COUNT 100`.
|
|
64
|
-
- Returns grouped nodes by `service`.
|
|
65
|
-
- **API**: `GET /api/pulse/nodes`
|
|
66
|
-
|
|
67
|
-
### C. Frontend Dashboard (Zenith UI)
|
|
68
|
-
- **Route**: `/pulse`
|
|
69
|
-
- **Components**:
|
|
70
|
-
- `ServiceGroup`: A container for nodes of a specific service.
|
|
71
|
-
- `NodeCard`: Displays CPU/RAM sparklines (optional) and current health.
|
|
72
|
-
- `HealthBadge`: Green (Fresh), Yellow (>15s ago), Red (Dead/Gone - though Redis TTL handles removal, frontend can handle stale UI).
|
|
73
|
-
|
|
74
|
-
## 3. Alerts (Phase 2)
|
|
75
|
-
- Server-side checker that monitors values from `PulseService`.
|
|
76
|
-
- Triggers `AlertService` if:
|
|
77
|
-
- CPU > 90% for 2 mins.
|
|
78
|
-
- Memory > 90% for 5 mins.
|
|
79
|
-
- Disk < 10% free.
|
|
80
|
-
|
|
81
|
-
## 4. Work Plan
|
|
82
|
-
1. **Define Types**: Add `PulseNode` interface to `@gravito/custom-types` or `flux-console` shared types.
|
|
83
|
-
2. **Implement Server Collector**: Create `PulseService` in `packages/flux-console/server/services`.
|
|
84
|
-
3. **Implement API**: Add route in `packages/flux-console/server/routes.ts`.
|
|
85
|
-
4. **Implement UI**: Create `PulsePage` and components.
|
|
86
|
-
5. **Implement Node Client**: Add `startPulse` to `packages/stream` (or separate package) to verify "dogfooding" by having the server monitor itself.
|
package/src/bin.ts
DELETED
package/src/client/App.tsx
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
|
-
import { RefreshCcw } from 'lucide-react'
|
|
3
|
-
import { BrowserRouter, Route, Routes } from 'react-router-dom'
|
|
4
|
-
import { AuthProvider, useAuth } from './contexts/AuthContext'
|
|
5
|
-
import { NotificationProvider } from './contexts/NotificationContext'
|
|
6
|
-
import { Layout } from './Layout'
|
|
7
|
-
import {
|
|
8
|
-
LoginPage,
|
|
9
|
-
MetricsPage,
|
|
10
|
-
OverviewPage,
|
|
11
|
-
PulsePage,
|
|
12
|
-
QueuesPage,
|
|
13
|
-
SchedulesPage,
|
|
14
|
-
SettingsPage,
|
|
15
|
-
WorkersPage,
|
|
16
|
-
} from './pages'
|
|
17
|
-
|
|
18
|
-
const queryClient = new QueryClient()
|
|
19
|
-
|
|
20
|
-
function AuthenticatedRoutes() {
|
|
21
|
-
const { isAuthenticated, isAuthEnabled, isLoading } = useAuth()
|
|
22
|
-
|
|
23
|
-
// Show loading spinner while checking auth
|
|
24
|
-
if (isLoading) {
|
|
25
|
-
return (
|
|
26
|
-
<div className="min-h-screen flex items-center justify-center bg-background">
|
|
27
|
-
<div className="flex flex-col items-center gap-4">
|
|
28
|
-
<RefreshCcw className="animate-spin text-primary" size={48} />
|
|
29
|
-
<p className="text-muted-foreground font-bold uppercase tracking-[0.3em] text-xs">
|
|
30
|
-
Initializing...
|
|
31
|
-
</p>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
34
|
-
)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// If auth is enabled and user is not authenticated, show login
|
|
38
|
-
if (isAuthEnabled && !isAuthenticated) {
|
|
39
|
-
return <LoginPage />
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// Otherwise, show the main app
|
|
43
|
-
return (
|
|
44
|
-
<NotificationProvider>
|
|
45
|
-
<Layout>
|
|
46
|
-
<Routes>
|
|
47
|
-
<Route path="/" element={<OverviewPage />} />
|
|
48
|
-
<Route path="/queues" element={<QueuesPage />} />
|
|
49
|
-
<Route path="/schedules" element={<SchedulesPage />} />
|
|
50
|
-
<Route path="/workers" element={<WorkersPage />} />
|
|
51
|
-
<Route path="/metrics" element={<MetricsPage />} />
|
|
52
|
-
<Route path="/pulse" element={<PulsePage />} />
|
|
53
|
-
<Route path="/settings" element={<SettingsPage />} />
|
|
54
|
-
</Routes>
|
|
55
|
-
</Layout>
|
|
56
|
-
</NotificationProvider>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function App() {
|
|
61
|
-
return (
|
|
62
|
-
<QueryClientProvider client={queryClient}>
|
|
63
|
-
<BrowserRouter>
|
|
64
|
-
<AuthProvider>
|
|
65
|
-
<AuthenticatedRoutes />
|
|
66
|
-
</AuthProvider>
|
|
67
|
-
</BrowserRouter>
|
|
68
|
-
</QueryClientProvider>
|
|
69
|
-
)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export default App
|