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,281 @@
1
+ import { useStore } from './store'
2
+ import type { WebhookEvent, GitHubWebhookStatus } from './types'
3
+
4
+ export interface PreflightCheck {
5
+ id: string
6
+ name: string
7
+ status: 'ok' | 'warn' | 'fail'
8
+ detail: string
9
+ fix?: string
10
+ }
11
+
12
+ export interface PreflightResult {
13
+ status: 'ok' | 'warn' | 'fail'
14
+ checks: PreflightCheck[]
15
+ failed: number
16
+ warned: number
17
+ passed: number
18
+ }
19
+
20
+ export interface TelegramStatus {
21
+ enabled: boolean
22
+ connected: boolean
23
+ botUsername: string | null
24
+ hasToken: boolean
25
+ hasChatId: boolean
26
+ tokenPreview: string
27
+ chatId: string
28
+ notifications: {
29
+ taskCompleted: boolean; taskFailed: boolean; swarmInit: boolean
30
+ swarmShutdown: boolean; agentError: boolean; taskProgress: boolean
31
+ }
32
+ }
33
+
34
+ const API_BASE = '/api'
35
+
36
+ function addApiLog(level: string, message: string) {
37
+ try { useStore.getState().addLog({ level, message, source: 'api' }) } catch { /* store not ready */ }
38
+ }
39
+
40
+ const DEFAULT_TIMEOUT = 45_000 // 45s timeout — CLI operations can take 30s+
41
+
42
+ async function request<T>(path: string, options?: RequestInit & { timeout?: number }): Promise<T> {
43
+ const method = options?.method ?? 'GET'
44
+ addApiLog('debug', `${method} ${path}`)
45
+
46
+ const timeoutMs = options?.timeout ?? DEFAULT_TIMEOUT
47
+ const controller = new AbortController()
48
+ const timeout = setTimeout(() => controller.abort(), timeoutMs)
49
+
50
+ try {
51
+ const res = await fetch(`${API_BASE}${path}`, {
52
+ headers: { 'Content-Type': 'application/json' },
53
+ ...options,
54
+ signal: options?.signal ?? controller.signal,
55
+ })
56
+ if (!res.ok) {
57
+ const err = await res.json().catch(() => ({ error: res.statusText }))
58
+ const msg = err.error || res.statusText
59
+ addApiLog('error', `${method} ${path} failed: ${msg}`)
60
+ throw new Error(msg)
61
+ }
62
+ addApiLog('info', `${method} ${path} OK`)
63
+ return res.json()
64
+ } catch (err) {
65
+ if (err instanceof DOMException && err.name === 'AbortError') {
66
+ const msg = `${method} ${path} timed out after ${timeoutMs / 1000}s`
67
+ addApiLog('error', msg)
68
+ throw new Error(msg)
69
+ }
70
+ throw err
71
+ } finally {
72
+ clearTimeout(timeout)
73
+ }
74
+ }
75
+
76
+ // System
77
+ export const api = {
78
+ system: {
79
+ health: () => request('/system/health', { timeout: 10_000 }),
80
+ info: () => request('/system/info'),
81
+ metrics: () => request('/system/metrics'),
82
+ status: () => request('/system/status'),
83
+ reset: () => request('/system/reset', { method: 'POST' }),
84
+ preflight: () => request<PreflightResult>('/system/preflight', { timeout: 60_000 }),
85
+ },
86
+
87
+ swarm: {
88
+ init: (opts: { topology?: string; maxAgents?: number; strategy?: string }) =>
89
+ request('/swarm/init', { method: 'POST', body: JSON.stringify(opts) }),
90
+ status: () => request('/swarm/status'),
91
+ health: () => request('/swarm/health'),
92
+ shutdown: () => request('/swarm/shutdown', { method: 'POST' }),
93
+ },
94
+
95
+ agents: {
96
+ list: () => request('/agents'),
97
+ spawn: (opts: { type: string; name: string; config?: Record<string, unknown> }) =>
98
+ request('/agents/spawn', { method: 'POST', body: JSON.stringify(opts) }),
99
+ status: (id: string) => request(`/agents/${id}/status`),
100
+ health: (id: string) => request(`/agents/${id}/health`),
101
+ terminate: (id: string) => request(`/agents/${id}/terminate`, { method: 'POST' }),
102
+ terminateAll: () => request('/agents/terminate-all', { method: 'POST' }),
103
+ update: (id: string, data: Record<string, unknown>) =>
104
+ request(`/agents/${id}`, { method: 'PATCH', body: JSON.stringify(data) }),
105
+ pool: () => request('/agents/pool'),
106
+ },
107
+
108
+ tasks: {
109
+ list: () => request('/tasks'),
110
+ create: (opts: { title: string; description: string; priority?: string; assignTo?: string; cwd?: string }) =>
111
+ request('/tasks', { method: 'POST', body: JSON.stringify(opts) }),
112
+ status: (id: string) => request(`/tasks/${id}/status`),
113
+ assign: (id: string, agentId: string) =>
114
+ request(`/tasks/${id}/assign`, { method: 'POST', body: JSON.stringify({ agentId }) }),
115
+ complete: (id: string, result?: string) =>
116
+ request(`/tasks/${id}/complete`, { method: 'POST', body: JSON.stringify({ result }) }),
117
+ cancel: (id: string) => request(`/tasks/${id}/cancel`, { method: 'POST' }),
118
+ continue: (id: string, instruction: string) =>
119
+ request(`/tasks/${id}/continue`, { method: 'POST', body: JSON.stringify({ instruction }) }),
120
+ output: (id: string, tail = 200) =>
121
+ request<{ taskId: string; lines: Array<{ type: string; content: string; timestamp: string }> }>(`/tasks/${id}/output?tail=${tail}`),
122
+ summary: () => request('/tasks/summary'),
123
+ },
124
+
125
+ memory: {
126
+ list: (namespace?: string, limit?: number) =>
127
+ request(`/memory?${new URLSearchParams({ ...(namespace && { namespace }), ...(limit && { limit: String(limit) }) })}`),
128
+ store: (opts: { key: string; value: string; namespace?: string; tags?: string[]; ttl?: number }) =>
129
+ request('/memory', { method: 'POST', body: JSON.stringify(opts) }),
130
+ retrieve: (key: string, namespace?: string) =>
131
+ request(`/memory/${key}?${new URLSearchParams({ ...(namespace && { namespace }) })}`),
132
+ search: (query: string, namespace?: string, limit?: number) =>
133
+ request('/memory/search', { method: 'POST', body: JSON.stringify({ query, namespace, limit }) }),
134
+ delete: (key: string, namespace?: string) =>
135
+ request(`/memory/${key}?${new URLSearchParams({ ...(namespace && { namespace }) })}`, { method: 'DELETE' }),
136
+ stats: () => request('/memory/stats'),
137
+ migrate: (opts: { from: string; to: string }) =>
138
+ request('/memory/migrate', { method: 'POST', body: JSON.stringify(opts) }),
139
+ },
140
+
141
+ sessions: {
142
+ list: () => request('/sessions'),
143
+ save: (name?: string) => request('/sessions/save', { method: 'POST', body: JSON.stringify({ name }) }),
144
+ restore: (id: string) => request(`/sessions/${id}/restore`, { method: 'POST' }),
145
+ info: (id: string) => request(`/sessions/${id}`),
146
+ delete: (id: string) => request(`/sessions/${id}`, { method: 'DELETE' }),
147
+ },
148
+
149
+ hiveMind: {
150
+ init: (opts?: { protocol?: string }) =>
151
+ request('/hive-mind/init', { method: 'POST', body: JSON.stringify(opts || {}) }),
152
+ status: () => request('/hive-mind/status'),
153
+ join: (agentId: string) =>
154
+ request('/hive-mind/join', { method: 'POST', body: JSON.stringify({ agentId }) }),
155
+ leave: (agentId: string) =>
156
+ request('/hive-mind/leave', { method: 'POST', body: JSON.stringify({ agentId }) }),
157
+ broadcast: (message: string) =>
158
+ request('/hive-mind/broadcast', { method: 'POST', body: JSON.stringify({ message }) }),
159
+ consensus: (topic: string, options: string[]) =>
160
+ request('/hive-mind/consensus', { method: 'POST', body: JSON.stringify({ topic, options }) }),
161
+ memory: () => request('/hive-mind/memory'),
162
+ shutdown: () => request('/hive-mind/shutdown', { method: 'POST' }),
163
+ },
164
+
165
+ neural: {
166
+ status: () => request('/neural/status'),
167
+ train: (opts: { model: string; data?: unknown }) =>
168
+ request('/neural/train', { method: 'POST', body: JSON.stringify(opts) }),
169
+ predict: (opts: { model: string; input: unknown }) =>
170
+ request('/neural/predict', { method: 'POST', body: JSON.stringify(opts) }),
171
+ optimize: () => request('/neural/optimize', { method: 'POST' }),
172
+ patterns: () => request('/neural/patterns'),
173
+ compress: () => request('/neural/compress', { method: 'POST' }),
174
+ },
175
+
176
+ performance: {
177
+ metrics: () => request('/performance/metrics'),
178
+ benchmark: (opts?: { type?: string }) =>
179
+ request('/performance/benchmark', { method: 'POST', body: JSON.stringify(opts || {}) }),
180
+ bottleneck: () => request('/performance/bottleneck'),
181
+ optimize: () => request('/performance/optimize', { method: 'POST' }),
182
+ profile: () => request('/performance/profile'),
183
+ report: () => request('/performance/report'),
184
+ },
185
+
186
+ hooks: {
187
+ list: () => request('/hooks'),
188
+ init: () => request('/hooks/init', { method: 'POST' }),
189
+ metrics: () => request('/hooks/metrics'),
190
+ explain: (hookName: string) => request(`/hooks/${hookName}/explain`),
191
+ },
192
+
193
+ workflows: {
194
+ list: () => request('/workflows'),
195
+ create: (opts: { name: string; steps: unknown[] }) =>
196
+ request('/workflows', { method: 'POST', body: JSON.stringify(opts) }),
197
+ execute: (id: string) => request(`/workflows/${id}/execute`, { method: 'POST' }),
198
+ status: (id: string) => request(`/workflows/${id}/status`),
199
+ cancel: (id: string) => request(`/workflows/${id}/cancel`, { method: 'POST' }),
200
+ pause: (id: string) => request(`/workflows/${id}/pause`, { method: 'POST' }),
201
+ resume: (id: string) => request(`/workflows/${id}/resume`, { method: 'POST' }),
202
+ delete: (id: string) => request(`/workflows/${id}`, { method: 'DELETE' }),
203
+ templates: () => request('/workflows/templates'),
204
+ },
205
+
206
+ coordination: {
207
+ metrics: () => request('/coordination/metrics'),
208
+ topology: () => request('/coordination/topology'),
209
+ sync: () => request('/coordination/sync', { method: 'POST' }),
210
+ consensus: (topic: string) =>
211
+ request('/coordination/consensus', { method: 'POST', body: JSON.stringify({ topic }) }),
212
+ },
213
+
214
+ config: {
215
+ list: () => request('/config'),
216
+ get: (key: string) => request(`/config/${key}`),
217
+ set: (key: string, value: unknown) =>
218
+ request(`/config/${key}`, { method: 'PUT', body: JSON.stringify({ value }) }),
219
+ reset: () => request('/config/reset', { method: 'POST' }),
220
+ export: () => request('/config/export'),
221
+ import: (data: unknown) =>
222
+ request('/config/import', { method: 'POST', body: JSON.stringify(data) }),
223
+ getServerSettings: () => request<{ skipPermissions: boolean }>('/config/server-settings'),
224
+ setServerSettings: (settings: { skipPermissions: boolean }) =>
225
+ request<{ skipPermissions: boolean }>('/config/server-settings', { method: 'PUT', body: JSON.stringify(settings) }),
226
+ getTelegramStatus: () => request<TelegramStatus>('/config/telegram'),
227
+ setTelegramConfig: (config: { enabled?: boolean; token?: string; chatId?: string; notifications?: Partial<TelegramStatus['notifications']> }) =>
228
+ request<TelegramStatus>('/config/telegram', { method: 'PUT', body: JSON.stringify(config) }),
229
+ testTelegram: () => request<{ ok: boolean; error?: string }>('/config/telegram/test', { method: 'POST' }),
230
+ getTelegramLog: () => request<{ log: Array<{ timestamp: string; direction: 'in' | 'out'; message: string }> }>('/config/telegram/log'),
231
+ },
232
+
233
+ viz: {
234
+ sessions: () => request('/viz/sessions'),
235
+ session: (id: string) => request(`/viz/sessions/${id}`),
236
+ nodeLogs: (sessionId: string, nodeId: string, tail = 100) =>
237
+ request(`/viz/sessions/${sessionId}/logs/${nodeId}?tail=${tail}`),
238
+ },
239
+
240
+ swarmMonitor: {
241
+ snapshot: (currentOnly = false) => request(`/swarm-monitor/snapshot${currentOnly ? '?current=true' : ''}`),
242
+ purge: () => request('/swarm-monitor/purge', { method: 'POST' }),
243
+ agents: () => request('/swarm-monitor/agents'),
244
+ health: () => request('/swarm-monitor/health'),
245
+ metrics: () => request('/swarm-monitor/metrics'),
246
+ agentOutput: (agentId: string) => request<{ agentId: string; lines: string[] }>(`/swarm-monitor/output/${agentId}`),
247
+ },
248
+
249
+ aiDefence: {
250
+ analyze: (input: string) =>
251
+ request('/ai-defence/analyze', { method: 'POST', body: JSON.stringify({ input }) }),
252
+ scan: () => request('/ai-defence/scan'),
253
+ stats: () => request('/ai-defence/stats'),
254
+ },
255
+
256
+ webhooks: {
257
+ getGitHubConfig: () => request<GitHubWebhookStatus>('/webhooks/github/config'),
258
+ setGitHubConfig: (config: Record<string, unknown>) =>
259
+ request('/webhooks/github/config', { method: 'PUT', body: JSON.stringify(config) }),
260
+ getGitHubEvents: () => request<WebhookEvent[]>('/webhooks/github/events'),
261
+ testGitHub: () => request<{ ok: boolean; eventId?: string; taskId?: string; error?: string }>(
262
+ '/webhooks/github/test', { method: 'POST' }),
263
+ },
264
+ }
265
+
266
+ // WebSocket connection
267
+ export function createWebSocket(onMessage: (msg: { type: string; payload: unknown }) => void): WebSocket {
268
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
269
+ const ws = new WebSocket(`${protocol}//${window.location.host}/ws`)
270
+
271
+ ws.onmessage = (event) => {
272
+ try {
273
+ const msg = JSON.parse(event.data)
274
+ onMessage(msg)
275
+ } catch {
276
+ console.error('Invalid WS message', event.data)
277
+ }
278
+ }
279
+
280
+ return ws
281
+ }
@@ -0,0 +1,98 @@
1
+ import { Component, type ReactNode, type ErrorInfo } from 'react'
2
+
3
+ interface Props {
4
+ children: ReactNode
5
+ }
6
+
7
+ interface State {
8
+ hasError: boolean
9
+ error: Error | null
10
+ errorInfo: ErrorInfo | null
11
+ }
12
+
13
+ export class ErrorBoundary extends Component<Props, State> {
14
+ state: State = { hasError: false, error: null, errorInfo: null }
15
+
16
+ static getDerivedStateFromError(error: Error): Partial<State> {
17
+ return { hasError: true, error }
18
+ }
19
+
20
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
21
+ this.setState({ errorInfo })
22
+ console.error('[RuFloUI] Uncaught error:', error, errorInfo)
23
+ }
24
+
25
+ handleReload = () => {
26
+ this.setState({ hasError: false, error: null, errorInfo: null })
27
+ }
28
+
29
+ handleHardReload = () => {
30
+ window.location.reload()
31
+ }
32
+
33
+ render() {
34
+ if (!this.state.hasError) return this.props.children
35
+
36
+ const { error, errorInfo } = this.state
37
+ return (
38
+ <div style={{
39
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
40
+ height: '100vh', background: 'var(--bg-primary)', color: 'var(--text-primary)',
41
+ fontFamily: 'Inter, system-ui, sans-serif',
42
+ }}>
43
+ <div style={{
44
+ maxWidth: 600, width: '100%', padding: 32,
45
+ background: 'var(--bg-card)', border: '1px solid var(--accent-red)',
46
+ borderRadius: 12, boxShadow: '0 0 40px rgba(239, 68, 68, 0.15)',
47
+ }}>
48
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 20 }}>
49
+ <div style={{
50
+ width: 40, height: 40, borderRadius: '50%',
51
+ background: 'rgba(239, 68, 68, 0.15)', display: 'flex',
52
+ alignItems: 'center', justifyContent: 'center', fontSize: 20,
53
+ }}>!</div>
54
+ <div>
55
+ <div style={{ fontSize: 18, fontWeight: 700 }}>Something went wrong</div>
56
+ <div style={{ fontSize: 13, color: 'var(--text-muted)', marginTop: 2 }}>
57
+ An unexpected error occurred in the application
58
+ </div>
59
+ </div>
60
+ </div>
61
+
62
+ <div style={{
63
+ background: 'var(--bg-primary)', border: '1px solid var(--border)',
64
+ borderRadius: 8, padding: 16, marginBottom: 20,
65
+ }}>
66
+ <div style={{ fontSize: 13, fontWeight: 600, color: 'var(--accent-red)', marginBottom: 8 }}>
67
+ {error?.name}: {error?.message}
68
+ </div>
69
+ <pre style={{
70
+ fontSize: 11, color: 'var(--text-muted)', margin: 0,
71
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
72
+ maxHeight: 200, overflow: 'auto', lineHeight: 1.6,
73
+ }}>
74
+ {errorInfo?.componentStack?.trim()}
75
+ </pre>
76
+ </div>
77
+
78
+ <div style={{ display: 'flex', gap: 12 }}>
79
+ <button onClick={this.handleReload} style={{
80
+ flex: 1, padding: '10px 16px', fontSize: 14, fontWeight: 600,
81
+ background: 'var(--accent-blue)', color: 'white',
82
+ border: 'none', borderRadius: 8, cursor: 'pointer',
83
+ }}>
84
+ Try Again
85
+ </button>
86
+ <button onClick={this.handleHardReload} style={{
87
+ flex: 1, padding: '10px 16px', fontSize: 14, fontWeight: 600,
88
+ background: 'transparent', color: 'var(--text-secondary)',
89
+ border: '1px solid var(--border)', borderRadius: 8, cursor: 'pointer',
90
+ }}>
91
+ Reload Page
92
+ </button>
93
+ </div>
94
+ </div>
95
+ </div>
96
+ )
97
+ }
98
+ }