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,957 @@
1
+ # GitHub Webhook Integration — Issue-to-Task Pipeline
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Receive GitHub webhook events (issue opened) and automatically create + assign swarm tasks that investigate, code, test, and submit a PR to resolve the issue.
6
+
7
+ **Architecture:** A new `webhook-github.ts` backend module receives `POST /api/webhooks/github` with HMAC-SHA256 validation, normalizes the payload into a `WebhookEvent`, persists it, creates a task via the existing `createAndAssignTask` pattern, and broadcasts events to the frontend. A new `WebhooksPanel.tsx` page lets users configure the GitHub integration (token, secret, repos) and view incoming event history. The existing swarm pipeline (`launchWorkflowForTask`) handles all execution — no changes needed there.
8
+
9
+ **Tech Stack:** Express routes, Node.js `crypto` (HMAC), GitHub REST API via `fetch`, React 19 page, Zustand store slice, existing swarm pipeline.
10
+
11
+ ---
12
+
13
+ ## Task 1: Backend — Webhook Config Persistence
14
+
15
+ **Files:**
16
+ - Create: `src/backend/webhook-github.ts`
17
+ - Modify: `src/backend/server.ts:23-35` (near telegram config)
18
+
19
+ ### Step 1: Create the config types and load/save functions
20
+
21
+ Create `src/backend/webhook-github.ts` with config types and persistence (following the Telegram pattern in `server.ts:23-80`):
22
+
23
+ ```typescript
24
+ import { createHmac } from 'crypto'
25
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'fs'
26
+ import { join } from 'path'
27
+
28
+ // ── Types ──────────────────────────────────────────────────────────────────
29
+
30
+ export interface GitHubWebhookConfig {
31
+ enabled: boolean
32
+ /** Personal access token (repo scope) for creating branches/PRs */
33
+ githubToken: string
34
+ /** Webhook secret for HMAC-SHA256 validation */
35
+ webhookSecret: string
36
+ /** Repos to monitor — array of "owner/repo" strings */
37
+ repos: string[]
38
+ /** Auto-assign new issue tasks to the active swarm */
39
+ autoAssign: boolean
40
+ }
41
+
42
+ export interface WebhookEvent {
43
+ id: string
44
+ provider: 'github'
45
+ repo: string
46
+ event: string // 'issues.opened', 'issues.reopened', etc.
47
+ title: string
48
+ body: string
49
+ url: string // HTML URL of the issue
50
+ number: number
51
+ author: string
52
+ labels: string[]
53
+ receivedAt: string
54
+ taskId?: string // Linked RuFloUI task ID (set after task creation)
55
+ status: 'received' | 'processing' | 'completed' | 'failed' | 'ignored'
56
+ }
57
+
58
+ export interface GitHubWebhookHandle {
59
+ getConfig: () => GitHubWebhookConfig
60
+ getEvents: () => WebhookEvent[]
61
+ }
62
+
63
+ const DEFAULT_CONFIG: GitHubWebhookConfig = {
64
+ enabled: false,
65
+ githubToken: '',
66
+ webhookSecret: '',
67
+ repos: [],
68
+ autoAssign: true,
69
+ }
70
+
71
+ // ── Persistence ────────────────────────────────────────────────────────────
72
+
73
+ function configPath(): string {
74
+ const dir = process.env.RUFLO_PERSIST_DIR || '.ruflo'
75
+ return join(dir, 'github-webhook.json')
76
+ }
77
+
78
+ export function loadGitHubWebhookConfig(): GitHubWebhookConfig {
79
+ try {
80
+ if (existsSync(configPath())) {
81
+ const raw = JSON.parse(readFileSync(configPath(), 'utf-8'))
82
+ return { ...DEFAULT_CONFIG, ...raw }
83
+ }
84
+ } catch { /* use defaults */ }
85
+ // Fallback to env vars
86
+ return {
87
+ ...DEFAULT_CONFIG,
88
+ enabled: process.env.GITHUB_WEBHOOK_ENABLED === 'true',
89
+ githubToken: process.env.GITHUB_TOKEN || '',
90
+ webhookSecret: process.env.GITHUB_WEBHOOK_SECRET || '',
91
+ repos: process.env.GITHUB_WEBHOOK_REPOS?.split(',').map(r => r.trim()).filter(Boolean) || [],
92
+ }
93
+ }
94
+
95
+ export function saveGitHubWebhookConfig(config: GitHubWebhookConfig): void {
96
+ const dir = process.env.RUFLO_PERSIST_DIR || '.ruflo'
97
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
98
+ writeFileSync(configPath(), JSON.stringify(config, null, 2), 'utf-8')
99
+ try { chmodSync(configPath(), 0o600) } catch { /* Windows */ }
100
+ }
101
+
102
+ // ── HMAC Validation ────────────────────────────────────────────────────────
103
+
104
+ export function verifyGitHubSignature(
105
+ payload: string,
106
+ signature: string | undefined,
107
+ secret: string,
108
+ ): boolean {
109
+ if (!signature || !secret) return false
110
+ const expected = 'sha256=' + createHmac('sha256', secret).update(payload).digest('hex')
111
+ // Constant-time comparison
112
+ if (expected.length !== signature.length) return false
113
+ let result = 0
114
+ for (let i = 0; i < expected.length; i++) {
115
+ result |= expected.charCodeAt(i) ^ signature.charCodeAt(i)
116
+ }
117
+ return result === 0
118
+ }
119
+ ```
120
+
121
+ ### Step 2: Commit
122
+
123
+ ```bash
124
+ git add src/backend/webhook-github.ts
125
+ git commit -m "feat(webhooks): add GitHub webhook config types and persistence"
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Task 2: Backend — Webhook Receiver Route
131
+
132
+ **Files:**
133
+ - Modify: `src/backend/webhook-github.ts` (add route factory)
134
+ - Modify: `src/backend/server.ts:2707-2721` (mount route)
135
+
136
+ ### Step 1: Add the webhook route factory and config API to webhook-github.ts
137
+
138
+ Append to `src/backend/webhook-github.ts`:
139
+
140
+ ```typescript
141
+ import { Router, Request, Response, RequestHandler } from 'express'
142
+
143
+ // ── In-memory event store ──────────────────────────────────────────────────
144
+
145
+ const MAX_EVENTS = 200
146
+ let webhookEvents: WebhookEvent[] = []
147
+
148
+ function addEvent(event: WebhookEvent): void {
149
+ webhookEvents.unshift(event)
150
+ if (webhookEvents.length > MAX_EVENTS) webhookEvents = webhookEvents.slice(0, MAX_EVENTS)
151
+ }
152
+
153
+ // ── Route Factory ──────────────────────────────────────────────────────────
154
+
155
+ export interface WebhookStores {
156
+ createAndAssignTask: (title: string, description: string) => Promise<{ taskId: string; assigned: boolean }>
157
+ broadcast: (type: string, payload: unknown) => void
158
+ }
159
+
160
+ export function githubWebhookRoutes(
161
+ getConfig: () => GitHubWebhookConfig,
162
+ setConfig: (c: GitHubWebhookConfig) => void,
163
+ stores: WebhookStores,
164
+ ): Router {
165
+ const router = Router()
166
+
167
+ // Error wrapper (same pattern as server.ts h())
168
+ const wrap = (fn: (req: Request, res: Response) => Promise<void>): RequestHandler =>
169
+ async (req, res, _next) => {
170
+ try { await fn(req, res) } catch (err: unknown) {
171
+ res.status(500).json({ error: err instanceof Error ? err.message : String(err) })
172
+ }
173
+ }
174
+
175
+ // ── Webhook receiver ───────────────────────────────────────────────────
176
+
177
+ // GitHub sends POST with X-Hub-Event header and X-Hub-Signature-256
178
+ // Raw body is needed for HMAC — we parse JSON ourselves
179
+ router.post('/github', wrap(async (req, res) => {
180
+ const config = getConfig()
181
+ if (!config.enabled) {
182
+ res.status(503).json({ error: 'GitHub webhooks disabled' })
183
+ return
184
+ }
185
+
186
+ // Validate HMAC signature
187
+ const rawBody = JSON.stringify(req.body) // express.json() already parsed
188
+ const sig = req.headers['x-hub-signature-256'] as string | undefined
189
+ if (config.webhookSecret && !verifyGitHubSignature(rawBody, sig, config.webhookSecret)) {
190
+ res.status(401).json({ error: 'Invalid signature' })
191
+ return
192
+ }
193
+
194
+ const ghEvent = req.headers['x-github-event'] as string || 'unknown'
195
+ const payload = req.body
196
+
197
+ // Only handle issue events for now
198
+ if (ghEvent !== 'issues') {
199
+ res.json({ ok: true, action: 'ignored', reason: `event type '${ghEvent}' not handled` })
200
+ return
201
+ }
202
+
203
+ const action = payload.action // 'opened', 'reopened', 'edited', etc.
204
+ if (action !== 'opened' && action !== 'reopened') {
205
+ res.json({ ok: true, action: 'ignored', reason: `issues.${action} not handled` })
206
+ return
207
+ }
208
+
209
+ const issue = payload.issue
210
+ const repo = payload.repository?.full_name || 'unknown/unknown'
211
+
212
+ // Check if this repo is monitored
213
+ if (config.repos.length > 0 && !config.repos.includes(repo)) {
214
+ res.json({ ok: true, action: 'ignored', reason: `repo '${repo}' not monitored` })
215
+ return
216
+ }
217
+
218
+ // Create webhook event
219
+ const event: WebhookEvent = {
220
+ id: `gh-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
221
+ provider: 'github',
222
+ repo,
223
+ event: `issues.${action}`,
224
+ title: issue.title || 'Untitled',
225
+ body: issue.body || '',
226
+ url: issue.html_url || '',
227
+ number: issue.number || 0,
228
+ author: issue.user?.login || 'unknown',
229
+ labels: (issue.labels || []).map((l: { name: string }) => l.name),
230
+ receivedAt: new Date().toISOString(),
231
+ status: 'received',
232
+ }
233
+ addEvent(event)
234
+ stores.broadcast('webhook:received', event)
235
+
236
+ // Auto-create task if enabled
237
+ if (config.autoAssign) {
238
+ event.status = 'processing'
239
+ stores.broadcast('webhook:updated', event)
240
+
241
+ const taskTitle = `[${repo}#${issue.number}] ${issue.title}`
242
+ const taskDesc = [
243
+ `GitHub Issue: ${issue.html_url}`,
244
+ `Author: ${issue.user?.login}`,
245
+ `Labels: ${event.labels.join(', ') || 'none'}`,
246
+ '',
247
+ issue.body?.slice(0, 2000) || 'No description provided.',
248
+ '',
249
+ '---',
250
+ 'Instructions: Analyze this issue, investigate the codebase, implement a fix, write tests, and prepare a summary of changes.',
251
+ ].join('\n')
252
+
253
+ try {
254
+ const result = await stores.createAndAssignTask(taskTitle, taskDesc)
255
+ event.taskId = result.taskId
256
+ event.status = result.assigned ? 'processing' : 'received'
257
+ stores.broadcast('webhook:updated', event)
258
+ } catch (err) {
259
+ event.status = 'failed'
260
+ stores.broadcast('webhook:updated', event)
261
+ }
262
+ }
263
+
264
+ res.json({ ok: true, action: 'task_created', eventId: event.id, taskId: event.taskId })
265
+ }))
266
+
267
+ // ── Config API ─────────────────────────────────────────────────────────
268
+
269
+ router.get('/github/config', wrap(async (_req, res) => {
270
+ const config = getConfig()
271
+ res.json({
272
+ enabled: config.enabled,
273
+ hasToken: !!config.githubToken,
274
+ tokenPreview: config.githubToken ? '...' + config.githubToken.slice(-4) : '',
275
+ webhookSecret: config.webhookSecret ? '****' : '',
276
+ hasSecret: !!config.webhookSecret,
277
+ repos: config.repos,
278
+ autoAssign: config.autoAssign,
279
+ })
280
+ }))
281
+
282
+ router.put('/github/config', wrap(async (req, res) => {
283
+ const config = getConfig()
284
+ const { enabled, githubToken, webhookSecret, repos, autoAssign } = req.body
285
+ if (typeof enabled === 'boolean') config.enabled = enabled
286
+ if (typeof githubToken === 'string') config.githubToken = githubToken
287
+ if (typeof webhookSecret === 'string') config.webhookSecret = webhookSecret
288
+ if (Array.isArray(repos)) config.repos = repos.filter((r: unknown) => typeof r === 'string' && r.includes('/'))
289
+ if (typeof autoAssign === 'boolean') config.autoAssign = autoAssign
290
+ setConfig(config)
291
+ saveGitHubWebhookConfig(config)
292
+ res.json({ ok: true })
293
+ }))
294
+
295
+ // ── Events API ─────────────────────────────────────────────────────────
296
+
297
+ router.get('/github/events', wrap(async (_req, res) => {
298
+ res.json(webhookEvents)
299
+ }))
300
+
301
+ return router
302
+ }
303
+ ```
304
+
305
+ ### Step 2: Mount in server.ts
306
+
307
+ Add import near line 11 (after telegram import):
308
+
309
+ ```typescript
310
+ import { loadGitHubWebhookConfig, saveGitHubWebhookConfig, GitHubWebhookConfig, githubWebhookRoutes } from './webhook-github'
311
+ ```
312
+
313
+ Add state near line 23 (after telegram config block):
314
+
315
+ ```typescript
316
+ let githubWebhookConfig = loadGitHubWebhookConfig()
317
+ ```
318
+
319
+ Mount route near line 2721 (after `app.use('/api/ai-defence', ...)`):
320
+
321
+ ```typescript
322
+ app.use('/api/webhooks', githubWebhookRoutes(
323
+ () => githubWebhookConfig,
324
+ (c) => { githubWebhookConfig = c },
325
+ {
326
+ createAndAssignTask: async (title: string, description: string) => {
327
+ const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
328
+ const task = { id, title, description, status: 'pending', priority: 'high', createdAt: new Date().toISOString() }
329
+ taskStore.set(id, task as any)
330
+ broadcast('task:added', task)
331
+ if (!swarmShutdown) {
332
+ ;(task as any).status = 'in_progress'
333
+ ;(task as any).startedAt = new Date().toISOString()
334
+ broadcast('task:updated', { ...task, id })
335
+ launchWorkflowForTask(id, title, description)
336
+ return { taskId: id, assigned: true }
337
+ }
338
+ return { taskId: id, assigned: false }
339
+ },
340
+ broadcast,
341
+ },
342
+ ))
343
+ ```
344
+
345
+ ### Step 3: Commit
346
+
347
+ ```bash
348
+ git add src/backend/webhook-github.ts src/backend/server.ts
349
+ git commit -m "feat(webhooks): add GitHub webhook receiver with HMAC validation and auto-task creation"
350
+ ```
351
+
352
+ ---
353
+
354
+ ## Task 3: Frontend — API Client + Types
355
+
356
+ **Files:**
357
+ - Modify: `src/frontend/api.ts:253` (before closing `}` of `api` object)
358
+ - Modify: `src/frontend/types.ts` (append)
359
+
360
+ ### Step 1: Add WebhookEvent type to types.ts
361
+
362
+ Append to `src/frontend/types.ts`:
363
+
364
+ ```typescript
365
+ export interface WebhookEvent {
366
+ id: string
367
+ provider: 'github'
368
+ repo: string
369
+ event: string
370
+ title: string
371
+ body: string
372
+ url: string
373
+ number: number
374
+ author: string
375
+ labels: string[]
376
+ receivedAt: string
377
+ taskId?: string
378
+ status: 'received' | 'processing' | 'completed' | 'failed' | 'ignored'
379
+ }
380
+
381
+ export interface GitHubWebhookStatus {
382
+ enabled: boolean
383
+ hasToken: boolean
384
+ tokenPreview: string
385
+ webhookSecret: string
386
+ hasSecret: boolean
387
+ repos: string[]
388
+ autoAssign: boolean
389
+ }
390
+ ```
391
+
392
+ ### Step 2: Add API namespace to api.ts
393
+
394
+ Insert before the closing `}` of the `api` object (before `aiDefence` closing, around line 253):
395
+
396
+ ```typescript
397
+ webhooks: {
398
+ getGitHubConfig: () => request<GitHubWebhookStatus>('/webhooks/github/config'),
399
+ setGitHubConfig: (config: Record<string, unknown>) =>
400
+ request('/webhooks/github/config', { method: 'PUT', body: JSON.stringify(config) }),
401
+ getGitHubEvents: () => request<WebhookEvent[]>('/webhooks/github/events'),
402
+ },
403
+ ```
404
+
405
+ Add to imports at top of `api.ts`:
406
+
407
+ ```typescript
408
+ import type { WebhookEvent, GitHubWebhookStatus } from '@/types'
409
+ ```
410
+
411
+ ### Step 3: Commit
412
+
413
+ ```bash
414
+ git add src/frontend/api.ts src/frontend/types.ts
415
+ git commit -m "feat(webhooks): add frontend API client and types for GitHub webhooks"
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Task 4: Frontend — WebhooksPanel Page
421
+
422
+ **Files:**
423
+ - Create: `src/frontend/pages/WebhooksPanel.tsx`
424
+
425
+ ### Step 1: Create the page
426
+
427
+ Create `src/frontend/pages/WebhooksPanel.tsx`:
428
+
429
+ ```tsx
430
+ import { useState, useEffect, useCallback } from 'react'
431
+ import { Card } from '@/components/ui/Card'
432
+ import { Button } from '@/components/ui/Button'
433
+ import { StatusBadge } from '@/components/ui/StatusBadge'
434
+ import { api } from '@/api'
435
+ import type { WebhookEvent, GitHubWebhookStatus } from '@/types'
436
+
437
+ const styles = {
438
+ page: {
439
+ display: 'flex', flexDirection: 'column' as const, gap: '1.5rem',
440
+ },
441
+ header: {
442
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
443
+ },
444
+ title: {
445
+ fontSize: '1.5rem', fontWeight: 700, color: 'var(--text-primary)',
446
+ },
447
+ subtitle: {
448
+ fontSize: '0.85rem', color: 'var(--text-muted)', marginTop: '0.25rem',
449
+ },
450
+ grid: {
451
+ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '1rem',
452
+ },
453
+ field: {
454
+ display: 'flex', flexDirection: 'column' as const, gap: '0.35rem', marginBottom: '0.75rem',
455
+ },
456
+ label: {
457
+ fontSize: '0.8rem', fontWeight: 600, color: 'var(--text-secondary)',
458
+ textTransform: 'uppercase' as const, letterSpacing: '0.05em',
459
+ },
460
+ input: {
461
+ padding: '0.5rem 0.75rem', borderRadius: '6px',
462
+ border: '1px solid var(--border-primary)', background: 'var(--bg-secondary)',
463
+ color: 'var(--text-primary)', fontSize: '0.9rem', width: '100%',
464
+ },
465
+ row: {
466
+ display: 'flex', gap: '0.5rem', alignItems: 'center',
467
+ },
468
+ toggle: {
469
+ display: 'flex', alignItems: 'center', gap: '0.5rem', cursor: 'pointer',
470
+ },
471
+ eventRow: {
472
+ display: 'flex', justifyContent: 'space-between', alignItems: 'center',
473
+ padding: '0.6rem 0', borderBottom: '1px solid var(--border-primary)',
474
+ },
475
+ eventTitle: {
476
+ fontWeight: 600, color: 'var(--text-primary)', fontSize: '0.9rem',
477
+ },
478
+ eventMeta: {
479
+ fontSize: '0.8rem', color: 'var(--text-muted)',
480
+ },
481
+ badge: {
482
+ fontSize: '0.75rem', padding: '0.15rem 0.5rem', borderRadius: '999px',
483
+ fontWeight: 600,
484
+ },
485
+ webhookUrl: {
486
+ padding: '0.5rem 0.75rem', borderRadius: '6px',
487
+ background: 'var(--bg-tertiary)', color: 'var(--accent-green)',
488
+ fontFamily: 'monospace', fontSize: '0.85rem', wordBreak: 'break-all' as const,
489
+ border: '1px solid var(--border-primary)',
490
+ },
491
+ instructions: {
492
+ fontSize: '0.85rem', color: 'var(--text-secondary)', lineHeight: 1.6,
493
+ },
494
+ msg: (ok: boolean) => ({
495
+ fontSize: '0.85rem', padding: '0.5rem 0.75rem', borderRadius: '6px', marginTop: '0.5rem',
496
+ background: ok ? 'rgba(34,197,94,0.1)' : 'rgba(239,68,68,0.1)',
497
+ color: ok ? 'var(--accent-green)' : 'var(--accent-red)',
498
+ border: `1px solid ${ok ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
499
+ }),
500
+ }
501
+
502
+ export default function WebhooksPanel() {
503
+ const [config, setConfig] = useState<GitHubWebhookStatus | null>(null)
504
+ const [events, setEvents] = useState<WebhookEvent[]>([])
505
+ const [editing, setEditing] = useState(false)
506
+ const [saving, setSaving] = useState(false)
507
+ const [msg, setMsg] = useState('')
508
+
509
+ // Form fields
510
+ const [enabled, setEnabled] = useState(false)
511
+ const [token, setToken] = useState('')
512
+ const [secret, setSecret] = useState('')
513
+ const [repos, setRepos] = useState('')
514
+ const [autoAssign, setAutoAssign] = useState(true)
515
+
516
+ const fetchConfig = useCallback(async () => {
517
+ try {
518
+ const c = await api.webhooks.getGitHubConfig()
519
+ setConfig(c)
520
+ setEnabled(c.enabled)
521
+ setAutoAssign(c.autoAssign)
522
+ setRepos(c.repos.join(', '))
523
+ } catch { /* ignore */ }
524
+ }, [])
525
+
526
+ const fetchEvents = useCallback(async () => {
527
+ try {
528
+ const evts = await api.webhooks.getGitHubEvents()
529
+ setEvents(evts)
530
+ } catch { /* ignore */ }
531
+ }, [])
532
+
533
+ useEffect(() => {
534
+ fetchConfig()
535
+ fetchEvents()
536
+ const interval = setInterval(fetchEvents, 10_000)
537
+ return () => clearInterval(interval)
538
+ }, [fetchConfig, fetchEvents])
539
+
540
+ const handleSave = async () => {
541
+ setSaving(true)
542
+ setMsg('')
543
+ try {
544
+ const update: Record<string, unknown> = {
545
+ enabled,
546
+ autoAssign,
547
+ repos: repos.split(',').map(r => r.trim()).filter(Boolean),
548
+ }
549
+ if (token) update.githubToken = token
550
+ if (secret) update.webhookSecret = secret
551
+ await api.webhooks.setGitHubConfig(update)
552
+ setMsg('Saved!')
553
+ setEditing(false)
554
+ setToken('')
555
+ setSecret('')
556
+ await fetchConfig()
557
+ } catch (err) {
558
+ setMsg(err instanceof Error ? err.message : 'Save failed')
559
+ } finally {
560
+ setSaving(false)
561
+ }
562
+ }
563
+
564
+ const webhookUrl = `${window.location.origin}/api/webhooks/github`
565
+
566
+ const statusColor = (s: string) => {
567
+ if (s === 'completed') return 'var(--accent-green)'
568
+ if (s === 'processing') return 'var(--accent-orange)'
569
+ if (s === 'failed') return 'var(--accent-red)'
570
+ if (s === 'ignored') return 'var(--text-muted)'
571
+ return 'var(--accent-blue)'
572
+ }
573
+
574
+ return (
575
+ <div style={styles.page}>
576
+ <div style={styles.header}>
577
+ <div>
578
+ <div style={styles.title}>Webhooks</div>
579
+ <div style={styles.subtitle}>Receive events from external services and trigger swarm tasks</div>
580
+ </div>
581
+ </div>
582
+
583
+ {/* GitHub Configuration */}
584
+ <Card title="GitHub Integration" actions={
585
+ !editing
586
+ ? <Button size="sm" onClick={() => setEditing(true)}>Edit</Button>
587
+ : undefined
588
+ }>
589
+ {/* Status banner */}
590
+ <div style={{
591
+ display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '1rem',
592
+ padding: '0.5rem 0.75rem', borderRadius: '6px',
593
+ background: config?.enabled ? 'rgba(34,197,94,0.1)' : 'rgba(100,100,100,0.1)',
594
+ border: `1px solid ${config?.enabled ? 'rgba(34,197,94,0.3)' : 'rgba(100,100,100,0.3)'}`,
595
+ }}>
596
+ <div style={{
597
+ width: 8, height: 8, borderRadius: '50%',
598
+ background: config?.enabled ? 'var(--accent-green)' : 'var(--text-muted)',
599
+ }} />
600
+ <span style={{ fontSize: '0.85rem', color: config?.enabled ? 'var(--accent-green)' : 'var(--text-muted)' }}>
601
+ {config?.enabled ? 'Enabled' : 'Disabled'}
602
+ {config?.enabled && config?.hasToken ? ' — Token configured' : ''}
603
+ </span>
604
+ </div>
605
+
606
+ {editing ? (
607
+ <>
608
+ <div style={styles.field}>
609
+ <label style={styles.toggle}>
610
+ <input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)} />
611
+ <span style={{ color: 'var(--text-primary)', fontWeight: 600 }}>Enable GitHub Webhooks</span>
612
+ </label>
613
+ </div>
614
+
615
+ <div style={styles.field}>
616
+ <span style={styles.label}>GitHub Token (repo scope)</span>
617
+ <input
618
+ style={styles.input}
619
+ type="password"
620
+ value={token}
621
+ onChange={e => setToken(e.target.value)}
622
+ placeholder={config?.hasToken ? `Current: ${config.tokenPreview}` : 'ghp_...'}
623
+ />
624
+ </div>
625
+
626
+ <div style={styles.field}>
627
+ <span style={styles.label}>Webhook Secret</span>
628
+ <input
629
+ style={styles.input}
630
+ type="password"
631
+ value={secret}
632
+ onChange={e => setSecret(e.target.value)}
633
+ placeholder={config?.hasSecret ? 'Current: ****' : 'Optional but recommended'}
634
+ />
635
+ </div>
636
+
637
+ <div style={styles.field}>
638
+ <span style={styles.label}>Monitored Repos (comma-separated, e.g. owner/repo)</span>
639
+ <input
640
+ style={styles.input}
641
+ value={repos}
642
+ onChange={e => setRepos(e.target.value)}
643
+ placeholder="owner/repo1, owner/repo2 (empty = all)"
644
+ />
645
+ </div>
646
+
647
+ <div style={styles.field}>
648
+ <label style={styles.toggle}>
649
+ <input type="checkbox" checked={autoAssign} onChange={e => setAutoAssign(e.target.checked)} />
650
+ <span style={{ color: 'var(--text-primary)' }}>Auto-create and assign tasks for new issues</span>
651
+ </label>
652
+ </div>
653
+
654
+ <div style={styles.row}>
655
+ <Button variant="primary" loading={saving} onClick={handleSave}>Save</Button>
656
+ <Button variant="ghost" onClick={() => setEditing(false)}>Cancel</Button>
657
+ </div>
658
+ {msg && <div style={styles.msg(/saved/i.test(msg))}>{msg}</div>}
659
+ </>
660
+ ) : (
661
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
662
+ <div style={styles.instructions}>
663
+ <strong>Repos:</strong> {config?.repos.length ? config.repos.join(', ') : 'All (no filter)'}
664
+ </div>
665
+ <div style={styles.instructions}>
666
+ <strong>Auto-assign:</strong> {config?.autoAssign ? 'Yes' : 'No'}
667
+ </div>
668
+ </div>
669
+ )}
670
+ </Card>
671
+
672
+ {/* Webhook URL */}
673
+ <Card title="Webhook URL">
674
+ <div style={styles.instructions}>
675
+ Copy this URL into your GitHub repo settings under <strong>Settings &gt; Webhooks &gt; Add webhook</strong>.
676
+ Set content type to <code>application/json</code> and select <strong>Issues</strong> events.
677
+ </div>
678
+ <div style={{ ...styles.webhookUrl, marginTop: '0.75rem' }}>{webhookUrl}</div>
679
+ </Card>
680
+
681
+ {/* Event History */}
682
+ <Card title="Recent Events" actions={
683
+ <Button size="sm" variant="ghost" onClick={fetchEvents}>Refresh</Button>
684
+ }>
685
+ {events.length === 0 ? (
686
+ <div style={{ color: 'var(--text-muted)', padding: '1rem 0', textAlign: 'center' }}>
687
+ No webhook events received yet
688
+ </div>
689
+ ) : (
690
+ events.map(evt => (
691
+ <div key={evt.id} style={styles.eventRow}>
692
+ <div>
693
+ <div style={styles.eventTitle}>
694
+ {evt.repo}#{evt.number} — {evt.title}
695
+ </div>
696
+ <div style={styles.eventMeta}>
697
+ {evt.event} by {evt.author} — {new Date(evt.receivedAt).toLocaleString()}
698
+ {evt.taskId && <span> — Task: {evt.taskId}</span>}
699
+ </div>
700
+ </div>
701
+ <span style={{
702
+ ...styles.badge,
703
+ color: statusColor(evt.status),
704
+ background: statusColor(evt.status) + '20',
705
+ }}>
706
+ {evt.status}
707
+ </span>
708
+ </div>
709
+ ))
710
+ )}
711
+ </Card>
712
+ </div>
713
+ )
714
+ }
715
+ ```
716
+
717
+ ### Step 2: Commit
718
+
719
+ ```bash
720
+ git add src/frontend/pages/WebhooksPanel.tsx
721
+ git commit -m "feat(webhooks): add WebhooksPanel page with config UI and event history"
722
+ ```
723
+
724
+ ---
725
+
726
+ ## Task 5: Frontend — Wire Up Route, Nav, and WebSocket
727
+
728
+ **Files:**
729
+ - Modify: `src/frontend/App.tsx:24` (add lazy import) and `App.tsx:395-ish` (add route)
730
+ - Modify: `src/frontend/components/Layout.tsx:22` (add icon import) and `Layout.tsx:54` (add nav item)
731
+
732
+ ### Step 1: Add lazy import and route in App.tsx
733
+
734
+ After line 24 (`const SwarmMonitorPanel = ...`), add:
735
+
736
+ ```typescript
737
+ const WebhooksPanel = React.lazy(() => import('./pages/WebhooksPanel'))
738
+ ```
739
+
740
+ In the `<Routes>` block, add a new `<Route>` after the config route (around line 395):
741
+
742
+ ```tsx
743
+ <Route
744
+ path="webhooks"
745
+ element={
746
+ <Suspense fallback={<LoadingSpinner />}>
747
+ <WebhooksPanel />
748
+ </Suspense>
749
+ }
750
+ />
751
+ ```
752
+
753
+ ### Step 2: Add sidebar nav item in Layout.tsx
754
+
755
+ Add `Webhook` icon to the import from `lucide-react` (line 4-22):
756
+
757
+ ```typescript
758
+ import { ..., Webhook } from 'lucide-react'
759
+ ```
760
+
761
+ Add nav item to the **Operations** group (line 65-71), after the Sessions entry:
762
+
763
+ ```typescript
764
+ {
765
+ title: 'Operations',
766
+ items: [
767
+ { label: 'Workflows', to: '/workflows', icon: Workflow },
768
+ { label: 'Hooks', to: '/hooks', icon: Terminal },
769
+ { label: 'Sessions', to: '/sessions', icon: Save },
770
+ { label: 'Webhooks', to: '/webhooks', icon: Webhook },
771
+ ],
772
+ },
773
+ ```
774
+
775
+ ### Step 3: Commit
776
+
777
+ ```bash
778
+ git add src/frontend/App.tsx src/frontend/components/Layout.tsx
779
+ git commit -m "feat(webhooks): wire up WebhooksPanel route and sidebar navigation"
780
+ ```
781
+
782
+ ---
783
+
784
+ ## Task 6: Backend — Update Webhook Event Status on Task Completion
785
+
786
+ **Files:**
787
+ - Modify: `src/backend/webhook-github.ts` (export function to update event status)
788
+ - Modify: `src/backend/server.ts` (hook into broadcast for task:updated)
789
+
790
+ ### Step 1: Add event status updater to webhook-github.ts
791
+
792
+ Add exported function:
793
+
794
+ ```typescript
795
+ export function updateWebhookEventByTaskId(taskId: string, status: WebhookEvent['status']): void {
796
+ const event = webhookEvents.find(e => e.taskId === taskId)
797
+ if (event) event.status = status
798
+ }
799
+
800
+ export function getWebhookEvents(): WebhookEvent[] {
801
+ return webhookEvents
802
+ }
803
+ ```
804
+
805
+ ### Step 2: Hook into broadcast in server.ts
806
+
807
+ In the `broadcast()` function (around line 268, after the telegram line), add:
808
+
809
+ ```typescript
810
+ // Update webhook event status when linked task completes/fails
811
+ if (type === 'task:updated') {
812
+ const p = payload as { id?: string; status?: string }
813
+ if (p?.id && (p.status === 'completed' || p.status === 'failed')) {
814
+ updateWebhookEventByTaskId(p.id, p.status as 'completed' | 'failed')
815
+ }
816
+ }
817
+ ```
818
+
819
+ Add the import at the top of server.ts:
820
+
821
+ ```typescript
822
+ import { ..., updateWebhookEventByTaskId } from './webhook-github'
823
+ ```
824
+
825
+ ### Step 3: Commit
826
+
827
+ ```bash
828
+ git add src/backend/webhook-github.ts src/backend/server.ts
829
+ git commit -m "feat(webhooks): update webhook event status when linked task completes"
830
+ ```
831
+
832
+ ---
833
+
834
+ ## Task 7: Documentation Updates
835
+
836
+ **Files:**
837
+ - Modify: `README.md`
838
+ - Modify: `CHANGELOG.md`
839
+ - Modify: `CLAUDE.md`
840
+ - Modify: `.env.example`
841
+
842
+ ### Step 1: Add to .env.example
843
+
844
+ ```
845
+ # GitHub Webhooks
846
+ GITHUB_WEBHOOK_ENABLED=false
847
+ GITHUB_TOKEN=
848
+ GITHUB_WEBHOOK_SECRET=
849
+ GITHUB_WEBHOOK_REPOS=owner/repo1,owner/repo2
850
+ ```
851
+
852
+ ### Step 2: Add Webhooks section to README.md
853
+
854
+ After the Telegram section, add:
855
+
856
+ ```markdown
857
+ ## GitHub Webhooks (Optional)
858
+
859
+ Automatically create swarm tasks when GitHub issues are opened.
860
+
861
+ ### Setup
862
+
863
+ 1. Open the RuFloUI dashboard, go to **Webhooks** in the sidebar.
864
+ 2. Click **Edit**, enable GitHub Webhooks, paste your GitHub token (needs `repo` scope).
865
+ 3. Optionally add a webhook secret and list repos to monitor.
866
+ 4. Copy the **Webhook URL** shown on the page.
867
+ 5. In your GitHub repo, go to **Settings > Webhooks > Add webhook**.
868
+ 6. Paste the URL, set content type to `application/json`, select **Issues** events.
869
+
870
+ ### How It Works
871
+
872
+ When a new issue is opened in a monitored repo:
873
+
874
+ 1. GitHub sends a POST to RuFloUI's webhook endpoint
875
+ 2. RuFloUI validates the HMAC signature (if secret configured)
876
+ 3. A high-priority task is created with the issue title and body
877
+ 4. If a swarm is active, the task is auto-assigned to the multi-agent pipeline
878
+ 5. Agents investigate, code, test, and produce a result
879
+ 6. Event status updates in the Webhooks page as the task progresses
880
+
881
+ ### Environment Variables (alternative to dashboard)
882
+
883
+ | Variable | Default | Description |
884
+ |----------|---------|-------------|
885
+ | `GITHUB_WEBHOOK_ENABLED` | `false` | Enable webhook receiver |
886
+ | `GITHUB_TOKEN` | — | GitHub PAT with `repo` scope |
887
+ | `GITHUB_WEBHOOK_SECRET` | — | HMAC secret for signature validation |
888
+ | `GITHUB_WEBHOOK_REPOS` | — | Comma-separated `owner/repo` list |
889
+ ```
890
+
891
+ ### Step 3: Add to CHANGELOG.md
892
+
893
+ Add under a new version header (or append to current if unreleased):
894
+
895
+ ```markdown
896
+ ## [Unreleased]
897
+
898
+ ### Added
899
+
900
+ - **GitHub Webhook Integration** — Receive GitHub issue events and auto-create swarm tasks
901
+ - Webhook endpoint `POST /api/webhooks/github` with HMAC-SHA256 signature validation
902
+ - Dashboard UI (Webhooks page) with config editor, webhook URL, and event history
903
+ - Auto-creates high-priority tasks from new/reopened issues
904
+ - Auto-assigns to active swarm pipeline (researcher → coder → tester → reviewer)
905
+ - Event status tracking (received → processing → completed/failed)
906
+ - Config persisted to `.ruflo/github-webhook.json`
907
+ - Fallback to environment variables when no dashboard config
908
+ ```
909
+
910
+ ### Step 4: Add webhook routes to CLAUDE.md API routes table
911
+
912
+ In the API routes table, add:
913
+
914
+ ```markdown
915
+ | `/api/webhooks` | POST github, GET/PUT github/config, GET github/events | GitHub webhook receiver + config |
916
+ ```
917
+
918
+ ### Step 5: Commit
919
+
920
+ ```bash
921
+ git add README.md CHANGELOG.md CLAUDE.md .env.example
922
+ git commit -m "docs: add GitHub webhook integration documentation"
923
+ ```
924
+
925
+ ---
926
+
927
+ ## Task 8: Manual Testing Checklist
928
+
929
+ Run these checks to verify the integration:
930
+
931
+ 1. **Build check**: `npx tsc --noEmit` — must pass with zero errors
932
+ 2. **No-config startup**: Start server without GitHub env vars → no errors, webhook endpoint returns 503
933
+ 3. **Config save**: Enable via Webhooks page, save token + secret + repos → config persists in `.ruflo/github-webhook.json`
934
+ 4. **Webhook URL**: Shown correctly on page, copyable
935
+ 5. **Simulated webhook**: `curl -X POST http://localhost:3001/api/webhooks/github -H "Content-Type: application/json" -H "X-GitHub-Event: issues" -d '{"action":"opened","issue":{"title":"Test issue","body":"Fix the bug","number":42,"html_url":"https://github.com/test/repo/issues/42","user":{"login":"testuser"},"labels":[]},"repository":{"full_name":"test/repo"}}'` → should create task and appear in events list
936
+ 6. **Event history**: Events page shows received webhook with status
937
+ 7. **Ignored events**: Push event returns `action: ignored`
938
+ 8. **HMAC validation**: Request with wrong signature returns 401
939
+ 9. **Sidebar nav**: "Webhooks" appears under Operations with Webhook icon
940
+ 10. **Telegram notification**: If Telegram enabled, task completion notification fires for webhook-created tasks
941
+
942
+ ---
943
+
944
+ ## Summary
945
+
946
+ | Task | Description | Files | Estimated Size |
947
+ |------|-------------|-------|---------------|
948
+ | 1 | Config types + persistence | `webhook-github.ts` (new) | ~100 lines |
949
+ | 2 | Webhook receiver route + config API | `webhook-github.ts` + `server.ts` | ~150 lines + 20 lines |
950
+ | 3 | Frontend API client + types | `api.ts` + `types.ts` | ~25 lines |
951
+ | 4 | WebhooksPanel page | `WebhooksPanel.tsx` (new) | ~250 lines |
952
+ | 5 | Route + nav wiring | `App.tsx` + `Layout.tsx` | ~10 lines each |
953
+ | 6 | Event status sync | `webhook-github.ts` + `server.ts` | ~15 lines |
954
+ | 7 | Documentation | `README`, `CHANGELOG`, `CLAUDE.md`, `.env.example` | ~50 lines |
955
+ | 8 | Manual testing | N/A | Verification only |
956
+
957
+ **Total new code:** ~600 lines across 2 new files + edits to 6 existing files.