@camstack/addon-admin-ui 0.1.2 → 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 +4 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -355
- package/src/components/addons/AddonUploadZone.tsx +0 -69
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -108
- 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 -172
- 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 -105
- package/src/components/metrics/IntegrationUsage.tsx +0 -73
- package/src/components/metrics/PipelineStatus.tsx +0 -74
- package/src/components/metrics/ProcessResources.tsx +0 -123
- 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 -254
- 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 -222
- package/src/pages/Integrations.tsx +0 -333
- 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 -396
- 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 -28
- 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,369 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
ChevronDown,
|
|
4
|
-
ChevronRight,
|
|
5
|
-
Server,
|
|
6
|
-
Cpu,
|
|
7
|
-
HardDrive,
|
|
8
|
-
Monitor,
|
|
9
|
-
Package,
|
|
10
|
-
GitBranch,
|
|
11
|
-
ScrollText,
|
|
12
|
-
Clock,
|
|
13
|
-
} from 'lucide-react'
|
|
14
|
-
|
|
15
|
-
// ---------------------------------------------------------------------------
|
|
16
|
-
// Types
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
export interface ProcessTreeEntry {
|
|
20
|
-
id: string
|
|
21
|
-
name: string
|
|
22
|
-
pid: number
|
|
23
|
-
state: string
|
|
24
|
-
cpuPercent: number
|
|
25
|
-
memoryPercent: number
|
|
26
|
-
memoryMB: number
|
|
27
|
-
isHub: boolean
|
|
28
|
-
platform: string
|
|
29
|
-
arch: string
|
|
30
|
-
host: string
|
|
31
|
-
capabilities: string[]
|
|
32
|
-
installedAddons: string[]
|
|
33
|
-
pythonRuntimes: string[]
|
|
34
|
-
connectedSince: number
|
|
35
|
-
subProcesses: SubProcess[]
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface SubProcess {
|
|
39
|
-
pid: number
|
|
40
|
-
name: string
|
|
41
|
-
command: string
|
|
42
|
-
state: 'running' | 'stopped' | 'crashed'
|
|
43
|
-
cpuPercent: number
|
|
44
|
-
memoryRss: number
|
|
45
|
-
uptimeSeconds: number
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Helpers
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
|
|
52
|
-
function formatMemory(mb: number): string {
|
|
53
|
-
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`
|
|
54
|
-
return `${Math.round(mb)} MB`
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function formatUptime(connectedSince: number): string {
|
|
58
|
-
const seconds = Math.floor((Date.now() - connectedSince) / 1000)
|
|
59
|
-
if (seconds < 60) return `${seconds}s`
|
|
60
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
|
|
61
|
-
const h = Math.floor(seconds / 3600)
|
|
62
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
63
|
-
return `${h}h ${m}m`
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function formatUptimeSeconds(seconds: number): string {
|
|
67
|
-
if (seconds < 60) return `${seconds}s`
|
|
68
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
|
|
69
|
-
const h = Math.floor(seconds / 3600)
|
|
70
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
71
|
-
return `${h}h ${m}m`
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function formatTimestamp(ts: number): string {
|
|
75
|
-
return new Date(ts).toLocaleString('en-GB', {
|
|
76
|
-
year: 'numeric',
|
|
77
|
-
month: '2-digit',
|
|
78
|
-
day: '2-digit',
|
|
79
|
-
hour: '2-digit',
|
|
80
|
-
minute: '2-digit',
|
|
81
|
-
second: '2-digit',
|
|
82
|
-
hour12: false,
|
|
83
|
-
})
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function stateColor(state: string): string {
|
|
87
|
-
switch (state) {
|
|
88
|
-
case 'running': return 'bg-green-400'
|
|
89
|
-
case 'stopped': return 'bg-foreground-subtle'
|
|
90
|
-
case 'crashed': return 'bg-red-400'
|
|
91
|
-
default: return 'bg-foreground-subtle'
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
// Sub-components
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
function SubProcessRow({ proc, isLast }: { proc: SubProcess; isLast: boolean }) {
|
|
100
|
-
return (
|
|
101
|
-
<div className="flex items-center gap-2 text-[10px] text-foreground-subtle pl-6 py-1">
|
|
102
|
-
<span className="text-foreground-subtle/40 font-mono select-none">
|
|
103
|
-
{isLast ? '\u2514\u2500\u2500' : '\u251C\u2500\u2500'}
|
|
104
|
-
</span>
|
|
105
|
-
<span className={`h-1.5 w-1.5 rounded-full shrink-0 ${stateColor(proc.state)}`} />
|
|
106
|
-
<span className="font-mono text-foreground truncate">{proc.name}</span>
|
|
107
|
-
<span className="text-foreground-subtle/60">PID {proc.pid}</span>
|
|
108
|
-
<span>CPU {proc.cpuPercent.toFixed(1)}%</span>
|
|
109
|
-
<span>{formatMemory(proc.memoryRss / 1024 / 1024)}</span>
|
|
110
|
-
{proc.uptimeSeconds > 0 && (
|
|
111
|
-
<span className="text-foreground-subtle/60">up {formatUptimeSeconds(proc.uptimeSeconds)}</span>
|
|
112
|
-
)}
|
|
113
|
-
</div>
|
|
114
|
-
)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function AgentTreeNode({ entry, isSelected, onSelectAgent }: {
|
|
118
|
-
entry: ProcessTreeEntry
|
|
119
|
-
isSelected?: boolean
|
|
120
|
-
onSelectAgent?: (name: string) => void
|
|
121
|
-
}) {
|
|
122
|
-
const [expanded, setExpanded] = useState(entry.isHub)
|
|
123
|
-
const hasExpandableContent = true // Always expandable — details section is always useful
|
|
124
|
-
|
|
125
|
-
return (
|
|
126
|
-
<div className="border border-border rounded-lg bg-surface overflow-hidden">
|
|
127
|
-
{/* Header row */}
|
|
128
|
-
<button
|
|
129
|
-
type="button"
|
|
130
|
-
className="w-full flex items-start justify-between gap-3 px-4 py-3 hover:bg-primary/5 transition-colors text-left"
|
|
131
|
-
onClick={() => setExpanded((e) => !e)}
|
|
132
|
-
>
|
|
133
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
134
|
-
{expanded
|
|
135
|
-
? <ChevronDown className="h-3.5 w-3.5 shrink-0 text-foreground-subtle" />
|
|
136
|
-
: <ChevronRight className="h-3.5 w-3.5 shrink-0 text-foreground-subtle" />
|
|
137
|
-
}
|
|
138
|
-
<span className={`h-2.5 w-2.5 rounded-full shrink-0 ${entry.state === 'running' ? 'bg-green-400' : 'bg-red-400'}`} />
|
|
139
|
-
<Server className="h-4 w-4 shrink-0 text-foreground-subtle" />
|
|
140
|
-
<div className="min-w-0">
|
|
141
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
142
|
-
<span className="text-sm font-semibold text-foreground truncate">{entry.name}</span>
|
|
143
|
-
{entry.isHub && (
|
|
144
|
-
<span className="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-[10px] font-medium shrink-0">HUB</span>
|
|
145
|
-
)}
|
|
146
|
-
</div>
|
|
147
|
-
<div className="flex items-center gap-2 mt-0.5 text-[10px] text-foreground-subtle flex-wrap">
|
|
148
|
-
{entry.host && <span className="font-mono">{entry.host}</span>}
|
|
149
|
-
<span>{entry.platform}/{entry.arch}</span>
|
|
150
|
-
<span className="flex items-center gap-1">
|
|
151
|
-
<Cpu className="h-2.5 w-2.5" />
|
|
152
|
-
{entry.cpuPercent.toFixed(0)}%
|
|
153
|
-
</span>
|
|
154
|
-
<span className="flex items-center gap-1">
|
|
155
|
-
<HardDrive className="h-2.5 w-2.5" />
|
|
156
|
-
{entry.memoryPercent.toFixed(0)}% ({formatMemory(entry.memoryMB)})
|
|
157
|
-
</span>
|
|
158
|
-
</div>
|
|
159
|
-
</div>
|
|
160
|
-
</div>
|
|
161
|
-
|
|
162
|
-
<div className="flex items-center gap-2 shrink-0 text-[10px] text-foreground-subtle">
|
|
163
|
-
<span>up {formatUptime(entry.connectedSince)}</span>
|
|
164
|
-
{entry.subProcesses.length > 0 && (
|
|
165
|
-
<span className="rounded-full bg-info/10 text-info px-2 py-0.5 font-medium">
|
|
166
|
-
{entry.subProcesses.length} proc{entry.subProcesses.length !== 1 ? 's' : ''}
|
|
167
|
-
</span>
|
|
168
|
-
)}
|
|
169
|
-
{onSelectAgent && (
|
|
170
|
-
<button
|
|
171
|
-
type="button"
|
|
172
|
-
onClick={(e) => {
|
|
173
|
-
e.stopPropagation()
|
|
174
|
-
onSelectAgent(entry.isHub ? 'Hub' : entry.name)
|
|
175
|
-
}}
|
|
176
|
-
className={`inline-flex items-center gap-1 rounded-lg border px-2 py-0.5 text-[10px] font-medium transition-colors ${
|
|
177
|
-
isSelected
|
|
178
|
-
? 'border-primary/30 bg-primary/10 text-primary'
|
|
179
|
-
: 'border-border bg-surface text-foreground-subtle hover:text-foreground hover:bg-primary/5'
|
|
180
|
-
}`}
|
|
181
|
-
title="View logs for this agent"
|
|
182
|
-
>
|
|
183
|
-
<ScrollText className="h-3 w-3" />
|
|
184
|
-
Logs
|
|
185
|
-
</button>
|
|
186
|
-
)}
|
|
187
|
-
</div>
|
|
188
|
-
</button>
|
|
189
|
-
|
|
190
|
-
{/* Load bars (collapsed only) */}
|
|
191
|
-
{!expanded && (
|
|
192
|
-
<div className="px-4 pb-2 space-y-1">
|
|
193
|
-
<div className="h-1.5 w-full rounded-full bg-border overflow-hidden">
|
|
194
|
-
<div
|
|
195
|
-
className="h-full rounded-full bg-blue-400 transition-all"
|
|
196
|
-
style={{ width: `${Math.min(100, entry.cpuPercent)}%` }}
|
|
197
|
-
/>
|
|
198
|
-
</div>
|
|
199
|
-
<div className="h-1.5 w-full rounded-full bg-border overflow-hidden">
|
|
200
|
-
<div
|
|
201
|
-
className="h-full rounded-full bg-purple-400 transition-all"
|
|
202
|
-
style={{ width: `${Math.min(100, entry.memoryPercent)}%` }}
|
|
203
|
-
/>
|
|
204
|
-
</div>
|
|
205
|
-
</div>
|
|
206
|
-
)}
|
|
207
|
-
|
|
208
|
-
{/* Expanded content */}
|
|
209
|
-
{expanded && (
|
|
210
|
-
<div className="border-t border-border">
|
|
211
|
-
{/* Platform & System Details */}
|
|
212
|
-
<div className="px-4 py-2 border-b border-border">
|
|
213
|
-
<div className="flex items-center gap-1.5 text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-1.5">
|
|
214
|
-
<Server className="h-3 w-3" />
|
|
215
|
-
System Details
|
|
216
|
-
</div>
|
|
217
|
-
<div className="grid grid-cols-2 sm:grid-cols-3 gap-x-4 gap-y-1 text-[10px] text-foreground-subtle">
|
|
218
|
-
<div className="flex items-center gap-1.5">
|
|
219
|
-
<Monitor className="h-3 w-3 shrink-0" />
|
|
220
|
-
<span>Platform: <span className="text-foreground font-medium">{entry.platform}</span></span>
|
|
221
|
-
</div>
|
|
222
|
-
<div className="flex items-center gap-1.5">
|
|
223
|
-
<Cpu className="h-3 w-3 shrink-0" />
|
|
224
|
-
<span>Arch: <span className="text-foreground font-medium">{entry.arch}</span></span>
|
|
225
|
-
</div>
|
|
226
|
-
<div className="flex items-center gap-1.5">
|
|
227
|
-
<HardDrive className="h-3 w-3 shrink-0" />
|
|
228
|
-
<span>Memory: <span className="text-foreground font-medium">{formatMemory(entry.memoryMB)}</span></span>
|
|
229
|
-
</div>
|
|
230
|
-
<div className="flex items-center gap-1.5">
|
|
231
|
-
<Cpu className="h-3 w-3 shrink-0" />
|
|
232
|
-
<span>CPU: <span className="text-foreground font-medium">{entry.cpuPercent.toFixed(1)}%</span></span>
|
|
233
|
-
</div>
|
|
234
|
-
<div className="flex items-center gap-1.5">
|
|
235
|
-
<HardDrive className="h-3 w-3 shrink-0" />
|
|
236
|
-
<span>Mem usage: <span className="text-foreground font-medium">{entry.memoryPercent.toFixed(1)}%</span></span>
|
|
237
|
-
</div>
|
|
238
|
-
<div className="flex items-center gap-1.5">
|
|
239
|
-
<Clock className="h-3 w-3 shrink-0" />
|
|
240
|
-
<span>Connected: <span className="text-foreground font-medium">{formatTimestamp(entry.connectedSince)}</span></span>
|
|
241
|
-
</div>
|
|
242
|
-
</div>
|
|
243
|
-
{/* Load bars in expanded */}
|
|
244
|
-
<div className="mt-2 space-y-1">
|
|
245
|
-
<div className="flex items-center gap-2 text-[9px] text-foreground-subtle">
|
|
246
|
-
<span className="w-8">CPU</span>
|
|
247
|
-
<div className="flex-1 h-1.5 rounded-full bg-border overflow-hidden">
|
|
248
|
-
<div
|
|
249
|
-
className="h-full rounded-full bg-blue-400 transition-all"
|
|
250
|
-
style={{ width: `${Math.min(100, entry.cpuPercent)}%` }}
|
|
251
|
-
/>
|
|
252
|
-
</div>
|
|
253
|
-
<span className="w-8 text-right">{entry.cpuPercent.toFixed(0)}%</span>
|
|
254
|
-
</div>
|
|
255
|
-
<div className="flex items-center gap-2 text-[9px] text-foreground-subtle">
|
|
256
|
-
<span className="w-8">MEM</span>
|
|
257
|
-
<div className="flex-1 h-1.5 rounded-full bg-border overflow-hidden">
|
|
258
|
-
<div
|
|
259
|
-
className="h-full rounded-full bg-purple-400 transition-all"
|
|
260
|
-
style={{ width: `${Math.min(100, entry.memoryPercent)}%` }}
|
|
261
|
-
/>
|
|
262
|
-
</div>
|
|
263
|
-
<span className="w-8 text-right">{entry.memoryPercent.toFixed(0)}%</span>
|
|
264
|
-
</div>
|
|
265
|
-
</div>
|
|
266
|
-
</div>
|
|
267
|
-
|
|
268
|
-
{/* Installed addons */}
|
|
269
|
-
{entry.installedAddons.length > 0 && (
|
|
270
|
-
<div className="px-4 py-2 border-b border-border">
|
|
271
|
-
<div className="flex items-center gap-1.5 text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-1.5">
|
|
272
|
-
<Package className="h-3 w-3" />
|
|
273
|
-
Installed Addons ({entry.installedAddons.length})
|
|
274
|
-
</div>
|
|
275
|
-
<div className="flex flex-wrap gap-1.5">
|
|
276
|
-
{entry.installedAddons.map((addon) => (
|
|
277
|
-
<span key={addon} className="inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-medium bg-primary/10 text-primary">
|
|
278
|
-
{addon}
|
|
279
|
-
</span>
|
|
280
|
-
))}
|
|
281
|
-
</div>
|
|
282
|
-
</div>
|
|
283
|
-
)}
|
|
284
|
-
|
|
285
|
-
{/* Capabilities */}
|
|
286
|
-
{entry.capabilities.length > 0 && (
|
|
287
|
-
<div className="px-4 py-2 border-b border-border">
|
|
288
|
-
<div className="flex items-center gap-1.5 text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-1.5">
|
|
289
|
-
<Monitor className="h-3 w-3" />
|
|
290
|
-
Capabilities ({entry.capabilities.length})
|
|
291
|
-
</div>
|
|
292
|
-
<div className="flex flex-wrap gap-1.5">
|
|
293
|
-
{entry.capabilities.map((cap) => (
|
|
294
|
-
<span key={cap} className="inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-medium bg-green-500/10 text-green-400">
|
|
295
|
-
{cap}
|
|
296
|
-
</span>
|
|
297
|
-
))}
|
|
298
|
-
</div>
|
|
299
|
-
</div>
|
|
300
|
-
)}
|
|
301
|
-
|
|
302
|
-
{/* Sub-processes */}
|
|
303
|
-
{entry.subProcesses.length > 0 && (
|
|
304
|
-
<div className="px-4 py-2">
|
|
305
|
-
<div className="flex items-center gap-1.5 text-[10px] font-medium text-foreground-subtle uppercase tracking-wide mb-1.5">
|
|
306
|
-
<GitBranch className="h-3 w-3" />
|
|
307
|
-
Sub-Processes ({entry.subProcesses.length})
|
|
308
|
-
</div>
|
|
309
|
-
{entry.subProcesses.map((proc, idx) => (
|
|
310
|
-
<SubProcessRow
|
|
311
|
-
key={proc.pid}
|
|
312
|
-
proc={proc}
|
|
313
|
-
isLast={idx === entry.subProcesses.length - 1}
|
|
314
|
-
/>
|
|
315
|
-
))}
|
|
316
|
-
</div>
|
|
317
|
-
)}
|
|
318
|
-
|
|
319
|
-
{entry.subProcesses.length === 0 && entry.installedAddons.length === 0 && entry.capabilities.length === 0 && (
|
|
320
|
-
<div className="px-4 py-3 text-[10px] text-foreground-subtle italic">
|
|
321
|
-
No sub-processes or addons reported
|
|
322
|
-
</div>
|
|
323
|
-
)}
|
|
324
|
-
</div>
|
|
325
|
-
)}
|
|
326
|
-
</div>
|
|
327
|
-
)
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ---------------------------------------------------------------------------
|
|
331
|
-
// ProcessTree
|
|
332
|
-
// ---------------------------------------------------------------------------
|
|
333
|
-
|
|
334
|
-
interface ProcessTreeProps {
|
|
335
|
-
entries: ProcessTreeEntry[]
|
|
336
|
-
isLoading: boolean
|
|
337
|
-
selectedAgentName?: string | null
|
|
338
|
-
onSelectAgent?: (name: string) => void
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export function ProcessTree({ entries, isLoading, selectedAgentName, onSelectAgent }: ProcessTreeProps) {
|
|
342
|
-
if (isLoading) {
|
|
343
|
-
return <div className="text-xs text-foreground-subtle animate-pulse">Loading process tree...</div>
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (entries.length === 0) {
|
|
347
|
-
return <div className="text-xs text-foreground-subtle italic">No agents connected</div>
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// Sort: hub first, then by name
|
|
351
|
-
const sorted = [...entries].sort((a, b) => {
|
|
352
|
-
if (a.isHub && !b.isHub) return -1
|
|
353
|
-
if (!a.isHub && b.isHub) return 1
|
|
354
|
-
return a.name.localeCompare(b.name)
|
|
355
|
-
})
|
|
356
|
-
|
|
357
|
-
return (
|
|
358
|
-
<div className="space-y-2">
|
|
359
|
-
{sorted.map((entry) => (
|
|
360
|
-
<AgentTreeNode
|
|
361
|
-
key={entry.id}
|
|
362
|
-
entry={entry}
|
|
363
|
-
isSelected={selectedAgentName === entry.name}
|
|
364
|
-
onSelectAgent={onSelectAgent}
|
|
365
|
-
/>
|
|
366
|
-
))}
|
|
367
|
-
</div>
|
|
368
|
-
)
|
|
369
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { Clock, Video } from 'lucide-react'
|
|
2
|
-
|
|
3
|
-
export interface AgentTask {
|
|
4
|
-
id: string
|
|
5
|
-
role: string
|
|
6
|
-
cameraId?: string
|
|
7
|
-
cameraName?: string
|
|
8
|
-
stats?: Record<string, unknown>
|
|
9
|
-
startedAt?: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function formatDuration(startedAt: number): string {
|
|
13
|
-
const seconds = Math.floor((Date.now() - startedAt) / 1000)
|
|
14
|
-
if (seconds < 60) return `${seconds}s`
|
|
15
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
|
|
16
|
-
const h = Math.floor(seconds / 3600)
|
|
17
|
-
const m = Math.floor((seconds % 3600) / 60)
|
|
18
|
-
return `${h}h ${m}m`
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const ROLE_COLORS: Record<string, string> = {
|
|
22
|
-
decoder: 'bg-blue-500/10 text-blue-400',
|
|
23
|
-
transcoder: 'bg-purple-500/10 text-purple-400',
|
|
24
|
-
detector: 'bg-green-500/10 text-green-400',
|
|
25
|
-
recorder: 'bg-orange-500/10 text-orange-400',
|
|
26
|
-
restreamer: 'bg-cyan-500/10 text-cyan-400',
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface TaskListProps {
|
|
30
|
-
tasks: AgentTask[]
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export function TaskList({ tasks }: TaskListProps) {
|
|
34
|
-
if (tasks.length === 0) {
|
|
35
|
-
return (
|
|
36
|
-
<div className="text-[10px] text-foreground-subtle italic">No active tasks</div>
|
|
37
|
-
)
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div className="space-y-1.5">
|
|
42
|
-
{tasks.map((task) => (
|
|
43
|
-
<div
|
|
44
|
-
key={task.id}
|
|
45
|
-
className="flex items-center gap-2 rounded-md bg-background px-2.5 py-1.5 text-[10px]"
|
|
46
|
-
>
|
|
47
|
-
<span className={`shrink-0 rounded-md px-1.5 py-0.5 font-medium ${ROLE_COLORS[task.role] ?? 'bg-primary/10 text-primary'}`}>
|
|
48
|
-
{task.role}
|
|
49
|
-
</span>
|
|
50
|
-
|
|
51
|
-
{task.cameraName && (
|
|
52
|
-
<span className="flex items-center gap-1 text-foreground-subtle truncate min-w-0">
|
|
53
|
-
<Video className="h-3 w-3 shrink-0" />
|
|
54
|
-
<span className="truncate">{task.cameraName}</span>
|
|
55
|
-
</span>
|
|
56
|
-
)}
|
|
57
|
-
|
|
58
|
-
{task.startedAt && (
|
|
59
|
-
<span className="ml-auto flex items-center gap-1 text-foreground-subtle shrink-0">
|
|
60
|
-
<Clock className="h-3 w-3" />
|
|
61
|
-
{formatDuration(task.startedAt)}
|
|
62
|
-
</span>
|
|
63
|
-
)}
|
|
64
|
-
</div>
|
|
65
|
-
))}
|
|
66
|
-
</div>
|
|
67
|
-
)
|
|
68
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { useNavigate } from 'react-router-dom'
|
|
2
|
-
import { Camera, Eye, Activity } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
interface CameraCardProps {
|
|
5
|
-
readonly id: string
|
|
6
|
-
readonly name: string
|
|
7
|
-
readonly status: string
|
|
8
|
-
readonly detectionsToday: number
|
|
9
|
-
readonly inferenceMs: number
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const STATUS_CONFIG: Record<string, { label: string; bg: string; text: string }> = {
|
|
13
|
-
online: { label: 'ACTIVE', bg: 'bg-success/15', text: 'text-success' },
|
|
14
|
-
running: { label: 'ACTIVE', bg: 'bg-success/15', text: 'text-success' },
|
|
15
|
-
watching: { label: 'WATCHING', bg: 'bg-warning/15', text: 'text-warning' },
|
|
16
|
-
idle: { label: 'WATCHING', bg: 'bg-warning/15', text: 'text-warning' },
|
|
17
|
-
offline: { label: 'OFFLINE', bg: 'bg-foreground-subtle/10', text: 'text-foreground-subtle' },
|
|
18
|
-
stopped: { label: 'OFFLINE', bg: 'bg-foreground-subtle/10', text: 'text-foreground-subtle' },
|
|
19
|
-
error: { label: 'OFFLINE', bg: 'bg-danger/15', text: 'text-danger' },
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export function CameraCard({ id, name, status, detectionsToday, inferenceMs }: CameraCardProps) {
|
|
23
|
-
const navigate = useNavigate()
|
|
24
|
-
const statusCfg = STATUS_CONFIG[status] ?? STATUS_CONFIG['offline']!
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<button
|
|
28
|
-
onClick={() => navigate(`/devices/${id}`)}
|
|
29
|
-
className="group flex flex-col rounded-lg border border-border bg-surface text-left hover:border-primary/30 hover:shadow-md hover:shadow-black/5 transition-all w-full overflow-hidden"
|
|
30
|
-
>
|
|
31
|
-
{/* 16:9 stream placeholder */}
|
|
32
|
-
<div className="relative aspect-video w-full bg-background flex items-center justify-center">
|
|
33
|
-
<Camera className="h-8 w-8 text-foreground-subtle/20" />
|
|
34
|
-
{/* Status badge */}
|
|
35
|
-
<span
|
|
36
|
-
className={`absolute top-2 right-2 inline-flex items-center rounded px-1.5 py-0.5 text-[9px] font-bold tracking-wider ${statusCfg.bg} ${statusCfg.text}`}
|
|
37
|
-
>
|
|
38
|
-
{statusCfg.label}
|
|
39
|
-
</span>
|
|
40
|
-
</div>
|
|
41
|
-
|
|
42
|
-
{/* Info section */}
|
|
43
|
-
<div className="px-3 py-2.5 space-y-1.5">
|
|
44
|
-
<p className="text-xs font-semibold text-foreground truncate group-hover:text-primary transition-colors">
|
|
45
|
-
{name}
|
|
46
|
-
</p>
|
|
47
|
-
<div className="flex items-center gap-3 text-[10px] text-foreground-subtle">
|
|
48
|
-
<span className="inline-flex items-center gap-1">
|
|
49
|
-
<Eye className="h-3 w-3" />
|
|
50
|
-
{detectionsToday} today
|
|
51
|
-
</span>
|
|
52
|
-
<span className="inline-flex items-center gap-1">
|
|
53
|
-
<Activity className="h-3 w-3" />
|
|
54
|
-
{inferenceMs}ms
|
|
55
|
-
</span>
|
|
56
|
-
</div>
|
|
57
|
-
</div>
|
|
58
|
-
</button>
|
|
59
|
-
)
|
|
60
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { ChevronUp, Bell } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
type EventType = 'DETECT' | 'PHASE' | 'MOTION' | 'REC' | 'STATE' | 'AUDIO'
|
|
5
|
-
|
|
6
|
-
interface LiveEvent {
|
|
7
|
-
readonly id: string
|
|
8
|
-
readonly type: EventType
|
|
9
|
-
readonly message: string
|
|
10
|
-
readonly timestamp: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const EVENT_COLORS: Record<EventType, { bg: string; text: string }> = {
|
|
14
|
-
DETECT: { bg: 'bg-success/15', text: 'text-success' },
|
|
15
|
-
PHASE: { bg: 'bg-info/15', text: 'text-info' },
|
|
16
|
-
MOTION: { bg: 'bg-warning/15', text: 'text-warning' },
|
|
17
|
-
REC: { bg: 'bg-danger/15', text: 'text-danger' },
|
|
18
|
-
STATE: { bg: 'bg-primary/15', text: 'text-primary' },
|
|
19
|
-
AUDIO: { bg: 'bg-purple-500/15', text: 'text-purple-400' },
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface LiveEventsPanelProps {
|
|
23
|
-
readonly deviceId: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export function LiveEventsPanel({ deviceId: _deviceId }: LiveEventsPanelProps) {
|
|
27
|
-
const [events] = useState<readonly LiveEvent[]>([])
|
|
28
|
-
|
|
29
|
-
return (
|
|
30
|
-
<div className="flex flex-col h-full">
|
|
31
|
-
{/* Header */}
|
|
32
|
-
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
|
33
|
-
<div className="flex items-center gap-2">
|
|
34
|
-
<span className="relative flex h-2 w-2">
|
|
35
|
-
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-success opacity-75" />
|
|
36
|
-
<span className="relative inline-flex rounded-full h-2 w-2 bg-success" />
|
|
37
|
-
</span>
|
|
38
|
-
<h3 className="text-[11px] font-semibold text-foreground uppercase tracking-wider">Live Events</h3>
|
|
39
|
-
</div>
|
|
40
|
-
<button
|
|
41
|
-
title="Float panel"
|
|
42
|
-
className="p-1 rounded hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
|
|
43
|
-
>
|
|
44
|
-
<ChevronUp className="h-3.5 w-3.5" />
|
|
45
|
-
</button>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
{/* Event type legend */}
|
|
49
|
-
<div className="flex flex-wrap gap-1.5 px-3 py-2 border-b border-border">
|
|
50
|
-
{(Object.entries(EVENT_COLORS) as Array<[EventType, { bg: string; text: string }]>).map(([type, colors]) => (
|
|
51
|
-
<span
|
|
52
|
-
key={type}
|
|
53
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[9px] font-medium ${colors.bg} ${colors.text}`}
|
|
54
|
-
>
|
|
55
|
-
{type}
|
|
56
|
-
</span>
|
|
57
|
-
))}
|
|
58
|
-
</div>
|
|
59
|
-
|
|
60
|
-
{/* Event stream */}
|
|
61
|
-
<div className="flex-1 overflow-y-auto">
|
|
62
|
-
{events.length === 0 ? (
|
|
63
|
-
<div className="flex flex-col items-center justify-center py-12 text-foreground-subtle">
|
|
64
|
-
<Bell className="h-6 w-6 mb-2 opacity-30" />
|
|
65
|
-
<p className="text-[11px]">Waiting for events...</p>
|
|
66
|
-
<p className="text-[10px] mt-0.5 opacity-70">Events will stream in real-time</p>
|
|
67
|
-
</div>
|
|
68
|
-
) : (
|
|
69
|
-
<div className="divide-y divide-border">
|
|
70
|
-
{events.map((event) => {
|
|
71
|
-
const colors = EVENT_COLORS[event.type] ?? EVENT_COLORS['DETECT']!
|
|
72
|
-
return (
|
|
73
|
-
<div key={event.id} className="px-3 py-2 hover:bg-surface-hover transition-colors">
|
|
74
|
-
<div className="flex items-center gap-2">
|
|
75
|
-
<span
|
|
76
|
-
className={`inline-flex items-center rounded px-1.5 py-0.5 text-[9px] font-medium ${colors.bg} ${colors.text}`}
|
|
77
|
-
>
|
|
78
|
-
{event.type}
|
|
79
|
-
</span>
|
|
80
|
-
<span className="text-[10px] text-foreground-subtle ml-auto">{event.timestamp}</span>
|
|
81
|
-
</div>
|
|
82
|
-
<p className="text-[11px] text-foreground mt-1">{event.message}</p>
|
|
83
|
-
</div>
|
|
84
|
-
)
|
|
85
|
-
})}
|
|
86
|
-
</div>
|
|
87
|
-
)}
|
|
88
|
-
</div>
|
|
89
|
-
</div>
|
|
90
|
-
)
|
|
91
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { ProviderIcon, getProviderLabel } from '../shared/ProviderIcon'
|
|
2
|
-
import { StatusBadge } from '../shared/StatusBadge'
|
|
3
|
-
import { CameraCard } from './CameraCard'
|
|
4
|
-
|
|
5
|
-
interface CameraDevice {
|
|
6
|
-
readonly id: string
|
|
7
|
-
readonly name: string
|
|
8
|
-
readonly status: string
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface ProviderSectionProps {
|
|
12
|
-
readonly providerType: string
|
|
13
|
-
readonly providerName: string
|
|
14
|
-
readonly providerStatus: string
|
|
15
|
-
readonly devices: readonly CameraDevice[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function ProviderSection({ providerType, providerName, providerStatus, devices }: ProviderSectionProps) {
|
|
19
|
-
return (
|
|
20
|
-
<section className="space-y-3">
|
|
21
|
-
{/* Provider header */}
|
|
22
|
-
<div className="flex items-center gap-3">
|
|
23
|
-
<ProviderIcon type={providerType} size="md" />
|
|
24
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
25
|
-
<h2 className="text-sm font-semibold text-foreground truncate">
|
|
26
|
-
{providerName || getProviderLabel(providerType)}
|
|
27
|
-
</h2>
|
|
28
|
-
<span className="text-[10px] text-foreground-subtle flex-shrink-0">
|
|
29
|
-
{devices.length} camera{devices.length !== 1 ? 's' : ''}
|
|
30
|
-
</span>
|
|
31
|
-
</div>
|
|
32
|
-
<StatusBadge status={providerStatus} />
|
|
33
|
-
</div>
|
|
34
|
-
|
|
35
|
-
{/* Camera grid */}
|
|
36
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
|
37
|
-
{devices.map((device) => (
|
|
38
|
-
<CameraCard
|
|
39
|
-
key={device.id}
|
|
40
|
-
id={device.id}
|
|
41
|
-
name={device.name}
|
|
42
|
-
status={device.status}
|
|
43
|
-
detectionsToday={0}
|
|
44
|
-
inferenceMs={0}
|
|
45
|
-
/>
|
|
46
|
-
))}
|
|
47
|
-
</div>
|
|
48
|
-
</section>
|
|
49
|
-
)
|
|
50
|
-
}
|