@camstack/addon-admin-ui 0.1.1

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 (122) hide show
  1. package/index.html +22 -0
  2. package/package.json +69 -0
  3. package/public/brand/logo-dark.svg +16 -0
  4. package/public/brand/logo-horizontal-dark.svg +21 -0
  5. package/public/brand/logo-horizontal-light.svg +21 -0
  6. package/public/brand/logo-light.svg +16 -0
  7. package/public/brand/logo-wide-dark.svg +24 -0
  8. package/public/brand/logo-wide-light.svg +24 -0
  9. package/public/favicon.svg +8 -0
  10. package/public/vendor/react-jsx-runtime.mjs +24 -0
  11. package/public/vendor/react.mjs +16 -0
  12. package/src/App.tsx +71 -0
  13. package/src/components/addons/AddonCard.tsx +339 -0
  14. package/src/components/addons/AddonUploadZone.tsx +307 -0
  15. package/src/components/addons/CapabilityBadge.tsx +55 -0
  16. package/src/components/addons/CapabilityMap.tsx +133 -0
  17. package/src/components/addons/UpdatesList.tsx +119 -0
  18. package/src/components/agents/AgentCard.tsx +281 -0
  19. package/src/components/agents/AgentLogs.tsx +231 -0
  20. package/src/components/agents/ProcessList.tsx +127 -0
  21. package/src/components/agents/ProcessTree.tsx +369 -0
  22. package/src/components/agents/TaskList.tsx +68 -0
  23. package/src/components/cameras/CameraCard.tsx +60 -0
  24. package/src/components/cameras/LiveEventsPanel.tsx +91 -0
  25. package/src/components/cameras/ProviderSection.tsx +50 -0
  26. package/src/components/cameras/StreamArea.tsx +107 -0
  27. package/src/components/cameras/tabs/AddonsTab.tsx +113 -0
  28. package/src/components/cameras/tabs/CameraEventsTab.tsx +129 -0
  29. package/src/components/cameras/tabs/PipelineTab.tsx +118 -0
  30. package/src/components/cameras/tabs/StreamsTab.tsx +114 -0
  31. package/src/components/dashboard/BlockPicker.tsx +54 -0
  32. package/src/components/dashboard/BlockWrapper.tsx +97 -0
  33. package/src/components/dashboard/DashboardGrid.tsx +160 -0
  34. package/src/components/dashboard/block-registry.ts +15 -0
  35. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +39 -0
  36. package/src/components/dashboard/blocks/StorageBlock.tsx +66 -0
  37. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +67 -0
  38. package/src/components/dashboard/blocks/index.ts +32 -0
  39. package/src/components/device/DeviceHeader.tsx +116 -0
  40. package/src/components/device/FloatingPanel.tsx +132 -0
  41. package/src/components/device/FloatingPanelManager.tsx +167 -0
  42. package/src/components/device/PanelContent.tsx +196 -0
  43. package/src/components/device/QuickConfigWizard.tsx +507 -0
  44. package/src/components/device/tabs/DetectionConfigTab.tsx +96 -0
  45. package/src/components/device/tabs/EventsTab.tsx +19 -0
  46. package/src/components/device/tabs/LogsTab.tsx +22 -0
  47. package/src/components/device/tabs/OverviewTab.tsx +104 -0
  48. package/src/components/device/tabs/ProviderSettingsTab.tsx +34 -0
  49. package/src/components/device/tabs/RecordingTab.tsx +47 -0
  50. package/src/components/device/tabs/ReplTab.tsx +153 -0
  51. package/src/components/device/tabs/TrackTrailTab.tsx +49 -0
  52. package/src/components/device/tabs/ZonesTab.tsx +98 -0
  53. package/src/components/device/zone-editor/ZoneCanvas.tsx +354 -0
  54. package/src/components/device/zone-editor/ZoneForm.tsx +128 -0
  55. package/src/components/device/zone-editor/ZoneList.tsx +150 -0
  56. package/src/components/form-builder/FormBuilder.tsx +135 -0
  57. package/src/components/form-builder/FormField.tsx +732 -0
  58. package/src/components/form-builder/ModelSelector.tsx +239 -0
  59. package/src/components/integrations/AddDeviceDialog.tsx +205 -0
  60. package/src/components/integrations/CompactDeviceCard.tsx +35 -0
  61. package/src/components/integrations/DeviceCard.tsx +29 -0
  62. package/src/components/integrations/DeviceDiscoveryStep.tsx +105 -0
  63. package/src/components/integrations/DeviceGrid.tsx +79 -0
  64. package/src/components/integrations/DeviceGroupHeader.tsx +17 -0
  65. package/src/components/integrations/DiscoveredDeviceCard.tsx +26 -0
  66. package/src/components/integrations/IntegrationCard.tsx +40 -0
  67. package/src/components/integrations/IntegrationWizard.tsx +171 -0
  68. package/src/components/integrations/ProviderConfigForm.tsx +89 -0
  69. package/src/components/integrations/ProviderPicker.tsx +91 -0
  70. package/src/components/integrations/SnapshotPopover.tsx +68 -0
  71. package/src/components/metrics/AgentLoad.tsx +113 -0
  72. package/src/components/metrics/IntegrationUsage.tsx +90 -0
  73. package/src/components/metrics/PipelineStatus.tsx +105 -0
  74. package/src/components/metrics/ProcessResources.tsx +139 -0
  75. package/src/components/pipeline/PhaseSettings.tsx +131 -0
  76. package/src/components/shared/CapabilityBadges.tsx +30 -0
  77. package/src/components/shared/ProviderIcon.tsx +42 -0
  78. package/src/components/shared/StatusBadge.tsx +23 -0
  79. package/src/components/shared/WebRtcPlayer.tsx +211 -0
  80. package/src/components/timeline/EventMarker.tsx +32 -0
  81. package/src/components/timeline/TimelineBar.tsx +131 -0
  82. package/src/components/ui/ConfirmDialog.tsx +115 -0
  83. package/src/components/ui/ToastContainer.tsx +92 -0
  84. package/src/contexts/auth-context.tsx +91 -0
  85. package/src/hooks/useBackendClient.ts +6 -0
  86. package/src/hooks/useTheme.ts +1 -0
  87. package/src/i18n/en.json +164 -0
  88. package/src/i18n/index.ts +29 -0
  89. package/src/i18n/it.json +164 -0
  90. package/src/index.css +63 -0
  91. package/src/layouts/AddonPageLoader.tsx +120 -0
  92. package/src/layouts/AppLayout.tsx +238 -0
  93. package/src/layouts/ProtectedRoute.tsx +25 -0
  94. package/src/lib/addon-page-context.ts +29 -0
  95. package/src/lib/backend.ts +16 -0
  96. package/src/main.tsx +21 -0
  97. package/src/pages/AccessDenied.tsx +22 -0
  98. package/src/pages/Cameras.tsx +127 -0
  99. package/src/pages/Dashboard.tsx +6 -0
  100. package/src/pages/DeviceDetail.tsx +175 -0
  101. package/src/pages/IntegrationDetail.tsx +224 -0
  102. package/src/pages/Integrations.tsx +330 -0
  103. package/src/pages/Login.tsx +106 -0
  104. package/src/pages/Metrics.tsx +18 -0
  105. package/src/pages/PipelineConfig.tsx +282 -0
  106. package/src/pages/Showroom.tsx +351 -0
  107. package/src/pages/Timeline.tsx +269 -0
  108. package/src/pages/system/Addons.tsx +525 -0
  109. package/src/pages/system/Agents.tsx +362 -0
  110. package/src/pages/system/Logs.tsx +131 -0
  111. package/src/pages/system/Models.tsx +102 -0
  112. package/src/pages/system/Processes.tsx +129 -0
  113. package/src/pages/system/Repl.tsx +148 -0
  114. package/src/pages/system/Settings.tsx +168 -0
  115. package/src/pages/system/Users.tsx +174 -0
  116. package/src/server/addon.ts +54 -0
  117. package/src/types/config-ui.ts +210 -0
  118. package/src/types/dashboard.ts +39 -0
  119. package/tsconfig.json +29 -0
  120. package/tsconfig.server.json +16 -0
  121. package/tsup.config.ts +20 -0
  122. package/vite.config.ts +68 -0
