@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
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import type { Redis } from 'ioredis'
|
|
2
|
-
|
|
3
|
-
export interface MaintenanceConfig {
|
|
4
|
-
autoCleanup: boolean
|
|
5
|
-
retentionDays: number
|
|
6
|
-
lastRun?: number
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
export class MaintenanceScheduler {
|
|
10
|
-
private static readonly ONE_DAY = 24 * 60 * 60 * 1000
|
|
11
|
-
private static readonly CHECK_INTERVAL = 3600000 // 1 hour
|
|
12
|
-
|
|
13
|
-
constructor(
|
|
14
|
-
private redis: Redis,
|
|
15
|
-
private cleanupCallback: (retentionDays: number) => Promise<number>
|
|
16
|
-
) {}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Start the maintenance loop
|
|
20
|
-
*/
|
|
21
|
-
start(initialDelay = 30000): void {
|
|
22
|
-
setTimeout(() => {
|
|
23
|
-
const loop = async () => {
|
|
24
|
-
try {
|
|
25
|
-
await this.checkMaintenance()
|
|
26
|
-
} catch (err) {
|
|
27
|
-
console.error('[Maintenance] Task Error:', err)
|
|
28
|
-
}
|
|
29
|
-
setTimeout(loop, MaintenanceScheduler.CHECK_INTERVAL)
|
|
30
|
-
}
|
|
31
|
-
loop()
|
|
32
|
-
}, initialDelay)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Check and run maintenance if needed
|
|
37
|
-
*/
|
|
38
|
-
private async checkMaintenance(): Promise<void> {
|
|
39
|
-
const config = await this.getConfig()
|
|
40
|
-
if (!config.autoCleanup) {
|
|
41
|
-
return
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const now = Date.now()
|
|
45
|
-
const lastRun = config.lastRun || 0
|
|
46
|
-
|
|
47
|
-
if (now - lastRun >= MaintenanceScheduler.ONE_DAY) {
|
|
48
|
-
console.log(
|
|
49
|
-
`[Maintenance] Starting Auto-Cleanup (Retention: ${config.retentionDays} days)...`
|
|
50
|
-
)
|
|
51
|
-
const deleted = await this.cleanupCallback(config.retentionDays)
|
|
52
|
-
console.log(`[Maintenance] Cleanup Complete. Removed ${deleted} records.`)
|
|
53
|
-
|
|
54
|
-
await this.saveConfig({
|
|
55
|
-
...config,
|
|
56
|
-
lastRun: now,
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Get maintenance configuration
|
|
63
|
-
*/
|
|
64
|
-
async getConfig(): Promise<MaintenanceConfig> {
|
|
65
|
-
const data = await this.redis.get('gravito:zenith:maintenance:config')
|
|
66
|
-
if (data) {
|
|
67
|
-
return JSON.parse(data)
|
|
68
|
-
}
|
|
69
|
-
return { autoCleanup: false, retentionDays: 30 }
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Save maintenance configuration
|
|
74
|
-
*/
|
|
75
|
-
async saveConfig(config: MaintenanceConfig): Promise<void> {
|
|
76
|
-
await this.redis.set('gravito:zenith:maintenance:config', JSON.stringify(config))
|
|
77
|
-
}
|
|
78
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { Redis } from 'ioredis'
|
|
2
|
-
import type { PulseNode } from '../../shared/types'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* PulseService manages the discovery and health monitoring of system nodes.
|
|
6
|
-
*
|
|
7
|
-
* It scans Redis for heartbeat keys emitted by Quasar agents and groups
|
|
8
|
-
* them by service name for the Zenith dashboard.
|
|
9
|
-
*
|
|
10
|
-
* @public
|
|
11
|
-
* @since 3.0.0
|
|
12
|
-
*/
|
|
13
|
-
export class PulseService {
|
|
14
|
-
private redis: Redis
|
|
15
|
-
private prefix = 'gravito:quasar:node:'
|
|
16
|
-
|
|
17
|
-
constructor(redisUrl: string) {
|
|
18
|
-
this.redis = new Redis(redisUrl, {
|
|
19
|
-
lazyConnect: true,
|
|
20
|
-
})
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async connect() {
|
|
24
|
-
await this.redis.connect()
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Discovers active Pulse nodes using SCAN.
|
|
29
|
-
*/
|
|
30
|
-
async getNodes(): Promise<Record<string, PulseNode[]>> {
|
|
31
|
-
const nodes: PulseNode[] = []
|
|
32
|
-
let cursor = '0'
|
|
33
|
-
const now = Date.now()
|
|
34
|
-
|
|
35
|
-
do {
|
|
36
|
-
// Scan for pulse keys
|
|
37
|
-
const result = await this.redis.scan(cursor, 'MATCH', `${this.prefix}*`, 'COUNT', 100)
|
|
38
|
-
cursor = result[0]
|
|
39
|
-
const keys = result[1]
|
|
40
|
-
|
|
41
|
-
if (keys.length > 0) {
|
|
42
|
-
// Fetch values
|
|
43
|
-
const values = await this.redis.mget(...keys)
|
|
44
|
-
|
|
45
|
-
values.forEach((v) => {
|
|
46
|
-
if (v) {
|
|
47
|
-
try {
|
|
48
|
-
const node = JSON.parse(v) as PulseNode
|
|
49
|
-
// Filter out stale nodes if TTL didn't catch them yet (grace period 60s)
|
|
50
|
-
if (now - node.timestamp < 60000) {
|
|
51
|
-
nodes.push(node)
|
|
52
|
-
}
|
|
53
|
-
} catch (_e) {
|
|
54
|
-
// Ignore malformed
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
}
|
|
59
|
-
} while (cursor !== '0')
|
|
60
|
-
|
|
61
|
-
// Group by service
|
|
62
|
-
const grouped: Record<string, PulseNode[]> = {}
|
|
63
|
-
|
|
64
|
-
// Sort nodes by service name, then by node id for stable UI positions
|
|
65
|
-
nodes.sort((a, b) => {
|
|
66
|
-
const sComp = a.service.localeCompare(b.service)
|
|
67
|
-
if (sComp !== 0) {
|
|
68
|
-
return sComp
|
|
69
|
-
}
|
|
70
|
-
return a.id.localeCompare(b.id)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
for (const node of nodes) {
|
|
74
|
-
if (!grouped[node.service]) {
|
|
75
|
-
grouped[node.service] = []
|
|
76
|
-
}
|
|
77
|
-
grouped[node.service].push(node)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return grouped
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Manually record a heartbeat (for this Zenith server itself).
|
|
85
|
-
*/
|
|
86
|
-
async recordHeartbeat(node: PulseNode): Promise<void> {
|
|
87
|
-
const key = `${this.prefix}${node.service}:${node.id}`
|
|
88
|
-
// TTL 30 seconds
|
|
89
|
-
await this.redis.set(key, JSON.stringify(node), 'EX', 30)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
import type { Redis } from 'ioredis'
|
|
2
|
-
import type { GlobalStats, QueueStats, WorkerReport } from './QueueService'
|
|
3
|
-
|
|
4
|
-
export class QueueMetricsCollector {
|
|
5
|
-
constructor(
|
|
6
|
-
private redis: Redis,
|
|
7
|
-
private prefix = 'queue:'
|
|
8
|
-
) {}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Discover all queues using SCAN to avoid blocking Redis
|
|
12
|
-
*/
|
|
13
|
-
async listQueues(): Promise<QueueStats[]> {
|
|
14
|
-
const queues = new Set<string>()
|
|
15
|
-
let cursor = '0'
|
|
16
|
-
let iterations = 0
|
|
17
|
-
const MAX_ITERATIONS = 1000
|
|
18
|
-
|
|
19
|
-
do {
|
|
20
|
-
const result = await this.redis.scan(cursor, 'MATCH', `${this.prefix}*`, 'COUNT', 100)
|
|
21
|
-
cursor = result[0]
|
|
22
|
-
const keys = result[1]
|
|
23
|
-
|
|
24
|
-
for (const key of keys) {
|
|
25
|
-
const relative = key.slice(this.prefix.length)
|
|
26
|
-
const parts = relative.split(':')
|
|
27
|
-
const candidateName = parts[0]
|
|
28
|
-
|
|
29
|
-
if (candidateName && !this.isSystemKey(candidateName)) {
|
|
30
|
-
queues.add(candidateName)
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
iterations++
|
|
35
|
-
} while (cursor !== '0' && iterations < MAX_ITERATIONS)
|
|
36
|
-
|
|
37
|
-
return this.collectQueueStats(Array.from(queues).sort())
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Collect statistics for a list of queues
|
|
42
|
-
*/
|
|
43
|
-
private async collectQueueStats(queueNames: string[]): Promise<QueueStats[]> {
|
|
44
|
-
const stats: QueueStats[] = []
|
|
45
|
-
const BATCH_SIZE = 10
|
|
46
|
-
|
|
47
|
-
for (let i = 0; i < queueNames.length; i += BATCH_SIZE) {
|
|
48
|
-
const batch = queueNames.slice(i, i + BATCH_SIZE)
|
|
49
|
-
const batchResults = await Promise.all(batch.map(async (name) => this.getQueueStats(name)))
|
|
50
|
-
stats.push(...batchResults)
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return stats
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Get statistics for a single queue
|
|
58
|
-
*/
|
|
59
|
-
private async getQueueStats(name: string): Promise<QueueStats> {
|
|
60
|
-
const waiting = await this.redis.llen(`${this.prefix}${name}`)
|
|
61
|
-
const delayed = await this.redis.zcard(`${this.prefix}${name}:delayed`)
|
|
62
|
-
const failed = await this.redis.llen(`${this.prefix}${name}:failed`)
|
|
63
|
-
const active = await this.redis.scard(`${this.prefix}${name}:active`)
|
|
64
|
-
const paused = await this.redis.get(`${this.prefix}${name}:paused`)
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
name,
|
|
68
|
-
waiting,
|
|
69
|
-
delayed,
|
|
70
|
-
failed,
|
|
71
|
-
active,
|
|
72
|
-
paused: paused === '1',
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Get global statistics including throughput
|
|
78
|
-
*/
|
|
79
|
-
async getGlobalStats(): Promise<GlobalStats> {
|
|
80
|
-
const queues = await this.listQueues()
|
|
81
|
-
const throughput = await this.getThroughputData()
|
|
82
|
-
|
|
83
|
-
return {
|
|
84
|
-
queues,
|
|
85
|
-
throughput,
|
|
86
|
-
workers: [],
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get throughput data for the last hour
|
|
92
|
-
*/
|
|
93
|
-
private async getThroughputData(): Promise<{ timestamp: string; count: number }[]> {
|
|
94
|
-
const data: { timestamp: string; count: number }[] = []
|
|
95
|
-
const currentMinute = Math.floor(Date.now() / 60000)
|
|
96
|
-
|
|
97
|
-
for (let i = 0; i < 60; i++) {
|
|
98
|
-
const minute = currentMinute - i
|
|
99
|
-
const count = await this.redis.get(`flux_console:throughput:${minute}`)
|
|
100
|
-
data.push({
|
|
101
|
-
timestamp: new Date(minute * 60000).toISOString(),
|
|
102
|
-
count: Number(count) || 0,
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return data.reverse()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get reports from all active workers
|
|
111
|
-
*/
|
|
112
|
-
async listWorkers(): Promise<WorkerReport[]> {
|
|
113
|
-
const workers = await this.redis.smembers('flux_console:workers')
|
|
114
|
-
|
|
115
|
-
return Promise.all(workers.map(async (workerId) => this.getWorkerReport(workerId)))
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Get report from a specific worker
|
|
120
|
-
*/
|
|
121
|
-
private async getWorkerReport(workerId: string): Promise<WorkerReport> {
|
|
122
|
-
const data = await this.redis.get(`flux_console:worker:${workerId}`)
|
|
123
|
-
|
|
124
|
-
if (!data) {
|
|
125
|
-
throw new Error(`Worker ${workerId} not found`)
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return JSON.parse(data) as WorkerReport
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Check if a key is a system key
|
|
133
|
-
*/
|
|
134
|
-
private isSystemKey(key: string): boolean {
|
|
135
|
-
const systemKeys = ['active', 'schedules', 'schedule', 'lock']
|
|
136
|
-
return systemKeys.includes(key)
|
|
137
|
-
}
|
|
138
|
-
}
|