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.
@@ -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
- export default function WebhooksPanel() {
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
- <div style={styles.page}>
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
- style={styles.input}
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
- style={styles.input}
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
- style={styles.input}
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
- onChange={e => setTaskTemplate(e.target.value)}
257
- placeholder="Default: Analyze this issue, investigate the codebase, implement a fix, write tests, and prepare a summary of changes.&#10;&#10;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.&#10;&#10;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
- <strong>Repos:</strong> {config?.repos.length ? config.repos.join(', ') : 'All (no filter)'}
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 &gt; Webhooks &gt; 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
- {/* Event History */}
303
- <Card title="Recent Events" actions={
304
- <Button size="sm" variant="ghost" onClick={fetchEvents}>Refresh</Button>
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
- {events.length === 0 ? (
307
- <div style={{ color: 'var(--text-muted)', padding: '1rem 0', textAlign: 'center' }}>
308
- No webhook events received yet
309
- </div>
310
- ) : (
311
- events.map(evt => (
312
- <div key={evt.id} style={styles.eventRow}>
313
- <div>
314
- <div style={styles.eventTitle}>
315
- {evt.repo}#{evt.number} — {evt.title}
316
- </div>
317
- <div style={styles.eventMeta}>
318
- {evt.event} by {evt.author}{new Date(evt.receivedAt).toLocaleString()}
319
- {evt.taskId && <span> — Task: {evt.taskId}</span>}
320
- </div>
321
- </div>
322
- <span style={{
323
- ...styles.badge,
324
- color: statusColor(evt.status),
325
- background: statusColor(evt.status) + '20',
326
- }}>
327
- {evt.status}
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.&#10;&#10;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 &gt; Webhooks &gt; 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
  }
@@ -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
@@ -13,5 +13,6 @@ export default defineConfig({
13
13
  environment: 'jsdom',
14
14
  globals: true,
15
15
  setupFiles: ['./src/frontend/test-setup.ts'],
16
+ exclude: ['node_modules', 'dist', 'e2e'],
16
17
  },
17
18
  })
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
File without changes