rufloui 0.3.2 → 0.3.35
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/TESTS.md +91 -0
- package/package.json +2 -2
- package/src/backend/__tests__/e2e-workflows.test.ts +438 -0
- package/src/backend/__tests__/server-integration.test.ts +444 -0
- package/src/backend/__tests__/server-utils.test.ts +200 -0
- package/src/backend/__tests__/webhook-gitlab.test.ts +605 -0
- package/src/backend/server.ts +301 -14
- package/src/backend/webhook-github.ts +0 -1
- package/src/backend/webhook-gitlab.ts +313 -0
- package/src/frontend/__tests__/api.test.ts +375 -0
- package/src/frontend/__tests__/components.test.tsx +195 -0
- package/src/frontend/__tests__/store.test.ts +295 -0
- package/src/frontend/api.ts +8 -1
- package/src/frontend/pages/TasksPanel.tsx +59 -2
- package/src/frontend/pages/WebhooksPanel.tsx +282 -116
- package/src/frontend/types.ts +12 -1
- package/vitest.config.ts +1 -0
- package/frontend +0 -0
- package/release-notes.md +0 -27
- package/{ +0 -0
- package/{, +0 -0
- package/{,+ +0 -0
- /package/{Webhooks) → {}),} +0 -0
|
@@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
|
|
2
2
|
import { Card } from '@/components/ui/Card'
|
|
3
3
|
import { Button } from '@/components/ui/Button'
|
|
4
4
|
import { api } from '@/api'
|
|
5
|
-
import type { WebhookEvent, GitHubWebhookStatus } from '@/types'
|
|
5
|
+
import type { WebhookEvent, GitHubWebhookStatus, GitLabWebhookStatus } from '@/types'
|
|
6
6
|
|
|
7
7
|
const styles = {
|
|
8
8
|
page: {
|
|
@@ -64,19 +64,45 @@ const styles = {
|
|
|
64
64
|
color: ok ? 'var(--accent-green)' : 'var(--accent-red)',
|
|
65
65
|
border: `1px solid ${ok ? 'rgba(34,197,94,0.3)' : 'rgba(239,68,68,0.3)'}`,
|
|
66
66
|
}),
|
|
67
|
+
tabs: {
|
|
68
|
+
display: 'flex', gap: '0', borderBottom: '2px solid var(--border-primary)', marginBottom: '0.5rem',
|
|
69
|
+
},
|
|
70
|
+
tab: (active: boolean) => ({
|
|
71
|
+
padding: '0.6rem 1.2rem', cursor: 'pointer', fontWeight: 600, fontSize: '0.9rem',
|
|
72
|
+
color: active ? 'var(--accent-blue)' : 'var(--text-muted)',
|
|
73
|
+
borderBottom: active ? '2px solid var(--accent-blue)' : '2px solid transparent',
|
|
74
|
+
marginBottom: '-2px', background: 'transparent', border: 'none',
|
|
75
|
+
borderBottomWidth: '2px', borderBottomStyle: 'solid' as const,
|
|
76
|
+
borderBottomColor: active ? 'var(--accent-blue)' : 'transparent',
|
|
77
|
+
}),
|
|
78
|
+
providerBadge: (provider: string) => ({
|
|
79
|
+
fontSize: '0.65rem', padding: '0.1rem 0.4rem', borderRadius: '4px', fontWeight: 700,
|
|
80
|
+
marginRight: '0.4rem', flexShrink: 0,
|
|
81
|
+
color: provider === 'gitlab' ? '#fc6d26' : '#fff',
|
|
82
|
+
background: provider === 'gitlab' ? 'rgba(252,109,38,0.15)' : 'rgba(255,255,255,0.1)',
|
|
83
|
+
border: `1px solid ${provider === 'gitlab' ? 'rgba(252,109,38,0.3)' : 'rgba(255,255,255,0.2)'}`,
|
|
84
|
+
}),
|
|
67
85
|
}
|
|
68
86
|
|
|
69
|
-
|
|
87
|
+
const statusColor = (s: string) => {
|
|
88
|
+
if (s === 'completed') return 'var(--accent-green)'
|
|
89
|
+
if (s === 'processing') return 'var(--accent-orange)'
|
|
90
|
+
if (s === 'failed') return 'var(--accent-red)'
|
|
91
|
+
if (s === 'ignored') return 'var(--text-muted)'
|
|
92
|
+
return 'var(--accent-blue)'
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── GitHub Section ──────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function GitHubSection() {
|
|
70
98
|
const [config, setConfig] = useState<GitHubWebhookStatus | null>(null)
|
|
71
99
|
const [events, setEvents] = useState<WebhookEvent[]>([])
|
|
72
100
|
const [editing, setEditing] = useState(false)
|
|
73
101
|
const [saving, setSaving] = useState(false)
|
|
74
102
|
const [msg, setMsg] = useState('')
|
|
75
|
-
|
|
76
103
|
const [testing, setTesting] = useState(false)
|
|
77
104
|
const [testMsg, setTestMsg] = useState('')
|
|
78
105
|
|
|
79
|
-
// Form fields
|
|
80
106
|
const [enabled, setEnabled] = useState(false)
|
|
81
107
|
const [token, setToken] = useState('')
|
|
82
108
|
const [secret, setSecret] = useState('')
|
|
@@ -96,94 +122,56 @@ export default function WebhooksPanel() {
|
|
|
96
122
|
}, [])
|
|
97
123
|
|
|
98
124
|
const fetchEvents = useCallback(async () => {
|
|
99
|
-
try {
|
|
100
|
-
const evts = await api.webhooks.getGitHubEvents()
|
|
101
|
-
setEvents(evts)
|
|
102
|
-
} catch { /* ignore */ }
|
|
125
|
+
try { setEvents(await api.webhooks.getGitHubEvents()) } catch { /* ignore */ }
|
|
103
126
|
}, [])
|
|
104
127
|
|
|
105
128
|
useEffect(() => {
|
|
106
129
|
fetchConfig()
|
|
107
130
|
fetchEvents()
|
|
108
131
|
const interval = setInterval(fetchEvents, 10_000)
|
|
109
|
-
// Real-time updates via WebSocket custom event
|
|
110
132
|
const onWsEvent = () => { fetchEvents() }
|
|
111
133
|
window.addEventListener('webhook-event', onWsEvent)
|
|
112
|
-
return () => {
|
|
113
|
-
clearInterval(interval)
|
|
114
|
-
window.removeEventListener('webhook-event', onWsEvent)
|
|
115
|
-
}
|
|
134
|
+
return () => { clearInterval(interval); window.removeEventListener('webhook-event', onWsEvent) }
|
|
116
135
|
}, [fetchConfig, fetchEvents])
|
|
117
136
|
|
|
118
137
|
const handleSave = async () => {
|
|
119
|
-
setSaving(true)
|
|
120
|
-
setMsg('')
|
|
138
|
+
setSaving(true); setMsg('')
|
|
121
139
|
try {
|
|
122
140
|
const update: Record<string, unknown> = {
|
|
123
|
-
enabled,
|
|
124
|
-
autoAssign,
|
|
141
|
+
enabled, autoAssign,
|
|
125
142
|
repos: repos.split(',').map(r => r.trim()).filter(Boolean),
|
|
126
143
|
}
|
|
127
144
|
if (token) update.githubToken = token
|
|
128
145
|
if (secret) update.webhookSecret = secret
|
|
129
146
|
update.taskTemplate = taskTemplate
|
|
130
147
|
await api.webhooks.setGitHubConfig(update)
|
|
131
|
-
setMsg('Saved!')
|
|
132
|
-
setEditing(false)
|
|
133
|
-
setToken('')
|
|
134
|
-
setSecret('')
|
|
148
|
+
setMsg('Saved!'); setEditing(false); setToken(''); setSecret('')
|
|
135
149
|
await fetchConfig()
|
|
136
150
|
} catch (err) {
|
|
137
151
|
setMsg(err instanceof Error ? err.message : 'Save failed')
|
|
138
|
-
} finally {
|
|
139
|
-
setSaving(false)
|
|
140
|
-
}
|
|
152
|
+
} finally { setSaving(false) }
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
const handleTest = async () => {
|
|
144
|
-
setTesting(true)
|
|
145
|
-
setTestMsg('')
|
|
156
|
+
setTesting(true); setTestMsg('')
|
|
146
157
|
try {
|
|
147
158
|
const result = await api.webhooks.testGitHub()
|
|
148
159
|
if (result.ok) {
|
|
149
160
|
setTestMsg(`Test event created!${result.taskId ? ` Task: ${result.taskId}` : ''}`)
|
|
150
161
|
fetchEvents()
|
|
151
|
-
} else {
|
|
152
|
-
setTestMsg(result.error || 'Test failed')
|
|
153
|
-
}
|
|
162
|
+
} else { setTestMsg(result.error || 'Test failed') }
|
|
154
163
|
} catch (err) {
|
|
155
164
|
setTestMsg(err instanceof Error ? err.message : 'Test failed')
|
|
156
|
-
} finally {
|
|
157
|
-
setTesting(false)
|
|
158
|
-
}
|
|
165
|
+
} finally { setTesting(false) }
|
|
159
166
|
}
|
|
160
167
|
|
|
161
168
|
const webhookUrl = `${window.location.origin}/api/webhooks/github`
|
|
162
169
|
|
|
163
|
-
const statusColor = (s: string) => {
|
|
164
|
-
if (s === 'completed') return 'var(--accent-green)'
|
|
165
|
-
if (s === 'processing') return 'var(--accent-orange)'
|
|
166
|
-
if (s === 'failed') return 'var(--accent-red)'
|
|
167
|
-
if (s === 'ignored') return 'var(--text-muted)'
|
|
168
|
-
return 'var(--accent-blue)'
|
|
169
|
-
}
|
|
170
|
-
|
|
171
170
|
return (
|
|
172
|
-
|
|
173
|
-
<div style={styles.header}>
|
|
174
|
-
<div>
|
|
175
|
-
<div style={styles.title}>Webhooks</div>
|
|
176
|
-
<div style={styles.subtitle}>Receive events from external services and trigger swarm tasks</div>
|
|
177
|
-
</div>
|
|
178
|
-
</div>
|
|
179
|
-
|
|
180
|
-
{/* GitHub Configuration */}
|
|
171
|
+
<>
|
|
181
172
|
<Card title="GitHub Integration" actions={
|
|
182
|
-
!editing
|
|
183
|
-
? <Button size="sm" onClick={() => setEditing(true)}>Edit</Button>
|
|
184
|
-
: undefined
|
|
173
|
+
!editing ? <Button size="sm" onClick={() => setEditing(true)}>Edit</Button> : undefined
|
|
185
174
|
}>
|
|
186
|
-
{/* Status banner */}
|
|
187
175
|
<div style={{
|
|
188
176
|
display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '1rem',
|
|
189
177
|
padding: '0.5rem 0.75rem', borderRadius: '6px',
|
|
@@ -208,59 +196,37 @@ export default function WebhooksPanel() {
|
|
|
208
196
|
<span style={{ color: 'var(--text-primary)', fontWeight: 600 }}>Enable GitHub Webhooks</span>
|
|
209
197
|
</label>
|
|
210
198
|
</div>
|
|
211
|
-
|
|
212
199
|
<div style={styles.field}>
|
|
213
200
|
<span style={styles.label}>GitHub Token (repo scope)</span>
|
|
214
|
-
<input
|
|
215
|
-
|
|
216
|
-
type="password"
|
|
217
|
-
value={token}
|
|
218
|
-
onChange={e => setToken(e.target.value)}
|
|
219
|
-
placeholder={config?.hasToken ? `Current: ${config.tokenPreview}` : 'ghp_...'}
|
|
220
|
-
/>
|
|
201
|
+
<input style={styles.input} type="password" value={token} onChange={e => setToken(e.target.value)}
|
|
202
|
+
placeholder={config?.hasToken ? `Current: ${config.tokenPreview}` : 'ghp_...'} />
|
|
221
203
|
</div>
|
|
222
|
-
|
|
223
204
|
<div style={styles.field}>
|
|
224
205
|
<span style={styles.label}>Webhook Secret</span>
|
|
225
|
-
<input
|
|
226
|
-
|
|
227
|
-
type="password"
|
|
228
|
-
value={secret}
|
|
229
|
-
onChange={e => setSecret(e.target.value)}
|
|
230
|
-
placeholder={config?.hasSecret ? 'Current: ****' : 'Optional but recommended'}
|
|
231
|
-
/>
|
|
206
|
+
<input style={styles.input} type="password" value={secret} onChange={e => setSecret(e.target.value)}
|
|
207
|
+
placeholder={config?.hasSecret ? 'Current: ****' : 'Optional but recommended'} />
|
|
232
208
|
</div>
|
|
233
|
-
|
|
234
209
|
<div style={styles.field}>
|
|
235
210
|
<span style={styles.label}>Monitored Repos (comma-separated, e.g. owner/repo)</span>
|
|
236
|
-
<input
|
|
237
|
-
|
|
238
|
-
value={repos}
|
|
239
|
-
onChange={e => setRepos(e.target.value)}
|
|
240
|
-
placeholder="owner/repo1, owner/repo2 (empty = all)"
|
|
241
|
-
/>
|
|
211
|
+
<input style={styles.input} value={repos} onChange={e => setRepos(e.target.value)}
|
|
212
|
+
placeholder="owner/repo1, owner/repo2 (empty = all)" />
|
|
242
213
|
</div>
|
|
243
|
-
|
|
244
214
|
<div style={styles.field}>
|
|
245
215
|
<label style={styles.toggle}>
|
|
246
216
|
<input type="checkbox" checked={autoAssign} onChange={e => setAutoAssign(e.target.checked)} />
|
|
247
217
|
<span style={{ color: 'var(--text-primary)' }}>Auto-create and assign tasks for new issues</span>
|
|
248
218
|
</label>
|
|
249
219
|
</div>
|
|
250
|
-
|
|
251
220
|
<div style={styles.field}>
|
|
252
221
|
<span style={styles.label}>Task Instructions Template</span>
|
|
253
222
|
<textarea
|
|
254
223
|
style={{ ...styles.input, minHeight: '80px', resize: 'vertical' as const, fontFamily: 'inherit' }}
|
|
255
|
-
value={taskTemplate}
|
|
256
|
-
|
|
257
|
-
placeholder="Default: Analyze this issue, investigate the codebase, implement a fix, write tests, and prepare a summary of changes. Placeholders: {{title}}, {{body}}, {{url}}, {{author}}, {{labels}}, {{repo}}, {{number}}"
|
|
258
|
-
/>
|
|
224
|
+
value={taskTemplate} onChange={e => setTaskTemplate(e.target.value)}
|
|
225
|
+
placeholder="Default: Analyze this issue, investigate the codebase, implement a fix, write tests, and prepare a summary of changes. Placeholders: {{title}}, {{body}}, {{url}}, {{author}}, {{labels}}, {{repo}}, {{number}}" />
|
|
259
226
|
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
|
|
260
227
|
Leave empty for default. Use {'{{title}}'}, {'{{body}}'}, {'{{url}}'}, {'{{author}}'}, {'{{labels}}'}, {'{{repo}}'}, {'{{number}}'} as placeholders.
|
|
261
228
|
</span>
|
|
262
229
|
</div>
|
|
263
|
-
|
|
264
230
|
<div style={styles.row}>
|
|
265
231
|
<Button variant="primary" loading={saving} onClick={handleSave}>Save</Button>
|
|
266
232
|
<Button variant="ghost" onClick={() => setEditing(false)}>Cancel</Button>
|
|
@@ -269,12 +235,8 @@ export default function WebhooksPanel() {
|
|
|
269
235
|
</>
|
|
270
236
|
) : (
|
|
271
237
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
272
|
-
<div style={styles.instructions}>
|
|
273
|
-
|
|
274
|
-
</div>
|
|
275
|
-
<div style={styles.instructions}>
|
|
276
|
-
<strong>Auto-assign:</strong> {config?.autoAssign ? 'Yes' : 'No'}
|
|
277
|
-
</div>
|
|
238
|
+
<div style={styles.instructions}><strong>Repos:</strong> {config?.repos.length ? config.repos.join(', ') : 'All (no filter)'}</div>
|
|
239
|
+
<div style={styles.instructions}><strong>Auto-assign:</strong> {config?.autoAssign ? 'Yes' : 'No'}</div>
|
|
278
240
|
{config?.taskTemplate && (
|
|
279
241
|
<div style={styles.instructions}>
|
|
280
242
|
<strong>Template:</strong> {config.taskTemplate.slice(0, 100)}{config.taskTemplate.length > 100 ? '...' : ''}
|
|
@@ -290,7 +252,6 @@ export default function WebhooksPanel() {
|
|
|
290
252
|
)}
|
|
291
253
|
</Card>
|
|
292
254
|
|
|
293
|
-
{/* Webhook URL */}
|
|
294
255
|
<Card title="Webhook URL">
|
|
295
256
|
<div style={styles.instructions}>
|
|
296
257
|
Copy this URL into your GitHub repo settings under <strong>Settings > Webhooks > Add webhook</strong>.
|
|
@@ -299,37 +260,242 @@ export default function WebhooksPanel() {
|
|
|
299
260
|
<div style={{ ...styles.webhookUrl, marginTop: '0.75rem' }}>{webhookUrl}</div>
|
|
300
261
|
</Card>
|
|
301
262
|
|
|
302
|
-
{
|
|
303
|
-
|
|
304
|
-
|
|
263
|
+
<EventList events={events} onRefresh={fetchEvents} />
|
|
264
|
+
</>
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ── GitLab Section ──────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
function GitLabSection() {
|
|
271
|
+
const [config, setConfig] = useState<GitLabWebhookStatus | null>(null)
|
|
272
|
+
const [events, setEvents] = useState<WebhookEvent[]>([])
|
|
273
|
+
const [editing, setEditing] = useState(false)
|
|
274
|
+
const [saving, setSaving] = useState(false)
|
|
275
|
+
const [msg, setMsg] = useState('')
|
|
276
|
+
const [testing, setTesting] = useState(false)
|
|
277
|
+
const [testMsg, setTestMsg] = useState('')
|
|
278
|
+
|
|
279
|
+
const [enabled, setEnabled] = useState(false)
|
|
280
|
+
const [token, setToken] = useState('')
|
|
281
|
+
const [secret, setSecret] = useState('')
|
|
282
|
+
const [repos, setRepos] = useState('')
|
|
283
|
+
const [autoAssign, setAutoAssign] = useState(true)
|
|
284
|
+
const [taskTemplate, setTaskTemplate] = useState('')
|
|
285
|
+
|
|
286
|
+
const fetchConfig = useCallback(async () => {
|
|
287
|
+
try {
|
|
288
|
+
const c = await api.webhooks.getGitLabConfig()
|
|
289
|
+
setConfig(c)
|
|
290
|
+
setEnabled(c.enabled)
|
|
291
|
+
setAutoAssign(c.autoAssign)
|
|
292
|
+
setRepos(c.repos.join(', '))
|
|
293
|
+
setTaskTemplate(c.taskTemplate || '')
|
|
294
|
+
} catch { /* ignore */ }
|
|
295
|
+
}, [])
|
|
296
|
+
|
|
297
|
+
const fetchEvents = useCallback(async () => {
|
|
298
|
+
try { setEvents(await api.webhooks.getGitLabEvents()) } catch { /* ignore */ }
|
|
299
|
+
}, [])
|
|
300
|
+
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
fetchConfig()
|
|
303
|
+
fetchEvents()
|
|
304
|
+
const interval = setInterval(fetchEvents, 10_000)
|
|
305
|
+
const onWsEvent = () => { fetchEvents() }
|
|
306
|
+
window.addEventListener('webhook-event', onWsEvent)
|
|
307
|
+
return () => { clearInterval(interval); window.removeEventListener('webhook-event', onWsEvent) }
|
|
308
|
+
}, [fetchConfig, fetchEvents])
|
|
309
|
+
|
|
310
|
+
const handleSave = async () => {
|
|
311
|
+
setSaving(true); setMsg('')
|
|
312
|
+
try {
|
|
313
|
+
const update: Record<string, unknown> = {
|
|
314
|
+
enabled, autoAssign,
|
|
315
|
+
repos: repos.split(',').map(r => r.trim()).filter(Boolean),
|
|
316
|
+
}
|
|
317
|
+
if (token) update.gitlabToken = token
|
|
318
|
+
if (secret) update.webhookSecret = secret
|
|
319
|
+
update.taskTemplate = taskTemplate
|
|
320
|
+
await api.webhooks.setGitLabConfig(update)
|
|
321
|
+
setMsg('Saved!'); setEditing(false); setToken(''); setSecret('')
|
|
322
|
+
await fetchConfig()
|
|
323
|
+
} catch (err) {
|
|
324
|
+
setMsg(err instanceof Error ? err.message : 'Save failed')
|
|
325
|
+
} finally { setSaving(false) }
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const handleTest = async () => {
|
|
329
|
+
setTesting(true); setTestMsg('')
|
|
330
|
+
try {
|
|
331
|
+
const result = await api.webhooks.testGitLab()
|
|
332
|
+
if (result.ok) {
|
|
333
|
+
setTestMsg(`Test event created!${result.taskId ? ` Task: ${result.taskId}` : ''}`)
|
|
334
|
+
fetchEvents()
|
|
335
|
+
} else { setTestMsg(result.error || 'Test failed') }
|
|
336
|
+
} catch (err) {
|
|
337
|
+
setTestMsg(err instanceof Error ? err.message : 'Test failed')
|
|
338
|
+
} finally { setTesting(false) }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const webhookUrl = `${window.location.origin}/api/webhooks/gitlab`
|
|
342
|
+
|
|
343
|
+
return (
|
|
344
|
+
<>
|
|
345
|
+
<Card title="GitLab Integration" actions={
|
|
346
|
+
!editing ? <Button size="sm" onClick={() => setEditing(true)}>Edit</Button> : undefined
|
|
305
347
|
}>
|
|
306
|
-
{
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
{
|
|
348
|
+
<div style={{
|
|
349
|
+
display: 'flex', gap: '0.5rem', alignItems: 'center', marginBottom: '1rem',
|
|
350
|
+
padding: '0.5rem 0.75rem', borderRadius: '6px',
|
|
351
|
+
background: config?.enabled ? 'rgba(252,109,38,0.1)' : 'rgba(100,100,100,0.1)',
|
|
352
|
+
border: `1px solid ${config?.enabled ? 'rgba(252,109,38,0.3)' : 'rgba(100,100,100,0.3)'}`,
|
|
353
|
+
}}>
|
|
354
|
+
<div style={{
|
|
355
|
+
width: 8, height: 8, borderRadius: '50%',
|
|
356
|
+
background: config?.enabled ? '#fc6d26' : 'var(--text-muted)',
|
|
357
|
+
}} />
|
|
358
|
+
<span style={{ fontSize: '0.85rem', color: config?.enabled ? '#fc6d26' : 'var(--text-muted)' }}>
|
|
359
|
+
{config?.enabled ? 'Enabled' : 'Disabled'}
|
|
360
|
+
{config?.enabled && config?.hasToken ? ' — Token configured' : ''}
|
|
361
|
+
</span>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
{editing ? (
|
|
365
|
+
<>
|
|
366
|
+
<div style={styles.field}>
|
|
367
|
+
<label style={styles.toggle}>
|
|
368
|
+
<input type="checkbox" checked={enabled} onChange={e => setEnabled(e.target.checked)} />
|
|
369
|
+
<span style={{ color: 'var(--text-primary)', fontWeight: 600 }}>Enable GitLab Webhooks</span>
|
|
370
|
+
</label>
|
|
371
|
+
</div>
|
|
372
|
+
<div style={styles.field}>
|
|
373
|
+
<span style={styles.label}>GitLab Personal Access Token</span>
|
|
374
|
+
<input style={styles.input} type="password" value={token} onChange={e => setToken(e.target.value)}
|
|
375
|
+
placeholder={config?.hasToken ? `Current: ${config.tokenPreview}` : 'glpat-...'} />
|
|
376
|
+
</div>
|
|
377
|
+
<div style={styles.field}>
|
|
378
|
+
<span style={styles.label}>Secret Token (X-Gitlab-Token)</span>
|
|
379
|
+
<input style={styles.input} type="password" value={secret} onChange={e => setSecret(e.target.value)}
|
|
380
|
+
placeholder={config?.hasSecret ? 'Current: ****' : 'Optional — plain token comparison'} />
|
|
381
|
+
</div>
|
|
382
|
+
<div style={styles.field}>
|
|
383
|
+
<span style={styles.label}>Monitored Projects (comma-separated, e.g. namespace/project)</span>
|
|
384
|
+
<input style={styles.input} value={repos} onChange={e => setRepos(e.target.value)}
|
|
385
|
+
placeholder="group/project1, group/project2 (empty = all)" />
|
|
386
|
+
</div>
|
|
387
|
+
<div style={styles.field}>
|
|
388
|
+
<label style={styles.toggle}>
|
|
389
|
+
<input type="checkbox" checked={autoAssign} onChange={e => setAutoAssign(e.target.checked)} />
|
|
390
|
+
<span style={{ color: 'var(--text-primary)' }}>Auto-create and assign tasks for new issues</span>
|
|
391
|
+
</label>
|
|
392
|
+
</div>
|
|
393
|
+
<div style={styles.field}>
|
|
394
|
+
<span style={styles.label}>Task Instructions Template</span>
|
|
395
|
+
<textarea
|
|
396
|
+
style={{ ...styles.input, minHeight: '80px', resize: 'vertical' as const, fontFamily: 'inherit' }}
|
|
397
|
+
value={taskTemplate} onChange={e => setTaskTemplate(e.target.value)}
|
|
398
|
+
placeholder="Default: Analyze this issue, investigate the codebase, implement a fix, write tests, and prepare a summary of changes. Placeholders: {{title}}, {{body}}, {{url}}, {{author}}, {{labels}}, {{repo}}, {{number}}" />
|
|
399
|
+
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>
|
|
400
|
+
Leave empty for default. Use {'{{title}}'}, {'{{body}}'}, {'{{url}}'}, {'{{author}}'}, {'{{labels}}'}, {'{{repo}}'}, {'{{number}}'} as placeholders.
|
|
328
401
|
</span>
|
|
329
402
|
</div>
|
|
330
|
-
|
|
403
|
+
<div style={styles.row}>
|
|
404
|
+
<Button variant="primary" loading={saving} onClick={handleSave}>Save</Button>
|
|
405
|
+
<Button variant="ghost" onClick={() => setEditing(false)}>Cancel</Button>
|
|
406
|
+
</div>
|
|
407
|
+
{msg && <div style={styles.msg(/saved/i.test(msg))}>{msg}</div>}
|
|
408
|
+
</>
|
|
409
|
+
) : (
|
|
410
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
|
|
411
|
+
<div style={styles.instructions}><strong>Projects:</strong> {config?.repos.length ? config.repos.join(', ') : 'All (no filter)'}</div>
|
|
412
|
+
<div style={styles.instructions}><strong>Auto-assign:</strong> {config?.autoAssign ? 'Yes' : 'No'}</div>
|
|
413
|
+
{config?.taskTemplate && (
|
|
414
|
+
<div style={styles.instructions}>
|
|
415
|
+
<strong>Template:</strong> {config.taskTemplate.slice(0, 100)}{config.taskTemplate.length > 100 ? '...' : ''}
|
|
416
|
+
</div>
|
|
417
|
+
)}
|
|
418
|
+
{config?.enabled && (
|
|
419
|
+
<div style={{ marginTop: '0.5rem' }}>
|
|
420
|
+
<Button size="sm" variant="ghost" loading={testing} onClick={handleTest}>Send Test</Button>
|
|
421
|
+
{testMsg && <div style={styles.msg(/created|ok/i.test(testMsg))}>{testMsg}</div>}
|
|
422
|
+
</div>
|
|
423
|
+
)}
|
|
424
|
+
</div>
|
|
331
425
|
)}
|
|
332
426
|
</Card>
|
|
427
|
+
|
|
428
|
+
<Card title="Webhook URL">
|
|
429
|
+
<div style={styles.instructions}>
|
|
430
|
+
In your GitLab project, go to <strong>Settings > Webhooks > Add new webhook</strong>.
|
|
431
|
+
Paste this URL, set the secret token (optional), and check <strong>Issues events</strong>.
|
|
432
|
+
</div>
|
|
433
|
+
<div style={{ ...styles.webhookUrl, marginTop: '0.75rem', color: '#fc6d26' }}>{webhookUrl}</div>
|
|
434
|
+
</Card>
|
|
435
|
+
|
|
436
|
+
<EventList events={events} onRefresh={fetchEvents} />
|
|
437
|
+
</>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// ── Shared Event List ───────────────────────────────────────────────
|
|
442
|
+
|
|
443
|
+
function EventList({ events, onRefresh }: { events: WebhookEvent[]; onRefresh: () => void }) {
|
|
444
|
+
return (
|
|
445
|
+
<Card title="Recent Events" actions={
|
|
446
|
+
<Button size="sm" variant="ghost" onClick={onRefresh}>Refresh</Button>
|
|
447
|
+
}>
|
|
448
|
+
{events.length === 0 ? (
|
|
449
|
+
<div style={{ color: 'var(--text-muted)', padding: '1rem 0', textAlign: 'center' }}>
|
|
450
|
+
No webhook events received yet
|
|
451
|
+
</div>
|
|
452
|
+
) : (
|
|
453
|
+
events.map(evt => (
|
|
454
|
+
<div key={evt.id} style={styles.eventRow}>
|
|
455
|
+
<div>
|
|
456
|
+
<div style={styles.eventTitle}>
|
|
457
|
+
<span style={styles.providerBadge(evt.provider)}>{evt.provider === 'gitlab' ? 'GL' : 'GH'}</span>
|
|
458
|
+
{evt.repo}#{evt.number} — {evt.title}
|
|
459
|
+
</div>
|
|
460
|
+
<div style={styles.eventMeta}>
|
|
461
|
+
{evt.event} by {evt.author} — {new Date(evt.receivedAt).toLocaleString()}
|
|
462
|
+
{evt.taskId && <span> — Task: {evt.taskId}</span>}
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
<span style={{
|
|
466
|
+
...styles.badge,
|
|
467
|
+
color: statusColor(evt.status),
|
|
468
|
+
background: statusColor(evt.status) + '20',
|
|
469
|
+
}}>
|
|
470
|
+
{evt.status}
|
|
471
|
+
</span>
|
|
472
|
+
</div>
|
|
473
|
+
))
|
|
474
|
+
)}
|
|
475
|
+
</Card>
|
|
476
|
+
)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// ── Main Panel ──────────────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
export default function WebhooksPanel() {
|
|
482
|
+
const [tab, setTab] = useState<'github' | 'gitlab'>('github')
|
|
483
|
+
|
|
484
|
+
return (
|
|
485
|
+
<div style={styles.page}>
|
|
486
|
+
<div style={styles.header}>
|
|
487
|
+
<div>
|
|
488
|
+
<div style={styles.title}>Webhooks</div>
|
|
489
|
+
<div style={styles.subtitle}>Receive events from external services and trigger swarm tasks</div>
|
|
490
|
+
</div>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div style={styles.tabs}>
|
|
494
|
+
<button style={styles.tab(tab === 'github')} onClick={() => setTab('github')}>GitHub</button>
|
|
495
|
+
<button style={styles.tab(tab === 'gitlab')} onClick={() => setTab('gitlab')}>GitLab</button>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
{tab === 'github' ? <GitHubSection /> : <GitLabSection />}
|
|
333
499
|
</div>
|
|
334
500
|
)
|
|
335
501
|
}
|
package/src/frontend/types.ts
CHANGED
|
@@ -213,7 +213,7 @@ export interface WSMessage {
|
|
|
213
213
|
|
|
214
214
|
export interface WebhookEvent {
|
|
215
215
|
id: string
|
|
216
|
-
provider: 'github'
|
|
216
|
+
provider: 'github' | 'gitlab'
|
|
217
217
|
repo: string
|
|
218
218
|
event: string
|
|
219
219
|
title: string
|
|
@@ -237,3 +237,14 @@ export interface GitHubWebhookStatus {
|
|
|
237
237
|
autoAssign: boolean
|
|
238
238
|
taskTemplate: string
|
|
239
239
|
}
|
|
240
|
+
|
|
241
|
+
export interface GitLabWebhookStatus {
|
|
242
|
+
enabled: boolean
|
|
243
|
+
hasToken: boolean
|
|
244
|
+
tokenPreview: string
|
|
245
|
+
webhookSecret: string
|
|
246
|
+
hasSecret: boolean
|
|
247
|
+
repos: string[]
|
|
248
|
+
autoAssign: boolean
|
|
249
|
+
taskTemplate: string
|
|
250
|
+
}
|
package/vitest.config.ts
CHANGED
package/frontend
DELETED
|
File without changes
|
package/release-notes.md
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
## Port Configuration Change
|
|
2
|
-
|
|
3
|
-
Default ports changed to avoid conflicts with Windows reserved ranges (Hyper-V, Docker, antivirus):
|
|
4
|
-
|
|
5
|
-
| Service | Old | New |
|
|
6
|
-
|---------|-----|-----|
|
|
7
|
-
| Backend API | 3001 | **28580** |
|
|
8
|
-
| Frontend UI | 5173 | **28588** |
|
|
9
|
-
| Daemon | 3002 | **28581** |
|
|
10
|
-
|
|
11
|
-
### Breaking Change
|
|
12
|
-
|
|
13
|
-
If you have bookmarks, scripts, or webhook URLs pointing to the old ports, update them.
|
|
14
|
-
|
|
15
|
-
### Other Fixes
|
|
16
|
-
|
|
17
|
-
- **Workflow cancel** now properly kills running processes and cancels linked tasks
|
|
18
|
-
- **Task cancel** now kills `claude -p` processes and cancels linked workflows
|
|
19
|
-
- **Workflow delete** works even when CLI fails
|
|
20
|
-
|
|
21
|
-
### Install
|
|
22
|
-
|
|
23
|
-
```bash
|
|
24
|
-
npm install rufloui
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
**Full Changelog**: https://github.com/Mario-PB/rufloui/compare/v0.3.1...v0.3.2
|
package/{
DELETED
|
File without changes
|
package/{,
DELETED
|
File without changes
|
package/{,+
DELETED
|
File without changes
|
/package/{Webhooks) → {}),}
RENAMED
|
File without changes
|