@camstack/addon-admin-ui 0.1.1 → 0.1.3

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