@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,231 @@
1
+ import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
2
+ import { useQuery } from '@tanstack/react-query'
3
+ import { ArrowDownToLine, Filter } from 'lucide-react'
4
+ import { useBackendClient } from '../../hooks/useBackendClient'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Types
8
+ // ---------------------------------------------------------------------------
9
+
10
+ type LogLevel = 'all' | 'debug' | 'info' | 'warn' | 'error'
11
+
12
+ interface AgentLogsProps {
13
+ /** List of agent names/IDs for the filter dropdown */
14
+ agentNames: readonly string[]
15
+ /** List of addon names for the filter dropdown */
16
+ addonNames: readonly string[]
17
+ /** Pre-selected agent name from clicking an agent in the tree */
18
+ preselectedAgent?: string
19
+ }
20
+
21
+ // ---------------------------------------------------------------------------
22
+ // Styles
23
+ // ---------------------------------------------------------------------------
24
+
25
+ const LEVEL_STYLES: Record<string, { badge: string; text: string }> = {
26
+ debug: { badge: 'bg-foreground-subtle/10 text-foreground-subtle', text: 'text-foreground-subtle' },
27
+ info: { badge: 'bg-info/10 text-info', text: 'text-info' },
28
+ warn: { badge: 'bg-warning/10 text-warning', text: 'text-warning' },
29
+ error: { badge: 'bg-danger/10 text-danger', text: 'text-danger' },
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Component
34
+ // ---------------------------------------------------------------------------
35
+
36
+ export function AgentLogs({ agentNames, addonNames, preselectedAgent }: AgentLogsProps) {
37
+ const client = useBackendClient()
38
+ const [levelFilter, setLevelFilter] = useState<LogLevel>('all')
39
+ const [agentFilter, setAgentFilter] = useState<string>(preselectedAgent ?? 'all')
40
+ const [addonFilter, setAddonFilter] = useState<string>('all')
41
+
42
+ // Sync agentFilter when preselectedAgent changes externally
43
+ const prevPreselected = useRef(preselectedAgent)
44
+ useEffect(() => {
45
+ if (preselectedAgent !== prevPreselected.current) {
46
+ setAgentFilter(preselectedAgent ?? 'all')
47
+ prevPreselected.current = preselectedAgent
48
+ }
49
+ }, [preselectedAgent])
50
+ const [autoScroll, setAutoScroll] = useState(true)
51
+ const scrollContainerRef = useRef<HTMLDivElement>(null)
52
+ // bottomRef removed — auto-scroll goes to top (newest first)
53
+
54
+ // Build scope filters from agent + addon selections
55
+ const scopeFilter = useMemo(() => {
56
+ const scopes: string[] = []
57
+ if (agentFilter !== 'all') scopes.push(agentFilter)
58
+ if (addonFilter !== 'all') scopes.push(addonFilter)
59
+ return scopes.length > 0 ? scopes : undefined
60
+ }, [agentFilter, addonFilter])
61
+
62
+ // Always fetch live -- no pause concept
63
+ const { data: logsData, isLoading, isError } = useQuery({
64
+ queryKey: ['agent-logs', levelFilter, agentFilter, addonFilter],
65
+ queryFn: () =>
66
+ client.getLogs({
67
+ ...(levelFilter !== 'all' ? { level: levelFilter as 'debug' | 'info' | 'warn' | 'error' } : {}),
68
+ limit: 300,
69
+ ...(scopeFilter ? { scope: scopeFilter } : {}),
70
+ } as Parameters<typeof client.getLogs>[0]),
71
+ refetchInterval: 3000,
72
+ })
73
+
74
+ const rawLogs = (logsData ?? []) as unknown as Array<Record<string, unknown>>
75
+
76
+ // Newest first — most recent logs at the top
77
+ const logs = useMemo(() => [...rawLogs].reverse(), [rawLogs])
78
+
79
+ // Auto-scroll to top when new logs arrive (newest is at top)
80
+ useEffect(() => {
81
+ if (autoScroll) {
82
+ scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
83
+ }
84
+ }, [logs, autoScroll])
85
+
86
+ // Detect user scroll to auto-disable auto-scroll when scrolling down
87
+ const handleScroll = useCallback(() => {
88
+ const container = scrollContainerRef.current
89
+ if (!container) return
90
+ const isAtTop = container.scrollTop < 40
91
+ setAutoScroll(isAtTop)
92
+ }, [])
93
+
94
+ function formatTs(ts: unknown): string {
95
+ if (!ts) return '--'
96
+ const d = new Date(typeof ts === 'number' ? ts : String(ts))
97
+ return d.toLocaleTimeString('en-GB', {
98
+ hour12: false,
99
+ hour: '2-digit',
100
+ minute: '2-digit',
101
+ second: '2-digit',
102
+ fractionalSecondDigits: 3,
103
+ })
104
+ }
105
+
106
+ return (
107
+ <div className="space-y-3">
108
+ {/* Filters bar */}
109
+ <div className="flex items-center gap-2 flex-wrap">
110
+ <Filter className="h-3.5 w-3.5 text-foreground-subtle shrink-0" />
111
+
112
+ <select
113
+ value={agentFilter}
114
+ onChange={(e) => setAgentFilter(e.target.value)}
115
+ className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
116
+ >
117
+ <option value="all">All agents</option>
118
+ <option value="Hub">Hub</option>
119
+ {agentNames.map((name) => (
120
+ <option key={name} value={name}>{name}</option>
121
+ ))}
122
+ </select>
123
+
124
+ <select
125
+ value={addonFilter}
126
+ onChange={(e) => setAddonFilter(e.target.value)}
127
+ className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
128
+ >
129
+ <option value="all">All addons</option>
130
+ {addonNames.map((name) => (
131
+ <option key={name} value={name}>{name}</option>
132
+ ))}
133
+ </select>
134
+
135
+ <select
136
+ value={levelFilter}
137
+ onChange={(e) => setLevelFilter(e.target.value as LogLevel)}
138
+ className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
139
+ >
140
+ <option value="all">All levels</option>
141
+ <option value="debug">Debug</option>
142
+ <option value="info">Info</option>
143
+ <option value="warn">Warn</option>
144
+ <option value="error">Error</option>
145
+ </select>
146
+
147
+ <button
148
+ type="button"
149
+ onClick={() => {
150
+ const next = !autoScroll
151
+ setAutoScroll(next)
152
+ if (next) {
153
+ scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
154
+ }
155
+ }}
156
+ className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors ${
157
+ autoScroll
158
+ ? 'border-primary/30 bg-primary/10 text-primary'
159
+ : 'border-border bg-surface text-foreground-subtle hover:text-foreground'
160
+ }`}
161
+ >
162
+ <ArrowDownToLine className="h-3.5 w-3.5" />
163
+ Auto-scroll: {autoScroll ? 'ON' : 'OFF'}
164
+ </button>
165
+ </div>
166
+
167
+ {/* Log display */}
168
+ {isLoading && (
169
+ <div className="text-xs text-foreground-subtle animate-pulse">Loading logs...</div>
170
+ )}
171
+
172
+ {isError && (
173
+ <div className="text-xs text-danger">Failed to load logs</div>
174
+ )}
175
+
176
+ {!isLoading && !isError && logs.length === 0 && (
177
+ <div className="text-xs text-foreground-subtle italic">No logs match the current filters</div>
178
+ )}
179
+
180
+ {logs.length > 0 && (
181
+ <div className="rounded-lg border border-border bg-surface overflow-hidden">
182
+ <div
183
+ ref={scrollContainerRef}
184
+ onScroll={handleScroll}
185
+ className="max-h-[400px] overflow-y-auto"
186
+ >
187
+ <table className="w-full text-[10px]">
188
+ <thead className="sticky top-0">
189
+ <tr>
190
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-24">Time</th>
191
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-14">Level</th>
192
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-28">Scope</th>
193
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border">Message</th>
194
+ </tr>
195
+ </thead>
196
+ <tbody>
197
+ {logs.map((entry, i) => {
198
+ const level = String(entry.level ?? 'info').toLowerCase()
199
+ const style = LEVEL_STYLES[level] ?? LEVEL_STYLES['info']!
200
+ const message = String(entry.message ?? entry.msg ?? entry.text ?? '')
201
+ const scope = Array.isArray(entry.scope)
202
+ ? (entry.scope as string[]).join(' > ')
203
+ : String(entry.scope ?? '')
204
+ return (
205
+ <tr key={i} className="hover:bg-primary/5">
206
+ <td className="px-2.5 py-1 text-foreground-subtle border-b border-border font-mono whitespace-nowrap">
207
+ {formatTs(entry.timestamp ?? entry.ts ?? entry.time)}
208
+ </td>
209
+ <td className="px-2.5 py-1 border-b border-border">
210
+ <span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-medium uppercase ${style.badge}`}>
211
+ {level}
212
+ </span>
213
+ </td>
214
+ <td className="px-2.5 py-1 border-b border-border text-foreground-subtle font-mono truncate max-w-[180px]" title={scope}>
215
+ {scope || '--'}
216
+ </td>
217
+ <td className={`px-2.5 py-1 border-b border-border font-mono ${style.text} break-all`}>
218
+ {message}
219
+ </td>
220
+ </tr>
221
+ )
222
+ })}
223
+ </tbody>
224
+ </table>
225
+ {/* newest-first: no bottom anchor needed */}
226
+ </div>
227
+ </div>
228
+ )}
229
+ </div>
230
+ )
231
+ }
@@ -0,0 +1,127 @@
1
+ import { useState } from 'react'
2
+ import { RotateCcw } from 'lucide-react'
3
+ import { useMutation, useQueryClient } from '@tanstack/react-query'
4
+ import { useBackendClient } from '../../hooks/useBackendClient'
5
+
6
+ export interface ProcessEntry {
7
+ id: string
8
+ name: string
9
+ pid?: number
10
+ status: string
11
+ cpu?: number
12
+ memoryMb?: number
13
+ uptime?: number
14
+ restarts?: number
15
+ }
16
+
17
+ function StatusDot({ status }: { status: string }) {
18
+ const color =
19
+ status === 'running'
20
+ ? 'bg-green-400'
21
+ : status === 'error'
22
+ ? 'bg-red-400'
23
+ : 'bg-foreground-subtle'
24
+ return <span className={`inline-block h-2 w-2 rounded-full shrink-0 ${color}`} title={status} />
25
+ }
26
+
27
+ function formatUptime(seconds: number): string {
28
+ if (seconds < 60) return `${seconds}s`
29
+ if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`
30
+ const h = Math.floor(seconds / 3600)
31
+ const m = Math.floor((seconds % 3600) / 60)
32
+ return `${h}h ${m}m`
33
+ }
34
+
35
+ interface ProcessListProps {
36
+ processes: ProcessEntry[]
37
+ }
38
+
39
+ export function ProcessList({ processes }: ProcessListProps) {
40
+ const client = useBackendClient()
41
+ const queryClient = useQueryClient()
42
+ const [confirmingId, setConfirmingId] = useState<string | null>(null)
43
+
44
+ const restartMutation = useMutation({
45
+ mutationFn: (id: string) => client.trpc.processes.restartProcess.mutate({ id }),
46
+ onSuccess: () => {
47
+ void queryClient.invalidateQueries({ queryKey: ['processes'] })
48
+ setConfirmingId(null)
49
+ },
50
+ })
51
+
52
+ if (processes.length === 0) {
53
+ return <div className="text-[10px] text-foreground-subtle italic">No processes</div>
54
+ }
55
+
56
+ return (
57
+ <div className="rounded-md border border-border overflow-hidden">
58
+ <table className="w-full text-[10px]">
59
+ <thead>
60
+ <tr>
61
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border w-4" />
62
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">Name</th>
63
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">PID</th>
64
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">CPU%</th>
65
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">RSS</th>
66
+ <th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-background border-b border-border">Uptime</th>
67
+ <th className="px-2.5 py-1.5 bg-background border-b border-border" />
68
+ </tr>
69
+ </thead>
70
+ <tbody>
71
+ {processes.map((proc) => {
72
+ const isConfirming = confirmingId === proc.id
73
+ return (
74
+ <tr key={proc.id} className="hover:bg-primary/5">
75
+ <td className="px-2.5 py-1.5 border-b border-border">
76
+ <StatusDot status={proc.status} />
77
+ </td>
78
+ <td className="px-2.5 py-1.5 border-b border-border font-mono text-foreground">
79
+ {proc.name}
80
+ </td>
81
+ <td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
82
+ {proc.pid ?? '—'}
83
+ </td>
84
+ <td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
85
+ {proc.cpu != null ? `${proc.cpu.toFixed(1)}%` : '—'}
86
+ </td>
87
+ <td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
88
+ {proc.memoryMb != null ? `${proc.memoryMb.toFixed(0)} MB` : '—'}
89
+ </td>
90
+ <td className="px-2.5 py-1.5 border-b border-border text-foreground-subtle">
91
+ {proc.uptime != null ? formatUptime(proc.uptime) : '—'}
92
+ </td>
93
+ <td className="px-2.5 py-1.5 border-b border-border">
94
+ {isConfirming ? (
95
+ <div className="flex items-center gap-1">
96
+ <button
97
+ onClick={() => restartMutation.mutate(proc.id)}
98
+ disabled={restartMutation.isPending}
99
+ className="rounded px-1.5 py-0.5 bg-danger/10 text-danger hover:bg-danger/20"
100
+ >
101
+ Confirm
102
+ </button>
103
+ <button
104
+ onClick={() => setConfirmingId(null)}
105
+ className="rounded px-1.5 py-0.5 bg-surface text-foreground-subtle hover:text-foreground border border-border"
106
+ >
107
+ Cancel
108
+ </button>
109
+ </div>
110
+ ) : (
111
+ <button
112
+ onClick={() => setConfirmingId(proc.id)}
113
+ className="inline-flex items-center gap-1 rounded px-1.5 py-0.5 bg-primary/10 text-primary hover:bg-primary/20"
114
+ >
115
+ <RotateCcw className="h-2.5 w-2.5" />
116
+ Restart
117
+ </button>
118
+ )}
119
+ </td>
120
+ </tr>
121
+ )
122
+ })}
123
+ </tbody>
124
+ </table>
125
+ </div>
126
+ )
127
+ }