@camstack/addon-admin-ui 0.1.1 → 0.1.2

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.
@@ -1,30 +1,6 @@
1
1
  import { useQuery } from '@tanstack/react-query'
2
2
  import { useBackendClient } from '../../hooks/useBackendClient'
3
3
 
4
- interface PipelineEntry {
5
- deviceId: string
6
- status: {
7
- active: boolean
8
- decode: {
9
- fps: number
10
- droppedFrames: number
11
- totalFrames: number
12
- }
13
- videoConsumers: number
14
- audioConsumers: number
15
- uptime: number
16
- source: {
17
- protocol: string
18
- connected: boolean
19
- } | null
20
- outputs: Array<{
21
- format: string
22
- consumers: number
23
- active: boolean
24
- }>
25
- }
26
- }
27
-
28
4
  export function PipelineStatus() {
29
5
  const client = useBackendClient()
30
6
 
@@ -34,9 +10,8 @@ export function PipelineStatus() {
34
10
  refetchInterval: 5_000,
35
11
  })
36
12
 
37
- const pipelines = (data ?? []) as unknown as PipelineEntry[]
38
- const activePipelines = pipelines.filter((p) => p.status.active)
39
- const totalFps = activePipelines.reduce((sum, p) => sum + (p.status.decode?.fps ?? 0), 0)
13
+ const pipelines = data ?? []
14
+ const withSlot = pipelines.filter((p) => p.slot != null)
40
15
 
41
16
  return (
42
17
  <div className="rounded-lg border border-border bg-surface p-4">
@@ -61,42 +36,36 @@ export function PipelineStatus() {
61
36
  {/* Aggregate */}
62
37
  <div className="flex items-center gap-3 mb-3 pb-3 border-b border-border">
63
38
  <span className="text-xs text-foreground-subtle">
64
- <span className="text-foreground font-semibold">{activePipelines.length}</span> active
39
+ <span className="text-foreground font-semibold">{withSlot.length}</span> active
65
40
  {' '}/ <span className="text-foreground font-semibold">{pipelines.length}</span> total
66
41
  </span>
67
- <span className="text-xs text-foreground-subtle">
68
- <span className="text-foreground font-semibold tabular-nums">
69
- {totalFps.toFixed(1)}
70
- </span>{' '}
71
- total FPS
72
- </span>
73
42
  </div>
74
43
 
75
44
  {/* Pipeline list */}
76
45
  <div className="space-y-1.5">
77
- {pipelines.map((p) => (
78
- <div
79
- key={p.deviceId}
80
- className="flex items-center justify-between gap-2 text-xs"
81
- >
82
- <div className="flex items-center gap-2 min-w-0">
83
- <span
84
- className={`h-2 w-2 flex-shrink-0 rounded-full ${
85
- p.status.active ? 'bg-success' : 'bg-foreground-subtle/40'
86
- }`}
87
- />
88
- <span className="text-foreground truncate font-medium">{p.deviceId}</span>
89
- </div>
90
- <div className="flex items-center gap-3 flex-shrink-0">
91
- <span className="text-foreground-subtle">
92
- {p.status.active ? 'running' : 'idle'}
93
- </span>
94
- <span className="tabular-nums text-foreground-subtle w-14 text-right">
95
- {p.status.active ? `${(p.status.decode?.fps ?? 0).toFixed(1)} fps` : '—'}
96
- </span>
46
+ {pipelines.map((p) => {
47
+ const isActive = p.slot != null
48
+ return (
49
+ <div
50
+ key={p.id}
51
+ className="flex items-center justify-between gap-2 text-xs"
52
+ >
53
+ <div className="flex items-center gap-2 min-w-0">
54
+ <span
55
+ className={`h-2 w-2 flex-shrink-0 rounded-full ${
56
+ isActive ? 'bg-success' : 'bg-foreground-subtle/40'
57
+ }`}
58
+ />
59
+ <span className="text-foreground truncate font-medium">{p.id}</span>
60
+ </div>
61
+ <div className="flex items-center gap-3 flex-shrink-0">
62
+ <span className="text-foreground-subtle">
63
+ {isActive ? p.slot! : 'idle'}
64
+ </span>
65
+ </div>
97
66
  </div>
98
- </div>
99
- ))}
67
+ )
68
+ })}
100
69
  </div>
101
70
  </>
102
71
  )}
