@camstack/addon-admin-ui 0.1.1 → 0.1.3
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/dist/assets/index-DjELGD4R.css +1 -0
- package/dist/assets/index-w55PwKyu.js +598 -0
- package/{index.html → dist/index.html} +3 -1
- package/dist/server/addon.d.ts +11 -0
- package/dist/server/addon.js +50 -0
- package/dist/server/addon.js.map +1 -0
- package/package.json +5 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -339
- package/src/components/addons/AddonUploadZone.tsx +0 -307
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -119
- package/src/components/agents/AgentCard.tsx +0 -281
- package/src/components/agents/AgentLogs.tsx +0 -231
- package/src/components/agents/ProcessList.tsx +0 -127
- package/src/components/agents/ProcessTree.tsx +0 -369
- package/src/components/agents/TaskList.tsx +0 -68
- package/src/components/cameras/CameraCard.tsx +0 -60
- package/src/components/cameras/LiveEventsPanel.tsx +0 -91
- package/src/components/cameras/ProviderSection.tsx +0 -50
- package/src/components/cameras/StreamArea.tsx +0 -107
- package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
- package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
- package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
- package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
- package/src/components/dashboard/BlockPicker.tsx +0 -54
- package/src/components/dashboard/BlockWrapper.tsx +0 -97
- package/src/components/dashboard/DashboardGrid.tsx +0 -160
- package/src/components/dashboard/block-registry.ts +0 -15
- package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
- package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
- package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
- package/src/components/dashboard/blocks/index.ts +0 -32
- package/src/components/device/DeviceHeader.tsx +0 -116
- package/src/components/device/FloatingPanel.tsx +0 -132
- package/src/components/device/FloatingPanelManager.tsx +0 -167
- package/src/components/device/PanelContent.tsx +0 -196
- package/src/components/device/QuickConfigWizard.tsx +0 -507
- package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
- package/src/components/device/tabs/EventsTab.tsx +0 -19
- package/src/components/device/tabs/LogsTab.tsx +0 -22
- package/src/components/device/tabs/OverviewTab.tsx +0 -104
- package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
- package/src/components/device/tabs/RecordingTab.tsx +0 -47
- package/src/components/device/tabs/ReplTab.tsx +0 -153
- package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
- package/src/components/device/tabs/ZonesTab.tsx +0 -98
- package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
- package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
- package/src/components/device/zone-editor/ZoneList.tsx +0 -150
- package/src/components/form-builder/FormBuilder.tsx +0 -135
- package/src/components/form-builder/FormField.tsx +0 -732
- package/src/components/form-builder/ModelSelector.tsx +0 -239
- package/src/components/integrations/AddDeviceDialog.tsx +0 -205
- package/src/components/integrations/CompactDeviceCard.tsx +0 -35
- package/src/components/integrations/DeviceCard.tsx +0 -29
- package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
- package/src/components/integrations/DeviceGrid.tsx +0 -79
- package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
- package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
- package/src/components/integrations/IntegrationCard.tsx +0 -40
- package/src/components/integrations/IntegrationWizard.tsx +0 -171
- package/src/components/integrations/ProviderConfigForm.tsx +0 -89
- package/src/components/integrations/ProviderPicker.tsx +0 -91
- package/src/components/integrations/SnapshotPopover.tsx +0 -68
- package/src/components/metrics/AgentLoad.tsx +0 -113
- package/src/components/metrics/IntegrationUsage.tsx +0 -90
- package/src/components/metrics/PipelineStatus.tsx +0 -105
- package/src/components/metrics/ProcessResources.tsx +0 -139
- package/src/components/pipeline/PhaseSettings.tsx +0 -131
- package/src/components/shared/CapabilityBadges.tsx +0 -30
- package/src/components/shared/ProviderIcon.tsx +0 -42
- package/src/components/shared/StatusBadge.tsx +0 -23
- package/src/components/shared/WebRtcPlayer.tsx +0 -211
- package/src/components/timeline/EventMarker.tsx +0 -32
- package/src/components/timeline/TimelineBar.tsx +0 -131
- package/src/components/ui/ConfirmDialog.tsx +0 -115
- package/src/components/ui/ToastContainer.tsx +0 -92
- package/src/contexts/auth-context.tsx +0 -91
- package/src/hooks/useBackendClient.ts +0 -6
- package/src/hooks/useTheme.ts +0 -1
- package/src/i18n/en.json +0 -164
- package/src/i18n/index.ts +0 -29
- package/src/i18n/it.json +0 -164
- package/src/index.css +0 -63
- package/src/layouts/AddonPageLoader.tsx +0 -120
- package/src/layouts/AppLayout.tsx +0 -238
- package/src/layouts/ProtectedRoute.tsx +0 -25
- package/src/lib/addon-page-context.ts +0 -29
- package/src/lib/backend.ts +0 -16
- package/src/main.tsx +0 -21
- package/src/pages/AccessDenied.tsx +0 -22
- package/src/pages/Cameras.tsx +0 -127
- package/src/pages/Dashboard.tsx +0 -6
- package/src/pages/DeviceDetail.tsx +0 -175
- package/src/pages/IntegrationDetail.tsx +0 -224
- package/src/pages/Integrations.tsx +0 -330
- package/src/pages/Login.tsx +0 -106
- package/src/pages/Metrics.tsx +0 -18
- package/src/pages/PipelineConfig.tsx +0 -282
- package/src/pages/Showroom.tsx +0 -351
- package/src/pages/Timeline.tsx +0 -269
- package/src/pages/system/Addons.tsx +0 -525
- package/src/pages/system/Agents.tsx +0 -362
- package/src/pages/system/Logs.tsx +0 -131
- package/src/pages/system/Models.tsx +0 -102
- package/src/pages/system/Processes.tsx +0 -129
- package/src/pages/system/Repl.tsx +0 -148
- package/src/pages/system/Settings.tsx +0 -168
- package/src/pages/system/Users.tsx +0 -174
- package/src/server/addon.ts +0 -54
- package/src/types/config-ui.ts +0 -210
- package/src/types/dashboard.ts +0 -39
- package/tsconfig.json +0 -29
- package/tsconfig.server.json +0 -16
- package/tsup.config.ts +0 -20
- package/vite.config.ts +0 -68
- /package/{public → dist}/brand/logo-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
- /package/{public → dist}/brand/logo-light.svg +0 -0
- /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
- /package/{public → dist}/brand/logo-wide-light.svg +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
- /package/{public → dist}/vendor/react.mjs +0 -0
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
ChevronDown,
|
|
4
|
-
ChevronUp,
|
|
5
|
-
Cpu,
|
|
6
|
-
HardDrive,
|
|
7
|
-
Monitor,
|
|
8
|
-
Layers,
|
|
9
|
-
Activity,
|
|
10
|
-
Server,
|
|
11
|
-
} from 'lucide-react'
|
|
12
|
-
import { TaskList } from './TaskList'
|
|
13
|
-
import { ProcessList } from './ProcessList'
|
|
14
|
-
import type { AgentTask } from './TaskList'
|
|
15
|
-
import type { ProcessEntry } from './ProcessList'
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Types
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export interface AgentRegistrationInfo {
|
|
22
|
-
id: string
|
|
23
|
-
name: string
|
|
24
|
-
capabilities: string[]
|
|
25
|
-
host: string
|
|
26
|
-
port: number
|
|
27
|
-
platform: string
|
|
28
|
-
arch: string
|
|
29
|
-
cpuModel?: string
|
|
30
|
-
cpuCores: number
|
|
31
|
-
memoryMB: number
|
|
32
|
-
gpuModel?: string
|
|
33
|
-
pythonRuntimes: string[]
|
|
34
|
-
httpPort: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface AgentRuntimeStatus {
|
|
38
|
-
activeCameras: number
|
|
39
|
-
cpuPercent: number
|
|
40
|
-
memoryPercent: number
|
|
41
|
-
fps: Record<string, number>
|
|
42
|
-
errors: string[]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ConnectedAgent {
|
|
46
|
-
info: AgentRegistrationInfo
|
|
47
|
-
status?: AgentRuntimeStatus
|
|
48
|
-
connectedSince: number
|
|
49
|
-
isHub?: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Sub-components
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
function StatusDot({ online }: { online: boolean }) {
|
|
57
|
-
return (
|
|
58
|
-
<span
|
|
59
|
-
className={`inline-block h-2.5 w-2.5 rounded-full shrink-0 ${online ? 'bg-green-400' : 'bg-red-400'}`}
|
|
60
|
-
title={online ? 'Online' : 'Offline'}
|
|
61
|
-
/>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function Bar({ percent, color = 'bg-primary' }: { percent: number; color?: string }) {
|
|
66
|
-
return (
|
|
67
|
-
<div className="h-1.5 w-full rounded-full bg-border overflow-hidden">
|
|
68
|
-
<div
|
|
69
|
-
className={`h-full rounded-full transition-all ${color}`}
|
|
70
|
-
style={{ width: `${Math.min(100, percent)}%` }}
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const CAPABILITY_COLORS: Record<string, string> = {
|
|
77
|
-
decode: 'bg-blue-500/10 text-blue-400',
|
|
78
|
-
decoder: 'bg-blue-500/10 text-blue-400',
|
|
79
|
-
detect: 'bg-green-500/10 text-green-400',
|
|
80
|
-
detector: 'bg-green-500/10 text-green-400',
|
|
81
|
-
record: 'bg-orange-500/10 text-orange-400',
|
|
82
|
-
recorder: 'bg-orange-500/10 text-orange-400',
|
|
83
|
-
transcode: 'bg-purple-500/10 text-purple-400',
|
|
84
|
-
transcoder: 'bg-purple-500/10 text-purple-400',
|
|
85
|
-
restream: 'bg-cyan-500/10 text-cyan-400',
|
|
86
|
-
restreamer: 'bg-cyan-500/10 text-cyan-400',
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const RUNTIME_COLORS: Record<string, string> = {
|
|
90
|
-
onnx: 'bg-yellow-500/10 text-yellow-400',
|
|
91
|
-
coreml: 'bg-pink-500/10 text-pink-400',
|
|
92
|
-
pytorch: 'bg-orange-500/10 text-orange-400',
|
|
93
|
-
openvino:'bg-blue-500/10 text-blue-400',
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function CapBadge({ cap }: { cap: string }) {
|
|
97
|
-
const lower = cap.toLowerCase()
|
|
98
|
-
const color = CAPABILITY_COLORS[lower] ?? 'bg-primary/10 text-primary'
|
|
99
|
-
return (
|
|
100
|
-
<span className={`inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-medium ${color}`}>
|
|
101
|
-
{cap}
|
|
102
|
-
</span>
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function RuntimeBadge({ runtime }: { runtime: string }) {
|
|
107
|
-
const lower = runtime.toLowerCase()
|
|
108
|
-
const color = RUNTIME_COLORS[lower] ?? 'bg-foreground-subtle/10 text-foreground-subtle'
|
|
109
|
-
return (
|
|
110
|
-
<span className={`inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-medium ${color}`}>
|
|
111
|
-
{runtime}
|
|
112
|
-
</span>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function formatUptime(connectedSince: number): string {
|
|
117
|
-
const seconds = Math.floor((Date.now() - connectedSince) / 1000)
|
|
118
|
-
if (seconds < 60) return `${seconds}s`
|
|
119
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
|
|
120
|
-
return `${Math.floor(seconds / 3600)}h`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// AgentCard
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
interface AgentCardProps {
|
|
128
|
-
agent: ConnectedAgent
|
|
129
|
-
processes: ProcessEntry[]
|
|
130
|
-
tasks: AgentTask[]
|
|
131
|
-
defaultExpanded?: boolean
|
|
132
|
-
online?: boolean
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function AgentCard({ agent, processes, tasks, defaultExpanded = false, online = true }: AgentCardProps) {
|
|
136
|
-
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
137
|
-
const { info, status } = agent
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<div className={`rounded-xl border border-border bg-surface overflow-hidden transition-opacity ${!online ? 'opacity-50' : ''}`}>
|
|
141
|
-
{/* Header */}
|
|
142
|
-
<button
|
|
143
|
-
type="button"
|
|
144
|
-
className="w-full flex items-start justify-between gap-3 px-4 py-3 hover:bg-primary/5 transition-colors text-left"
|
|
145
|
-
onClick={() => setExpanded((e) => !e)}
|
|
146
|
-
>
|
|
147
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
148
|
-
<StatusDot online={online} />
|
|
149
|
-
<Server className="h-4 w-4 shrink-0 text-foreground-subtle" />
|
|
150
|
-
<div className="min-w-0">
|
|
151
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
152
|
-
<span className="text-sm font-semibold text-foreground truncate">{info.name}</span>
|
|
153
|
-
{agent.isHub && (
|
|
154
|
-
<span className="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-[10px] font-medium shrink-0">HUB</span>
|
|
155
|
-
)}
|
|
156
|
-
{!online && (
|
|
157
|
-
<span className="rounded-full bg-foreground-subtle/10 text-foreground-subtle px-2 py-0.5 text-[10px] shrink-0">Offline</span>
|
|
158
|
-
)}
|
|
159
|
-
</div>
|
|
160
|
-
<div className="flex items-center gap-2 mt-0.5 text-[10px] text-foreground-subtle flex-wrap">
|
|
161
|
-
<span className="font-mono">{info.host}:{info.port}</span>
|
|
162
|
-
<span>{info.platform}/{info.arch}</span>
|
|
163
|
-
{status && (
|
|
164
|
-
<>
|
|
165
|
-
<span>CPU {status.cpuPercent.toFixed(0)}%</span>
|
|
166
|
-
<span>MEM {status.memoryPercent.toFixed(0)}%</span>
|
|
167
|
-
</>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
<div className="flex items-center gap-2 shrink-0">
|
|
174
|
-
{/* task count badge */}
|
|
175
|
-
{tasks.length > 0 && (
|
|
176
|
-
<span className="rounded-full bg-info/10 text-info px-2 py-0.5 text-[10px] font-medium">
|
|
177
|
-
{tasks.length} task{tasks.length !== 1 ? 's' : ''}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
{/* runtime badges */}
|
|
181
|
-
{info.pythonRuntimes.slice(0, 2).map((rt) => (
|
|
182
|
-
<RuntimeBadge key={rt} runtime={rt} />
|
|
183
|
-
))}
|
|
184
|
-
<span className="text-[10px] text-foreground-subtle">up {formatUptime(agent.connectedSince)}</span>
|
|
185
|
-
{expanded ? <ChevronUp className="h-4 w-4 text-foreground-subtle" /> : <ChevronDown className="h-4 w-4 text-foreground-subtle" />}
|
|
186
|
-
</div>
|
|
187
|
-
</button>
|
|
188
|
-
|
|
189
|
-
{/* Load bars (always visible when online) */}
|
|
190
|
-
{status && !expanded && (
|
|
191
|
-
<div className="px-4 pb-3 space-y-1">
|
|
192
|
-
<Bar percent={status.cpuPercent} color="bg-blue-400" />
|
|
193
|
-
<Bar percent={status.memoryPercent} color="bg-purple-400" />
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
196
|
-
|
|
197
|
-
{/* Expanded content */}
|
|
198
|
-
{expanded && (
|
|
199
|
-
<div className="border-t border-border divide-y divide-border">
|
|
200
|
-
{/* Hardware details */}
|
|
201
|
-
<div className="px-4 py-3 grid grid-cols-2 gap-x-4 gap-y-1.5 text-[10px] text-foreground-subtle">
|
|
202
|
-
<div className="flex items-center gap-1.5">
|
|
203
|
-
<Cpu className="h-3 w-3 shrink-0" />
|
|
204
|
-
<span className="truncate">{info.cpuModel ?? `${info.cpuCores} cores`}</span>
|
|
205
|
-
</div>
|
|
206
|
-
<div className="flex items-center gap-1.5">
|
|
207
|
-
<HardDrive className="h-3 w-3 shrink-0" />
|
|
208
|
-
<span>{Math.round(info.memoryMB / 1024)} GB RAM</span>
|
|
209
|
-
</div>
|
|
210
|
-
<div className="flex items-center gap-1.5">
|
|
211
|
-
<Monitor className="h-3 w-3 shrink-0" />
|
|
212
|
-
<span>{info.platform}/{info.arch}</span>
|
|
213
|
-
</div>
|
|
214
|
-
{info.gpuModel && (
|
|
215
|
-
<div className="flex items-center gap-1.5">
|
|
216
|
-
<Layers className="h-3 w-3 shrink-0" />
|
|
217
|
-
<span className="truncate">{info.gpuModel}</span>
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
{/* Load bars */}
|
|
223
|
-
{status && (
|
|
224
|
-
<div className="px-4 py-3 space-y-2">
|
|
225
|
-
<div className="flex items-center justify-between text-[10px] text-foreground-subtle mb-1">
|
|
226
|
-
<span className="flex items-center gap-1">
|
|
227
|
-
<Activity className="h-3 w-3" />
|
|
228
|
-
{status.activeCameras} active camera{status.activeCameras !== 1 ? 's' : ''}
|
|
229
|
-
</span>
|
|
230
|
-
<span>CPU {status.cpuPercent.toFixed(0)}% / MEM {status.memoryPercent.toFixed(0)}%</span>
|
|
231
|
-
</div>
|
|
232
|
-
<Bar percent={status.cpuPercent} color="bg-blue-400" />
|
|
233
|
-
<Bar percent={status.memoryPercent} color="bg-purple-400" />
|
|
234
|
-
</div>
|
|
235
|
-
)}
|
|
236
|
-
|
|
237
|
-
{/* Capabilities */}
|
|
238
|
-
<div className="px-4 py-3 space-y-2">
|
|
239
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">Capabilities</div>
|
|
240
|
-
<div className="flex flex-wrap gap-1.5">
|
|
241
|
-
{info.capabilities.map((cap) => (
|
|
242
|
-
<CapBadge key={cap} cap={cap} />
|
|
243
|
-
))}
|
|
244
|
-
{info.capabilities.length === 0 && (
|
|
245
|
-
<span className="text-[10px] text-foreground-subtle italic">None registered</span>
|
|
246
|
-
)}
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
{/* Runtimes */}
|
|
251
|
-
{info.pythonRuntimes.length > 0 && (
|
|
252
|
-
<div className="px-4 py-3 space-y-2">
|
|
253
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">Runtimes</div>
|
|
254
|
-
<div className="flex flex-wrap gap-1.5">
|
|
255
|
-
{info.pythonRuntimes.map((rt) => (
|
|
256
|
-
<RuntimeBadge key={rt} runtime={rt} />
|
|
257
|
-
))}
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
{/* Active Tasks */}
|
|
263
|
-
<div className="px-4 py-3 space-y-2">
|
|
264
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">
|
|
265
|
-
Active Tasks ({tasks.length})
|
|
266
|
-
</div>
|
|
267
|
-
<TaskList tasks={tasks} />
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
{/* Processes */}
|
|
271
|
-
<div className="px-4 py-3 space-y-2">
|
|
272
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">
|
|
273
|
-
Processes ({processes.length})
|
|
274
|
-
</div>
|
|
275
|
-
<ProcessList processes={processes} />
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
)}
|
|
279
|
-
</div>
|
|
280
|
-
)
|
|
281
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
|
2
|
-
import { useQuery } from '@tanstack/react-query'
|
|
3
|
-
import { ArrowDownToLine, Filter } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Types
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
type LogLevel = 'all' | 'debug' | 'info' | 'warn' | 'error'
|
|
11
|
-
|
|
12
|
-
interface AgentLogsProps {
|
|
13
|
-
/** List of agent names/IDs for the filter dropdown */
|
|
14
|
-
agentNames: readonly string[]
|
|
15
|
-
/** List of addon names for the filter dropdown */
|
|
16
|
-
addonNames: readonly string[]
|
|
17
|
-
/** Pre-selected agent name from clicking an agent in the tree */
|
|
18
|
-
preselectedAgent?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Styles
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const LEVEL_STYLES: Record<string, { badge: string; text: string }> = {
|
|
26
|
-
debug: { badge: 'bg-foreground-subtle/10 text-foreground-subtle', text: 'text-foreground-subtle' },
|
|
27
|
-
info: { badge: 'bg-info/10 text-info', text: 'text-info' },
|
|
28
|
-
warn: { badge: 'bg-warning/10 text-warning', text: 'text-warning' },
|
|
29
|
-
error: { badge: 'bg-danger/10 text-danger', text: 'text-danger' },
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Component
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
export function AgentLogs({ agentNames, addonNames, preselectedAgent }: AgentLogsProps) {
|
|
37
|
-
const client = useBackendClient()
|
|
38
|
-
const [levelFilter, setLevelFilter] = useState<LogLevel>('all')
|
|
39
|
-
const [agentFilter, setAgentFilter] = useState<string>(preselectedAgent ?? 'all')
|
|
40
|
-
const [addonFilter, setAddonFilter] = useState<string>('all')
|
|
41
|
-
|
|
42
|
-
// Sync agentFilter when preselectedAgent changes externally
|
|
43
|
-
const prevPreselected = useRef(preselectedAgent)
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (preselectedAgent !== prevPreselected.current) {
|
|
46
|
-
setAgentFilter(preselectedAgent ?? 'all')
|
|
47
|
-
prevPreselected.current = preselectedAgent
|
|
48
|
-
}
|
|
49
|
-
}, [preselectedAgent])
|
|
50
|
-
const [autoScroll, setAutoScroll] = useState(true)
|
|
51
|
-
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
52
|
-
// bottomRef removed — auto-scroll goes to top (newest first)
|
|
53
|
-
|
|
54
|
-
// Build scope filters from agent + addon selections
|
|
55
|
-
const scopeFilter = useMemo(() => {
|
|
56
|
-
const scopes: string[] = []
|
|
57
|
-
if (agentFilter !== 'all') scopes.push(agentFilter)
|
|
58
|
-
if (addonFilter !== 'all') scopes.push(addonFilter)
|
|
59
|
-
return scopes.length > 0 ? scopes : undefined
|
|
60
|
-
}, [agentFilter, addonFilter])
|
|
61
|
-
|
|
62
|
-
// Always fetch live -- no pause concept
|
|
63
|
-
const { data: logsData, isLoading, isError } = useQuery({
|
|
64
|
-
queryKey: ['agent-logs', levelFilter, agentFilter, addonFilter],
|
|
65
|
-
queryFn: () =>
|
|
66
|
-
client.getLogs({
|
|
67
|
-
...(levelFilter !== 'all' ? { level: levelFilter as 'debug' | 'info' | 'warn' | 'error' } : {}),
|
|
68
|
-
limit: 300,
|
|
69
|
-
...(scopeFilter ? { scope: scopeFilter } : {}),
|
|
70
|
-
} as Parameters<typeof client.getLogs>[0]),
|
|
71
|
-
refetchInterval: 3000,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const rawLogs = (logsData ?? []) as unknown as Array<Record<string, unknown>>
|
|
75
|
-
|
|
76
|
-
// Newest first — most recent logs at the top
|
|
77
|
-
const logs = useMemo(() => [...rawLogs].reverse(), [rawLogs])
|
|
78
|
-
|
|
79
|
-
// Auto-scroll to top when new logs arrive (newest is at top)
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (autoScroll) {
|
|
82
|
-
scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
83
|
-
}
|
|
84
|
-
}, [logs, autoScroll])
|
|
85
|
-
|
|
86
|
-
// Detect user scroll to auto-disable auto-scroll when scrolling down
|
|
87
|
-
const handleScroll = useCallback(() => {
|
|
88
|
-
const container = scrollContainerRef.current
|
|
89
|
-
if (!container) return
|
|
90
|
-
const isAtTop = container.scrollTop < 40
|
|
91
|
-
setAutoScroll(isAtTop)
|
|
92
|
-
}, [])
|
|
93
|
-
|
|
94
|
-
function formatTs(ts: unknown): string {
|
|
95
|
-
if (!ts) return '--'
|
|
96
|
-
const d = new Date(typeof ts === 'number' ? ts : String(ts))
|
|
97
|
-
return d.toLocaleTimeString('en-GB', {
|
|
98
|
-
hour12: false,
|
|
99
|
-
hour: '2-digit',
|
|
100
|
-
minute: '2-digit',
|
|
101
|
-
second: '2-digit',
|
|
102
|
-
fractionalSecondDigits: 3,
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className="space-y-3">
|
|
108
|
-
{/* Filters bar */}
|
|
109
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
110
|
-
<Filter className="h-3.5 w-3.5 text-foreground-subtle shrink-0" />
|
|
111
|
-
|
|
112
|
-
<select
|
|
113
|
-
value={agentFilter}
|
|
114
|
-
onChange={(e) => setAgentFilter(e.target.value)}
|
|
115
|
-
className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
116
|
-
>
|
|
117
|
-
<option value="all">All agents</option>
|
|
118
|
-
<option value="Hub">Hub</option>
|
|
119
|
-
{agentNames.map((name) => (
|
|
120
|
-
<option key={name} value={name}>{name}</option>
|
|
121
|
-
))}
|
|
122
|
-
</select>
|
|
123
|
-
|
|
124
|
-
<select
|
|
125
|
-
value={addonFilter}
|
|
126
|
-
onChange={(e) => setAddonFilter(e.target.value)}
|
|
127
|
-
className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
128
|
-
>
|
|
129
|
-
<option value="all">All addons</option>
|
|
130
|
-
{addonNames.map((name) => (
|
|
131
|
-
<option key={name} value={name}>{name}</option>
|
|
132
|
-
))}
|
|
133
|
-
</select>
|
|
134
|
-
|
|
135
|
-
<select
|
|
136
|
-
value={levelFilter}
|
|
137
|
-
onChange={(e) => setLevelFilter(e.target.value as LogLevel)}
|
|
138
|
-
className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
139
|
-
>
|
|
140
|
-
<option value="all">All levels</option>
|
|
141
|
-
<option value="debug">Debug</option>
|
|
142
|
-
<option value="info">Info</option>
|
|
143
|
-
<option value="warn">Warn</option>
|
|
144
|
-
<option value="error">Error</option>
|
|
145
|
-
</select>
|
|
146
|
-
|
|
147
|
-
<button
|
|
148
|
-
type="button"
|
|
149
|
-
onClick={() => {
|
|
150
|
-
const next = !autoScroll
|
|
151
|
-
setAutoScroll(next)
|
|
152
|
-
if (next) {
|
|
153
|
-
scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
154
|
-
}
|
|
155
|
-
}}
|
|
156
|
-
className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
|
157
|
-
autoScroll
|
|
158
|
-
? 'border-primary/30 bg-primary/10 text-primary'
|
|
159
|
-
: 'border-border bg-surface text-foreground-subtle hover:text-foreground'
|
|
160
|
-
}`}
|
|
161
|
-
>
|
|
162
|
-
<ArrowDownToLine className="h-3.5 w-3.5" />
|
|
163
|
-
Auto-scroll: {autoScroll ? 'ON' : 'OFF'}
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
{/* Log display */}
|
|
168
|
-
{isLoading && (
|
|
169
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading logs...</div>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
{isError && (
|
|
173
|
-
<div className="text-xs text-danger">Failed to load logs</div>
|
|
174
|
-
)}
|
|
175
|
-
|
|
176
|
-
{!isLoading && !isError && logs.length === 0 && (
|
|
177
|
-
<div className="text-xs text-foreground-subtle italic">No logs match the current filters</div>
|
|
178
|
-
)}
|
|
179
|
-
|
|
180
|
-
{logs.length > 0 && (
|
|
181
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
182
|
-
<div
|
|
183
|
-
ref={scrollContainerRef}
|
|
184
|
-
onScroll={handleScroll}
|
|
185
|
-
className="max-h-[400px] overflow-y-auto"
|
|
186
|
-
>
|
|
187
|
-
<table className="w-full text-[10px]">
|
|
188
|
-
<thead className="sticky top-0">
|
|
189
|
-
<tr>
|
|
190
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-24">Time</th>
|
|
191
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-14">Level</th>
|
|
192
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-28">Scope</th>
|
|
193
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border">Message</th>
|
|
194
|
-
</tr>
|
|
195
|
-
</thead>
|
|
196
|
-
<tbody>
|
|
197
|
-
{logs.map((entry, i) => {
|
|
198
|
-
const level = String(entry.level ?? 'info').toLowerCase()
|
|
199
|
-
const style = LEVEL_STYLES[level] ?? LEVEL_STYLES['info']!
|
|
200
|
-
const message = String(entry.message ?? entry.msg ?? entry.text ?? '')
|
|
201
|
-
const scope = Array.isArray(entry.scope)
|
|
202
|
-
? (entry.scope as string[]).join(' > ')
|
|
203
|
-
: String(entry.scope ?? '')
|
|
204
|
-
return (
|
|
205
|
-
<tr key={i} className="hover:bg-primary/5">
|
|
206
|
-
<td className="px-2.5 py-1 text-foreground-subtle border-b border-border font-mono whitespace-nowrap">
|
|
207
|
-
{formatTs(entry.timestamp ?? entry.ts ?? entry.time)}
|
|
208
|
-
</td>
|
|
209
|
-
<td className="px-2.5 py-1 border-b border-border">
|
|
210
|
-
<span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-medium uppercase ${style.badge}`}>
|
|
211
|
-
{level}
|
|
212
|
-
</span>
|
|
213
|
-
</td>
|
|
214
|
-
<td className="px-2.5 py-1 border-b border-border text-foreground-subtle font-mono truncate max-w-[180px]" title={scope}>
|
|
215
|
-
{scope || '--'}
|
|
216
|
-
</td>
|
|
217
|
-
<td className={`px-2.5 py-1 border-b border-border font-mono ${style.text} break-all`}>
|
|
218
|
-
{message}
|
|
219
|
-
</td>
|
|
220
|
-
</tr>
|
|
221
|
-
)
|
|
222
|
-
})}
|
|
223
|
-
</tbody>
|
|
224
|
-
</table>
|
|
225
|
-
{/* newest-first: no bottom anchor needed */}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
)
|
|
231
|
-
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { RotateCcw } from 'lucide-react'
|
|
3
|
-
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
export interface ProcessEntry {
|
|
7
|
-
id: string
|
|
8
|
-
name: string
|
|
9
|
-
pid?: number
|
|
10
|
-
status: string
|
|
11
|
-
cpu?: number
|
|
12
|
-
memoryMb?: number
|
|
13
|
-
uptime?: number
|
|
14
|
-
restarts?: number
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function StatusDot({ status }: { status: string }) {
|
|
18
|
-
const color =
|
|
19
|
-
status === 'running'
|
|
20
|
-
? 'bg-green-400'
|
|
21
|
-
: status === 'error'
|
|
22
|
-
? 'bg-red-400'
|
|
23
|
-
: 'bg-foreground-subtle'
|
|
24
|
-
return <span className={`inline-block h-2 w-2 rounded-full shrink-0 ${color}`} title={status} />
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function formatUptime(seconds: number): string {
|
|
28
|
-
if (seconds < 60) return `${seconds}s`
|
|
29
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
|
|
30
|
-
const h = Math.floor(seconds / 3600)
|
|
31
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
32
|
-
return `${h}h ${m}m`
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
interface ProcessListProps {
|
|
36
|
-
processes: ProcessEntry[]
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function ProcessList({ processes }: ProcessListProps) {
|
|
40
|
-
const client = useBackendClient()
|
|
41
|
-
const queryClient = useQueryClient()
|
|
42
|
-
const [confirmingId, setConfirmingId] = useState<string | null>(null)
|
|
43
|
-
|
|
44
|
-
const restartMutation = useMutation({
|
|
45
|
-
mutationFn: (id: string) => client.trpc.processes.restartProcess.mutate({ id }),
|
|
46
|
-
onSuccess: () => {
|
|
47
|
-
void queryClient.invalidateQueries({ queryKey: ['processes'] })
|
|
48
|
-
setConfirmingId(null)
|
|
49
|
-
},
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
if (processes.length === 0) {
|
|
53
|
-
return <div className="text-[10px] text-foreground-subtle italic">No processes</div>
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
return (
|
|
57
|
-
<div className="rounded-md border border-border overflow-hidden">
|
|
58
|
-
<table className="w-full text-[10px]">
|
|
59
|
-
<thead>
|
|
60
|
-
<tr>
|
|
61
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border w-4" />
|
|
62
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">Name</th>
|
|
63
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">PID</th>
|
|
64
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">CPU%</th>
|
|
65
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">RSS</th>
|
|
66
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">Uptime</th>
|
|
67
|
-
<th className="px-2.5 py-1.5 bg-background border-b border-border" />
|
|
68
|
-
</tr>
|
|
69
|
-
</thead>
|
|
70
|
-
<tbody>
|
|
71
|
-
{processes.map((proc) => {
|
|
72
|
-
const isConfirming = confirmingId === proc.id
|
|
73
|
-
return (
|
|
74
|
-
<tr key={proc.id} className="hover:bg-primary/5">
|
|
75
|
-
<td className="px-2.5 py-1.5 border-b border-border">
|
|
76
|
-
<StatusDot status={proc.status} />
|
|
77
|
-
</td>
|
|
78
|
-
<td className="px-2.5 py-1.5 border-b border-border font-mono text-foreground">
|
|
79
|
-
{proc.name}
|
|
80
|
-
</td>
|
|
81
|
-
<td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
|
|
82
|
-
{proc.pid ?? '—'}
|
|
83
|
-
</td>
|
|
84
|
-
<td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
|
|
85
|
-
{proc.cpu != null ? `${proc.cpu.toFixed(1)}%` : '—'}
|
|
86
|
-
</td>
|
|
87
|
-
<td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
|
|
88
|
-
{proc.memoryMb != null ? `${proc.memoryMb.toFixed(0)} MB` : '—'}
|
|
89
|
-
</td>
|
|
90
|
-
<td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
|
|
91
|
-
{proc.uptime != null ? formatUptime(proc.uptime) : '—'}
|
|
92
|
-
</td>
|
|
93
|
-
<td className="px-2.5 py-1.5 border-b border-border">
|
|
94
|
-
{isConfirming ? (
|
|
95
|
-
<div className="flex items-center gap-1">
|
|
96
|
-
<button
|
|
97
|
-
onClick={() => restartMutation.mutate(proc.id)}
|
|
98
|
-
disabled={restartMutation.isPending}
|
|
99
|
-
className="rounded px-1.5 py-0.5 bg-danger/10 text-danger hover:bg-danger/20"
|
|
100
|
-
>
|
|
101
|
-
Confirm
|
|
102
|
-
</button>
|
|
103
|
-
<button
|
|
104
|
-
onClick={() => setConfirmingId(null)}
|
|
105
|
-
className="rounded px-1.5 py-0.5 bg-surface text-foreground-subtle hover:text-foreground border border-border"
|
|
106
|
-
>
|
|
107
|
-
Cancel
|
|
108
|
-
</button>
|
|
109
|
-
</div>
|
|
110
|
-
) : (
|
|
111
|
-
<button
|
|
112
|
-
onClick={() => setConfirmingId(proc.id)}
|
|
113
|
-
className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 bg-primary/10 text-primary hover:bg-primary/20"
|
|
114
|
-
>
|
|
115
|
-
<RotateCcw className="h-2.5 w-2.5" />
|
|
116
|
-
Restart
|
|
117
|
-
</button>
|
|
118
|
-
)}
|
|
119
|
-
</td>
|
|
120
|
-
</tr>
|
|
121
|
-
)
|
|
122
|
-
})}
|
|
123
|
-
</tbody>
|
|
124
|
-
</table>
|
|
125
|
-
</div>
|
|
126
|
-
)
|
|
127
|
-
}
|