@camstack/addon-admin-ui 0.1.2 → 0.1.4
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-BoVZEQ1j.js +598 -0
- package/dist/assets/index-DwSc8ann.css +1 -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,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
|
-
}
|
|
@@ -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
|
-
}
|