@@ -1,24 +1,8 @@
1
1
  import { useQuery } from '@tanstack/react-query'
2
2
  import { useBackendClient } from '../../hooks/useBackendClient'
3
+ import type { BackendClient } from '@camstack/sdk'
3
4
 
4
- interface ProcessStats {
5
- pid: number
6
- cpu: number
7
- memory: number
8
- uptime: number
9
- restartCount: number
10
- }
11
-
12
- interface ProcessEntry {
13
- id: string
14
- label: string
15
- state: string
16
- pid?: number
17
- stats?: ProcessStats
18
- restartCount: number
19
- lastCrashAt?: number
20
- lastCrashError?: string
21
- }
5
+ type ProcessEntry = Awaited<ReturnType<BackendClient['listProcesses']>>[number]
22
6
 
23
7
  function formatMemoryMB(bytes?: number): string {
24
8
  if (bytes == null) return '—'
@@ -56,7 +40,7 @@ export function ProcessResources() {
56
40
  refetchInterval: 5_000,
57
41
  })
58
42
 
59
- const processes = (data ?? []) as ProcessEntry[]
43
+ const processes = data ?? []
60
44
  const runningCount = processes.filter((p) => p.state === 'running').length
61
45
 
62
46
  const totalCpu = processes.reduce((sum, p) => sum + (p.stats?.cpu ?? 0), 0)
