@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.
Files changed (127) hide show
  1. package/dist/assets/index-BoVZEQ1j.js +598 -0
  2. package/dist/assets/index-DwSc8ann.css +1 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -108
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -172
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -254
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -222
  98. package/src/pages/Integrations.tsx +0 -333
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -396
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -28
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /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
- }