@@ -0,0 +1,269 @@
1
+ import { useState, useMemo } from 'react'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { Film, Calendar, ChevronLeft, ChevronRight } from 'lucide-react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { useBackendClient } from '../hooks/useBackendClient'
6
+ import { TimelineBar, type RecordingSegment, type TimelineEvent } from '../components/timeline/TimelineBar'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Class → colour mapping
10
+ // ---------------------------------------------------------------------------
11
+ const CLASS_COLORS: Record<string, string> = {
12
+ person: '#f59e0b',
13
+ vehicle: '#3b82f6',
14
+ car: '#3b82f6',
15
+ face: '#a855f7',
16
+ plate: '#ec4899',
17
+ animal: '#10b981',
18
+ motion: '#ef4444',
19
+ unknown: '#6b7280',
20
+ }
21
+
22
+ function colorForClass(cls: string): string {
23
+ return CLASS_COLORS[cls.toLowerCase()] ?? CLASS_COLORS['unknown']!
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Date helpers
28
+ // ---------------------------------------------------------------------------
29
+ function startOfDay(date: Date): number {
30
+ const d = new Date(date)
31
+ d.setHours(0, 0, 0, 0)
32
+ return d.getTime()
33
+ }
34
+
35
+ function formatDateInput(ms: number): string {
36
+ return new Date(ms).toISOString().slice(0, 10)
37
+ }
38
+
39
+ function parseDateInput(value: string): number {
40
+ return new Date(value + 'T00:00:00').getTime()
41
+ }
42
+
43
+ // ---------------------------------------------------------------------------
44
+ // Placeholder segments (shown when no real API data exists)
45
+ // ---------------------------------------------------------------------------
46
+ function buildPlaceholderSegments(dayStartMs: number): readonly RecordingSegment[] {
47
+ return [
48
+ { startMs: dayStartMs + 0 * 3_600_000, endMs: dayStartMs + 4 * 3_600_000 },
49
+ { startMs: dayStartMs + 6 * 3_600_000, endMs: dayStartMs + 10 * 3_600_000 },
50
+ { startMs: dayStartMs + 12 * 3_600_000, endMs: dayStartMs + 16 * 3_600_000 },
51
+ { startMs: dayStartMs + 18 * 3_600_000, endMs: dayStartMs + 23 * 3_600_000 },
52
+ ]
53
+ }
54
+
55
+ function buildPlaceholderEvents(dayStartMs: number): readonly TimelineEvent[] {
56
+ const classes = ['person', 'vehicle', 'face', 'motion']
57
+ return Array.from({ length: 24 }, (_, i) => ({
58
+ id: `placeholder-${i}`,
59
+ timestampMs: dayStartMs + Math.floor(Math.random() * 24 * 3_600_000),
60
+ label: classes[i % classes.length]!,
61
+ color: colorForClass(classes[i % classes.length]!),
62
+ })).sort((a, b) => a.timestampMs - b.timestampMs)
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // Component
67
+ // ---------------------------------------------------------------------------
68
+ export function TimelinePage() {
69
+ const { t } = useTranslation()
70
+ const client = useBackendClient()
71
+
72
+ const today = startOfDay(new Date())
73
+ const [selectedDayMs, setSelectedDayMs] = useState(today)
74
+ const [selectedCameraId, setSelectedCameraId] = useState<string | null>(null)
75
+ const [playheadMs, setPlayheadMs] = useState(today + 12 * 3_600_000)
76
+
77
+ const dayStartMs = selectedDayMs
78
+ const dayEndMs = selectedDayMs + 24 * 3_600_000
79
+
80
+ // Fetch devices to populate camera tabs
81
+ const { data: devices, isLoading: devicesLoading } = useQuery({
82
+ queryKey: ['devices'],
83
+ queryFn: () => client.listDevices(),
84
+ staleTime: 30_000,
85
+ })
86
+
87
+ const deviceList = (devices ?? []) as unknown as Array<Record<string, unknown>>
88
+
89
+ const cameras = useMemo(() => {
90
+ const list = deviceList.filter((d) => {
91
+ const type = String(d.type ?? '').toLowerCase()
92
+ const caps = (d.capabilities ?? []) as string[]
93
+ return (
94
+ type === 'camera' ||
95
+ type.includes('camera') ||
96
+ caps.some((c) => c.toLowerCase().includes('video') || c.toLowerCase().includes('stream'))
97
+ )
98
+ })
99
+ return list.map((d) => ({ id: String(d.id), name: String(d.name ?? d.id) }))
100
+ }, [deviceList])
101
+
102
+ // Auto-select first camera
103
+ const activeCameraId = selectedCameraId ?? cameras[0]?.id ?? null
104
+
105
+ // Fetch real events for the selected camera on the selected day
106
+ const { data: rawEvents } = useQuery({
107
+ queryKey: ['events', activeCameraId, dayStartMs],
108
+ queryFn: (): Promise<unknown> =>
109
+ activeCameraId ? client.getEvents(activeCameraId, { limit: 200 }) : Promise.resolve(null),
110
+ enabled: !!activeCameraId,
111
+ staleTime: 60_000,
112
+ })
113
+
114
+ const timelineEvents: readonly TimelineEvent[] = useMemo(() => {
115
+ const raw = rawEvents as unknown
116
+ // getEvents returns { events: [...], total: n } or an array depending on router version
117
+ const evtsArr: unknown[] =
118
+ Array.isArray(raw)
119
+ ? (raw as unknown[])
120
+ : raw != null && typeof raw === 'object' && Array.isArray((raw as Record<string, unknown>)['events'])
121
+ ? ((raw as Record<string, unknown>)['events'] as unknown[])
122
+ : []
123
+ const evts = evtsArr as Array<Record<string, unknown>>
124
+ if (evts.length === 0) {
125
+ return activeCameraId ? buildPlaceholderEvents(dayStartMs) : []
126
+ }
127
+ return evts.map((ev, idx) => {
128
+ const cls = String((ev.class ?? ev.type ?? ev.label ?? 'unknown')).toLowerCase()
129
+ const ts = typeof ev.timestamp === 'number' ? ev.timestamp : dayStartMs
130
+ return {
131
+ id: String(ev.id ?? idx),
132
+ timestampMs: ts,
133
+ label: cls,
134
+ color: colorForClass(cls),
135
+ }
136
+ })
137
+ }, [rawEvents, activeCameraId, dayStartMs])
138
+
139
+ // Use placeholder segments (no real segment API exposed yet)
140
+ const segments: readonly RecordingSegment[] = useMemo(
141
+ () => (activeCameraId ? buildPlaceholderSegments(dayStartMs) : []),
142
+ [activeCameraId, dayStartMs],
143
+ )
144
+
145
+ function navigateDay(delta: number) {
146
+ setSelectedDayMs((prev) => prev + delta * 24 * 3_600_000)
147
+ setPlayheadMs((prev) => prev + delta * 24 * 3_600_000)
148
+ }
149
+
150
+ return (
151
+ <div className="flex flex-col h-full p-6 gap-5 overflow-hidden">
152
+ {/* ── Header ── */}
153
+ <div className="flex items-center justify-between gap-4 shrink-0">
154
+ <h1 className="text-lg font-semibold text-foreground">{t('nav.timeline', 'Timeline')}</h1>
155
+
156
+ {/* Date navigation */}
157
+ <div className="flex items-center gap-2">
158
+ <button
159
+ onClick={() => navigateDay(-1)}
160
+ className="p-1 rounded hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
161
+ >
162
+ <ChevronLeft className="h-4 w-4" />
163
+ </button>
164
+ <div className="relative flex items-center">
165
+ <Calendar className="absolute left-2 h-3.5 w-3.5 text-foreground-subtle pointer-events-none" />
166
+ <input
167
+ type="date"
168
+ value={formatDateInput(selectedDayMs)}
169
+ onChange={(e) => {
170
+ if (e.target.value) {
171
+ const ms = parseDateInput(e.target.value)
172
+ setSelectedDayMs(ms)
173
+ setPlayheadMs(ms + 12 * 3_600_000)
174
+ }
175
+ }}
176
+ className="rounded-lg border border-border bg-surface pl-7 pr-3 py-1.5 text-xs text-foreground focus:outline-none focus:border-primary/50"
177
+ />
178
+ </div>
179
+ <button
180
+ onClick={() => navigateDay(1)}
181
+ className="p-1 rounded hover:bg-surface-hover text-foreground-subtle hover:text-foreground transition-colors"
182
+ disabled={selectedDayMs >= today}
183
+ >
184
+ <ChevronRight className="h-4 w-4" />
185
+ </button>
186
+ </div>
187
+ </div>
188
+
189
+ {/* ── Camera selector tabs ── */}
190
+ <div className="flex gap-1.5 shrink-0 overflow-x-auto pb-1">
191
+ {devicesLoading && (
192
+ <div className="h-8 w-48 rounded-lg bg-surface animate-pulse" />
193
+ )}
194
+ {!devicesLoading && cameras.length === 0 && (
195
+ <span className="text-xs text-foreground-subtle">No cameras found</span>
196
+ )}
197
+ {cameras.map((cam) => {
198
+ const active = cam.id === activeCameraId
199
+ return (
200
+ <button
201
+ key={cam.id}
202
+ onClick={() => setSelectedCameraId(cam.id)}
203
+ className={`shrink-0 rounded-lg px-3 py-1.5 text-[12px] font-medium transition-all border ${
204
+ active
205
+ ? 'bg-primary/12 text-primary border-primary/30'
206
+ : 'bg-surface text-foreground-subtle border-border hover:bg-surface-hover hover:text-foreground'
207
+ }`}
208
+ >
209
+ {cam.name}
210
+ </button>
211
+ )
212
+ })}
213
+ </div>
214
+
215
+ {/* ── Video player placeholder ── */}
216
+ <div className="relative rounded-xl border border-border bg-black shrink-0 overflow-hidden" style={{ aspectRatio: '16/9', maxHeight: '45vh' }}>
217
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-3 text-foreground-subtle">
218
+ <Film className="h-12 w-12 opacity-20" />
219
+ <p className="text-sm opacity-50">
220
+ {activeCameraId
221
+ ? `${cameras.find((c) => c.id === activeCameraId)?.name ?? activeCameraId} — seek to play`
222
+ : 'Select a camera'}
223
+ </p>
224
+ </div>
225
+
226
+ {/* Playhead timestamp overlay */}
227
+ <div className="absolute top-3 left-3 rounded bg-black/60 px-2 py-1 text-[11px] text-white font-mono">
228
+ {new Date(playheadMs).toLocaleTimeString()}
229
+ </div>
230
+ </div>
231
+
232
+ {/* ── Timeline scrub bar ── */}
233
+ <div className="rounded-xl border border-border bg-surface px-4 pt-3 pb-4 shrink-0">
234
+ {/* Legend */}
235
+ <div className="flex items-center gap-4 mb-3">
236
+ <span className="text-[10px] font-semibold text-foreground-subtle uppercase tracking-wider">Timeline</span>
237
+ <div className="flex items-center gap-1.5">
238
+ <span className="inline-block h-2 w-4 rounded-sm bg-success/60" />
239
+ <span className="text-[10px] text-foreground-subtle">Recording</span>
240
+ </div>
241
+ {Array.from(new Set(timelineEvents.map((e) => e.label))).slice(0, 5).map((cls) => (
242
+ <div key={cls} className="flex items-center gap-1.5">
243
+ <span
244
+ className="inline-block h-2.5 w-2.5 rounded-full"
245
+ style={{ backgroundColor: colorForClass(cls) }}
246
+ />
247
+ <span className="text-[10px] text-foreground-subtle capitalize">{cls}</span>
248
+ </div>
249
+ ))}
250
+ </div>
251
+
252
+ {activeCameraId ? (
253
+ <TimelineBar
254
+ startMs={dayStartMs}
255
+ endMs={dayEndMs}
256
+ segments={segments}
257
+ events={timelineEvents}
258
+ playheadMs={playheadMs}
259
+ onSeek={setPlayheadMs}
260
+ />
261
+ ) : (
262
+ <div className="h-8 rounded bg-surface-hover flex items-center justify-center text-xs text-foreground-subtle">
263
+ Select a camera to view timeline
264
+ </div>
265
+ )}
266
+ </div>
267
+ </div>
268
+ )
269
+ }