@@ -37,6 +37,16 @@ export function AppLayout() {
37
37
  const { t, i18n } = useTranslation()
38
38
  const client = getBackendClient()
39
39
 
40
+ const { data: updateCount = 0 } = useQuery({
41
+ queryKey: ['updates', 'count'],
42
+ queryFn: async () => {
43
+ const updates = await client.trpc.update.listUpdates.query()
44
+ return updates.length
45
+ },
46
+ staleTime: 30_000,
47
+ refetchInterval: 30_000,
48
+ })
49
+
40
50
  const { data: addonPages } = useQuery({
41
51
  queryKey: ['addon-pages'],
42
52
  queryFn: async (): Promise<readonly AddonPageInfo[]> => {
@@ -148,6 +158,7 @@ export function AppLayout() {
148
158
  {SYSTEM_ITEMS.map((item) => {
149
159
  const Icon = item.icon
150
160
  const active = isActive(item.path)
161
+ const isAddons = item.path === '/system/addons'
151
162
  return (
152
163
  <button
153
164
  key={item.path}
@@ -159,7 +170,12 @@ export function AppLayout() {
159
170
  }`}
160
171
  >
161
172
  <Icon className={`h-3.5 w-3.5 shrink-0 ${active ? 'text-primary' : ''}`} />
162
- {item.label}
173
+ <span className="flex-1 text-left">{item.label}</span>
174
+ {isAddons && updateCount > 0 && (
175
+ <span className="ml-auto rounded-full bg-primary px-1.5 py-0.5 text-[9px] font-bold text-primary-foreground leading-none">
176
+ {updateCount}
177
+ </span>
178
+ )}
163
179
  </button>
164
180
  )
165
181
  })}
@@ -22,15 +22,15 @@ export function CamerasPage() {
22
22
  queryFn: () => client.listProviders(),
23
23
  })
24
24
 
25
- const deviceList = (devices ?? []) as unknown as Array<Record<string, unknown>>
26
- const providerList = (providers ?? []) as unknown as Array<Record<string, unknown>>
25
+ const deviceList = devices ?? []
26
+ const providerList = providers ?? []
27
27
 
28
28
  // Filter camera-type devices only and apply search
29
29
  const cameraDevices = useMemo(() => {
30
30
  const searchLower = search.toLowerCase()
31
31
  return deviceList.filter((d) => {
32
- const type = String(d.type ?? '').toLowerCase()
33
- const capabilities = (d.capabilities ?? []) as string[]
32
+ const type = (d.type ?? '').toLowerCase()
33
+ const capabilities = d.capabilities ?? []
34
34
  const isCamera =
35
35
  type === 'camera' ||
36
36
  type.includes('camera') ||
@@ -42,16 +42,16 @@ export function CamerasPage() {
42
42
  if (!include) return false
43
43
  if (!searchLower) return true
44
44
 
45
- const name = String(d.name ?? d.id ?? '').toLowerCase()
45
+ const name = (d.name ?? d.id ?? '').toLowerCase()
46
46
  return name.includes(searchLower)
47
47
  })
48
48
  }, [deviceList, search])
49
49
 
50
50
  // Group by provider
51
51
  const groupedByProvider = useMemo(() => {
52
- const groups = new Map<string, Array<Record<string, unknown>>>()
52
+ const groups = new Map<string, typeof deviceList>()
53
53
  for (const device of cameraDevices) {
54
- const providerId = String(device.providerId ?? '')
54
+ const providerId = device.providerId ?? ''
55
55
  if (!groups.has(providerId)) {
56
56
  groups.set(providerId, [])
57
57
  }
@@ -107,17 +107,17 @@ export function CamerasPage() {
107
107
 
108
108
  {/* Provider sections */}
109
109
  {!isLoading && Array.from(groupedByProvider.entries()).map(([providerId, providerDevices]) => {
110
- const provider = providerList.find((p) => String(p.id) === providerId)
110
+ const provider = providerList.find((p) => p.id === providerId)
111
111
  return (
112
112
  <ProviderSection
113
113
  key={providerId}
114
- providerType={String(provider?.type ?? 'rtsp')}
115
- providerName={String(provider?.name ?? providerId)}
116
- providerStatus={String(provider?.status ?? 'stopped')}
114
+ providerType={provider?.type ?? 'rtsp'}
115
+ providerName={provider?.name ?? providerId}
116
+ providerStatus={provider?.status?.connected ? 'running' : 'stopped'}
117
117
  devices={providerDevices.map((d) => ({
118
- id: String(d.id),
119
- name: String(d.name ?? d.id),
120
- status: String(d.status ?? 'offline'),
118
+ id: d.id,
119
+ name: d.name ?? d.id,
120
+ status: d.state?.online ? 'online' : 'offline',
121
121
  }))}
122
122
  />
123
123
  )
@@ -84,24 +84,22 @@ export function IntegrationDetailPage() {
84
84
  )
85
85
  }
86
86
 
87
- const devices = ((allDevices ?? []) as unknown as Array<Record<string, unknown>>)
88
- .filter((d) => String(d.providerId) === integrationId)
87
+ const devices = (allDevices ?? [])
88
+ .filter((d) => d.providerId === integrationId)
89
89
 
90
90
  const activeDevices = devices.map((d) => ({
91
- id: String(d.id),
92
- name: String(d.name ?? d.id),
93
- status: String(d.status ?? 'offline'),
94
- snapshotUrl: d.snapshotUrl ? String(d.snapshotUrl) : null,
91
+ id: d.id,
92
+ name: d.name ?? d.id,
93
+ status: d.state?.online ? 'online' : 'offline',
94
+ snapshotUrl: null as string | null,
95
95
  }))
96
96
 
97
- const discoveredDevices = (discovered ?? []).map((d: any) => ({
98
- externalId: String(d.externalId ?? d.id),
99
- name: String(d.name ?? d.externalId ?? d.id),
97
+ const discoveredDevices = (discovered ?? []).map((d) => ({
98
+ externalId: d.externalId,
99
+ name: d.name ?? d.externalId,
100
100
  }))
101
101
 
102
- const providerStatus = typeof provider.status === 'object'
103
- ? (provider.status as any).connected ? 'running' : 'stopped'
104
- : String(provider.status ?? 'stopped')
102
+ const providerStatus = provider.status.connected ? 'running' : 'stopped'
105
103
 
106
104
  const isAutoDiscovery = provider.discoveryMode !== 'manual'
107
105
  const isManual = provider.discoveryMode === 'manual'
@@ -30,8 +30,11 @@ const TYPE_FILTERS: Array<{ id: DeviceTypeFilter; label: string; icon: React.Com
30
30
  { id: 'other', label: 'Other', icon: HelpCircle },
31
31
  ]
32
32
 
33
- function getDeviceType(device: Record<string, unknown>): string {
34
- const type = String(device.type ?? '').toLowerCase()
33
+ type DeviceEntry = Awaited<ReturnType<ReturnType<typeof useBackendClient>['listDevices']>>[number]
34
+ type ProviderEntry = Awaited<ReturnType<ReturnType<typeof useBackendClient>['listProviders']>>[number]
35
+
36
+ function getDeviceType(device: DeviceEntry): string {
37
+ const type = (device.type ?? '').toLowerCase()
35
38
  if (type.includes('camera') || type === 'camera') return 'camera'
36
39
  if (type.includes('sensor')) return 'sensor'
37
40
  if (type.includes('light')) return 'light'
@@ -73,8 +76,8 @@ export function IntegrationsPage() {
73
76
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
74
77
  })
75
78
 
76
- const providerList = (providers ?? []) as unknown as Array<Record<string, unknown>>
77
- const deviceList = (devices ?? []) as unknown as Array<Record<string, unknown>>
79
+ const providerList = providers ?? []
80
+ const deviceList = devices ?? []
78
81
 
79
82
  // Filter devices
80
83
  const filteredDevices = useMemo(() => {
@@ -82,7 +85,7 @@ export function IntegrationsPage() {
82
85
  return deviceList.filter((d) => {
83
86
  if (typeFilter !== 'all' && getDeviceType(d) !== typeFilter) return false
84
87
  if (searchLower) {
85
- const name = String(d.name ?? d.id ?? '').toLowerCase()
88
+ const name = (d.name ?? d.id ?? '').toLowerCase()
86
89
  if (!name.includes(searchLower)) return false
87
90
  }
88
91
  return true
@@ -91,18 +94,18 @@ export function IntegrationsPage() {
91
94
 
92
95
  // Group devices
93
96
  const grouped = useMemo(() => {
94
- const groups = new Map<string, Array<Record<string, unknown>>>()
97
+ const groups = new Map<string, DeviceEntry[]>()
95
98
  for (const device of filteredDevices) {
96
99
  let key: string
97
100
  switch (groupBy) {
98
101
  case 'provider':
99
- key = String(device.providerId ?? 'unknown')
102
+ key = device.providerId ?? 'unknown'
100
103
  break
101
104
  case 'type':
102
105
  key = getDeviceType(device)
103
106
  break
104
107
  case 'status':
105
- key = String(device.status ?? 'offline')
108
+ key = device.state?.online ? 'online' : 'offline'
106
109
  break
107
110
  }
108
111
  if (!groups.has(key)) {
@@ -127,24 +130,24 @@ export function IntegrationsPage() {
127
130
 
128
131
  function getGroupLabel(key: string): string {
129
132
  if (groupBy === 'provider') {
130
- const provider = providerList.find((p) => String(p.id) === key)
131
- return provider ? String(provider.name ?? key) : key
133
+ const provider = providerList.find((p) => p.id === key)
134
+ return provider ? (provider.name ?? key) : key
132
135
  }
133
136
  return key.charAt(0).toUpperCase() + key.slice(1)
134
137
  }
135
138
 
136
139
  function getGroupProviderType(key: string): string {
137
140
  if (groupBy === 'provider') {
138
- const provider = providerList.find((p) => String(p.id) === key)
139
- return String(provider?.type ?? 'rtsp')
141
+ const provider = providerList.find((p) => p.id === key)
142
+ return provider?.type ?? 'rtsp'
140
143
  }
141
144
  return 'rtsp'
142
145
  }
143
146
 
144
147
  function getGroupProviderStatus(key: string): string {
145
148
  if (groupBy === 'provider') {
146
- const provider = providerList.find((p) => String(p.id) === key)
147
- return String(provider?.status ?? 'stopped')
149
+ const provider = providerList.find((p) => p.id === key)
150
+ return provider?.status?.connected ? 'running' : 'stopped'
148
151
  }
149
152
  return ''
150
153
  }
@@ -297,11 +300,11 @@ export function IntegrationsPage() {
297
300
 
298
301
  {/* Device rows */}
299
302
  {groupDevices.map((device) => {
300
- const deviceId = String(device.id)
301
- const deviceName = String(device.name ?? device.id)
303
+ const deviceId = device.id
304
+ const deviceName = device.name ?? device.id
302
305
  const deviceType = getDeviceType(device)
303
- const deviceStatus = String(device.status ?? 'offline')
304
- const phase = String(device.phase ?? '—')
306
+ const deviceStatus = device.state?.online ? 'online' : 'offline'
307
+ const phase = (device as Record<string, unknown>).phase as string ?? '—'
305
308
 
306
309
  return (
307
310
  <button