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.
Files changed (56) hide show
  1. package/'1' +0 -0
  2. package/.env.example +46 -0
  3. package/CHANGELOG.md +87 -0
  4. package/CLAUDE.md +287 -0
  5. package/LICENSE +21 -0
  6. package/README.md +316 -0
  7. package/Webhooks) +0 -0
  8. package/docs/plans/2026-03-11-github-webhooks.md +957 -0
  9. package/docs/screenshot-swarm-monitor.png +0 -0
  10. package/frontend +0 -0
  11. package/index.html +13 -0
  12. package/package.json +56 -0
  13. package/public/vite.svg +4 -0
  14. package/src/backend/__tests__/webhook-github.test.ts +934 -0
  15. package/src/backend/jsonl-monitor.ts +430 -0
  16. package/src/backend/server.ts +2972 -0
  17. package/src/backend/telegram-bot.ts +511 -0
  18. package/src/backend/webhook-github.ts +350 -0
  19. package/src/frontend/App.tsx +461 -0
  20. package/src/frontend/api.ts +281 -0
  21. package/src/frontend/components/ErrorBoundary.tsx +98 -0
  22. package/src/frontend/components/Layout.tsx +431 -0
  23. package/src/frontend/components/ui/Button.tsx +111 -0
  24. package/src/frontend/components/ui/Card.tsx +51 -0
  25. package/src/frontend/components/ui/StatusBadge.tsx +60 -0
  26. package/src/frontend/main.tsx +63 -0
  27. package/src/frontend/pages/AgentVizPanel.tsx +428 -0
  28. package/src/frontend/pages/AgentsPanel.tsx +445 -0
  29. package/src/frontend/pages/ConfigPanel.tsx +661 -0
  30. package/src/frontend/pages/Dashboard.tsx +482 -0
  31. package/src/frontend/pages/HiveMindPanel.tsx +355 -0
  32. package/src/frontend/pages/HooksPanel.tsx +240 -0
  33. package/src/frontend/pages/LogsPanel.tsx +261 -0
  34. package/src/frontend/pages/MemoryPanel.tsx +444 -0
  35. package/src/frontend/pages/NeuralPanel.tsx +301 -0
  36. package/src/frontend/pages/PerformancePanel.tsx +198 -0
  37. package/src/frontend/pages/SessionsPanel.tsx +428 -0
  38. package/src/frontend/pages/SetupWizard.tsx +181 -0
  39. package/src/frontend/pages/SwarmMonitorPanel.tsx +634 -0
  40. package/src/frontend/pages/SwarmPanel.tsx +322 -0
  41. package/src/frontend/pages/TasksPanel.tsx +535 -0
  42. package/src/frontend/pages/WebhooksPanel.tsx +335 -0
  43. package/src/frontend/pages/WorkflowsPanel.tsx +448 -0
  44. package/src/frontend/store.ts +185 -0
  45. package/src/frontend/styles/global.css +113 -0
  46. package/src/frontend/test-setup.ts +1 -0
  47. package/src/frontend/tour/TourContext.tsx +161 -0
  48. package/src/frontend/tour/tourSteps.ts +181 -0
  49. package/src/frontend/tour/tourStyles.css +116 -0
  50. package/src/frontend/types.ts +239 -0
  51. package/src/frontend/utils/formatTime.test.ts +83 -0
  52. package/src/frontend/utils/formatTime.ts +23 -0
  53. package/tsconfig.json +23 -0
  54. package/vite.config.ts +26 -0
  55. package/vitest.config.ts +17 -0
  56. 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
+ }