@camstack/addon-admin-ui 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (127) hide show
  1. package/dist/assets/index-DjELGD4R.css +1 -0
  2. package/dist/assets/index-w55PwKyu.js +598 -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 +5 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -339
  10. package/src/components/addons/AddonUploadZone.tsx +0 -307
  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 -119
  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 -171
  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 -113
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -90
  69. package/src/components/metrics/PipelineStatus.tsx +0 -105
  70. package/src/components/metrics/ProcessResources.tsx +0 -139
  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 -238
  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 -224
  98. package/src/pages/Integrations.tsx +0 -330
  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 -525
  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 -210
  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,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
- }