rufloui 0.3.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.
- package/'1' +0 -0
- package/.env.example +46 -0
- package/CHANGELOG.md +87 -0
- package/CLAUDE.md +287 -0
- package/LICENSE +21 -0
- package/README.md +316 -0
- package/Webhooks) +0 -0
- package/docs/plans/2026-03-11-github-webhooks.md +957 -0
- package/docs/screenshot-swarm-monitor.png +0 -0
- package/frontend +0 -0
- package/index.html +13 -0
- package/package.json +56 -0
- package/public/vite.svg +4 -0
- package/src/backend/__tests__/webhook-github.test.ts +934 -0
- package/src/backend/jsonl-monitor.ts +430 -0
- package/src/backend/server.ts +2972 -0
- package/src/backend/telegram-bot.ts +511 -0
- package/src/backend/webhook-github.ts +350 -0
- package/src/frontend/App.tsx +461 -0
- package/src/frontend/api.ts +281 -0
- package/src/frontend/components/ErrorBoundary.tsx +98 -0
- package/src/frontend/components/Layout.tsx +431 -0
- package/src/frontend/components/ui/Button.tsx +111 -0
- package/src/frontend/components/ui/Card.tsx +51 -0
- package/src/frontend/components/ui/StatusBadge.tsx +60 -0
- package/src/frontend/main.tsx +63 -0
- package/src/frontend/pages/AgentVizPanel.tsx +428 -0
- package/src/frontend/pages/AgentsPanel.tsx +445 -0
- package/src/frontend/pages/ConfigPanel.tsx +661 -0
- package/src/frontend/pages/Dashboard.tsx +482 -0
- package/src/frontend/pages/HiveMindPanel.tsx +355 -0
- package/src/frontend/pages/HooksPanel.tsx +240 -0
- package/src/frontend/pages/LogsPanel.tsx +261 -0
- package/src/frontend/pages/MemoryPanel.tsx +444 -0
- package/src/frontend/pages/NeuralPanel.tsx +301 -0
- package/src/frontend/pages/PerformancePanel.tsx +198 -0
- package/src/frontend/pages/SessionsPanel.tsx +428 -0
- package/src/frontend/pages/SetupWizard.tsx +181 -0
- package/src/frontend/pages/SwarmMonitorPanel.tsx +634 -0
- package/src/frontend/pages/SwarmPanel.tsx +322 -0
- package/src/frontend/pages/TasksPanel.tsx +535 -0
- package/src/frontend/pages/WebhooksPanel.tsx +335 -0
- package/src/frontend/pages/WorkflowsPanel.tsx +448 -0
- package/src/frontend/store.ts +185 -0
- package/src/frontend/styles/global.css +113 -0
- package/src/frontend/test-setup.ts +1 -0
- package/src/frontend/tour/TourContext.tsx +161 -0
- package/src/frontend/tour/tourSteps.ts +181 -0
- package/src/frontend/tour/tourStyles.css +116 -0
- package/src/frontend/types.ts +239 -0
- package/src/frontend/utils/formatTime.test.ts +83 -0
- package/src/frontend/utils/formatTime.ts +23 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/{,+ +0 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import React, { Suspense, useEffect, useRef, useState, useCallback } from 'react'
|
|
2
|
+
import { Routes, Route } from 'react-router-dom'
|
|
3
|
+
import { Layout } from './components/Layout'
|
|
4
|
+
import { createWebSocket, api } from '@/api'
|
|
5
|
+
import { useStore } from '@/store'
|
|
6
|
+
import type { SwarmAgent } from '@/types'
|
|
7
|
+
import SetupWizard from './pages/SetupWizard'
|
|
8
|
+
import { TourProvider } from './tour/TourContext'
|
|
9
|
+
|
|
10
|
+
const Dashboard = React.lazy(() => import('./pages/Dashboard'))
|
|
11
|
+
const SwarmPanel = React.lazy(() => import('./pages/SwarmPanel'))
|
|
12
|
+
const AgentsPanel = React.lazy(() => import('./pages/AgentsPanel'))
|
|
13
|
+
const TasksPanel = React.lazy(() => import('./pages/TasksPanel'))
|
|
14
|
+
const MemoryPanel = React.lazy(() => import('./pages/MemoryPanel'))
|
|
15
|
+
const SessionsPanel = React.lazy(() => import('./pages/SessionsPanel'))
|
|
16
|
+
const HiveMindPanel = React.lazy(() => import('./pages/HiveMindPanel'))
|
|
17
|
+
const NeuralPanel = React.lazy(() => import('./pages/NeuralPanel'))
|
|
18
|
+
const PerformancePanel = React.lazy(() => import('./pages/PerformancePanel'))
|
|
19
|
+
const HooksPanel = React.lazy(() => import('./pages/HooksPanel'))
|
|
20
|
+
const WorkflowsPanel = React.lazy(() => import('./pages/WorkflowsPanel'))
|
|
21
|
+
const ConfigPanel = React.lazy(() => import('./pages/ConfigPanel'))
|
|
22
|
+
const LogsPanel = React.lazy(() => import('./pages/LogsPanel'))
|
|
23
|
+
const AgentVizPanel = React.lazy(() => import('./pages/AgentVizPanel'))
|
|
24
|
+
const SwarmMonitorPanel = React.lazy(() => import('./pages/SwarmMonitorPanel'))
|
|
25
|
+
const WebhooksPanel = React.lazy(() => import('./pages/WebhooksPanel'))
|
|
26
|
+
|
|
27
|
+
function LoadingSpinner() {
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
style={{
|
|
31
|
+
display: 'flex',
|
|
32
|
+
alignItems: 'center',
|
|
33
|
+
justifyContent: 'center',
|
|
34
|
+
height: '100%',
|
|
35
|
+
width: '100%',
|
|
36
|
+
}}
|
|
37
|
+
>
|
|
38
|
+
<div
|
|
39
|
+
style={{
|
|
40
|
+
width: 32,
|
|
41
|
+
height: 32,
|
|
42
|
+
border: '3px solid var(--border)',
|
|
43
|
+
borderTopColor: 'var(--accent-blue)',
|
|
44
|
+
borderRadius: '50%',
|
|
45
|
+
animation: 'spin 0.8s linear infinite',
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
</div>
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function App() {
|
|
53
|
+
const wsRef = useRef<WebSocket | null>(null)
|
|
54
|
+
const store = useStore()
|
|
55
|
+
|
|
56
|
+
// Preflight wizard — skip if already passed this session
|
|
57
|
+
const [preflightPassed, setPreflightPassed] = useState(() => {
|
|
58
|
+
return sessionStorage.getItem('ruflo-preflight-passed') === 'true'
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const handlePreflightContinue = useCallback(() => {
|
|
62
|
+
sessionStorage.setItem('ruflo-preflight-passed', 'true')
|
|
63
|
+
setPreflightPassed(true)
|
|
64
|
+
}, [])
|
|
65
|
+
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
// Auto-check preflight on first load — if all OK, skip wizard
|
|
68
|
+
if (preflightPassed) return
|
|
69
|
+
api.system.preflight()
|
|
70
|
+
.then(result => {
|
|
71
|
+
if (result.status === 'ok') {
|
|
72
|
+
handlePreflightContinue()
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.catch(() => { /* backend not ready, show wizard */ })
|
|
76
|
+
}, [preflightPassed, handlePreflightContinue])
|
|
77
|
+
|
|
78
|
+
// Shared function to fetch core data — used on startup and after WS reconnection
|
|
79
|
+
const refreshCoreData = useCallback(async () => {
|
|
80
|
+
const s = useStore.getState()
|
|
81
|
+
try {
|
|
82
|
+
const [health, tasks, agents, workflows, sessions] = await Promise.allSettled([
|
|
83
|
+
api.system.health(),
|
|
84
|
+
api.tasks.list(),
|
|
85
|
+
api.agents.list(),
|
|
86
|
+
api.workflows.list(),
|
|
87
|
+
api.sessions.list(),
|
|
88
|
+
])
|
|
89
|
+
if (health.status === 'fulfilled') s.setSystemHealth(health.value as Parameters<typeof s.setSystemHealth>[0])
|
|
90
|
+
if (tasks.status === 'fulfilled') {
|
|
91
|
+
const tv = tasks.value as unknown
|
|
92
|
+
const tArr = Array.isArray(tv) ? tv : ((tv as Record<string, unknown>)?.tasks ?? [])
|
|
93
|
+
s.setTasks(tArr as Parameters<typeof s.setTasks>[0])
|
|
94
|
+
}
|
|
95
|
+
if (agents.status === 'fulfilled') {
|
|
96
|
+
const av = agents.value as unknown
|
|
97
|
+
const aArr = Array.isArray(av) ? av : ((av as Record<string, unknown>)?.agents ?? [])
|
|
98
|
+
s.setAgents(aArr as Parameters<typeof s.setAgents>[0])
|
|
99
|
+
}
|
|
100
|
+
if (workflows.status === 'fulfilled') {
|
|
101
|
+
const wv = workflows.value as unknown
|
|
102
|
+
const wArr = Array.isArray(wv) ? wv : ((wv as Record<string, unknown>)?.workflows ?? [])
|
|
103
|
+
s.setWorkflows(wArr as Parameters<typeof s.setWorkflows>[0])
|
|
104
|
+
}
|
|
105
|
+
if (sessions.status === 'fulfilled') {
|
|
106
|
+
const sv = sessions.value as unknown
|
|
107
|
+
const sArr = Array.isArray(sv) ? sv : ((sv as Record<string, unknown>)?.sessions ?? [])
|
|
108
|
+
s.setSessions(sArr as Parameters<typeof s.setSessions>[0])
|
|
109
|
+
}
|
|
110
|
+
s.setInitialLoaded()
|
|
111
|
+
s.setBackendReachable(true)
|
|
112
|
+
} catch { /* backend not ready yet */ }
|
|
113
|
+
}, [])
|
|
114
|
+
|
|
115
|
+
// WebSocket with automatic reconnection (exponential backoff)
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!preflightPassed) return
|
|
118
|
+
|
|
119
|
+
let retryCount = 0
|
|
120
|
+
let retryTimer: ReturnType<typeof setTimeout> | null = null
|
|
121
|
+
let intentionalClose = false
|
|
122
|
+
const MAX_RETRIES = 50
|
|
123
|
+
const BASE_DELAY = 1000
|
|
124
|
+
const MAX_DELAY = 30000
|
|
125
|
+
|
|
126
|
+
function handleMessage(msg: { type: string; payload: unknown }) {
|
|
127
|
+
const { type, payload } = msg
|
|
128
|
+
const s = useStore.getState()
|
|
129
|
+
|
|
130
|
+
switch (type) {
|
|
131
|
+
case 'system:health':
|
|
132
|
+
s.setSystemHealth(payload as Parameters<typeof s.setSystemHealth>[0])
|
|
133
|
+
break
|
|
134
|
+
case 'swarm:status':
|
|
135
|
+
s.setSwarm(payload as Parameters<typeof s.setSwarm>[0])
|
|
136
|
+
break
|
|
137
|
+
case 'agent:list':
|
|
138
|
+
s.setAgents(payload as Parameters<typeof s.setAgents>[0])
|
|
139
|
+
break
|
|
140
|
+
case 'agent:added':
|
|
141
|
+
s.addAgent(payload as Parameters<typeof s.addAgent>[0])
|
|
142
|
+
break
|
|
143
|
+
case 'agent:updated': {
|
|
144
|
+
const agentUpdate = payload as { id: string } & Record<string, unknown>
|
|
145
|
+
s.updateAgent(agentUpdate.id, agentUpdate)
|
|
146
|
+
break
|
|
147
|
+
}
|
|
148
|
+
case 'agent:removed':
|
|
149
|
+
s.removeAgent((payload as { id: string }).id)
|
|
150
|
+
break
|
|
151
|
+
case 'task:list':
|
|
152
|
+
s.setTasks(payload as Parameters<typeof s.setTasks>[0])
|
|
153
|
+
break
|
|
154
|
+
case 'task:added':
|
|
155
|
+
s.addTask(payload as Parameters<typeof s.addTask>[0])
|
|
156
|
+
break
|
|
157
|
+
case 'task:updated': {
|
|
158
|
+
const taskUpdate = payload as { id: string } & Record<string, unknown>
|
|
159
|
+
s.updateTask(taskUpdate.id, taskUpdate)
|
|
160
|
+
break
|
|
161
|
+
}
|
|
162
|
+
case 'memory:entries':
|
|
163
|
+
s.setMemoryEntries(payload as Parameters<typeof s.setMemoryEntries>[0])
|
|
164
|
+
break
|
|
165
|
+
case 'memory:stats':
|
|
166
|
+
s.setMemoryStats(payload as Parameters<typeof s.setMemoryStats>[0])
|
|
167
|
+
break
|
|
168
|
+
case 'session:list':
|
|
169
|
+
s.setSessions(payload as Parameters<typeof s.setSessions>[0])
|
|
170
|
+
break
|
|
171
|
+
case 'session:active':
|
|
172
|
+
s.setActiveSession(payload as Parameters<typeof s.setActiveSession>[0])
|
|
173
|
+
break
|
|
174
|
+
case 'hivemind:status':
|
|
175
|
+
s.setHiveMind(payload as Parameters<typeof s.setHiveMind>[0])
|
|
176
|
+
break
|
|
177
|
+
case 'neural:status':
|
|
178
|
+
s.setNeural(payload as Parameters<typeof s.setNeural>[0])
|
|
179
|
+
break
|
|
180
|
+
case 'performance:metrics':
|
|
181
|
+
s.setPerformance(payload as Parameters<typeof s.setPerformance>[0])
|
|
182
|
+
break
|
|
183
|
+
case 'hooks:list':
|
|
184
|
+
s.setHooks(payload as Parameters<typeof s.setHooks>[0])
|
|
185
|
+
break
|
|
186
|
+
case 'workflow:list':
|
|
187
|
+
s.setWorkflows(payload as Parameters<typeof s.setWorkflows>[0])
|
|
188
|
+
break
|
|
189
|
+
case 'coordination:metrics':
|
|
190
|
+
s.setCoordination(payload as Parameters<typeof s.setCoordination>[0])
|
|
191
|
+
break
|
|
192
|
+
case 'viz:update': {
|
|
193
|
+
const vizPayload = payload as { sessionId: string; tree: Parameters<typeof s.updateVizSession>[1] }
|
|
194
|
+
s.updateVizSession(vizPayload.sessionId, vizPayload.tree)
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
case 'swarm-monitor:update':
|
|
198
|
+
s.setSwarmMonitor(payload as Parameters<typeof s.setSwarmMonitor>[0])
|
|
199
|
+
break
|
|
200
|
+
case 'agent:activity':
|
|
201
|
+
if (s.swarmMonitor) {
|
|
202
|
+
const act = payload as { agentId: string; status: string; currentTask?: string; currentAction?: string }
|
|
203
|
+
const updatedAgents = s.swarmMonitor.agents.map(a =>
|
|
204
|
+
a.id === act.agentId
|
|
205
|
+
? { ...a, status: act.status as SwarmAgent['status'], currentTask: act.currentTask, currentAction: act.currentAction }
|
|
206
|
+
: a
|
|
207
|
+
)
|
|
208
|
+
s.setSwarmMonitor({ ...s.swarmMonitor, agents: updatedAgents })
|
|
209
|
+
}
|
|
210
|
+
break
|
|
211
|
+
case 'webhook:received':
|
|
212
|
+
case 'webhook:updated':
|
|
213
|
+
window.dispatchEvent(new CustomEvent('webhook-event', { detail: payload }))
|
|
214
|
+
break
|
|
215
|
+
case 'log':
|
|
216
|
+
s.addLog(payload as Parameters<typeof s.addLog>[0])
|
|
217
|
+
break
|
|
218
|
+
default:
|
|
219
|
+
console.warn('Unknown WS message type:', type)
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function connect() {
|
|
224
|
+
const ws = createWebSocket(handleMessage)
|
|
225
|
+
|
|
226
|
+
ws.onopen = () => {
|
|
227
|
+
const s = useStore.getState()
|
|
228
|
+
s.setConnected(true)
|
|
229
|
+
s.setWsStatus('connected')
|
|
230
|
+
if (retryCount > 0) {
|
|
231
|
+
s.addLog({ level: 'info', message: `WebSocket reconnected after ${retryCount} attempt(s)`, source: 'system' })
|
|
232
|
+
// Re-fetch data after reconnection to sync state
|
|
233
|
+
refreshCoreData()
|
|
234
|
+
} else {
|
|
235
|
+
s.addLog({ level: 'info', message: 'WebSocket connected to backend', source: 'system' })
|
|
236
|
+
}
|
|
237
|
+
retryCount = 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
ws.onclose = () => {
|
|
241
|
+
const s = useStore.getState()
|
|
242
|
+
s.setConnected(false)
|
|
243
|
+
wsRef.current = null
|
|
244
|
+
|
|
245
|
+
if (intentionalClose) return
|
|
246
|
+
|
|
247
|
+
if (retryCount < MAX_RETRIES) {
|
|
248
|
+
const delay = Math.min(BASE_DELAY * Math.pow(2, retryCount), MAX_DELAY)
|
|
249
|
+
retryCount++
|
|
250
|
+
s.setWsStatus('reconnecting')
|
|
251
|
+
s.addLog({
|
|
252
|
+
level: 'warn',
|
|
253
|
+
message: `WebSocket disconnected. Reconnecting in ${(delay / 1000).toFixed(0)}s (attempt ${retryCount}/${MAX_RETRIES})...`,
|
|
254
|
+
source: 'system',
|
|
255
|
+
})
|
|
256
|
+
retryTimer = setTimeout(connect, delay)
|
|
257
|
+
} else {
|
|
258
|
+
s.setWsStatus('disconnected')
|
|
259
|
+
s.addLog({ level: 'error', message: 'WebSocket connection lost. Reload the page to retry.', source: 'system' })
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
ws.onerror = () => {
|
|
264
|
+
// onclose will fire after this, so just let it handle reconnection
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
wsRef.current = ws
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
connect()
|
|
271
|
+
|
|
272
|
+
return () => {
|
|
273
|
+
intentionalClose = true
|
|
274
|
+
if (retryTimer) clearTimeout(retryTimer)
|
|
275
|
+
wsRef.current?.close()
|
|
276
|
+
wsRef.current = null
|
|
277
|
+
}
|
|
278
|
+
}, [preflightPassed, refreshCoreData])
|
|
279
|
+
|
|
280
|
+
// Fetch core data on startup
|
|
281
|
+
useEffect(() => {
|
|
282
|
+
if (!preflightPassed) return
|
|
283
|
+
refreshCoreData()
|
|
284
|
+
}, [preflightPassed, refreshCoreData])
|
|
285
|
+
|
|
286
|
+
// Health polling — every 30s, ping backend to detect silent disconnects
|
|
287
|
+
useEffect(() => {
|
|
288
|
+
if (!preflightPassed) return
|
|
289
|
+
|
|
290
|
+
let failCount = 0
|
|
291
|
+
const HEALTH_INTERVAL = 30_000
|
|
292
|
+
const FAIL_THRESHOLD = 3
|
|
293
|
+
|
|
294
|
+
const checkHealth = async () => {
|
|
295
|
+
try {
|
|
296
|
+
await api.system.health()
|
|
297
|
+
failCount = 0
|
|
298
|
+
const s = useStore.getState()
|
|
299
|
+
if (!s.backendReachable) {
|
|
300
|
+
s.setBackendReachable(true)
|
|
301
|
+
s.addLog({ level: 'info', message: 'Backend is reachable again', source: 'health' })
|
|
302
|
+
}
|
|
303
|
+
} catch {
|
|
304
|
+
failCount++
|
|
305
|
+
if (failCount >= FAIL_THRESHOLD) {
|
|
306
|
+
const s = useStore.getState()
|
|
307
|
+
if (s.backendReachable) {
|
|
308
|
+
s.setBackendReachable(false)
|
|
309
|
+
s.addLog({ level: 'error', message: `Backend unreachable (${failCount} consecutive failures)`, source: 'health' })
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Initial check
|
|
316
|
+
checkHealth()
|
|
317
|
+
const interval = setInterval(checkHealth, HEALTH_INTERVAL)
|
|
318
|
+
return () => clearInterval(interval)
|
|
319
|
+
}, [preflightPassed])
|
|
320
|
+
|
|
321
|
+
if (!preflightPassed) {
|
|
322
|
+
return <SetupWizard onContinue={handlePreflightContinue} />
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return (
|
|
326
|
+
<TourProvider>
|
|
327
|
+
<Routes>
|
|
328
|
+
<Route element={<Layout />}>
|
|
329
|
+
<Route
|
|
330
|
+
index
|
|
331
|
+
element={
|
|
332
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
333
|
+
<Dashboard />
|
|
334
|
+
</Suspense>
|
|
335
|
+
}
|
|
336
|
+
/>
|
|
337
|
+
<Route
|
|
338
|
+
path="swarm"
|
|
339
|
+
element={
|
|
340
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
341
|
+
<SwarmPanel />
|
|
342
|
+
</Suspense>
|
|
343
|
+
}
|
|
344
|
+
/>
|
|
345
|
+
<Route
|
|
346
|
+
path="agents"
|
|
347
|
+
element={
|
|
348
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
349
|
+
<AgentsPanel />
|
|
350
|
+
</Suspense>
|
|
351
|
+
}
|
|
352
|
+
/>
|
|
353
|
+
<Route
|
|
354
|
+
path="tasks"
|
|
355
|
+
element={
|
|
356
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
357
|
+
<TasksPanel />
|
|
358
|
+
</Suspense>
|
|
359
|
+
}
|
|
360
|
+
/>
|
|
361
|
+
<Route
|
|
362
|
+
path="memory"
|
|
363
|
+
element={
|
|
364
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
365
|
+
<MemoryPanel />
|
|
366
|
+
</Suspense>
|
|
367
|
+
}
|
|
368
|
+
/>
|
|
369
|
+
<Route
|
|
370
|
+
path="sessions"
|
|
371
|
+
element={
|
|
372
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
373
|
+
<SessionsPanel />
|
|
374
|
+
</Suspense>
|
|
375
|
+
}
|
|
376
|
+
/>
|
|
377
|
+
<Route
|
|
378
|
+
path="hive-mind"
|
|
379
|
+
element={
|
|
380
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
381
|
+
<HiveMindPanel />
|
|
382
|
+
</Suspense>
|
|
383
|
+
}
|
|
384
|
+
/>
|
|
385
|
+
<Route
|
|
386
|
+
path="neural"
|
|
387
|
+
element={
|
|
388
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
389
|
+
<NeuralPanel />
|
|
390
|
+
</Suspense>
|
|
391
|
+
}
|
|
392
|
+
/>
|
|
393
|
+
<Route
|
|
394
|
+
path="performance"
|
|
395
|
+
element={
|
|
396
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
397
|
+
<PerformancePanel />
|
|
398
|
+
</Suspense>
|
|
399
|
+
}
|
|
400
|
+
/>
|
|
401
|
+
<Route
|
|
402
|
+
path="hooks"
|
|
403
|
+
element={
|
|
404
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
405
|
+
<HooksPanel />
|
|
406
|
+
</Suspense>
|
|
407
|
+
}
|
|
408
|
+
/>
|
|
409
|
+
<Route
|
|
410
|
+
path="workflows"
|
|
411
|
+
element={
|
|
412
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
413
|
+
<WorkflowsPanel />
|
|
414
|
+
</Suspense>
|
|
415
|
+
}
|
|
416
|
+
/>
|
|
417
|
+
<Route
|
|
418
|
+
path="config"
|
|
419
|
+
element={
|
|
420
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
421
|
+
<ConfigPanel />
|
|
422
|
+
</Suspense>
|
|
423
|
+
}
|
|
424
|
+
/>
|
|
425
|
+
<Route
|
|
426
|
+
path="webhooks"
|
|
427
|
+
element={
|
|
428
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
429
|
+
<WebhooksPanel />
|
|
430
|
+
</Suspense>
|
|
431
|
+
}
|
|
432
|
+
/>
|
|
433
|
+
<Route
|
|
434
|
+
path="agent-viz"
|
|
435
|
+
element={
|
|
436
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
437
|
+
<AgentVizPanel />
|
|
438
|
+
</Suspense>
|
|
439
|
+
}
|
|
440
|
+
/>
|
|
441
|
+
<Route
|
|
442
|
+
path="swarm-monitor"
|
|
443
|
+
element={
|
|
444
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
445
|
+
<SwarmMonitorPanel />
|
|
446
|
+
</Suspense>
|
|
447
|
+
}
|
|
448
|
+
/>
|
|
449
|
+
<Route
|
|
450
|
+
path="logs"
|
|
451
|
+
element={
|
|
452
|
+
<Suspense fallback={<LoadingSpinner />}>
|
|
453
|
+
<LogsPanel />
|
|
454
|
+
</Suspense>
|
|
455
|
+
}
|
|
456
|
+
/>
|
|
457
|
+
</Route>
|
|
458
|
+
</Routes>
|
|
459
|
+
</TourProvider>
|
|
460
|
+
)
|
|
461
|
+
}
|