@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.
- package/dist/assets/index-DjELGD4R.css +1 -0
- package/dist/assets/index-w55PwKyu.js +598 -0
- package/{index.html → dist/index.html} +3 -1
- package/dist/server/addon.d.ts +11 -0
- package/dist/server/addon.js +50 -0
- package/dist/server/addon.js.map +1 -0
- package/package.json +5 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -339
- package/src/components/addons/AddonUploadZone.tsx +0 -307
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -119
- package/src/components/agents/AgentCard.tsx +0 -281
- package/src/components/agents/AgentLogs.tsx +0 -231
- package/src/components/agents/ProcessList.tsx +0 -127
- package/src/components/agents/ProcessTree.tsx +0 -369
- package/src/components/agents/TaskList.tsx +0 -68
- package/src/components/cameras/CameraCard.tsx +0 -60
- package/src/components/cameras/LiveEventsPanel.tsx +0 -91
- package/src/components/cameras/ProviderSection.tsx +0 -50
- package/src/components/cameras/StreamArea.tsx +0 -107
- package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
- package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
- package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
- package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
- package/src/components/dashboard/BlockPicker.tsx +0 -54
- package/src/components/dashboard/BlockWrapper.tsx +0 -97
- package/src/components/dashboard/DashboardGrid.tsx +0 -160
- package/src/components/dashboard/block-registry.ts +0 -15
- package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
- package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
- package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
- package/src/components/dashboard/blocks/index.ts +0 -32
- package/src/components/device/DeviceHeader.tsx +0 -116
- package/src/components/device/FloatingPanel.tsx +0 -132
- package/src/components/device/FloatingPanelManager.tsx +0 -167
- package/src/components/device/PanelContent.tsx +0 -196
- package/src/components/device/QuickConfigWizard.tsx +0 -507
- package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
- package/src/components/device/tabs/EventsTab.tsx +0 -19
- package/src/components/device/tabs/LogsTab.tsx +0 -22
- package/src/components/device/tabs/OverviewTab.tsx +0 -104
- package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
- package/src/components/device/tabs/RecordingTab.tsx +0 -47
- package/src/components/device/tabs/ReplTab.tsx +0 -153
- package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
- package/src/components/device/tabs/ZonesTab.tsx +0 -98
- package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
- package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
- package/src/components/device/zone-editor/ZoneList.tsx +0 -150
- package/src/components/form-builder/FormBuilder.tsx +0 -135
- package/src/components/form-builder/FormField.tsx +0 -732
- package/src/components/form-builder/ModelSelector.tsx +0 -239
- package/src/components/integrations/AddDeviceDialog.tsx +0 -205
- package/src/components/integrations/CompactDeviceCard.tsx +0 -35
- package/src/components/integrations/DeviceCard.tsx +0 -29
- package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
- package/src/components/integrations/DeviceGrid.tsx +0 -79
- package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
- package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
- package/src/components/integrations/IntegrationCard.tsx +0 -40
- package/src/components/integrations/IntegrationWizard.tsx +0 -171
- package/src/components/integrations/ProviderConfigForm.tsx +0 -89
- package/src/components/integrations/ProviderPicker.tsx +0 -91
- package/src/components/integrations/SnapshotPopover.tsx +0 -68
- package/src/components/metrics/AgentLoad.tsx +0 -113
- package/src/components/metrics/IntegrationUsage.tsx +0 -90
- package/src/components/metrics/PipelineStatus.tsx +0 -105
- package/src/components/metrics/ProcessResources.tsx +0 -139
- package/src/components/pipeline/PhaseSettings.tsx +0 -131
- package/src/components/shared/CapabilityBadges.tsx +0 -30
- package/src/components/shared/ProviderIcon.tsx +0 -42
- package/src/components/shared/StatusBadge.tsx +0 -23
- package/src/components/shared/WebRtcPlayer.tsx +0 -211
- package/src/components/timeline/EventMarker.tsx +0 -32
- package/src/components/timeline/TimelineBar.tsx +0 -131
- package/src/components/ui/ConfirmDialog.tsx +0 -115
- package/src/components/ui/ToastContainer.tsx +0 -92
- package/src/contexts/auth-context.tsx +0 -91
- package/src/hooks/useBackendClient.ts +0 -6
- package/src/hooks/useTheme.ts +0 -1
- package/src/i18n/en.json +0 -164
- package/src/i18n/index.ts +0 -29
- package/src/i18n/it.json +0 -164
- package/src/index.css +0 -63
- package/src/layouts/AddonPageLoader.tsx +0 -120
- package/src/layouts/AppLayout.tsx +0 -238
- package/src/layouts/ProtectedRoute.tsx +0 -25
- package/src/lib/addon-page-context.ts +0 -29
- package/src/lib/backend.ts +0 -16
- package/src/main.tsx +0 -21
- package/src/pages/AccessDenied.tsx +0 -22
- package/src/pages/Cameras.tsx +0 -127
- package/src/pages/Dashboard.tsx +0 -6
- package/src/pages/DeviceDetail.tsx +0 -175
- package/src/pages/IntegrationDetail.tsx +0 -224
- package/src/pages/Integrations.tsx +0 -330
- package/src/pages/Login.tsx +0 -106
- package/src/pages/Metrics.tsx +0 -18
- package/src/pages/PipelineConfig.tsx +0 -282
- package/src/pages/Showroom.tsx +0 -351
- package/src/pages/Timeline.tsx +0 -269
- package/src/pages/system/Addons.tsx +0 -525
- package/src/pages/system/Agents.tsx +0 -362
- package/src/pages/system/Logs.tsx +0 -131
- package/src/pages/system/Models.tsx +0 -102
- package/src/pages/system/Processes.tsx +0 -129
- package/src/pages/system/Repl.tsx +0 -148
- package/src/pages/system/Settings.tsx +0 -168
- package/src/pages/system/Users.tsx +0 -174
- package/src/server/addon.ts +0 -54
- package/src/types/config-ui.ts +0 -210
- package/src/types/dashboard.ts +0 -39
- package/tsconfig.json +0 -29
- package/tsconfig.server.json +0 -16
- package/tsup.config.ts +0 -20
- package/vite.config.ts +0 -68
- /package/{public → dist}/brand/logo-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
- /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
- /package/{public → dist}/brand/logo-light.svg +0 -0
- /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
- /package/{public → dist}/brand/logo-wide-light.svg +0 -0
- /package/{public → dist}/favicon.svg +0 -0
- /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
- /package/{public → dist}/vendor/react.mjs +0 -0
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useCallback } from 'react'
|
|
2
|
-
import { Upload, FileArchive, X, Loader2, CheckCircle, AlertCircle } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Types
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
type UploadStatus = 'idle' | 'dragging' | 'uploading' | 'success' | 'error'
|
|
9
|
-
|
|
10
|
-
interface UploadResult {
|
|
11
|
-
success: boolean
|
|
12
|
-
packageName?: string
|
|
13
|
-
version?: string
|
|
14
|
-
error?: string
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface AddonUploadZoneProps {
|
|
18
|
-
onUploadSuccess: () => void
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Accepted file extensions
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const ACCEPTED_EXTENSIONS = ['.tgz', '.tar.gz', '.zip']
|
|
26
|
-
const ACCEPT_MIME = '.tgz,.tar.gz,.zip'
|
|
27
|
-
|
|
28
|
-
function isAcceptedFile(file: File): boolean {
|
|
29
|
-
const name = file.name.toLowerCase()
|
|
30
|
-
return ACCEPTED_EXTENSIONS.some((ext) => name.endsWith(ext))
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// AddonUploadZone
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
|
|
37
|
-
export function AddonUploadZone({ onUploadSuccess }: AddonUploadZoneProps) {
|
|
38
|
-
const [status, setStatus] = useState<UploadStatus>('idle')
|
|
39
|
-
const [progress, setProgress] = useState(0)
|
|
40
|
-
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
|
41
|
-
const [result, setResult] = useState<UploadResult | null>(null)
|
|
42
|
-
const fileInputRef = useRef<HTMLInputElement>(null)
|
|
43
|
-
|
|
44
|
-
const resetState = useCallback(() => {
|
|
45
|
-
setStatus('idle')
|
|
46
|
-
setProgress(0)
|
|
47
|
-
setSelectedFile(null)
|
|
48
|
-
setResult(null)
|
|
49
|
-
}, [])
|
|
50
|
-
|
|
51
|
-
const uploadFile = useCallback(
|
|
52
|
-
async (file: File) => {
|
|
53
|
-
if (!isAcceptedFile(file)) {
|
|
54
|
-
setStatus('error')
|
|
55
|
-
setResult({ success: false, error: `Invalid file type. Accepted: ${ACCEPTED_EXTENSIONS.join(', ')}` })
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
setSelectedFile(file)
|
|
60
|
-
setStatus('uploading')
|
|
61
|
-
setProgress(0)
|
|
62
|
-
setResult(null)
|
|
63
|
-
|
|
64
|
-
const formData = new FormData()
|
|
65
|
-
formData.append('file', file)
|
|
66
|
-
|
|
67
|
-
const token = localStorage.getItem('camstack_admin_token') ?? ''
|
|
68
|
-
|
|
69
|
-
try {
|
|
70
|
-
const xhr = new XMLHttpRequest()
|
|
71
|
-
|
|
72
|
-
const uploadResult = await new Promise<UploadResult>((resolve, reject) => {
|
|
73
|
-
xhr.upload.addEventListener('progress', (event) => {
|
|
74
|
-
if (event.lengthComputable) {
|
|
75
|
-
const pct = Math.round((event.loaded / event.total) * 100)
|
|
76
|
-
setProgress(pct)
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
xhr.addEventListener('load', () => {
|
|
81
|
-
try {
|
|
82
|
-
const body = JSON.parse(xhr.responseText)
|
|
83
|
-
if (xhr.status >= 200 && xhr.status < 300 && body.success) {
|
|
84
|
-
resolve({
|
|
85
|
-
success: true,
|
|
86
|
-
packageName: body.packageName,
|
|
87
|
-
version: body.version,
|
|
88
|
-
})
|
|
89
|
-
} else {
|
|
90
|
-
resolve({
|
|
91
|
-
success: false,
|
|
92
|
-
error: body.error ?? `Upload failed (HTTP ${xhr.status})`,
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
} catch {
|
|
96
|
-
resolve({ success: false, error: `Unexpected response (HTTP ${xhr.status})` })
|
|
97
|
-
}
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
xhr.addEventListener('error', () => {
|
|
101
|
-
reject(new Error('Network error'))
|
|
102
|
-
})
|
|
103
|
-
|
|
104
|
-
xhr.addEventListener('abort', () => {
|
|
105
|
-
reject(new Error('Upload aborted'))
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
xhr.open('POST', '/api/addons/upload')
|
|
109
|
-
xhr.setRequestHeader('Authorization', `Bearer ${token}`)
|
|
110
|
-
xhr.send(formData)
|
|
111
|
-
})
|
|
112
|
-
|
|
113
|
-
setResult(uploadResult)
|
|
114
|
-
setStatus(uploadResult.success ? 'success' : 'error')
|
|
115
|
-
|
|
116
|
-
if (uploadResult.success) {
|
|
117
|
-
onUploadSuccess()
|
|
118
|
-
}
|
|
119
|
-
} catch (err) {
|
|
120
|
-
const message = err instanceof Error ? err.message : 'Upload failed'
|
|
121
|
-
setResult({ success: false, error: message })
|
|
122
|
-
setStatus('error')
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
[onUploadSuccess],
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
// --- Drag event handlers ---
|
|
129
|
-
|
|
130
|
-
const handleDragEnter = useCallback((e: React.DragEvent) => {
|
|
131
|
-
e.preventDefault()
|
|
132
|
-
e.stopPropagation()
|
|
133
|
-
setStatus((prev) => (prev === 'uploading' ? prev : 'dragging'))
|
|
134
|
-
}, [])
|
|
135
|
-
|
|
136
|
-
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
137
|
-
e.preventDefault()
|
|
138
|
-
e.stopPropagation()
|
|
139
|
-
// Only leave when exiting the drop zone itself
|
|
140
|
-
if (e.currentTarget.contains(e.relatedTarget as Node)) return
|
|
141
|
-
setStatus((prev) => (prev === 'uploading' ? prev : 'idle'))
|
|
142
|
-
}, [])
|
|
143
|
-
|
|
144
|
-
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
145
|
-
e.preventDefault()
|
|
146
|
-
e.stopPropagation()
|
|
147
|
-
}, [])
|
|
148
|
-
|
|
149
|
-
const handleDrop = useCallback(
|
|
150
|
-
(e: React.DragEvent) => {
|
|
151
|
-
e.preventDefault()
|
|
152
|
-
e.stopPropagation()
|
|
153
|
-
setStatus('idle')
|
|
154
|
-
|
|
155
|
-
const file = e.dataTransfer.files[0]
|
|
156
|
-
if (file) {
|
|
157
|
-
uploadFile(file)
|
|
158
|
-
}
|
|
159
|
-
},
|
|
160
|
-
[uploadFile],
|
|
161
|
-
)
|
|
162
|
-
|
|
163
|
-
const handleFileSelect = useCallback(
|
|
164
|
-
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
165
|
-
const file = e.target.files?.[0]
|
|
166
|
-
if (file) {
|
|
167
|
-
uploadFile(file)
|
|
168
|
-
}
|
|
169
|
-
// Reset input so the same file can be re-selected
|
|
170
|
-
e.target.value = ''
|
|
171
|
-
},
|
|
172
|
-
[uploadFile],
|
|
173
|
-
)
|
|
174
|
-
|
|
175
|
-
const handleBrowseClick = useCallback(() => {
|
|
176
|
-
fileInputRef.current?.click()
|
|
177
|
-
}, [])
|
|
178
|
-
|
|
179
|
-
// --- Render helpers ---
|
|
180
|
-
|
|
181
|
-
const isIdle = status === 'idle' || status === 'dragging'
|
|
182
|
-
const isDragging = status === 'dragging'
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<div className="space-y-2">
|
|
186
|
-
{/* Hidden file input */}
|
|
187
|
-
<input
|
|
188
|
-
ref={fileInputRef}
|
|
189
|
-
type="file"
|
|
190
|
-
accept={ACCEPT_MIME}
|
|
191
|
-
onChange={handleFileSelect}
|
|
192
|
-
className="hidden"
|
|
193
|
-
/>
|
|
194
|
-
|
|
195
|
-
{/* Drop zone */}
|
|
196
|
-
<div
|
|
197
|
-
onDragEnter={handleDragEnter}
|
|
198
|
-
onDragLeave={handleDragLeave}
|
|
199
|
-
onDragOver={handleDragOver}
|
|
200
|
-
onDrop={handleDrop}
|
|
201
|
-
onClick={isIdle ? handleBrowseClick : undefined}
|
|
202
|
-
className={[
|
|
203
|
-
'relative rounded-lg border-2 border-dashed transition-all cursor-pointer',
|
|
204
|
-
'flex flex-col items-center justify-center gap-2 px-6 py-6',
|
|
205
|
-
isDragging
|
|
206
|
-
? 'border-primary bg-primary/5 scale-[1.01]'
|
|
207
|
-
: status === 'uploading'
|
|
208
|
-
? 'border-border bg-surface cursor-default'
|
|
209
|
-
: status === 'success'
|
|
210
|
-
? 'border-green-500/40 bg-green-500/5 cursor-default'
|
|
211
|
-
: status === 'error'
|
|
212
|
-
? 'border-red-500/40 bg-red-500/5 cursor-default'
|
|
213
|
-
: 'border-border bg-surface hover:border-primary/50 hover:bg-surface-hover/50',
|
|
214
|
-
].join(' ')}
|
|
215
|
-
>
|
|
216
|
-
{/* Idle / dragging state */}
|
|
217
|
-
{isIdle && (
|
|
218
|
-
<>
|
|
219
|
-
<Upload className={`h-6 w-6 ${isDragging ? 'text-primary' : 'text-foreground-subtle'}`} />
|
|
220
|
-
<div className="text-center">
|
|
221
|
-
<p className="text-xs font-medium text-foreground">
|
|
222
|
-
{isDragging ? 'Drop addon file here' : 'Drag & drop addon file'}
|
|
223
|
-
</p>
|
|
224
|
-
<p className="text-[10px] text-foreground-subtle mt-0.5">
|
|
225
|
-
or{' '}
|
|
226
|
-
<span className="text-primary underline underline-offset-2">browse files</span>
|
|
227
|
-
{' '}· .tgz, .tar.gz, .zip
|
|
228
|
-
</p>
|
|
229
|
-
</div>
|
|
230
|
-
</>
|
|
231
|
-
)}
|
|
232
|
-
|
|
233
|
-
{/* Uploading state */}
|
|
234
|
-
{status === 'uploading' && selectedFile && (
|
|
235
|
-
<>
|
|
236
|
-
<div className="flex items-center gap-2">
|
|
237
|
-
<FileArchive className="h-5 w-5 text-primary shrink-0" />
|
|
238
|
-
<div className="min-w-0">
|
|
239
|
-
<p className="text-xs font-medium text-foreground truncate">{selectedFile.name}</p>
|
|
240
|
-
<p className="text-[10px] text-foreground-subtle">
|
|
241
|
-
{(selectedFile.size / 1024).toFixed(1)} KB
|
|
242
|
-
</p>
|
|
243
|
-
</div>
|
|
244
|
-
</div>
|
|
245
|
-
|
|
246
|
-
{/* Progress bar */}
|
|
247
|
-
<div className="w-full max-w-xs">
|
|
248
|
-
<div className="h-1.5 w-full rounded-full bg-border overflow-hidden">
|
|
249
|
-
<div
|
|
250
|
-
className="h-full rounded-full bg-primary transition-all duration-300 ease-out"
|
|
251
|
-
style={{ width: `${progress}%` }}
|
|
252
|
-
/>
|
|
253
|
-
</div>
|
|
254
|
-
<div className="flex items-center justify-between mt-1">
|
|
255
|
-
<span className="text-[10px] text-foreground-subtle flex items-center gap-1">
|
|
256
|
-
<Loader2 className="h-3 w-3 animate-spin" />
|
|
257
|
-
Uploading...
|
|
258
|
-
</span>
|
|
259
|
-
<span className="text-[10px] text-foreground-subtle font-mono">{progress}%</span>
|
|
260
|
-
</div>
|
|
261
|
-
</div>
|
|
262
|
-
</>
|
|
263
|
-
)}
|
|
264
|
-
|
|
265
|
-
{/* Success state */}
|
|
266
|
-
{status === 'success' && result && (
|
|
267
|
-
<>
|
|
268
|
-
<CheckCircle className="h-6 w-6 text-green-500" />
|
|
269
|
-
<div className="text-center">
|
|
270
|
-
<p className="text-xs font-medium text-foreground">Addon installed</p>
|
|
271
|
-
<p className="text-[10px] text-foreground-subtle mt-0.5">
|
|
272
|
-
{result.packageName} v{result.version}
|
|
273
|
-
</p>
|
|
274
|
-
</div>
|
|
275
|
-
<button
|
|
276
|
-
type="button"
|
|
277
|
-
onClick={(e) => { e.stopPropagation(); resetState() }}
|
|
278
|
-
className="absolute top-2 right-2 p-1 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
|
|
279
|
-
title="Dismiss"
|
|
280
|
-
>
|
|
281
|
-
<X className="h-3.5 w-3.5" />
|
|
282
|
-
</button>
|
|
283
|
-
</>
|
|
284
|
-
)}
|
|
285
|
-
|
|
286
|
-
{/* Error state */}
|
|
287
|
-
{status === 'error' && result && (
|
|
288
|
-
<>
|
|
289
|
-
<AlertCircle className="h-6 w-6 text-red-400" />
|
|
290
|
-
<div className="text-center">
|
|
291
|
-
<p className="text-xs font-medium text-red-400">Upload failed</p>
|
|
292
|
-
<p className="text-[10px] text-foreground-subtle mt-0.5">{result.error}</p>
|
|
293
|
-
</div>
|
|
294
|
-
<button
|
|
295
|
-
type="button"
|
|
296
|
-
onClick={(e) => { e.stopPropagation(); resetState() }}
|
|
297
|
-
className="absolute top-2 right-2 p-1 rounded-md text-foreground-subtle hover:text-foreground hover:bg-surface-hover transition-colors"
|
|
298
|
-
title="Dismiss"
|
|
299
|
-
>
|
|
300
|
-
<X className="h-3.5 w-3.5" />
|
|
301
|
-
</button>
|
|
302
|
-
</>
|
|
303
|
-
)}
|
|
304
|
-
</div>
|
|
305
|
-
</div>
|
|
306
|
-
)
|
|
307
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
const CAPABILITY_COLORS: Record<string, string> = {
|
|
2
|
-
// detection / vision
|
|
3
|
-
detection: 'bg-green-500/10 text-green-400',
|
|
4
|
-
detector: 'bg-green-500/10 text-green-400',
|
|
5
|
-
// streaming
|
|
6
|
-
streaming: 'bg-blue-500/10 text-blue-400',
|
|
7
|
-
decode: 'bg-blue-500/10 text-blue-400',
|
|
8
|
-
decoder: 'bg-blue-500/10 text-blue-400',
|
|
9
|
-
// recording
|
|
10
|
-
recording: 'bg-orange-500/10 text-orange-400',
|
|
11
|
-
recorder: 'bg-orange-500/10 text-orange-400',
|
|
12
|
-
// transcoding
|
|
13
|
-
transcoding: 'bg-purple-500/10 text-purple-400',
|
|
14
|
-
transcoder: 'bg-purple-500/10 text-purple-400',
|
|
15
|
-
transcode: 'bg-purple-500/10 text-purple-400',
|
|
16
|
-
// restream
|
|
17
|
-
restream: 'bg-cyan-500/10 text-cyan-400',
|
|
18
|
-
restreamer: 'bg-cyan-500/10 text-cyan-400',
|
|
19
|
-
// storage
|
|
20
|
-
storage: 'bg-yellow-500/10 text-yellow-400',
|
|
21
|
-
// notification
|
|
22
|
-
notification: 'bg-pink-500/10 text-pink-400',
|
|
23
|
-
notifier: 'bg-pink-500/10 text-pink-400',
|
|
24
|
-
// faces
|
|
25
|
-
faces: 'bg-indigo-500/10 text-indigo-400',
|
|
26
|
-
// default
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface CapabilityBadgeProps {
|
|
30
|
-
/** Capability name (string) or declaration object ({ name, mode }) */
|
|
31
|
-
capability: string | { name: string; mode?: string }
|
|
32
|
-
/** Optional mode label shown after a separator (overrides object mode) */
|
|
33
|
-
mode?: string
|
|
34
|
-
size?: 'sm' | 'xs'
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function CapabilityBadge({ capability, mode, size = 'xs' }: CapabilityBadgeProps) {
|
|
38
|
-
const capName = typeof capability === 'string' ? capability : capability.name
|
|
39
|
-
const capMode = mode ?? (typeof capability === 'object' ? capability.mode : undefined)
|
|
40
|
-
const lower = capName.toLowerCase()
|
|
41
|
-
const color = CAPABILITY_COLORS[lower] ?? 'bg-primary/10 text-primary'
|
|
42
|
-
const textSize = size === 'sm' ? 'text-xs' : 'text-[10px]'
|
|
43
|
-
|
|
44
|
-
return (
|
|
45
|
-
<span className={`inline-flex items-center rounded-md px-1.5 py-0.5 font-medium gap-1 ${textSize} ${color}`}>
|
|
46
|
-
{capName}
|
|
47
|
-
{capMode && (
|
|
48
|
-
<>
|
|
49
|
-
<span className="opacity-40">·</span>
|
|
50
|
-
<span className="opacity-70">{capMode}</span>
|
|
51
|
-
</>
|
|
52
|
-
)}
|
|
53
|
-
</span>
|
|
54
|
-
)
|
|
55
|
-
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { CheckCircle } from 'lucide-react'
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Types
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
export type CapabilityMode = 'singleton' | 'collection'
|
|
8
|
-
|
|
9
|
-
export interface CapabilityProvider {
|
|
10
|
-
addonId: string
|
|
11
|
-
addonName: string
|
|
12
|
-
active: boolean
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface CapabilityEntry {
|
|
16
|
-
name: string
|
|
17
|
-
mode: CapabilityMode
|
|
18
|
-
providers: CapabilityProvider[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Color helpers
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const CAPABILITY_BORDER_COLORS: Record<string, string> = {
|
|
26
|
-
detection: 'border-green-500',
|
|
27
|
-
detector: 'border-green-500',
|
|
28
|
-
streaming: 'border-blue-500',
|
|
29
|
-
decode: 'border-blue-500',
|
|
30
|
-
decoder: 'border-blue-500',
|
|
31
|
-
recording: 'border-orange-500',
|
|
32
|
-
recorder: 'border-orange-500',
|
|
33
|
-
transcoding: 'border-purple-500',
|
|
34
|
-
transcoder: 'border-purple-500',
|
|
35
|
-
restream: 'border-cyan-500',
|
|
36
|
-
restreamer: 'border-cyan-500',
|
|
37
|
-
storage: 'border-yellow-500',
|
|
38
|
-
notification: 'border-pink-500',
|
|
39
|
-
notifier: 'border-pink-500',
|
|
40
|
-
faces: 'border-indigo-500',
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const CAPABILITY_BG_COLORS: Record<string, string> = {
|
|
44
|
-
detection: 'bg-green-500/5',
|
|
45
|
-
detector: 'bg-green-500/5',
|
|
46
|
-
streaming: 'bg-blue-500/5',
|
|
47
|
-
decode: 'bg-blue-500/5',
|
|
48
|
-
decoder: 'bg-blue-500/5',
|
|
49
|
-
recording: 'bg-orange-500/5',
|
|
50
|
-
recorder: 'bg-orange-500/5',
|
|
51
|
-
transcoding: 'bg-purple-500/5',
|
|
52
|
-
transcoder: 'bg-purple-500/5',
|
|
53
|
-
restream: 'bg-cyan-500/5',
|
|
54
|
-
restreamer: 'bg-cyan-500/5',
|
|
55
|
-
storage: 'bg-yellow-500/5',
|
|
56
|
-
notification: 'bg-pink-500/5',
|
|
57
|
-
notifier: 'bg-pink-500/5',
|
|
58
|
-
faces: 'bg-indigo-500/5',
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
// CapabilityCard
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
|
|
65
|
-
function CapabilityCard({ entry }: { entry: CapabilityEntry }) {
|
|
66
|
-
const lower = entry.name.toLowerCase()
|
|
67
|
-
const borderColor = CAPABILITY_BORDER_COLORS[lower] ?? 'border-primary'
|
|
68
|
-
const bgColor = CAPABILITY_BG_COLORS[lower] ?? 'bg-primary/5'
|
|
69
|
-
const activeProvider = entry.providers.find((p) => p.active)
|
|
70
|
-
|
|
71
|
-
return (
|
|
72
|
-
<div className={`rounded-lg border-t-2 border border-border ${borderColor} ${bgColor} p-3 space-y-2`}>
|
|
73
|
-
<div className="flex items-center justify-between gap-2">
|
|
74
|
-
<span className="text-xs font-semibold text-foreground capitalize">{entry.name}</span>
|
|
75
|
-
<span className={`rounded-full px-2 py-0.5 text-[10px] font-medium ${entry.mode === 'singleton' ? 'bg-primary/10 text-primary' : 'bg-info/10 text-info'}`}>
|
|
76
|
-
{entry.mode}
|
|
77
|
-
</span>
|
|
78
|
-
</div>
|
|
79
|
-
|
|
80
|
-
{entry.mode === 'singleton' ? (
|
|
81
|
-
<div>
|
|
82
|
-
{activeProvider ? (
|
|
83
|
-
<div className="flex items-center gap-1.5 text-[10px] text-foreground">
|
|
84
|
-
<CheckCircle className="h-3 w-3 text-green-400 shrink-0" />
|
|
85
|
-
<span className="font-medium">{activeProvider.addonName}</span>
|
|
86
|
-
<span className="rounded-full bg-green-500/10 text-green-400 px-1.5 py-0.5 font-medium ml-auto">ACTIVE</span>
|
|
87
|
-
</div>
|
|
88
|
-
) : (
|
|
89
|
-
<div className="text-[10px] text-foreground-subtle italic">No active provider</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
) : (
|
|
93
|
-
<div className="space-y-1">
|
|
94
|
-
{entry.providers.length === 0 && (
|
|
95
|
-
<div className="text-[10px] text-foreground-subtle italic">No providers</div>
|
|
96
|
-
)}
|
|
97
|
-
{entry.providers.map((p) => (
|
|
98
|
-
<div key={p.addonId} className="flex items-center justify-between gap-2 text-[10px]">
|
|
99
|
-
<span className="text-foreground-subtle">{p.addonName}</span>
|
|
100
|
-
{p.active && (
|
|
101
|
-
<span className="rounded-full bg-green-500/10 text-green-400 px-1.5 py-0.5 font-medium">ACTIVE</span>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
))}
|
|
105
|
-
</div>
|
|
106
|
-
)}
|
|
107
|
-
</div>
|
|
108
|
-
)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
// CapabilityMap
|
|
113
|
-
// ---------------------------------------------------------------------------
|
|
114
|
-
|
|
115
|
-
interface CapabilityMapProps {
|
|
116
|
-
capabilities: CapabilityEntry[]
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function CapabilityMap({ capabilities }: CapabilityMapProps) {
|
|
120
|
-
if (capabilities.length === 0) {
|
|
121
|
-
return (
|
|
122
|
-
<div className="text-xs text-foreground-subtle text-center py-8">No capabilities registered</div>
|
|
123
|
-
)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return (
|
|
127
|
-
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
|
128
|
-
{capabilities.map((cap) => (
|
|
129
|
-
<CapabilityCard key={cap.name} entry={cap} />
|
|
130
|
-
))}
|
|
131
|
-
</div>
|
|
132
|
-
)
|
|
133
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { ArrowRight, RotateCcw, Zap } from 'lucide-react'
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Types
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
export interface UpdateEntry {
|
|
8
|
-
id: string
|
|
9
|
-
name: string
|
|
10
|
-
currentVersion: string
|
|
11
|
-
newVersion: string
|
|
12
|
-
changelog?: string
|
|
13
|
-
requiresRestart: boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// UpdatesList
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
interface UpdatesListProps {
|
|
21
|
-
updates: UpdateEntry[]
|
|
22
|
-
onUpdate: (id: string) => void
|
|
23
|
-
onUpdateAll: () => void
|
|
24
|
-
onCheckUpdates: () => void
|
|
25
|
-
isChecking: boolean
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function UpdatesList({ updates, onUpdate, onUpdateAll, onCheckUpdates, isChecking }: UpdatesListProps) {
|
|
29
|
-
return (
|
|
30
|
-
<div className="space-y-4">
|
|
31
|
-
{/* Toolbar */}
|
|
32
|
-
<div className="flex items-center justify-between gap-3">
|
|
33
|
-
<button
|
|
34
|
-
type="button"
|
|
35
|
-
onClick={onCheckUpdates}
|
|
36
|
-
disabled={isChecking}
|
|
37
|
-
className="flex items-center gap-1.5 rounded-md border border-border bg-background px-3 py-1.5 text-xs text-foreground hover:bg-surface transition-colors disabled:opacity-50"
|
|
38
|
-
>
|
|
39
|
-
<RotateCcw className={`h-3.5 w-3.5 ${isChecking ? 'animate-spin' : ''}`} />
|
|
40
|
-
{isChecking ? 'Checking…' : 'Check for updates'}
|
|
41
|
-
</button>
|
|
42
|
-
|
|
43
|
-
{updates.length > 0 && (
|
|
44
|
-
<button
|
|
45
|
-
type="button"
|
|
46
|
-
onClick={onUpdateAll}
|
|
47
|
-
className="flex items-center gap-1.5 rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 transition-colors"
|
|
48
|
-
>
|
|
49
|
-
Update all ({updates.length})
|
|
50
|
-
</button>
|
|
51
|
-
)}
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
{updates.length === 0 && !isChecking && (
|
|
55
|
-
<div className="rounded-lg border border-border bg-surface py-10 text-center text-xs text-foreground-subtle">
|
|
56
|
-
All packages are up to date
|
|
57
|
-
</div>
|
|
58
|
-
)}
|
|
59
|
-
|
|
60
|
-
{updates.length > 0 && (
|
|
61
|
-
<div className="space-y-2">
|
|
62
|
-
{updates.map((update) => (
|
|
63
|
-
<div
|
|
64
|
-
key={update.id}
|
|
65
|
-
className="rounded-lg border border-border bg-surface px-4 py-3 flex items-start justify-between gap-4"
|
|
66
|
-
>
|
|
67
|
-
<div className="flex items-start gap-3 min-w-0">
|
|
68
|
-
{/* Icon letter */}
|
|
69
|
-
<div className="flex-shrink-0 h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
70
|
-
<span className="text-xs font-bold text-primary uppercase">
|
|
71
|
-
{update.name.charAt(0)}
|
|
72
|
-
</span>
|
|
73
|
-
</div>
|
|
74
|
-
|
|
75
|
-
<div className="min-w-0">
|
|
76
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
77
|
-
<span className="text-sm font-semibold text-foreground">{update.name}</span>
|
|
78
|
-
{update.requiresRestart ? (
|
|
79
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-warning/10 text-warning px-2 py-0.5 text-[10px] font-medium">
|
|
80
|
-
<RotateCcw className="h-2.5 w-2.5" />
|
|
81
|
-
Restart
|
|
82
|
-
</span>
|
|
83
|
-
) : (
|
|
84
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-green-500/10 text-green-400 px-2 py-0.5 text-[10px] font-medium">
|
|
85
|
-
<Zap className="h-2.5 w-2.5" />
|
|
86
|
-
Hot-swap
|
|
87
|
-
</span>
|
|
88
|
-
)}
|
|
89
|
-
</div>
|
|
90
|
-
|
|
91
|
-
{/* Version diff */}
|
|
92
|
-
<div className="flex items-center gap-1.5 mt-1 text-[10px]">
|
|
93
|
-
<span className="text-foreground-subtle font-mono">v{update.currentVersion}</span>
|
|
94
|
-
<ArrowRight className="h-3 w-3 text-foreground-subtle" />
|
|
95
|
-
<span className="text-green-400 font-mono font-medium">v{update.newVersion}</span>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{update.changelog && (
|
|
99
|
-
<p className="mt-1.5 text-[10px] text-foreground-subtle leading-relaxed line-clamp-2">
|
|
100
|
-
{update.changelog}
|
|
101
|
-
</p>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
|
-
<button
|
|
107
|
-
type="button"
|
|
108
|
-
onClick={() => onUpdate(update.id)}
|
|
109
|
-
className="flex-shrink-0 rounded-md bg-primary/10 text-primary hover:bg-primary/20 px-3 py-1.5 text-xs font-medium transition-colors"
|
|
110
|
-
>
|
|
111
|
-
Update
|
|
112
|
-
</button>
|
|
113
|
-
</div>
|
|
114
|
-
))}
|
|
115
|
-
</div>
|
|
116
|
-
)}
|
|
117
|
-
</div>
|
|
118
|
-
)
|
|
119
|
-
}
|