@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,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
|
-
}
|