@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,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
- }
@@ -1,107 +0,0 @@
1
- import { useState } from 'react'
2
- import { Camera, Eye, Activity, Crosshair, HardDrive } from 'lucide-react'
3
-
4
- const STREAM_SOURCES = ['WebRTC', 'HLS', 'MJPEG', 'go2rtc'] as const
5
- const OVERLAY_OPTIONS = ['BBox', 'Segm', 'Zones', 'Trail'] as const
6
-
7
- interface StreamAreaProps {
8
- readonly deviceName: string
9
- readonly detectionsToday: number
10
- readonly inferenceMs: number
11
- readonly activeTracks: number
12
- readonly storageGb: number
13
- }
14
-
15
- export function StreamArea({ deviceName, detectionsToday, inferenceMs, activeTracks, storageGb }: StreamAreaProps) {
16
- const [activeSource, setActiveSource] = useState<string>('WebRTC')
17
- const [activeOverlays, setActiveOverlays] = useState<ReadonlySet<string>>(new Set())
18
-
19
- function toggleOverlay(overlay: string) {
20
- setActiveOverlays((prev) => {
21
- const next = new Set(prev)
22
- if (next.has(overlay)) {
23
- next.delete(overlay)
24
- } else {
25
- next.add(overlay)
26
- }
27
- return next
28
- })
29
- }
30
-
31
- return (
32
- <div className="space-y-3">
33
- {/* Stream preview */}
34
- <div className="relative w-full max-w-[320px] aspect-video bg-background rounded-lg border border-border flex items-center justify-center overflow-hidden">
35
- <Camera className="h-10 w-10 text-foreground-subtle/20" />
36
- <div className="absolute bottom-2 left-2 text-[10px] text-foreground-subtle bg-background/80 rounded px-1.5 py-0.5">
37
- {deviceName}
38
- </div>
39
- </div>
40
-
41
- {/* Source selector chips */}
42
- <div className="flex flex-wrap gap-1.5">
43
- <span className="text-[10px] text-foreground-subtle mr-1 self-center">Source:</span>
44
- {STREAM_SOURCES.map((source) => (
45
- <button
46
- key={source}
47
- onClick={() => setActiveSource(source)}
48
- className={`rounded-full px-2.5 py-1 text-[10px] font-medium transition-colors ${
49
- activeSource === source
50
- ? 'bg-primary/15 text-primary border border-primary/30'
51
- : 'bg-surface border border-border text-foreground-subtle hover:text-foreground'
52
- }`}
53
- >
54
- {source}
55
- </button>
56
- ))}
57
- </div>
58
-
59
- {/* Overlay toggle chips */}
60
- <div className="flex flex-wrap gap-1.5">
61
- <span className="text-[10px] text-foreground-subtle mr-1 self-center">Overlay:</span>
62
- {OVERLAY_OPTIONS.map((overlay) => {
63
- const isActive = activeOverlays.has(overlay)
64
- return (
65
- <button
66
- key={overlay}
67
- onClick={() => toggleOverlay(overlay)}
68
- className={`rounded-full px-2.5 py-1 text-[10px] font-medium transition-colors ${
69
- isActive
70
- ? 'bg-info/15 text-info border border-info/30'
71
- : 'bg-surface border border-border text-foreground-subtle hover:text-foreground'
72
- }`}
73
- >
74
- {overlay}
75
- </button>
76
- )
77
- })}
78
- </div>
79
-
80
- {/* Quick stats */}
81
- <div className="grid grid-cols-4 gap-2">
82
- <StatCounter icon={Eye} label="Today" value={String(detectionsToday)} />
83
- <StatCounter icon={Activity} label="Inference" value={`${inferenceMs}ms`} />
84
- <StatCounter icon={Crosshair} label="Tracks" value={String(activeTracks)} />
85
- <StatCounter icon={HardDrive} label="Storage" value={`${storageGb.toFixed(1)}GB`} />
86
- </div>
87
- </div>
88
- )
89
- }
90
-
91
- function StatCounter({
92
- icon: Icon,
93
- label,
94
- value,
95
- }: {
96
- readonly icon: React.ComponentType<{ className?: string }>
97
- readonly label: string
98
- readonly value: string
99
- }) {
100
- return (
101
- <div className="flex flex-col items-center gap-1 rounded-lg border border-border bg-surface px-2 py-2">
102
- <Icon className="h-3.5 w-3.5 text-foreground-subtle" />
103
- <span className="text-xs font-semibold text-foreground">{value}</span>
104
- <span className="text-[9px] text-foreground-subtle">{label}</span>
105
- </div>
106
- )
107
- }
@@ -1,113 +0,0 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import { Settings, Package } from 'lucide-react'
3
- import { useBackendClient } from '../../../hooks/useBackendClient'
4
-
5
- interface AddonsTabProps {
6
- readonly deviceId: string
7
- }
8
-
9
- interface RawAddon {
10
- readonly id: string
11
- readonly packageName: string
12
- readonly slot: string | null
13
- }
14
-
15
- interface AddonDisplay {
16
- readonly id: string
17
- readonly name: string
18
- readonly type: string
19
- readonly enabled: boolean
20
- readonly version?: string
21
- }
22
-
23
- function toAddonDisplay(raw: RawAddon): AddonDisplay {
24
- return {
25
- id: raw.id,
26
- name: raw.packageName ?? raw.id,
27
- type: raw.slot ?? 'addon',
28
- enabled: true,
29
- }
30
- }
31
-
32
- export function AddonsTab({ deviceId: _deviceId }: AddonsTabProps) {
33
- const client = useBackendClient()
34
-
35
- const { data: addonsData, isLoading } = useQuery({
36
- queryKey: ['bridge-addons'],
37
- queryFn: () => client.bridgeListAddons(),
38
- })
39
-
40
- const rawAddons = (addonsData ?? []) as unknown as readonly RawAddon[]
41
- const addons = rawAddons.map(toAddonDisplay)
42
- const visionAddons = addons.filter(
43
- (a) => a.type === 'vision' || a.type === 'detection' || a.type === 'inference'
44
- )
45
-
46
- if (isLoading) {
47
- return (
48
- <div className="space-y-2">
49
- {[1, 2, 3].map((i) => (
50
- <div key={i} className="h-16 rounded-lg border border-border bg-surface animate-pulse" />
51
- ))}
52
- </div>
53
- )
54
- }
55
-
56
- if (!visionAddons.length && !addons.length) {
57
- return (
58
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
59
- <div className="border-b border-border px-4 py-2.5">
60
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Vision Addons</h2>
61
- </div>
62
- <div className="flex flex-col items-center justify-center py-16 text-foreground-subtle">
63
- <Package className="h-8 w-8 mb-3 opacity-30" />
64
- <p className="text-sm">No vision addons installed</p>
65
- <p className="text-xs mt-1 opacity-70">Install addons to enable detection and inference</p>
66
- </div>
67
- </div>
68
- )
69
- }
70
-
71
- const displayAddons: readonly AddonDisplay[] = visionAddons.length > 0 ? visionAddons : addons
72
-
73
- return (
74
- <div className="space-y-4">
75
- <div className="flex items-center justify-between">
76
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Vision Addons</h2>
77
- <span className="text-[10px] text-foreground-subtle">
78
- {displayAddons.length} addon{displayAddons.length !== 1 ? 's' : ''}
79
- </span>
80
- </div>
81
-
82
- <div className="space-y-2">
83
- {displayAddons.map((addon) => (
84
- <div
85
- key={addon.id}
86
- className="flex items-center gap-3 rounded-lg border border-border bg-surface px-4 py-3"
87
- >
88
- <div className="flex items-center justify-center rounded-lg h-8 w-8 bg-primary/10 flex-shrink-0">
89
- <Package className="h-4 w-4 text-primary" />
90
- </div>
91
- <div className="flex-1 min-w-0">
92
- <p className="text-xs font-medium text-foreground truncate">{addon.name || addon.id}</p>
93
- <p className="text-[10px] text-foreground-subtle">
94
- {addon.type} {addon.version ? `v${addon.version}` : ''}
95
- </p>
96
- </div>
97
- <div
98
- className={`h-2 w-2 rounded-full flex-shrink-0 ${
99
- addon.enabled !== false ? 'bg-success' : 'bg-foreground-subtle/30'
100
- }`}
101
- />
102
- <button
103
- className="p-1.5 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
104
- title="Settings"
105
- >
106
- <Settings className="h-3.5 w-3.5" />
107
- </button>
108
- </div>
109
- ))}
110
- </div>
111
- </div>
112
- )
113
- }
@@ -1,129 +0,0 @@
1
- import { useQuery } from '@tanstack/react-query'
2
- import { Bell, User, Car, Dog, Mic } from 'lucide-react'
3
- import { useBackendClient } from '../../../hooks/useBackendClient'
4
-
5
- interface CameraEventsTabProps {
6
- readonly deviceId: string
7
- }
8
-
9
- type EventCategory = 'person' | 'vehicle' | 'animal' | 'audio' | 'other'
10
-
11
- const CATEGORY_CONFIG: Record<EventCategory, { color: string; borderColor: string; icon: React.ComponentType<{ className?: string }> }> = {
12
- person: { color: 'text-success', borderColor: 'border-l-success', icon: User },
13
- vehicle: { color: 'text-info', borderColor: 'border-l-info', icon: Car },
14
- animal: { color: 'text-warning', borderColor: 'border-l-warning', icon: Dog },
15
- audio: { color: 'text-danger', borderColor: 'border-l-danger', icon: Mic },
16
- other: { color: 'text-foreground-subtle', borderColor: 'border-l-foreground-subtle', icon: Bell },
17
- }
18
-
19
- function classifyEvent(event: Record<string, unknown>): EventCategory {
20
- const label = String(event.label ?? event.type ?? '').toLowerCase()
21
- if (label.includes('person') || label.includes('face')) return 'person'
22
- if (label.includes('vehicle') || label.includes('car') || label.includes('truck')) return 'vehicle'
23
- if (label.includes('animal') || label.includes('dog') || label.includes('cat')) return 'animal'
24
- if (label.includes('audio') || label.includes('sound')) return 'audio'
25
- return 'other'
26
- }
27
-
28
- export function CameraEventsTab({ deviceId }: CameraEventsTabProps) {
29
- const client = useBackendClient()
30
-
31
- const { data: eventsData, isLoading } = useQuery({
32
- queryKey: ['events', deviceId],
33
- queryFn: () => client.getEvents(deviceId, { limit: 50 }),
34
- enabled: !!deviceId,
35
- refetchInterval: 5_000,
36
- })
37
-
38
- const events = (eventsData ?? []) as readonly Record<string, unknown>[]
39
-
40
- if (isLoading) {
41
- return (
42
- <div className="space-y-2">
43
- {[1, 2, 3, 4].map((i) => (
44
- <div key={i} className="h-16 rounded-lg border border-border bg-surface animate-pulse" />
45
- ))}
46
- </div>
47
- )
48
- }
49
-
50
- return (
51
- <div className="space-y-4">
52
- <div className="flex items-center justify-between">
53
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Detection Events</h2>
54
- <span className="text-[10px] text-foreground-subtle">
55
- {events.length} event{events.length !== 1 ? 's' : ''}
56
- </span>
57
- </div>
58
-
59
- {/* Category legend */}
60
- <div className="flex flex-wrap gap-3">
61
- {(Object.entries(CATEGORY_CONFIG) as Array<[EventCategory, typeof CATEGORY_CONFIG[EventCategory]]>).map(
62
- ([cat, cfg]) => {
63
- const Icon = cfg.icon
64
- return (
65
- <span key={cat} className={`inline-flex items-center gap-1 text-[10px] ${cfg.color}`}>
66
- <Icon className="h-3 w-3" />
67
- {cat}
68
- </span>
69
- )
70
- }
71
- )}
72
- </div>
73
-
74
- {events.length === 0 ? (
75
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
76
- <div className="flex flex-col items-center justify-center py-16 text-foreground-subtle">
77
- <Bell className="h-8 w-8 mb-3 opacity-30" />
78
- <p className="text-sm">No events yet</p>
79
- <p className="text-xs mt-1 opacity-70">Detection events will appear here as they occur</p>
80
- </div>
81
- </div>
82
- ) : (
83
- <div className="space-y-1.5">
84
- {events.map((event, index) => {
85
- const category = classifyEvent(event)
86
- const cfg = CATEGORY_CONFIG[category]
87
- const Icon = cfg.icon
88
- const label = String(event.label ?? event.type ?? 'Unknown')
89
- const score = event.score != null ? Number(event.score) : null
90
- const zone = event.zone ? String(event.zone) : null
91
- const trackId = event.trackId ? String(event.trackId) : null
92
- const timestamp = event.timestamp
93
- ? new Date(event.timestamp as string | number).toLocaleTimeString('en-GB', { hour12: false })
94
- : ''
95
-
96
- return (
97
- <div
98
- key={event.id ? String(event.id) : index}
99
- className={`flex items-start gap-3 rounded-lg border border-border border-l-4 ${cfg.borderColor} bg-surface px-3 py-2.5 hover:bg-surface-hover transition-colors`}
100
- >
101
- {/* Crop thumbnail placeholder */}
102
- <div className="h-10 w-10 rounded bg-background flex items-center justify-center flex-shrink-0">
103
- <Icon className={`h-4 w-4 ${cfg.color}`} />
104
- </div>
105
-
106
- <div className="flex-1 min-w-0">
107
- <div className="flex items-center gap-2">
108
- <span className="text-xs font-medium text-foreground capitalize">{label}</span>
109
- {score != null && (
110
- <span className="text-[9px] bg-surface-hover rounded px-1.5 py-0.5 text-foreground-subtle">
111
- {(score * 100).toFixed(0)}%
112
- </span>
113
- )}
114
- </div>
115
- <div className="flex items-center gap-2 mt-0.5 text-[10px] text-foreground-subtle">
116
- {trackId && <span>Track: {trackId}</span>}
117
- {zone && <span>Zone: {zone}</span>}
118
- </div>
119
- </div>
120
-
121
- <span className="text-[10px] text-foreground-subtle flex-shrink-0">{timestamp}</span>
122
- </div>
123
- )
124
- })}
125
- </div>
126
- )}
127
- </div>
128
- )
129
- }
@@ -1,118 +0,0 @@
1
- import { useState } from 'react'
2
- import { useQuery } from '@tanstack/react-query'
3
- import { ChevronDown, ChevronRight, GitBranch } from 'lucide-react'
4
- import { useBackendClient } from '../../../hooks/useBackendClient'
5
-
6
- interface PipelineTabProps {
7
- readonly deviceId: string
8
- }
9
-
10
- interface PipelineStep {
11
- readonly id: string
12
- readonly name: string
13
- readonly type: string
14
- readonly enabled: boolean
15
- readonly config: Record<string, unknown>
16
- }
17
-
18
- export function PipelineTab({ deviceId }: PipelineTabProps) {
19
- const client = useBackendClient()
20
- const [expandedSteps, setExpandedSteps] = useState<ReadonlySet<string>>(new Set())
21
-
22
- const { data: pipelineData, isLoading } = useQuery({
23
- queryKey: ['pipeline', deviceId],
24
- queryFn: () => client.bridgeGetPipeline(deviceId),
25
- enabled: !!deviceId,
26
- })
27
-
28
- const pipeline = pipelineData as Record<string, unknown> | null | undefined
29
- const videoSteps = (pipeline?.video ?? []) as readonly PipelineStep[]
30
-
31
- function toggleStep(stepId: string) {
32
- setExpandedSteps((prev) => {
33
- const next = new Set(prev)
34
- if (next.has(stepId)) {
35
- next.delete(stepId)
36
- } else {
37
- next.add(stepId)
38
- }
39
- return next
40
- })
41
- }
42
-
43
- if (isLoading) {
44
- return (
45
- <div className="space-y-3">
46
- {[1, 2, 3].map((i) => (
47
- <div key={i} className="h-14 rounded-lg border border-border bg-surface animate-pulse" />
48
- ))}
49
- </div>
50
- )
51
- }
52
-
53
- if (!videoSteps.length) {
54
- return (
55
- <div className="rounded-lg border border-border bg-surface overflow-hidden">
56
- <div className="border-b border-border px-4 py-2.5">
57
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Pipeline Config</h2>
58
- </div>
59
- <div className="flex flex-col items-center justify-center py-16 text-foreground-subtle">
60
- <GitBranch className="h-8 w-8 mb-3 opacity-30" />
61
- <p className="text-sm">No pipeline configured</p>
62
- <p className="text-xs mt-1 opacity-70">Pipeline steps will appear here once configured</p>
63
- </div>
64
- </div>
65
- )
66
- }
67
-
68
- return (
69
- <div className="space-y-2">
70
- <div className="flex items-center justify-between mb-3">
71
- <h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Pipeline Steps</h2>
72
- <span className="text-[10px] text-foreground-subtle">
73
- {videoSteps.length} step{videoSteps.length !== 1 ? 's' : ''} — read-only
74
- </span>
75
- </div>
76
-
77
- {videoSteps.map((step, index) => {
78
- const isExpanded = expandedSteps.has(step.id ?? String(index))
79
- const stepId = step.id ?? String(index)
80
-
81
- return (
82
- <div key={stepId} className="rounded-lg border border-border bg-surface overflow-hidden">
83
- <button
84
- onClick={() => toggleStep(stepId)}
85
- className="w-full flex items-center gap-3 px-4 py-3 text-left hover:bg-surface-hover transition-colors"
86
- >
87
- {isExpanded ? (
88
- <ChevronDown className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
89
- ) : (
90
- <ChevronRight className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
91
- )}
92
- <span className="text-[10px] text-foreground-subtle w-5">{index + 1}</span>
93
- <span className="text-xs font-medium text-foreground flex-1 truncate">
94
- {step.name || step.type || `Step ${index + 1}`}
95
- </span>
96
- <span className="text-[10px] text-foreground-subtle bg-surface-hover rounded px-1.5 py-0.5">
97
- {step.type || 'unknown'}
98
- </span>
99
- <div
100
- className={`h-2 w-2 rounded-full flex-shrink-0 ${
101
- step.enabled !== false ? 'bg-success' : 'bg-foreground-subtle/30'
102
- }`}
103
- />
104
- </button>
105
-
106
- {isExpanded && (
107
- <div className="border-t border-border px-4 py-3 bg-background">
108
- <pre className="text-[11px] font-mono text-foreground-subtle whitespace-pre-wrap break-all">
109
- {JSON.stringify(step.config ?? step, null, 2)}
110
- </pre>
111
- </div>
112
- )}
113
- </div>
114
- )
115
- })}
116
- </div>
117
- )
118
- }