@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.
- package/dist/assets/index-DjELGD4R.css +1 -0
- package/dist/assets/index-w55PwKyu.js +598 -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 +5 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -339
- package/src/components/addons/AddonUploadZone.tsx +0 -307
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -119
- 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 -171
- 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 -113
- package/src/components/metrics/IntegrationUsage.tsx +0 -90
- package/src/components/metrics/PipelineStatus.tsx +0 -105
- package/src/components/metrics/ProcessResources.tsx +0 -139
- 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 -238
- 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 -224
- package/src/pages/Integrations.tsx +0 -330
- 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 -525
- 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 -210
- 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,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
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { Radio, Copy, Check } from 'lucide-react'
|
|
3
|
-
import { useState } from 'react'
|
|
4
|
-
import { useBackendClient } from '../../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
interface StreamsTabProps {
|
|
7
|
-
readonly deviceId: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function StreamsTab({ deviceId }: StreamsTabProps) {
|
|
11
|
-
const client = useBackendClient()
|
|
12
|
-
const [copiedUrl, setCopiedUrl] = useState<string | null>(null)
|
|
13
|
-
|
|
14
|
-
const { data: deviceData } = useQuery({
|
|
15
|
-
queryKey: ['device', deviceId],
|
|
16
|
-
queryFn: () => client.getDevice(deviceId),
|
|
17
|
-
enabled: !!deviceId,
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
const device = (deviceData ?? {}) as Record<string, unknown>
|
|
21
|
-
const serverUrl = (client as unknown as { serverUrl?: string }).serverUrl ?? window.location.origin
|
|
22
|
-
|
|
23
|
-
// Build stream URLs from known patterns
|
|
24
|
-
const streams = [
|
|
25
|
-
{
|
|
26
|
-
id: 'webrtc',
|
|
27
|
-
label: 'WebRTC',
|
|
28
|
-
resolution: 'Native',
|
|
29
|
-
codec: 'H.264',
|
|
30
|
-
url: `${serverUrl}/webrtc/${deviceId}`,
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
id: 'hls',
|
|
34
|
-
label: 'HLS',
|
|
35
|
-
resolution: 'Native',
|
|
36
|
-
codec: 'H.264',
|
|
37
|
-
url: `${serverUrl}/hls/${deviceId}/index.m3u8`,
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: 'mjpeg',
|
|
41
|
-
label: 'MJPEG',
|
|
42
|
-
resolution: 'Scaled',
|
|
43
|
-
codec: 'MJPEG',
|
|
44
|
-
url: `${serverUrl}/mjpeg/${deviceId}`,
|
|
45
|
-
},
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
async function handleCopy(url: string) {
|
|
49
|
-
try {
|
|
50
|
-
await navigator.clipboard.writeText(url)
|
|
51
|
-
setCopiedUrl(url)
|
|
52
|
-
setTimeout(() => setCopiedUrl(null), 2000)
|
|
53
|
-
} catch {
|
|
54
|
-
// clipboard not available
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<div className="space-y-4">
|
|
60
|
-
{/* Available streams */}
|
|
61
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
62
|
-
<div className="border-b border-border px-4 py-2.5 flex items-center justify-between">
|
|
63
|
-
<h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Available Streams</h2>
|
|
64
|
-
<span className="text-[10px] text-foreground-subtle">{streams.length} streams</span>
|
|
65
|
-
</div>
|
|
66
|
-
<div className="divide-y divide-border">
|
|
67
|
-
{streams.map((stream) => (
|
|
68
|
-
<div key={stream.id} className="flex items-center gap-3 px-4 py-3">
|
|
69
|
-
<div className="flex items-center justify-center rounded-lg h-8 w-8 bg-info/10 flex-shrink-0">
|
|
70
|
-
<Radio className="h-4 w-4 text-info" />
|
|
71
|
-
</div>
|
|
72
|
-
<div className="flex-1 min-w-0">
|
|
73
|
-
<div className="flex items-center gap-2">
|
|
74
|
-
<p className="text-xs font-medium text-foreground">{stream.label}</p>
|
|
75
|
-
<span className="text-[9px] bg-surface-hover rounded px-1.5 py-0.5 text-foreground-subtle">
|
|
76
|
-
{stream.resolution}
|
|
77
|
-
</span>
|
|
78
|
-
<span className="text-[9px] bg-surface-hover rounded px-1.5 py-0.5 text-foreground-subtle">
|
|
79
|
-
{stream.codec}
|
|
80
|
-
</span>
|
|
81
|
-
</div>
|
|
82
|
-
<p className="text-[10px] font-mono text-foreground-subtle mt-0.5 truncate">{stream.url}</p>
|
|
83
|
-
</div>
|
|
84
|
-
<button
|
|
85
|
-
onClick={() => handleCopy(stream.url)}
|
|
86
|
-
className="p-1.5 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors flex-shrink-0"
|
|
87
|
-
title="Copy URL"
|
|
88
|
-
>
|
|
89
|
-
{copiedUrl === stream.url ? (
|
|
90
|
-
<Check className="h-3.5 w-3.5 text-success" />
|
|
91
|
-
) : (
|
|
92
|
-
<Copy className="h-3.5 w-3.5" />
|
|
93
|
-
)}
|
|
94
|
-
</button>
|
|
95
|
-
</div>
|
|
96
|
-
))}
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
99
|
-
|
|
100
|
-
{/* Restreamer URLs */}
|
|
101
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
102
|
-
<div className="border-b border-border px-4 py-2.5">
|
|
103
|
-
<h2 className="text-xs font-semibold text-foreground uppercase tracking-wider">Restreamer</h2>
|
|
104
|
-
</div>
|
|
105
|
-
<div className="px-4 py-8 flex flex-col items-center text-center text-foreground-subtle">
|
|
106
|
-
<p className="text-sm">Restreamer URLs</p>
|
|
107
|
-
<p className="text-xs mt-1 opacity-70">
|
|
108
|
-
Restreamer endpoints will be displayed here when a restreamer is configured
|
|
109
|
-
</p>
|
|
110
|
-
</div>
|
|
111
|
-
</div>
|
|
112
|
-
</div>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { X } from 'lucide-react'
|
|
2
|
-
import { getAllBlocks } from './block-registry'
|
|
3
|
-
import type { DashboardBlock } from '../../types/dashboard'
|
|
4
|
-
|
|
5
|
-
interface BlockPickerProps {
|
|
6
|
-
open: boolean
|
|
7
|
-
onClose: () => void
|
|
8
|
-
onSelect: (block: DashboardBlock) => void
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function BlockPicker({ open, onClose, onSelect }: BlockPickerProps) {
|
|
12
|
-
if (!open) return null
|
|
13
|
-
|
|
14
|
-
const blocks = getAllBlocks()
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm" onClick={onClose}>
|
|
18
|
-
<div
|
|
19
|
-
className="w-full max-w-md rounded-xl border border-border bg-surface p-5 shadow-2xl shadow-black/20"
|
|
20
|
-
onClick={(e) => e.stopPropagation()}
|
|
21
|
-
>
|
|
22
|
-
<div className="flex items-center justify-between mb-4">
|
|
23
|
-
<h2 className="text-sm font-semibold text-foreground">Add Block</h2>
|
|
24
|
-
<button
|
|
25
|
-
onClick={onClose}
|
|
26
|
-
className="p-1 rounded-md hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
|
|
27
|
-
>
|
|
28
|
-
<X className="h-4 w-4" />
|
|
29
|
-
</button>
|
|
30
|
-
</div>
|
|
31
|
-
<div className="grid grid-cols-2 gap-2">
|
|
32
|
-
{blocks.map((block) => {
|
|
33
|
-
const Icon = block.icon
|
|
34
|
-
return (
|
|
35
|
-
<button
|
|
36
|
-
key={block.id}
|
|
37
|
-
onClick={() => { onSelect(block); onClose() }}
|
|
38
|
-
className="flex items-center gap-3 rounded-lg border border-border p-3 text-left hover:bg-surface-hover hover:border-primary/30 transition-all group"
|
|
39
|
-
>
|
|
40
|
-
<div className="flex h-8 w-8 items-center justify-center rounded-md bg-primary/10 group-hover:bg-primary/15 transition-colors shrink-0">
|
|
41
|
-
<Icon className="h-4 w-4 text-primary" />
|
|
42
|
-
</div>
|
|
43
|
-
<div>
|
|
44
|
-
<span className="text-xs font-medium text-foreground block">{block.name}</span>
|
|
45
|
-
<span className="text-[10px] text-foreground-subtle">{block.defaultSize.w}x{block.defaultSize.h}</span>
|
|
46
|
-
</div>
|
|
47
|
-
</button>
|
|
48
|
-
)
|
|
49
|
-
})}
|
|
50
|
-
</div>
|
|
51
|
-
</div>
|
|
52
|
-
</div>
|
|
53
|
-
)
|
|
54
|
-
}
|