@camstack/addon-admin-ui 0.1.2 → 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 +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  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 -108
  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 -172
  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 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  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 -254
  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 -222
  98. package/src/pages/Integrations.tsx +0 -333
  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 -396
  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 -28
  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,222 +0,0 @@
1
- import { useState } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
- import { useParams } from 'react-router-dom'
4
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
5
- import { RefreshCw, Settings, Plus, Play, Square } from 'lucide-react'
6
- import { useBackendClient } from '../hooks/useBackendClient'
7
- import { StatusBadge } from '../components/shared/StatusBadge'
8
- import { DeviceGrid } from '../components/integrations/DeviceGrid'
9
- import { AddDeviceDialog } from '../components/integrations/AddDeviceDialog'
10
-
11
- export function IntegrationDetailPage() {
12
- const { t } = useTranslation()
13
- const { integrationId } = useParams<{ integrationId: string }>()
14
- const client = useBackendClient()
15
- const queryClient = useQueryClient()
16
- const [showAddDevice, setShowAddDevice] = useState(false)
17
- const [showConfig, setShowConfig] = useState(false)
18
-
19
- const { data: provider, isLoading: providerLoading } = useQuery({
20
- queryKey: ['provider', integrationId],
21
- queryFn: () => client.getProvider(integrationId!),
22
- enabled: !!integrationId,
23
- })
24
-
25
- const { data: allDevices } = useQuery({
26
- queryKey: ['devices'],
27
- queryFn: () => client.listDevices(),
28
- refetchInterval: 5_000,
29
- })
30
-
31
- const { data: discovered, refetch: refetchDiscovery } = useQuery({
32
- queryKey: ['discover-devices', integrationId],
33
- queryFn: () => client.discoverDevices(integrationId!),
34
- enabled: !!integrationId && provider?.discoveryMode !== 'manual',
35
- })
36
-
37
- const startMutation = useMutation({
38
- mutationFn: () => client.startProvider(integrationId!),
39
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['provider', integrationId] }),
40
- })
41
-
42
- const stopMutation = useMutation({
43
- mutationFn: () => client.stopProvider(integrationId!),
44
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['provider', integrationId] }),
45
- })
46
-
47
- const adoptMutation = useMutation({
48
- mutationFn: (externalId: string) => client.adoptDevice(integrationId!, externalId),
49
- onSuccess: () => {
50
- queryClient.invalidateQueries({ queryKey: ['devices'] })
51
- queryClient.invalidateQueries({ queryKey: ['discover-devices', integrationId] })
52
- },
53
- })
54
-
55
- const [importingIds, setImportingIds] = useState<Set<string>>(new Set())
56
-
57
- async function handleImport(externalId: string) {
58
- setImportingIds((prev) => new Set([...prev, externalId]))
59
- try {
60
- await adoptMutation.mutateAsync(externalId)
61
- } finally {
62
- setImportingIds((prev) => {
63
- const next = new Set(prev)
64
- next.delete(externalId)
65
- return next
66
- })
67
- }
68
- }
69
-
70
- if (providerLoading) {
71
- return (
72
- <div className="p-6">
73
- <div className="h-16 rounded-lg bg-surface animate-pulse mb-4" />
74
- <div className="h-40 rounded-lg bg-surface animate-pulse" />
75
- </div>
76
- )
77
- }
78
-
79
- if (!provider) {
80
- return (
81
- <div className="flex items-center justify-center py-20 text-foreground-subtle text-sm">
82
- {t('integrations.integrationNotFound')}
83
- </div>
84
- )
85
- }
86
-
87
- const devices = (allDevices ?? [])
88
- .filter((d) => d.providerId === integrationId)
89
-
90
- const activeDevices = devices.map((d) => ({
91
- id: d.id,
92
- name: d.name ?? d.id,
93
- status: d.state?.online ? 'online' : 'offline',
94
- snapshotUrl: null as string | null,
95
- }))
96
-
97
- const discoveredDevices = (discovered ?? []).map((d) => ({
98
- externalId: d.externalId,
99
- name: d.name ?? d.externalId,
100
- }))
101
-
102
- const providerStatus = provider.status.connected ? 'running' : 'stopped'
103
-
104
- const isAutoDiscovery = provider.discoveryMode !== 'manual'
105
- const isManual = provider.discoveryMode === 'manual'
106
-
107
- return (
108
- <div className="p-6 space-y-5">
109
- {/* Header */}
110
- <div className="flex items-center justify-between">
111
- <div className="flex items-center gap-3">
112
- <img
113
- src={client.getAddonAssetUrl(provider.type, 'assets/icon.svg')}
114
- alt=""
115
- className="h-8 w-8 rounded-lg"
116
- onError={(e) => { (e.target as HTMLImageElement).style.display = 'none' }}
117
- />
118
- <div>
119
- <h1 className="text-lg font-semibold text-foreground">{provider.name}</h1>
120
- <p className="text-[11px] text-foreground-subtle mt-0.5">
121
- {provider.type} · {activeDevices.length} {t('integrations.devices')}
122
- </p>
123
- </div>
124
- <StatusBadge status={providerStatus} />
125
- </div>
126
- <div className="flex items-center gap-2">
127
- {isAutoDiscovery && (
128
- <button
129
- onClick={() => refetchDiscovery()}
130
- className="flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-[11px] text-foreground hover:bg-surface-hover transition-colors"
131
- >
132
- <RefreshCw className="h-3 w-3" />
133
- {t('integrations.rediscover')}
134
- </button>
135
- )}
136
- {/* TODO: open provider config dialog */}
137
- <button
138
- onClick={() => setShowConfig(true)}
139
- className="flex items-center gap-1.5 rounded-lg border border-border bg-surface px-3 py-1.5 text-[11px] text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
140
- >
141
- <Settings className="h-3 w-3" />
142
- {t('integrations.config')}
143
- </button>
144
- {providerStatus === 'running' ? (
145
- <button
146
- onClick={() => stopMutation.mutate()}
147
- className="rounded-lg border border-border bg-surface p-1.5 text-foreground-subtle hover:text-danger hover:bg-danger/10 transition-colors"
148
- title="Stop"
149
- >
150
- <Square className="h-3.5 w-3.5" />
151
- </button>
152
- ) : (
153
- <button
154
- onClick={() => startMutation.mutate()}
155
- className="rounded-lg border border-border bg-surface p-1.5 text-foreground-subtle hover:text-success hover:bg-success/10 transition-colors"
156
- title="Start"
157
- >
158
- <Play className="h-3.5 w-3.5" />
159
- </button>
160
- )}
161
- </div>
162
- </div>
163
-
164
- {/* Device grid */}
165
- <DeviceGrid
166
- activeDevices={activeDevices}
167
- discoveredDevices={isAutoDiscovery ? discoveredDevices : undefined}
168
- onImport={handleImport}
169
- importingIds={importingIds}
170
- />
171
-
172
- {/* FAB */}
173
- <button
174
- onClick={() => {
175
- if (isManual) {
176
- setShowAddDevice(true)
177
- } else {
178
- refetchDiscovery()
179
- }
180
- }}
181
- className="fixed bottom-6 right-6 flex h-12 w-12 items-center justify-center rounded-full bg-primary shadow-lg shadow-primary/30 hover:shadow-primary/40 transition-shadow"
182
- title={isManual ? t('integrations.addDevice') : t('integrations.rediscover')}
183
- >
184
- {isManual ? (
185
- <Plus className="h-5 w-5 text-primary-foreground" />
186
- ) : (
187
- <RefreshCw className="h-5 w-5 text-primary-foreground" />
188
- )}
189
- </button>
190
-
191
- {showAddDevice && integrationId && (
192
- <AddDeviceDialog
193
- open={showAddDevice}
194
- providerId={integrationId}
195
- onClose={() => setShowAddDevice(false)}
196
- />
197
- )}
198
-
199
- {/* TODO: replace with a real FormBuilder config dialog */}
200
- {showConfig && (
201
- <div
202
- className="fixed inset-0 z-50 flex items-center justify-center bg-black/50"
203
- onClick={() => setShowConfig(false)}
204
- >
205
- <div
206
- className="rounded-xl border border-border bg-surface p-6 shadow-xl w-80"
207
- onClick={(e) => e.stopPropagation()}
208
- >
209
- <h2 className="text-sm font-semibold text-foreground mb-2">{t('integrations.providerConfig')}</h2>
210
- <p className="text-[11px] text-foreground-subtle">{t('integrations.configComingSoon')}</p>
211
- <button
212
- onClick={() => setShowConfig(false)}
213
- className="mt-4 w-full rounded-lg bg-primary px-3 py-1.5 text-[11px] text-primary-foreground hover:bg-primary/90 transition-colors"
214
- >
215
- Close
216
- </button>
217
- </div>
218
- </div>
219
- )}
220
- </div>
221
- )
222
- }
@@ -1,333 +0,0 @@
1
- import { useState, useMemo } from 'react'
2
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
3
- import { useNavigate } from 'react-router-dom'
4
- import {
5
- Plus,
6
- Search,
7
- ChevronDown,
8
- ChevronRight,
9
- Settings,
10
- Camera,
11
- Lightbulb,
12
- ToggleLeft,
13
- Radio,
14
- HelpCircle,
15
- } from 'lucide-react'
16
- import { useBackendClient } from '../hooks/useBackendClient'
17
- import { ProviderIcon, getProviderLabel } from '../components/shared/ProviderIcon'
18
- import { StatusBadge } from '../components/shared/StatusBadge'
19
- import { IntegrationWizard } from '../components/integrations/IntegrationWizard'
20
-
21
- type DeviceTypeFilter = 'all' | 'camera' | 'sensor' | 'light' | 'switch' | 'other'
22
- type GroupBy = 'provider' | 'type' | 'status'
23
-
24
- const TYPE_FILTERS: Array<{ id: DeviceTypeFilter; label: string; icon: React.ComponentType<{ className?: string }> }> = [
25
- { id: 'all', label: 'All', icon: HelpCircle },
26
- { id: 'camera', label: 'Camera', icon: Camera },
27
- { id: 'sensor', label: 'Sensor', icon: Radio },
28
- { id: 'light', label: 'Light', icon: Lightbulb },
29
- { id: 'switch', label: 'Switch', icon: ToggleLeft },
30
- { id: 'other', label: 'Other', icon: HelpCircle },
31
- ]
32
-
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()
38
- if (type.includes('camera') || type === 'camera') return 'camera'
39
- if (type.includes('sensor')) return 'sensor'
40
- if (type.includes('light')) return 'light'
41
- if (type.includes('switch')) return 'switch'
42
- if (type) return 'other'
43
- return 'camera' // default for untyped devices
44
- }
45
-
46
- export function IntegrationsPage() {
47
- const client = useBackendClient()
48
- const queryClient = useQueryClient()
49
- const navigate = useNavigate()
50
-
51
- const [wizardOpen, setWizardOpen] = useState(false)
52
- const [search, setSearch] = useState('')
53
- const [typeFilter, setTypeFilter] = useState<DeviceTypeFilter>('all')
54
- const [groupBy, setGroupBy] = useState<GroupBy>('provider')
55
- const [expandedSections, setExpandedSections] = useState<ReadonlySet<string>>(new Set(['__all__']))
56
-
57
- const { data: providers, isLoading: providersLoading } = useQuery({
58
- queryKey: ['providers'],
59
- queryFn: () => client.listProviders(),
60
- refetchInterval: 5_000,
61
- })
62
-
63
- const { data: devices, isLoading: devicesLoading } = useQuery({
64
- queryKey: ['devices'],
65
- queryFn: () => client.listDevices(),
66
- refetchInterval: 5_000,
67
- })
68
-
69
- const startMutation = useMutation({
70
- mutationFn: (id: string) => client.startProvider(id),
71
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
72
- })
73
-
74
- const stopMutation = useMutation({
75
- mutationFn: (id: string) => client.stopProvider(id),
76
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['providers'] }),
77
- })
78
-
79
- const providerList = providers ?? []
80
- const deviceList = devices ?? []
81
-
82
- // Filter devices
83
- const filteredDevices = useMemo(() => {
84
- const searchLower = search.toLowerCase()
85
- return deviceList.filter((d) => {
86
- if (typeFilter !== 'all' && getDeviceType(d) !== typeFilter) return false
87
- if (searchLower) {
88
- const name = (d.name ?? d.id ?? '').toLowerCase()
89
- if (!name.includes(searchLower)) return false
90
- }
91
- return true
92
- })
93
- }, [deviceList, typeFilter, search])
94
-
95
- // Group devices
96
- const grouped = useMemo(() => {
97
- const groups = new Map<string, DeviceEntry[]>()
98
- for (const device of filteredDevices) {
99
- let key: string
100
- switch (groupBy) {
101
- case 'provider':
102
- key = device.providerId ?? 'unknown'
103
- break
104
- case 'type':
105
- key = getDeviceType(device)
106
- break
107
- case 'status':
108
- key = device.state?.online ? 'online' : 'offline'
109
- break
110
- }
111
- if (!groups.has(key)) {
112
- groups.set(key, [])
113
- }
114
- groups.get(key)!.push(device)
115
- }
116
- return groups
117
- }, [filteredDevices, groupBy])
118
-
119
- function toggleSection(sectionId: string) {
120
- setExpandedSections((prev) => {
121
- const next = new Set(prev)
122
- if (next.has(sectionId)) {
123
- next.delete(sectionId)
124
- } else {
125
- next.add(sectionId)
126
- }
127
- return next
128
- })
129
- }
130
-
131
- function getGroupLabel(key: string): string {
132
- if (groupBy === 'provider') {
133
- const provider = providerList.find((p) => p.id === key)
134
- return provider ? (provider.name ?? key) : key
135
- }
136
- return key.charAt(0).toUpperCase() + key.slice(1)
137
- }
138
-
139
- function getGroupProviderType(key: string): string {
140
- if (groupBy === 'provider') {
141
- const provider = providerList.find((p) => p.id === key)
142
- return provider?.type ?? 'rtsp'
143
- }
144
- return 'rtsp'
145
- }
146
-
147
- function getGroupProviderStatus(key: string): string {
148
- if (groupBy === 'provider') {
149
- const provider = providerList.find((p) => p.id === key)
150
- return provider?.status?.connected ? 'running' : 'stopped'
151
- }
152
- return ''
153
- }
154
-
155
- const isLoading = providersLoading || devicesLoading
156
-
157
- return (
158
- <div className="p-6 space-y-4">
159
- {/* Header */}
160
- <div className="flex items-center justify-between gap-4">
161
- <h1 className="text-lg font-semibold text-foreground">Integrations</h1>
162
- <button
163
- onClick={() => setWizardOpen(true)}
164
- className="flex items-center gap-1.5 rounded-lg bg-primary px-3 py-1.5 text-xs font-medium text-primary-foreground shadow-sm"
165
- >
166
- <Plus className="h-3.5 w-3.5" />
167
- New Integration
168
- </button>
169
- </div>
170
-
171
- {/* Toolbar: search + filters + group by */}
172
- <div className="flex items-center gap-3 flex-wrap">
173
- {/* Search */}
174
- <div className="relative">
175
- <Search className="absolute left-2.5 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-foreground-subtle" />
176
- <input
177
- type="text"
178
- value={search}
179
- onChange={(e) => setSearch(e.target.value)}
180
- placeholder="Search devices..."
181
- className="rounded-lg border border-border bg-surface pl-8 pr-3 py-1.5 text-xs text-foreground placeholder:text-foreground-subtle focus:outline-none focus:border-primary/50 w-48"
182
- />
183
- </div>
184
-
185
- {/* Type filter chips */}
186
- <div className="flex items-center gap-1">
187
- {TYPE_FILTERS.map(({ id, label }) => (
188
- <button
189
- key={id}
190
- onClick={() => setTypeFilter(id)}
191
- className={`rounded-full px-2.5 py-1 text-[10px] font-medium transition-colors ${
192
- typeFilter === id
193
- ? 'bg-primary/15 text-primary border border-primary/30'
194
- : 'bg-surface border border-border text-foreground-subtle hover:text-foreground'
195
- }`}
196
- >
197
- {label}
198
- </button>
199
- ))}
200
- </div>
201
-
202
- {/* Group by selector */}
203
- <div className="flex items-center gap-1.5 ml-auto">
204
- <span className="text-[10px] text-foreground-subtle">Group by:</span>
205
- <select
206
- value={groupBy}
207
- onChange={(e) => setGroupBy(e.target.value as GroupBy)}
208
- className="rounded-lg border border-border bg-surface px-2 py-1 text-[11px] text-foreground focus:outline-none focus:border-primary/50"
209
- >
210
- <option value="provider">Provider</option>
211
- <option value="type">Type</option>
212
- <option value="status">Status</option>
213
- </select>
214
- </div>
215
- </div>
216
-
217
- {/* Loading state */}
218
- {isLoading && (
219
- <div className="space-y-2">
220
- {[1, 2, 3].map((i) => (
221
- <div key={i} className="h-14 rounded-lg border border-border bg-surface animate-pulse" />
222
- ))}
223
- </div>
224
- )}
225
-
226
- {/* Empty state */}
227
- {!isLoading && filteredDevices.length === 0 && (
228
- <div className="flex flex-col items-center py-20 text-foreground-subtle">
229
- <p className="text-sm">No devices found.</p>
230
- <p className="text-xs mt-1">
231
- {search || typeFilter !== 'all'
232
- ? 'Try adjusting your filters'
233
- : 'Start an integration to discover devices.'}
234
- </p>
235
- </div>
236
- )}
237
-
238
- {/* Collapsible sections */}
239
- {!isLoading && Array.from(grouped.entries()).map(([key, groupDevices]) => {
240
- const isExpanded = expandedSections.has(key) || expandedSections.has('__all__')
241
- const providerType = getGroupProviderType(key)
242
- const providerStatus = getGroupProviderStatus(key)
243
- const providerId = groupBy === 'provider' ? key : null
244
-
245
- return (
246
- <div key={key} className="rounded-lg border border-border bg-surface overflow-hidden">
247
- {/* Section header */}
248
- <button
249
- onClick={() => toggleSection(key)}
250
- className="w-full flex items-center gap-3 px-4 py-3 hover:bg-surface-hover transition-colors"
251
- >
252
- {isExpanded ? (
253
- <ChevronDown className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
254
- ) : (
255
- <ChevronRight className="h-3.5 w-3.5 text-foreground-subtle flex-shrink-0" />
256
- )}
257
-
258
- {groupBy === 'provider' && <ProviderIcon type={providerType} size="sm" />}
259
-
260
- {groupBy === 'provider' ? (
261
- <span
262
- className="text-xs font-semibold text-foreground hover:text-primary hover:underline cursor-pointer"
263
- onClick={(e) => { e.stopPropagation(); navigate(`/integrations/${key}`) }}
264
- >
265
- {getGroupLabel(key)}
266
- </span>
267
- ) : (
268
- <span className="text-xs font-semibold text-foreground">{getGroupLabel(key)}</span>
269
- )}
270
- <span className="text-[10px] text-foreground-subtle">
271
- {groupDevices.length} device{groupDevices.length !== 1 ? 's' : ''}
272
- </span>
273
-
274
- {providerStatus && <StatusBadge status={providerStatus} />}
275
-
276
- <div className="ml-auto flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
277
- {providerId && (
278
- <button
279
- className="p-1.5 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
280
- title="Provider settings"
281
- >
282
- <Settings className="h-3.5 w-3.5" />
283
- </button>
284
- )}
285
- </div>
286
- </button>
287
-
288
- {/* Device table */}
289
- {isExpanded && (
290
- <div className="border-t border-border">
291
- {/* Table header */}
292
- <div className="grid grid-cols-[1fr_80px_80px_80px_100px_100px] gap-2 px-4 py-2 text-[10px] font-medium text-foreground-subtle uppercase tracking-wider border-b border-border bg-background">
293
- <span>Name</span>
294
- <span>Type</span>
295
- <span>Status</span>
296
- <span>Phase</span>
297
- <span>Today</span>
298
- <span>Last Event</span>
299
- </div>
300
-
301
- {/* Device rows */}
302
- {groupDevices.map((device) => {
303
- const deviceId = device.id
304
- const deviceName = device.name ?? device.id
305
- const deviceType = getDeviceType(device)
306
- const deviceStatus = device.state?.online ? 'online' : 'offline'
307
- const phase = (device as Record<string, unknown>).phase as string ?? '—'
308
-
309
- return (
310
- <button
311
- key={deviceId}
312
- onClick={() => navigate(`/devices/${deviceId}`)}
313
- className="w-full grid grid-cols-[1fr_80px_80px_80px_100px_100px] gap-2 px-4 py-2.5 text-left hover:bg-surface-hover transition-colors border-b border-border last:border-b-0"
314
- >
315
- <span className="text-xs font-medium text-foreground truncate">{deviceName}</span>
316
- <span className="text-[11px] text-foreground-subtle capitalize">{deviceType}</span>
317
- <span><StatusBadge status={deviceStatus} /></span>
318
- <span className="text-[11px] text-foreground-subtle">{phase}</span>
319
- <span className="text-[11px] text-foreground-subtle">—</span>
320
- <span className="text-[11px] text-foreground-subtle">—</span>
321
- </button>
322
- )
323
- })}
324
- </div>
325
- )}
326
- </div>
327
- )
328
- })}
329
-
330
- <IntegrationWizard open={wizardOpen} onClose={() => setWizardOpen(false)} />
331
- </div>
332
- )
333
- }
@@ -1,106 +0,0 @@
1
- import { useState } from 'react'
2
- import { useNavigate } from 'react-router-dom'
3
- import { Eye, EyeOff } from 'lucide-react'
4
- import { useThemeMode } from '@camstack/ui'
5
- import { useAuth } from '../contexts/auth-context'
6
-
7
- export function LoginPage() {
8
- const { login } = useAuth()
9
- const theme = useThemeMode()
10
- const navigate = useNavigate()
11
- const [username, setUsername] = useState('')
12
- const [password, setPassword] = useState('')
13
- const [showPassword, setShowPassword] = useState(false)
14
- const [error, setError] = useState('')
15
- const [submitting, setSubmitting] = useState(false)
16
-
17
- const handleSubmit = async (e: React.FormEvent) => {
18
- e.preventDefault()
19
- setError('')
20
- setSubmitting(true)
21
- try {
22
- await login(username, password)
23
- navigate('/dashboard', { replace: true })
24
- } catch (err) {
25
- setError(err instanceof Error ? err.message : 'Login failed')
26
- } finally {
27
- setSubmitting(false)
28
- }
29
- }
30
-
31
- return (
32
- <div className="flex min-h-screen items-center justify-center bg-background p-4">
33
- <div className="w-full max-w-sm">
34
- {/* Logo */}
35
- <div className="flex justify-center mb-8">
36
- <img
37
- src={theme?.resolvedMode === 'light' ? '/brand/logo-horizontal-light.svg' : '/brand/logo-horizontal-dark.svg'}
38
- alt="CamStack Admin"
39
- className="h-12"
40
- />
41
- </div>
42
-
43
- {/* Form */}
44
- <form onSubmit={handleSubmit} className="space-y-4 rounded-xl border border-border bg-surface p-6 shadow-xl shadow-black/10">
45
- {error && (
46
- <div className="rounded-md bg-danger/10 border border-danger/20 px-3 py-2 text-xs text-danger">
47
- {error}
48
- </div>
49
- )}
50
-
51
- <div className="space-y-1.5">
52
- <label className="text-xs font-medium text-foreground-subtle">Username</label>
53
- <input
54
- type="text"
55
- placeholder="admin"
56
- value={username}
57
- onChange={(e) => setUsername(e.target.value)}
58
- className="w-full rounded-lg border border-border bg-background px-3 py-2.5 text-sm text-foreground placeholder:text-foreground-disabled focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none transition-all"
59
- autoFocus
60
- />
61
- </div>
62
-
63
- <div className="space-y-1.5">
64
- <label className="text-xs font-medium text-foreground-subtle">Password</label>
65
- <div className="relative">
66
- <input
67
- type={showPassword ? 'text' : 'password'}
68
- placeholder="Enter password"
69
- value={password}
70
- onChange={(e) => setPassword(e.target.value)}
71
- className="w-full rounded-lg border border-border bg-background px-3 py-2.5 pr-10 text-sm text-foreground placeholder:text-foreground-disabled focus:border-primary focus:ring-1 focus:ring-primary/30 outline-none transition-all"
72
- />
73
- <button
74
- type="button"
75
- onClick={() => setShowPassword(!showPassword)}
76
- className="absolute right-2.5 top-1/2 -translate-y-1/2 text-foreground-subtle hover:text-foreground transition-colors"
77
- tabIndex={-1}
78
- >
79
- {showPassword ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
80
- </button>
81
- </div>
82
- </div>
83
-
84
- <button
85
- type="submit"
86
- disabled={submitting || !username || !password}
87
- className="w-full rounded-lg bg-primary px-4 py-2.5 text-sm font-semibold text-primary-foreground shadow-md shadow-primary/20 hover:shadow-lg hover:shadow-primary/30 disabled:opacity-50 disabled:shadow-none transition-all"
88
- >
89
- {submitting ? (
90
- <span className="flex items-center justify-center gap-2">
91
- <span className="h-4 w-4 animate-spin rounded-full border-2 border-primary-foreground/30 border-t-primary-foreground" />
92
- Logging in...
93
- </span>
94
- ) : (
95
- 'Log in'
96
- )}
97
- </button>
98
- </form>
99
-
100
- <p className="mt-6 text-center text-[10px] text-foreground-subtle/50">
101
- CamStack v0.1.0
102
- </p>
103
- </div>
104
- </div>
105
- )
106
- }
@@ -1,18 +0,0 @@
1
- import { PipelineStatus } from '../components/metrics/PipelineStatus'
2
- import { IntegrationUsage } from '../components/metrics/IntegrationUsage'
3
- import { ProcessResources } from '../components/metrics/ProcessResources'
4
- import { AgentLoad } from '../components/metrics/AgentLoad'
5
-
6
- export function MetricsPage() {
7
- return (
8
- <div className="p-6 space-y-4">
9
- <h1 className="text-lg font-semibold text-foreground">Metrics</h1>
10
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
11
- <PipelineStatus />
12
- <IntegrationUsage />
13
- <ProcessResources />
14
- <AgentLoad />
15
- </div>
16
- </div>
17
- )
18
- }