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,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 > Webhooks > 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.
|