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,428 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback, type CSSProperties } from 'react'
|
|
2
|
+
import { useStore } from '@/store'
|
|
3
|
+
import { api } from '@/api'
|
|
4
|
+
import { Card } from '@/components/ui/Card'
|
|
5
|
+
import { Button } from '@/components/ui/Button'
|
|
6
|
+
import { StatusBadge } from '@/components/ui/StatusBadge'
|
|
7
|
+
import { Database, Save, Trash2, RefreshCw, Plus, Search } from 'lucide-react'
|
|
8
|
+
import type { Session } from '@/types'
|
|
9
|
+
|
|
10
|
+
const s = {
|
|
11
|
+
page: { display: 'flex', flexDirection: 'column', gap: 20 } as CSSProperties,
|
|
12
|
+
banner: {
|
|
13
|
+
background: 'var(--bg-card)', border: '1px solid var(--border)',
|
|
14
|
+
borderRadius: 'var(--radius-lg)', padding: '20px 24px',
|
|
15
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
16
|
+
gap: 20, flexWrap: 'wrap',
|
|
17
|
+
} as CSSProperties,
|
|
18
|
+
bannerInfo: { display: 'flex', flexDirection: 'column', gap: 6 } as CSSProperties,
|
|
19
|
+
bannerTitle: { fontSize: 16, fontWeight: 600, color: 'var(--text-primary)' } as CSSProperties,
|
|
20
|
+
bannerMeta: { display: 'flex', gap: 20, flexWrap: 'wrap' } as CSSProperties,
|
|
21
|
+
metaItem: { fontSize: 13, color: 'var(--text-secondary)' } as CSSProperties,
|
|
22
|
+
metaLabel: { fontSize: 11, color: 'var(--text-muted)', marginRight: 4 } as CSSProperties,
|
|
23
|
+
actionsRow: { display: 'flex', gap: 10, alignItems: 'center', flexWrap: 'wrap' } as CSSProperties,
|
|
24
|
+
grid: {
|
|
25
|
+
display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(340, 1fr))', gap: 14,
|
|
26
|
+
} as CSSProperties,
|
|
27
|
+
sessionCard: {
|
|
28
|
+
background: 'var(--bg-card)', border: '1px solid var(--border)',
|
|
29
|
+
borderRadius: 'var(--radius-lg)', overflow: 'hidden',
|
|
30
|
+
transition: 'border-color var(--transition)',
|
|
31
|
+
} as CSSProperties,
|
|
32
|
+
cardHeader: {
|
|
33
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
34
|
+
padding: '14px 18px', borderBottom: '1px solid var(--border)',
|
|
35
|
+
} as CSSProperties,
|
|
36
|
+
cardName: { fontSize: 14, fontWeight: 600, color: 'var(--text-primary)' } as CSSProperties,
|
|
37
|
+
cardBody: { padding: '14px 18px' } as CSSProperties,
|
|
38
|
+
cardMeta: {
|
|
39
|
+
display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '8px 16px',
|
|
40
|
+
} as CSSProperties,
|
|
41
|
+
cardLabel: { fontSize: 11, color: 'var(--text-muted)', textTransform: 'uppercase' as const } as CSSProperties,
|
|
42
|
+
cardValue: { fontSize: 13, color: 'var(--text-secondary)', marginTop: 2 } as CSSProperties,
|
|
43
|
+
cardActions: {
|
|
44
|
+
display: 'flex', gap: 6, padding: '10px 18px',
|
|
45
|
+
borderTop: '1px solid var(--border)',
|
|
46
|
+
} as CSSProperties,
|
|
47
|
+
overlay: {
|
|
48
|
+
position: 'fixed' as const, inset: 0, background: 'rgba(0,0,0,0.6)',
|
|
49
|
+
display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000,
|
|
50
|
+
} as CSSProperties,
|
|
51
|
+
dialog: {
|
|
52
|
+
background: 'var(--bg-card)', border: '1px solid var(--border)',
|
|
53
|
+
borderRadius: 'var(--radius-lg)', padding: 24, minWidth: 360, maxWidth: 480,
|
|
54
|
+
} as CSSProperties,
|
|
55
|
+
dialogTitle: { fontSize: 16, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 16 } as CSSProperties,
|
|
56
|
+
input: {
|
|
57
|
+
background: 'var(--bg-primary)', border: '1px solid var(--border)',
|
|
58
|
+
borderRadius: 'var(--radius)', padding: '7px 12px', fontSize: 13,
|
|
59
|
+
color: 'var(--text-primary)', outline: 'none', width: '100%',
|
|
60
|
+
} as CSSProperties,
|
|
61
|
+
field: { display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 14 } as CSSProperties,
|
|
62
|
+
label: { fontSize: 12, color: 'var(--text-muted)' } as CSSProperties,
|
|
63
|
+
empty: { textAlign: 'center' as const, color: 'var(--text-muted)', padding: 40, fontSize: 14 } as CSSProperties,
|
|
64
|
+
detail: {
|
|
65
|
+
padding: '14px 18px', borderTop: '1px solid var(--border)',
|
|
66
|
+
background: 'var(--bg-primary)',
|
|
67
|
+
} as CSSProperties,
|
|
68
|
+
detailSection: { marginBottom: 12 } as CSSProperties,
|
|
69
|
+
detailTitle: { fontSize: 12, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6, textTransform: 'uppercase' as const } as CSSProperties,
|
|
70
|
+
detailList: { fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.8 } as CSSProperties,
|
|
71
|
+
noBanner: {
|
|
72
|
+
background: 'var(--bg-card)', border: '1px dashed var(--border)',
|
|
73
|
+
borderRadius: 'var(--radius-lg)', padding: '28px 24px',
|
|
74
|
+
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
75
|
+
gap: 16,
|
|
76
|
+
} as CSSProperties,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface SessionDetail {
|
|
80
|
+
id: string
|
|
81
|
+
name: string
|
|
82
|
+
status: string
|
|
83
|
+
createdAt: string
|
|
84
|
+
agentCount: number
|
|
85
|
+
taskCount: number
|
|
86
|
+
agents?: Array<{ id: string; name: string; type: string; status: string }>
|
|
87
|
+
tasks?: Array<{ id: string; title: string; status: string }>
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default function SessionsPanel() {
|
|
91
|
+
const { sessions, activeSession, setSessions, setActiveSession } = useStore()
|
|
92
|
+
|
|
93
|
+
const [loading, setLoading] = useState(false)
|
|
94
|
+
const [showSaveDialog, setShowSaveDialog] = useState(false)
|
|
95
|
+
const [saveName, setSaveName] = useState('')
|
|
96
|
+
const [saving, setSaving] = useState(false)
|
|
97
|
+
|
|
98
|
+
const [restoreConfirm, setRestoreConfirm] = useState<string | null>(null)
|
|
99
|
+
const [deleteConfirm, setDeleteConfirm] = useState<string | null>(null)
|
|
100
|
+
const [expandedId, setExpandedId] = useState<string | null>(null)
|
|
101
|
+
const [details, setDetails] = useState<Record<string, SessionDetail>>({})
|
|
102
|
+
const [loadingDetail, setLoadingDetail] = useState<string | null>(null)
|
|
103
|
+
|
|
104
|
+
const fetchAll = useCallback(async () => {
|
|
105
|
+
setLoading(true)
|
|
106
|
+
try {
|
|
107
|
+
const data = await api.sessions.list() as { sessions?: Session[] } | Session[]
|
|
108
|
+
setSessions(Array.isArray(data) ? data : (data.sessions ?? []))
|
|
109
|
+
} catch (e) {
|
|
110
|
+
console.error('Failed to fetch sessions', e)
|
|
111
|
+
} finally {
|
|
112
|
+
setLoading(false)
|
|
113
|
+
}
|
|
114
|
+
}, [setSessions])
|
|
115
|
+
|
|
116
|
+
useEffect(() => { fetchAll() }, [fetchAll])
|
|
117
|
+
|
|
118
|
+
const handleSave = async () => {
|
|
119
|
+
setSaving(true)
|
|
120
|
+
try {
|
|
121
|
+
const result = await api.sessions.save(saveName || undefined) as Partial<Session> & Record<string, unknown>
|
|
122
|
+
if (result.id) {
|
|
123
|
+
setActiveSession(result as Session)
|
|
124
|
+
}
|
|
125
|
+
setSaveName('')
|
|
126
|
+
setShowSaveDialog(false)
|
|
127
|
+
fetchAll()
|
|
128
|
+
} catch (e) {
|
|
129
|
+
console.error('Save session failed', e)
|
|
130
|
+
} finally {
|
|
131
|
+
setSaving(false)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const handleRestore = async (id: string) => {
|
|
136
|
+
try {
|
|
137
|
+
await api.sessions.restore(id)
|
|
138
|
+
setRestoreConfirm(null)
|
|
139
|
+
fetchAll()
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error('Restore failed', e)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const handleDelete = async (id: string) => {
|
|
146
|
+
try {
|
|
147
|
+
await api.sessions.delete(id)
|
|
148
|
+
setDeleteConfirm(null)
|
|
149
|
+
if (activeSession?.id === id) setActiveSession(null)
|
|
150
|
+
fetchAll()
|
|
151
|
+
} catch (e) {
|
|
152
|
+
console.error('Delete failed', e)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const toggleDetail = async (id: string) => {
|
|
157
|
+
if (expandedId === id) {
|
|
158
|
+
setExpandedId(null)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
setExpandedId(id)
|
|
162
|
+
if (!details[id]) {
|
|
163
|
+
setLoadingDetail(id)
|
|
164
|
+
try {
|
|
165
|
+
const info = await api.sessions.info(id) as SessionDetail
|
|
166
|
+
setDetails((prev) => ({ ...prev, [id]: info }))
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error('Failed to fetch session info', e)
|
|
169
|
+
} finally {
|
|
170
|
+
setLoadingDetail(null)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div style={s.page}>
|
|
177
|
+
{/* Active Session Banner */}
|
|
178
|
+
{activeSession ? (
|
|
179
|
+
<div style={s.banner}>
|
|
180
|
+
<div style={s.bannerInfo as CSSProperties}>
|
|
181
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
182
|
+
<span style={s.bannerTitle}>{activeSession.name || 'Active Session'}</span>
|
|
183
|
+
<StatusBadge status={activeSession.status} size="sm" />
|
|
184
|
+
</div>
|
|
185
|
+
<div style={s.bannerMeta as CSSProperties}>
|
|
186
|
+
<span style={s.metaItem}>
|
|
187
|
+
<span style={s.metaLabel}>ID:</span>{(activeSession.id ?? '').slice(0, 12) || '—'}
|
|
188
|
+
</span>
|
|
189
|
+
<span style={s.metaItem}>
|
|
190
|
+
<span style={s.metaLabel}>Agents:</span>{activeSession.agentCount}
|
|
191
|
+
</span>
|
|
192
|
+
<span style={s.metaItem}>
|
|
193
|
+
<span style={s.metaLabel}>Tasks:</span>{activeSession.taskCount}
|
|
194
|
+
</span>
|
|
195
|
+
<span style={s.metaItem}>
|
|
196
|
+
<span style={s.metaLabel}>Created:</span>
|
|
197
|
+
{new Date(activeSession.createdAt).toLocaleString()}
|
|
198
|
+
</span>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
<div style={s.actionsRow as CSSProperties}>
|
|
202
|
+
<Button size="sm" onClick={() => setShowSaveDialog(true)}>
|
|
203
|
+
<Save size={14} /> Save Session
|
|
204
|
+
</Button>
|
|
205
|
+
<Button variant="ghost" size="sm" onClick={fetchAll} loading={loading}>
|
|
206
|
+
<RefreshCw size={14} />
|
|
207
|
+
</Button>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
) : (
|
|
211
|
+
<div style={s.noBanner}>
|
|
212
|
+
<div>
|
|
213
|
+
<div style={{ fontSize: 15, fontWeight: 600, color: 'var(--text-primary)', marginBottom: 4 }}>
|
|
214
|
+
No Active Session
|
|
215
|
+
</div>
|
|
216
|
+
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>
|
|
217
|
+
Save your current state or restore a previous session.
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
<div style={s.actionsRow as CSSProperties}>
|
|
221
|
+
<Button size="sm" onClick={() => setShowSaveDialog(true)}>
|
|
222
|
+
<Save size={14} /> Save Current
|
|
223
|
+
</Button>
|
|
224
|
+
<Button variant="ghost" size="sm" onClick={fetchAll} loading={loading}>
|
|
225
|
+
<RefreshCw size={14} />
|
|
226
|
+
</Button>
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)}
|
|
230
|
+
|
|
231
|
+
{/* Sessions List */}
|
|
232
|
+
<Card title={`Sessions (${sessions.length})`} actions={
|
|
233
|
+
<Button variant="ghost" size="sm" onClick={fetchAll} loading={loading}>
|
|
234
|
+
<RefreshCw size={14} /> Refresh
|
|
235
|
+
</Button>
|
|
236
|
+
}>
|
|
237
|
+
{sessions.length === 0 ? (
|
|
238
|
+
<div style={s.empty}>No saved sessions yet. Save your current state to get started.</div>
|
|
239
|
+
) : (
|
|
240
|
+
<div style={s.grid}>
|
|
241
|
+
{sessions.map((session) => (
|
|
242
|
+
<div
|
|
243
|
+
key={session.id}
|
|
244
|
+
style={{
|
|
245
|
+
...s.sessionCard,
|
|
246
|
+
borderColor: activeSession?.id === session.id ? 'var(--accent-blue)' : undefined,
|
|
247
|
+
}}
|
|
248
|
+
onMouseEnter={(e) => { e.currentTarget.style.borderColor = 'var(--accent-blue)' }}
|
|
249
|
+
onMouseLeave={(e) => {
|
|
250
|
+
e.currentTarget.style.borderColor =
|
|
251
|
+
activeSession?.id === session.id ? 'var(--accent-blue)' : 'var(--border)'
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<div style={s.cardHeader}>
|
|
255
|
+
<span style={s.cardName}>{session.name || (session.id ?? '').slice(0, 12) || '—'}</span>
|
|
256
|
+
<StatusBadge status={session.status} size="sm" />
|
|
257
|
+
</div>
|
|
258
|
+
<div style={s.cardBody}>
|
|
259
|
+
<div style={s.cardMeta}>
|
|
260
|
+
<div>
|
|
261
|
+
<div style={s.cardLabel}>Session ID</div>
|
|
262
|
+
<div style={s.cardValue}>{(session.id ?? '').slice(0, 16) || '—'}</div>
|
|
263
|
+
</div>
|
|
264
|
+
<div>
|
|
265
|
+
<div style={s.cardLabel}>Created</div>
|
|
266
|
+
<div style={s.cardValue}>
|
|
267
|
+
{new Date(session.createdAt).toLocaleDateString()}
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
<div>
|
|
271
|
+
<div style={s.cardLabel}>Agents</div>
|
|
272
|
+
<div style={s.cardValue}>{session.agentCount}</div>
|
|
273
|
+
</div>
|
|
274
|
+
<div>
|
|
275
|
+
<div style={s.cardLabel}>Tasks</div>
|
|
276
|
+
<div style={s.cardValue}>{session.taskCount}</div>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
{/* Expandable detail */}
|
|
282
|
+
{expandedId === session.id && (
|
|
283
|
+
<div style={s.detail}>
|
|
284
|
+
{loadingDetail === session.id ? (
|
|
285
|
+
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>Loading...</div>
|
|
286
|
+
) : details[session.id] ? (
|
|
287
|
+
<>
|
|
288
|
+
<div style={s.detailSection}>
|
|
289
|
+
<div style={s.detailTitle}>Full Details</div>
|
|
290
|
+
<div style={s.detailList}>
|
|
291
|
+
<div>ID: {details[session.id].id}</div>
|
|
292
|
+
<div>Name: {details[session.id].name || '(unnamed)'}</div>
|
|
293
|
+
<div>Status: {details[session.id].status}</div>
|
|
294
|
+
<div>Created: {new Date(details[session.id].createdAt).toLocaleString()}</div>
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
{details[session.id].agents && details[session.id].agents!.length > 0 && (
|
|
298
|
+
<div style={s.detailSection}>
|
|
299
|
+
<div style={s.detailTitle}>
|
|
300
|
+
Agents ({details[session.id].agents!.length})
|
|
301
|
+
</div>
|
|
302
|
+
<div style={s.detailList}>
|
|
303
|
+
{details[session.id].agents!.map((a) => (
|
|
304
|
+
<div key={a.id} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
305
|
+
<span>{a.name}</span>
|
|
306
|
+
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>({a.type})</span>
|
|
307
|
+
<StatusBadge status={a.status} size="sm" />
|
|
308
|
+
</div>
|
|
309
|
+
))}
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
{details[session.id].tasks && details[session.id].tasks!.length > 0 && (
|
|
314
|
+
<div style={s.detailSection}>
|
|
315
|
+
<div style={s.detailTitle}>
|
|
316
|
+
Tasks ({details[session.id].tasks!.length})
|
|
317
|
+
</div>
|
|
318
|
+
<div style={s.detailList}>
|
|
319
|
+
{details[session.id].tasks!.map((t) => (
|
|
320
|
+
<div key={t.id} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
|
|
321
|
+
<span>{t.title}</span>
|
|
322
|
+
<StatusBadge status={t.status} size="sm" />
|
|
323
|
+
</div>
|
|
324
|
+
))}
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
)}
|
|
328
|
+
</>
|
|
329
|
+
) : (
|
|
330
|
+
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>
|
|
331
|
+
Failed to load details.
|
|
332
|
+
</div>
|
|
333
|
+
)}
|
|
334
|
+
</div>
|
|
335
|
+
)}
|
|
336
|
+
|
|
337
|
+
<div style={s.cardActions}>
|
|
338
|
+
<Button variant="primary" size="sm"
|
|
339
|
+
onClick={() => setRestoreConfirm(session.id)}>
|
|
340
|
+
<RefreshCw size={12} /> Restore
|
|
341
|
+
</Button>
|
|
342
|
+
<Button variant="secondary" size="sm"
|
|
343
|
+
onClick={() => toggleDetail(session.id)}>
|
|
344
|
+
<Search size={12} /> {expandedId === session.id ? 'Hide' : 'Info'}
|
|
345
|
+
</Button>
|
|
346
|
+
<Button variant="danger" size="sm"
|
|
347
|
+
onClick={() => setDeleteConfirm(session.id)}>
|
|
348
|
+
<Trash2 size={12} />
|
|
349
|
+
</Button>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
))}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
</Card>
|
|
356
|
+
|
|
357
|
+
{/* Save Session Dialog */}
|
|
358
|
+
{showSaveDialog && (
|
|
359
|
+
<div style={s.overlay} onClick={() => setShowSaveDialog(false)}>
|
|
360
|
+
<div style={s.dialog} onClick={(e) => e.stopPropagation()}>
|
|
361
|
+
<div style={s.dialogTitle}>Save Session</div>
|
|
362
|
+
<div style={s.field as CSSProperties}>
|
|
363
|
+
<span style={s.label}>Session Name (optional)</span>
|
|
364
|
+
<input
|
|
365
|
+
style={s.input}
|
|
366
|
+
placeholder="My session..."
|
|
367
|
+
value={saveName}
|
|
368
|
+
onChange={(e) => setSaveName(e.target.value)}
|
|
369
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSave()}
|
|
370
|
+
autoFocus
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
374
|
+
<Button variant="secondary" size="sm" onClick={() => setShowSaveDialog(false)}>
|
|
375
|
+
Cancel
|
|
376
|
+
</Button>
|
|
377
|
+
<Button size="sm" onClick={handleSave} loading={saving}>
|
|
378
|
+
<Save size={12} /> Save
|
|
379
|
+
</Button>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
)}
|
|
384
|
+
|
|
385
|
+
{/* Restore Confirmation Dialog */}
|
|
386
|
+
{restoreConfirm && (
|
|
387
|
+
<div style={s.overlay} onClick={() => setRestoreConfirm(null)}>
|
|
388
|
+
<div style={s.dialog} onClick={(e) => e.stopPropagation()}>
|
|
389
|
+
<div style={s.dialogTitle}>Restore Session</div>
|
|
390
|
+
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 20 }}>
|
|
391
|
+
This will replace the current state with the saved session.
|
|
392
|
+
Any unsaved changes will be lost. Continue?
|
|
393
|
+
</p>
|
|
394
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
395
|
+
<Button variant="secondary" size="sm" onClick={() => setRestoreConfirm(null)}>
|
|
396
|
+
Cancel
|
|
397
|
+
</Button>
|
|
398
|
+
<Button size="sm" onClick={() => handleRestore(restoreConfirm)}>
|
|
399
|
+
<RefreshCw size={12} /> Restore
|
|
400
|
+
</Button>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
)}
|
|
405
|
+
|
|
406
|
+
{/* Delete Confirmation Dialog */}
|
|
407
|
+
{deleteConfirm && (
|
|
408
|
+
<div style={s.overlay} onClick={() => setDeleteConfirm(null)}>
|
|
409
|
+
<div style={s.dialog} onClick={(e) => e.stopPropagation()}>
|
|
410
|
+
<div style={s.dialogTitle}>Delete Session</div>
|
|
411
|
+
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 20 }}>
|
|
412
|
+
Are you sure you want to permanently delete this session?
|
|
413
|
+
This action cannot be undone.
|
|
414
|
+
</p>
|
|
415
|
+
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
|
|
416
|
+
<Button variant="secondary" size="sm" onClick={() => setDeleteConfirm(null)}>
|
|
417
|
+
Cancel
|
|
418
|
+
</Button>
|
|
419
|
+
<Button variant="danger" size="sm" onClick={() => handleDelete(deleteConfirm)}>
|
|
420
|
+
<Trash2 size={12} /> Delete
|
|
421
|
+
</Button>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</div>
|
|
425
|
+
)}
|
|
426
|
+
</div>
|
|
427
|
+
)
|
|
428
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react'
|
|
2
|
+
import { api } from '@/api'
|
|
3
|
+
import type { PreflightResult, PreflightCheck } from '@/api'
|
|
4
|
+
|
|
5
|
+
const statusIcon: Record<string, string> = { ok: '●', warn: '▲', fail: '✕' }
|
|
6
|
+
const statusColor: Record<string, string> = {
|
|
7
|
+
ok: 'var(--accent-green)',
|
|
8
|
+
warn: 'var(--accent-yellow)',
|
|
9
|
+
fail: 'var(--accent-red)',
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function CheckRow({ check }: { check: PreflightCheck }) {
|
|
13
|
+
return (
|
|
14
|
+
<div style={{
|
|
15
|
+
display: 'flex', alignItems: 'flex-start', gap: 12, padding: '12px 16px',
|
|
16
|
+
background: 'var(--bg-secondary)', borderRadius: 'var(--radius)', border: '1px solid var(--border)',
|
|
17
|
+
}}>
|
|
18
|
+
<span style={{ color: statusColor[check.status], fontSize: 18, lineHeight: '24px', flexShrink: 0 }}>
|
|
19
|
+
{statusIcon[check.status]}
|
|
20
|
+
</span>
|
|
21
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
22
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
23
|
+
<span style={{ fontWeight: 600, color: 'var(--text-primary)' }}>{check.name}</span>
|
|
24
|
+
<span style={{
|
|
25
|
+
fontSize: 11, padding: '1px 8px', borderRadius: 10,
|
|
26
|
+
background: `${statusColor[check.status]}22`, color: statusColor[check.status],
|
|
27
|
+
textTransform: 'uppercase', fontWeight: 600, letterSpacing: '0.5px',
|
|
28
|
+
}}>
|
|
29
|
+
{check.status}
|
|
30
|
+
</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div style={{ color: 'var(--text-secondary)', fontSize: 13, marginTop: 2 }}>{check.detail}</div>
|
|
33
|
+
{check.fix && (
|
|
34
|
+
<div style={{
|
|
35
|
+
marginTop: 6, padding: '6px 10px', background: 'var(--bg-tertiary)',
|
|
36
|
+
borderRadius: 'var(--radius)', fontSize: 12, fontFamily: 'monospace',
|
|
37
|
+
color: 'var(--accent-yellow)', borderLeft: '3px solid var(--accent-yellow)',
|
|
38
|
+
}}>
|
|
39
|
+
{check.fix}
|
|
40
|
+
</div>
|
|
41
|
+
)}
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Props {
|
|
48
|
+
onContinue: () => void
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default function SetupWizard({ onContinue }: Props) {
|
|
52
|
+
const [result, setResult] = useState<PreflightResult | null>(null)
|
|
53
|
+
const [loading, setLoading] = useState(true)
|
|
54
|
+
const [error, setError] = useState<string | null>(null)
|
|
55
|
+
|
|
56
|
+
const runChecks = async () => {
|
|
57
|
+
setLoading(true)
|
|
58
|
+
setError(null)
|
|
59
|
+
try {
|
|
60
|
+
const data = await api.system.preflight()
|
|
61
|
+
setResult(data)
|
|
62
|
+
} catch (err) {
|
|
63
|
+
setError(err instanceof Error ? err.message : 'Cannot reach backend. Is it running?')
|
|
64
|
+
} finally {
|
|
65
|
+
setLoading(false)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
useEffect(() => { runChecks() }, [])
|
|
70
|
+
|
|
71
|
+
const canContinue = result && result.failed === 0
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div style={{
|
|
75
|
+
minHeight: '100vh', background: 'var(--bg-primary)', display: 'flex',
|
|
76
|
+
alignItems: 'center', justifyContent: 'center', padding: 24,
|
|
77
|
+
}}>
|
|
78
|
+
<div style={{
|
|
79
|
+
maxWidth: 560, width: '100%', background: 'var(--bg-card)',
|
|
80
|
+
borderRadius: 'var(--radius-lg)', border: '1px solid var(--border)',
|
|
81
|
+
overflow: 'hidden',
|
|
82
|
+
}}>
|
|
83
|
+
{/* Header */}
|
|
84
|
+
<div style={{
|
|
85
|
+
padding: '28px 28px 20px', borderBottom: '1px solid var(--border)',
|
|
86
|
+
background: 'linear-gradient(135deg, rgba(59,130,246,0.08), rgba(6,182,212,0.05))',
|
|
87
|
+
}}>
|
|
88
|
+
<h1 style={{ fontSize: 22, fontWeight: 700, color: 'var(--text-primary)', margin: 0 }}>
|
|
89
|
+
RuFloUI Setup
|
|
90
|
+
</h1>
|
|
91
|
+
<p style={{ color: 'var(--text-secondary)', fontSize: 14, marginTop: 6 }}>
|
|
92
|
+
Checking dependencies and environment before starting...
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Body */}
|
|
97
|
+
<div style={{ padding: 24 }}>
|
|
98
|
+
{loading && (
|
|
99
|
+
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 40, gap: 12 }}>
|
|
100
|
+
<div style={{
|
|
101
|
+
width: 24, height: 24, border: '3px solid var(--border)',
|
|
102
|
+
borderTopColor: 'var(--accent-blue)', borderRadius: '50%',
|
|
103
|
+
animation: 'spin 0.8s linear infinite',
|
|
104
|
+
}} />
|
|
105
|
+
<span style={{ color: 'var(--text-secondary)' }}>Running checks...</span>
|
|
106
|
+
</div>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{error && (
|
|
110
|
+
<div style={{
|
|
111
|
+
padding: 16, background: 'rgba(239,68,68,0.1)', borderRadius: 'var(--radius)',
|
|
112
|
+
border: '1px solid var(--accent-red)', color: 'var(--accent-red)',
|
|
113
|
+
}}>
|
|
114
|
+
<div style={{ fontWeight: 600, marginBottom: 4 }}>Backend unreachable</div>
|
|
115
|
+
<div style={{ fontSize: 13 }}>{error}</div>
|
|
116
|
+
<div style={{ fontSize: 12, marginTop: 8, color: 'var(--text-secondary)' }}>
|
|
117
|
+
Make sure the backend is running: <code style={{ color: 'var(--accent-cyan)' }}>npm run dev:backend</code>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{result && !loading && (
|
|
123
|
+
<>
|
|
124
|
+
{/* Summary bar */}
|
|
125
|
+
<div style={{
|
|
126
|
+
display: 'flex', gap: 16, marginBottom: 16, padding: '10px 16px',
|
|
127
|
+
background: result.status === 'ok' ? 'rgba(16,185,129,0.08)' : result.status === 'warn' ? 'rgba(245,158,11,0.08)' : 'rgba(239,68,68,0.08)',
|
|
128
|
+
borderRadius: 'var(--radius)', border: `1px solid ${statusColor[result.status]}44`,
|
|
129
|
+
}}>
|
|
130
|
+
<span style={{ color: statusColor[result.status], fontWeight: 600 }}>
|
|
131
|
+
{result.status === 'ok' ? 'All checks passed' : result.status === 'warn' ? `${result.warned} warning(s)` : `${result.failed} check(s) failed`}
|
|
132
|
+
</span>
|
|
133
|
+
<span style={{ color: 'var(--text-muted)', marginLeft: 'auto', fontSize: 13 }}>
|
|
134
|
+
{result.passed} passed / {result.checks.length} total
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Check list */}
|
|
139
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
140
|
+
{result.checks.map(c => <CheckRow key={c.id} check={c} />)}
|
|
141
|
+
</div>
|
|
142
|
+
</>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{/* Footer */}
|
|
147
|
+
<div style={{
|
|
148
|
+
padding: '16px 24px', borderTop: '1px solid var(--border)',
|
|
149
|
+
display: 'flex', justifyContent: 'flex-end', gap: 10,
|
|
150
|
+
}}>
|
|
151
|
+
<button
|
|
152
|
+
onClick={runChecks}
|
|
153
|
+
disabled={loading}
|
|
154
|
+
style={{
|
|
155
|
+
padding: '8px 18px', borderRadius: 'var(--radius)', fontSize: 13,
|
|
156
|
+
background: 'var(--bg-tertiary)', color: 'var(--text-primary)',
|
|
157
|
+
border: '1px solid var(--border)', cursor: loading ? 'not-allowed' : 'pointer',
|
|
158
|
+
opacity: loading ? 0.5 : 1,
|
|
159
|
+
}}
|
|
160
|
+
>
|
|
161
|
+
Re-check
|
|
162
|
+
</button>
|
|
163
|
+
<button
|
|
164
|
+
onClick={onContinue}
|
|
165
|
+
disabled={!canContinue && !result}
|
|
166
|
+
style={{
|
|
167
|
+
padding: '8px 18px', borderRadius: 'var(--radius)', fontSize: 13, fontWeight: 600,
|
|
168
|
+
background: canContinue ? 'var(--accent-blue)' : 'var(--bg-tertiary)',
|
|
169
|
+
color: canContinue ? '#fff' : 'var(--text-secondary)',
|
|
170
|
+
border: 'none',
|
|
171
|
+
cursor: canContinue ? 'pointer' : result ? 'pointer' : 'not-allowed',
|
|
172
|
+
opacity: !canContinue && !result ? 0.5 : 1,
|
|
173
|
+
}}
|
|
174
|
+
>
|
|
175
|
+
{canContinue ? 'Continue to Dashboard' : result ? 'Continue Anyway' : 'Continue'}
|
|
176
|
+
</button>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|