@camstack/addon-admin-ui 0.1.2 → 0.1.4
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-BoVZEQ1j.js +598 -0
- package/dist/assets/index-DwSc8ann.css +1 -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 +4 -1
- package/src/App.tsx +0 -71
- package/src/components/addons/AddonCard.tsx +0 -355
- package/src/components/addons/AddonUploadZone.tsx +0 -69
- package/src/components/addons/CapabilityBadge.tsx +0 -55
- package/src/components/addons/CapabilityMap.tsx +0 -133
- package/src/components/addons/UpdatesList.tsx +0 -108
- 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 -172
- 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 -105
- package/src/components/metrics/IntegrationUsage.tsx +0 -73
- package/src/components/metrics/PipelineStatus.tsx +0 -74
- package/src/components/metrics/ProcessResources.tsx +0 -123
- 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 -254
- 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 -222
- package/src/pages/Integrations.tsx +0 -333
- 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 -396
- 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 -28
- 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,105 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
3
|
-
import { CapabilityBadges } from '../shared/CapabilityBadges'
|
|
4
|
-
|
|
5
|
-
// agents.listAgents returns Record<string, unknown>[] — typed view for rendering
|
|
6
|
-
interface AgentView {
|
|
7
|
-
id: string
|
|
8
|
-
name: string
|
|
9
|
-
state: 'online' | 'offline' | 'degraded'
|
|
10
|
-
capabilities: string[]
|
|
11
|
-
activeTaskCount: number
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function stateStyle(state: string): { dot: string; label: string } {
|
|
15
|
-
switch (state) {
|
|
16
|
-
case 'online': return { dot: 'bg-success', label: 'text-success' }
|
|
17
|
-
case 'degraded': return { dot: 'bg-warning', label: 'text-warning' }
|
|
18
|
-
case 'offline':
|
|
19
|
-
default: return { dot: 'bg-foreground-subtle/40', label: 'text-foreground-subtle' }
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function AgentLoad() {
|
|
24
|
-
const client = useBackendClient()
|
|
25
|
-
|
|
26
|
-
const { data, isLoading, isError } = useQuery({
|
|
27
|
-
queryKey: ['agents'],
|
|
28
|
-
queryFn: () => client.listAgents(),
|
|
29
|
-
refetchInterval: 5_000,
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
// agents.listAgents returns Record<string, unknown>[] — cast needed until tRPC router is strongly typed
|
|
33
|
-
const agents = (data ?? []) as unknown as readonly AgentView[]
|
|
34
|
-
const onlineCount = agents.filter((a) => a.state === 'online').length
|
|
35
|
-
const totalActive = agents.reduce((sum, a) => sum + (a.activeTaskCount ?? 0), 0)
|
|
36
|
-
|
|
37
|
-
return (
|
|
38
|
-
<div className="rounded-lg border border-border bg-surface p-4">
|
|
39
|
-
<h2 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">
|
|
40
|
-
Agent Load
|
|
41
|
-
</h2>
|
|
42
|
-
|
|
43
|
-
{isLoading && (
|
|
44
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
45
|
-
)}
|
|
46
|
-
|
|
47
|
-
{isError && (
|
|
48
|
-
<div className="text-xs text-danger">Failed to load data</div>
|
|
49
|
-
)}
|
|
50
|
-
|
|
51
|
-
{!isLoading && !isError && agents.length === 0 && (
|
|
52
|
-
<div className="text-xs text-foreground-subtle">No data available</div>
|
|
53
|
-
)}
|
|
54
|
-
|
|
55
|
-
{!isLoading && !isError && agents.length > 0 && (
|
|
56
|
-
<>
|
|
57
|
-
{/* Aggregate */}
|
|
58
|
-
<div className="flex items-center gap-3 mb-3 pb-3 border-b border-border">
|
|
59
|
-
<span className="text-xs text-foreground-subtle">
|
|
60
|
-
<span className="text-foreground font-semibold">{onlineCount}</span>/{agents.length} online
|
|
61
|
-
</span>
|
|
62
|
-
<span className="text-xs text-foreground-subtle">
|
|
63
|
-
<span className="text-foreground font-semibold">{totalActive}</span> active tasks
|
|
64
|
-
</span>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{/* Agent list */}
|
|
68
|
-
<div className="space-y-3">
|
|
69
|
-
{agents.map((a) => {
|
|
70
|
-
const { dot, label: labelClass } = stateStyle(a.state)
|
|
71
|
-
const isIdle = (a.activeTaskCount ?? 0) === 0
|
|
72
|
-
return (
|
|
73
|
-
<div key={a.id} className="space-y-1">
|
|
74
|
-
<div className="flex items-center justify-between gap-2">
|
|
75
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
76
|
-
<span className={`h-2 w-2 flex-shrink-0 rounded-full ${dot}`} />
|
|
77
|
-
<span className="text-xs text-foreground font-medium truncate">
|
|
78
|
-
{a.name ?? a.id}
|
|
79
|
-
</span>
|
|
80
|
-
</div>
|
|
81
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
82
|
-
<span className={`text-[10px] font-medium ${labelClass}`}>
|
|
83
|
-
{a.state}
|
|
84
|
-
</span>
|
|
85
|
-
<span className="text-[10px] text-foreground-subtle tabular-nums bg-surface-hover rounded px-1.5 py-0.5">
|
|
86
|
-
{isIdle
|
|
87
|
-
? 'idle'
|
|
88
|
-
: `${a.activeTaskCount} task${a.activeTaskCount !== 1 ? 's' : ''}`}
|
|
89
|
-
</span>
|
|
90
|
-
</div>
|
|
91
|
-
</div>
|
|
92
|
-
{a.capabilities && a.capabilities.length > 0 && (
|
|
93
|
-
<div className="pl-4">
|
|
94
|
-
<CapabilityBadges capabilities={a.capabilities} />
|
|
95
|
-
</div>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
)
|
|
99
|
-
})}
|
|
100
|
-
</div>
|
|
101
|
-
</>
|
|
102
|
-
)}
|
|
103
|
-
</div>
|
|
104
|
-
)
|
|
105
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
3
|
-
import { ProviderIcon } from '../shared/ProviderIcon'
|
|
4
|
-
import { StatusBadge } from '../shared/StatusBadge'
|
|
5
|
-
|
|
6
|
-
import type { ProviderStatus } from '@camstack/types'
|
|
7
|
-
|
|
8
|
-
function resolveStatus(status: ProviderStatus): string {
|
|
9
|
-
return status.connected ? 'running' : 'stopped'
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function IntegrationUsage() {
|
|
13
|
-
const client = useBackendClient()
|
|
14
|
-
|
|
15
|
-
const { data, isLoading, isError } = useQuery({
|
|
16
|
-
queryKey: ['providers'],
|
|
17
|
-
queryFn: () => client.listProviders(),
|
|
18
|
-
refetchInterval: 10_000,
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
const providers = data ?? []
|
|
22
|
-
|
|
23
|
-
return (
|
|
24
|
-
<div className="rounded-lg border border-border bg-surface p-4">
|
|
25
|
-
<h2 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">
|
|
26
|
-
Integration Usage
|
|
27
|
-
</h2>
|
|
28
|
-
|
|
29
|
-
{isLoading && (
|
|
30
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
31
|
-
)}
|
|
32
|
-
|
|
33
|
-
{isError && (
|
|
34
|
-
<div className="text-xs text-danger">Failed to load data</div>
|
|
35
|
-
)}
|
|
36
|
-
|
|
37
|
-
{!isLoading && !isError && providers.length === 0 && (
|
|
38
|
-
<div className="text-xs text-foreground-subtle">No data available</div>
|
|
39
|
-
)}
|
|
40
|
-
|
|
41
|
-
{!isLoading && !isError && providers.length > 0 && (
|
|
42
|
-
<>
|
|
43
|
-
{/* Aggregate */}
|
|
44
|
-
<div className="flex items-center gap-3 mb-3 pb-3 border-b border-border">
|
|
45
|
-
<span className="text-xs text-foreground-subtle">
|
|
46
|
-
<span className="text-foreground font-semibold">{providers.length}</span> providers
|
|
47
|
-
</span>
|
|
48
|
-
</div>
|
|
49
|
-
|
|
50
|
-
{/* Provider list */}
|
|
51
|
-
<div className="space-y-2">
|
|
52
|
-
{providers.map((p) => (
|
|
53
|
-
<div
|
|
54
|
-
key={p.id}
|
|
55
|
-
className="flex items-center justify-between gap-2"
|
|
56
|
-
>
|
|
57
|
-
<div className="flex items-center gap-2 min-w-0">
|
|
58
|
-
<ProviderIcon type={p.type} size="sm" />
|
|
59
|
-
<span className="text-xs text-foreground font-medium truncate">
|
|
60
|
-
{p.name ?? p.id}
|
|
61
|
-
</span>
|
|
62
|
-
</div>
|
|
63
|
-
<div className="flex items-center gap-2 flex-shrink-0">
|
|
64
|
-
<StatusBadge status={resolveStatus(p.status)} />
|
|
65
|
-
</div>
|
|
66
|
-
</div>
|
|
67
|
-
))}
|
|
68
|
-
</div>
|
|
69
|
-
</>
|
|
70
|
-
)}
|
|
71
|
-
</div>
|
|
72
|
-
)
|
|
73
|
-
}
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
3
|
-
|
|
4
|
-
export function PipelineStatus() {
|
|
5
|
-
const client = useBackendClient()
|
|
6
|
-
|
|
7
|
-
const { data, isLoading, isError } = useQuery({
|
|
8
|
-
queryKey: ['pipelines'],
|
|
9
|
-
queryFn: () => client.listPipelines(),
|
|
10
|
-
refetchInterval: 5_000,
|
|
11
|
-
})
|
|
12
|
-
|
|
13
|
-
const pipelines = data ?? []
|
|
14
|
-
const withSlot = pipelines.filter((p) => p.slot != null)
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="rounded-lg border border-border bg-surface p-4">
|
|
18
|
-
<h2 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">
|
|
19
|
-
Pipeline Status
|
|
20
|
-
</h2>
|
|
21
|
-
|
|
22
|
-
{isLoading && (
|
|
23
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
24
|
-
)}
|
|
25
|
-
|
|
26
|
-
{isError && (
|
|
27
|
-
<div className="text-xs text-danger">Failed to load data</div>
|
|
28
|
-
)}
|
|
29
|
-
|
|
30
|
-
{!isLoading && !isError && pipelines.length === 0 && (
|
|
31
|
-
<div className="text-xs text-foreground-subtle">No data available</div>
|
|
32
|
-
)}
|
|
33
|
-
|
|
34
|
-
{!isLoading && !isError && pipelines.length > 0 && (
|
|
35
|
-
<>
|
|
36
|
-
{/* Aggregate */}
|
|
37
|
-
<div className="flex items-center gap-3 mb-3 pb-3 border-b border-border">
|
|
38
|
-
<span className="text-xs text-foreground-subtle">
|
|
39
|
-
<span className="text-foreground font-semibold">{withSlot.length}</span> active
|
|
40
|
-
{' '}/ <span className="text-foreground font-semibold">{pipelines.length}</span> total
|
|
41
|
-
</span>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
{/* Pipeline list */}
|
|
45
|
-
<div className="space-y-1.5">
|
|
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>
|
|
66
|
-
</div>
|
|
67
|
-
)
|
|
68
|
-
})}
|
|
69
|
-
</div>
|
|
70
|
-
</>
|
|
71
|
-
)}
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
3
|
-
import type { BackendClient } from '@camstack/sdk'
|
|
4
|
-
|
|
5
|
-
type ProcessEntry = Awaited<ReturnType<BackendClient['listProcesses']>>[number]
|
|
6
|
-
|
|
7
|
-
function formatMemoryMB(bytes?: number): string {
|
|
8
|
-
if (bytes == null) return '—'
|
|
9
|
-
return `${(bytes / 1024 / 1024).toFixed(0)} MB`
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function formatUptime(ms?: number): string {
|
|
13
|
-
if (ms == null || ms <= 0) return '—'
|
|
14
|
-
const totalSeconds = Math.floor(ms / 1000)
|
|
15
|
-
const h = Math.floor(totalSeconds / 3600)
|
|
16
|
-
const m = Math.floor((totalSeconds % 3600) / 60)
|
|
17
|
-
const s = totalSeconds % 60
|
|
18
|
-
if (h > 0) return `${h}h ${m}m`
|
|
19
|
-
if (m > 0) return `${m}m ${s}s`
|
|
20
|
-
return `${s}s`
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function stateColor(state: string): string {
|
|
24
|
-
switch (state) {
|
|
25
|
-
case 'running': return 'text-success'
|
|
26
|
-
case 'stopped': return 'text-foreground-subtle'
|
|
27
|
-
case 'crashed':
|
|
28
|
-
case 'error': return 'text-danger'
|
|
29
|
-
case 'starting': return 'text-warning'
|
|
30
|
-
default: return 'text-foreground-subtle'
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export function ProcessResources() {
|
|
35
|
-
const client = useBackendClient()
|
|
36
|
-
|
|
37
|
-
const { data, isLoading, isError } = useQuery({
|
|
38
|
-
queryKey: ['processes'],
|
|
39
|
-
queryFn: () => client.listProcesses(),
|
|
40
|
-
refetchInterval: 5_000,
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const processes = data ?? []
|
|
44
|
-
const runningCount = processes.filter((p) => p.state === 'running').length
|
|
45
|
-
|
|
46
|
-
const totalCpu = processes.reduce((sum, p) => sum + (p.stats?.cpu ?? 0), 0)
|
|
47
|
-
const totalMemory = processes.reduce((sum, p) => sum + (p.stats?.memory ?? 0), 0)
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div className="rounded-lg border border-border bg-surface p-4">
|
|
51
|
-
<h2 className="text-xs font-semibold text-foreground-subtle uppercase tracking-wider mb-3">
|
|
52
|
-
Process Resources
|
|
53
|
-
</h2>
|
|
54
|
-
|
|
55
|
-
{isLoading && (
|
|
56
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
57
|
-
)}
|
|
58
|
-
|
|
59
|
-
{isError && (
|
|
60
|
-
<div className="text-xs text-danger">Failed to load data</div>
|
|
61
|
-
)}
|
|
62
|
-
|
|
63
|
-
{!isLoading && !isError && processes.length === 0 && (
|
|
64
|
-
<div className="text-xs text-foreground-subtle">No data available</div>
|
|
65
|
-
)}
|
|
66
|
-
|
|
67
|
-
{!isLoading && !isError && processes.length > 0 && (
|
|
68
|
-
<>
|
|
69
|
-
{/* System totals */}
|
|
70
|
-
<div className="flex items-center gap-4 mb-3 pb-3 border-b border-border">
|
|
71
|
-
<span className="text-xs text-foreground-subtle">
|
|
72
|
-
<span className="text-foreground font-semibold">{runningCount}</span>/{processes.length} running
|
|
73
|
-
</span>
|
|
74
|
-
<span className="text-xs text-foreground-subtle">
|
|
75
|
-
CPU: <span className="text-foreground font-semibold tabular-nums">{totalCpu.toFixed(1)}%</span>
|
|
76
|
-
</span>
|
|
77
|
-
<span className="text-xs text-foreground-subtle">
|
|
78
|
-
Mem: <span className="text-foreground font-semibold tabular-nums">{formatMemoryMB(totalMemory)}</span>
|
|
79
|
-
</span>
|
|
80
|
-
</div>
|
|
81
|
-
|
|
82
|
-
{/* Process table */}
|
|
83
|
-
<div className="space-y-1.5">
|
|
84
|
-
{/* Header */}
|
|
85
|
-
<div className="grid grid-cols-[1fr_4rem_5rem_4rem_3rem] gap-2 text-[10px] font-medium text-foreground-subtle uppercase tracking-wider pb-1">
|
|
86
|
-
<span>Process</span>
|
|
87
|
-
<span className="text-right">CPU</span>
|
|
88
|
-
<span className="text-right">Memory</span>
|
|
89
|
-
<span className="text-right">Uptime</span>
|
|
90
|
-
<span className="text-right">Rst</span>
|
|
91
|
-
</div>
|
|
92
|
-
|
|
93
|
-
{processes.map((p) => (
|
|
94
|
-
<div
|
|
95
|
-
key={p.id}
|
|
96
|
-
className="grid grid-cols-[1fr_4rem_5rem_4rem_3rem] gap-2 text-xs items-center"
|
|
97
|
-
>
|
|
98
|
-
<div className="flex items-center gap-1.5 min-w-0">
|
|
99
|
-
<span className={`text-[10px] font-semibold ${stateColor(p.state)}`}>
|
|
100
|
-
●
|
|
101
|
-
</span>
|
|
102
|
-
<span className="text-foreground truncate">{p.label ?? p.id}</span>
|
|
103
|
-
</div>
|
|
104
|
-
<span className="tabular-nums text-foreground-subtle text-right">
|
|
105
|
-
{p.stats?.cpu != null ? `${p.stats.cpu.toFixed(1)}%` : '—'}
|
|
106
|
-
</span>
|
|
107
|
-
<span className="tabular-nums text-foreground-subtle text-right">
|
|
108
|
-
{formatMemoryMB(p.stats?.memory)}
|
|
109
|
-
</span>
|
|
110
|
-
<span className="tabular-nums text-foreground-subtle text-right">
|
|
111
|
-
{formatUptime(p.stats?.uptime)}
|
|
112
|
-
</span>
|
|
113
|
-
<span className="tabular-nums text-foreground-subtle text-right">
|
|
114
|
-
{p.restartCount ?? 0}
|
|
115
|
-
</span>
|
|
116
|
-
</div>
|
|
117
|
-
))}
|
|
118
|
-
</div>
|
|
119
|
-
</>
|
|
120
|
-
)}
|
|
121
|
-
</div>
|
|
122
|
-
)
|
|
123
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
interface PhaseSettingsProps {
|
|
2
|
-
readonly motionFps: number
|
|
3
|
-
readonly detectionFps: number
|
|
4
|
-
readonly cooldownMs: number
|
|
5
|
-
readonly maxConcurrentInferences: number | null
|
|
6
|
-
readonly onMotionFpsChange: (v: number) => void
|
|
7
|
-
readonly onDetectionFpsChange: (v: number) => void
|
|
8
|
-
readonly onCooldownMsChange: (v: number) => void
|
|
9
|
-
readonly onMaxConcurrentInferencesChange: (v: number | null) => void
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
interface SliderFieldProps {
|
|
13
|
-
readonly label: string
|
|
14
|
-
readonly value: number
|
|
15
|
-
readonly min: number
|
|
16
|
-
readonly max: number
|
|
17
|
-
readonly step?: number
|
|
18
|
-
readonly unit?: string
|
|
19
|
-
readonly hint?: string
|
|
20
|
-
readonly onChange: (v: number) => void
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function SliderField({ label, value, min, max, step = 1, unit, hint, onChange }: SliderFieldProps) {
|
|
24
|
-
return (
|
|
25
|
-
<div>
|
|
26
|
-
<div className="flex items-center justify-between mb-1">
|
|
27
|
-
<label className="text-[10px] font-medium text-foreground-subtle">{label}</label>
|
|
28
|
-
{hint && <span className="text-[9px] text-foreground-subtle/50">{hint}</span>}
|
|
29
|
-
</div>
|
|
30
|
-
<div className="flex items-center gap-3">
|
|
31
|
-
<input
|
|
32
|
-
type="range"
|
|
33
|
-
className="flex-1 h-1 accent-primary cursor-pointer"
|
|
34
|
-
min={min}
|
|
35
|
-
max={max}
|
|
36
|
-
step={step}
|
|
37
|
-
value={value}
|
|
38
|
-
onChange={(e) => onChange(Number(e.target.value))}
|
|
39
|
-
/>
|
|
40
|
-
<span className="text-xs text-foreground tabular-nums min-w-[3rem] text-right">
|
|
41
|
-
{value}{unit ? ` ${unit}` : ''}
|
|
42
|
-
</span>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function PhaseSettings({
|
|
49
|
-
motionFps,
|
|
50
|
-
detectionFps,
|
|
51
|
-
cooldownMs,
|
|
52
|
-
maxConcurrentInferences,
|
|
53
|
-
onMotionFpsChange,
|
|
54
|
-
onDetectionFpsChange,
|
|
55
|
-
onCooldownMsChange,
|
|
56
|
-
onMaxConcurrentInferencesChange,
|
|
57
|
-
}: PhaseSettingsProps) {
|
|
58
|
-
const isAuto = maxConcurrentInferences === null
|
|
59
|
-
|
|
60
|
-
return (
|
|
61
|
-
<div className="space-y-4">
|
|
62
|
-
<h3 className="text-[10px] font-semibold text-foreground uppercase tracking-wider">
|
|
63
|
-
Orchestrator
|
|
64
|
-
</h3>
|
|
65
|
-
<SliderField
|
|
66
|
-
label="Motion FPS"
|
|
67
|
-
value={motionFps}
|
|
68
|
-
min={1}
|
|
69
|
-
max={15}
|
|
70
|
-
onChange={onMotionFpsChange}
|
|
71
|
-
unit="fps"
|
|
72
|
-
hint="Frame-diff rate"
|
|
73
|
-
/>
|
|
74
|
-
<SliderField
|
|
75
|
-
label="Detection FPS"
|
|
76
|
-
value={detectionFps}
|
|
77
|
-
min={1}
|
|
78
|
-
max={30}
|
|
79
|
-
onChange={onDetectionFpsChange}
|
|
80
|
-
unit="fps"
|
|
81
|
-
hint="Inference rate"
|
|
82
|
-
/>
|
|
83
|
-
<SliderField
|
|
84
|
-
label="Motion Cooldown"
|
|
85
|
-
value={cooldownMs / 1000}
|
|
86
|
-
min={1}
|
|
87
|
-
max={60}
|
|
88
|
-
onChange={(v) => onCooldownMsChange(v * 1000)}
|
|
89
|
-
unit="s"
|
|
90
|
-
hint="No motion → idle"
|
|
91
|
-
/>
|
|
92
|
-
|
|
93
|
-
{/* Max concurrent inferences with Auto toggle */}
|
|
94
|
-
<div>
|
|
95
|
-
<div className="flex items-center justify-between mb-1">
|
|
96
|
-
<label className="text-[10px] font-medium text-foreground-subtle">Max Parallel</label>
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
onClick={() => onMaxConcurrentInferencesChange(isAuto ? 2 : null)}
|
|
100
|
-
className={`text-[9px] font-medium px-1.5 py-0.5 rounded transition-colors ${
|
|
101
|
-
isAuto
|
|
102
|
-
? 'bg-primary/15 text-primary'
|
|
103
|
-
: 'bg-surface text-foreground-subtle hover:text-foreground'
|
|
104
|
-
}`}
|
|
105
|
-
>
|
|
106
|
-
{isAuto ? 'Auto' : 'Manual'}
|
|
107
|
-
</button>
|
|
108
|
-
</div>
|
|
109
|
-
{isAuto ? (
|
|
110
|
-
<p className="text-[10px] text-foreground-subtle/60">
|
|
111
|
-
Auto-detected from hardware (CPU cores, GPU)
|
|
112
|
-
</p>
|
|
113
|
-
) : (
|
|
114
|
-
<div className="flex items-center gap-3">
|
|
115
|
-
<input
|
|
116
|
-
type="range"
|
|
117
|
-
className="flex-1 h-1 accent-primary cursor-pointer"
|
|
118
|
-
min={1}
|
|
119
|
-
max={8}
|
|
120
|
-
value={maxConcurrentInferences}
|
|
121
|
-
onChange={(e) => onMaxConcurrentInferencesChange(Number(e.target.value))}
|
|
122
|
-
/>
|
|
123
|
-
<span className="text-xs text-foreground tabular-nums min-w-[3rem] text-right">
|
|
124
|
-
{maxConcurrentInferences}
|
|
125
|
-
</span>
|
|
126
|
-
</div>
|
|
127
|
-
)}
|
|
128
|
-
</div>
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { Video, Eye, CircleDot, Mic } from 'lucide-react'
|
|
2
|
-
import type { LucideIcon } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
const CAPABILITIES: Record<string, { icon: LucideIcon; label: string }> = {
|
|
5
|
-
streaming: { icon: Video, label: 'Stream' },
|
|
6
|
-
detection: { icon: Eye, label: 'Detection' },
|
|
7
|
-
recording: { icon: CircleDot, label: 'Recording' },
|
|
8
|
-
audio: { icon: Mic, label: 'Audio' },
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
interface CapabilityBadgesProps {
|
|
12
|
-
capabilities: string[]
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function CapabilityBadges({ capabilities }: CapabilityBadgesProps) {
|
|
16
|
-
return (
|
|
17
|
-
<div className="flex items-center gap-1">
|
|
18
|
-
{capabilities.map((cap) => {
|
|
19
|
-
const info = CAPABILITIES[cap]
|
|
20
|
-
if (!info) return null
|
|
21
|
-
const Icon = info.icon
|
|
22
|
-
return (
|
|
23
|
-
<span key={cap} className="inline-flex items-center gap-1 rounded-md bg-surface-hover px-1.5 py-0.5 text-[10px] text-foreground-subtle" title={info.label}>
|
|
24
|
-
<Icon className="h-3 w-3" />
|
|
25
|
-
</span>
|
|
26
|
-
)
|
|
27
|
-
})}
|
|
28
|
-
</div>
|
|
29
|
-
)
|
|
30
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { Cctv, Server, Radio, Home, Wifi } from 'lucide-react'
|
|
2
|
-
import type { LucideIcon } from 'lucide-react'
|
|
3
|
-
|
|
4
|
-
const PROVIDERS: Record<string, { icon: LucideIcon; color: string; label: string }> = {
|
|
5
|
-
frigate: { icon: Cctv, color: '#3b82f6', label: 'Frigate' },
|
|
6
|
-
scrypted: { icon: Server, color: '#a855f7', label: 'Scrypted' },
|
|
7
|
-
reolink: { icon: Radio, color: '#06b6d4', label: 'Reolink' },
|
|
8
|
-
homeassistant: { icon: Home, color: '#22d3ee', label: 'Home Assistant' },
|
|
9
|
-
rtsp: { icon: Wifi, color: '#78716c', label: 'RTSP' },
|
|
10
|
-
onvif: { icon: Wifi, color: '#78716c', label: 'ONVIF' },
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface ProviderIconProps {
|
|
14
|
-
type: string
|
|
15
|
-
size?: 'sm' | 'md' | 'lg'
|
|
16
|
-
showLabel?: boolean
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const SIZES = { sm: 'h-4 w-4', md: 'h-5 w-5', lg: 'h-6 w-6' }
|
|
20
|
-
const CONTAINER_SIZES = { sm: 'h-7 w-7', md: 'h-8 w-8', lg: 'h-10 w-10' }
|
|
21
|
-
|
|
22
|
-
export function ProviderIcon({ type, size = 'md', showLabel }: ProviderIconProps) {
|
|
23
|
-
const provider = PROVIDERS[type] ?? PROVIDERS['rtsp']!
|
|
24
|
-
const Icon = provider.icon
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className="flex items-center gap-2">
|
|
28
|
-
<div className={`flex items-center justify-center rounded-lg ${CONTAINER_SIZES[size]}`} style={{ backgroundColor: `${provider.color}15` }}>
|
|
29
|
-
<Icon className={SIZES[size]} style={{ color: provider.color }} />
|
|
30
|
-
</div>
|
|
31
|
-
{showLabel && <span className="text-xs font-medium text-foreground-subtle">{provider.label}</span>}
|
|
32
|
-
</div>
|
|
33
|
-
)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function getProviderColor(type: string): string {
|
|
37
|
-
return PROVIDERS[type]?.color ?? '#78716c'
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getProviderLabel(type: string): string {
|
|
41
|
-
return PROVIDERS[type]?.label ?? type
|
|
42
|
-
}
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
const STATUS_STYLES: Record<string, { bg: string; text: string; dot: string; label: string }> = {
|
|
2
|
-
running: { bg: 'bg-success/10', text: 'text-success', dot: 'bg-success', label: 'Running' },
|
|
3
|
-
stopped: { bg: 'bg-foreground-subtle/10', text: 'text-foreground-subtle', dot: 'bg-foreground-subtle', label: 'Stopped' },
|
|
4
|
-
error: { bg: 'bg-danger/10', text: 'text-danger', dot: 'bg-danger', label: 'Error' },
|
|
5
|
-
online: { bg: 'bg-success/10', text: 'text-success', dot: 'bg-success', label: 'Online' },
|
|
6
|
-
offline: { bg: 'bg-foreground-subtle/10', text: 'text-foreground-subtle', dot: 'bg-foreground-subtle', label: 'Offline' },
|
|
7
|
-
idle: { bg: 'bg-info/10', text: 'text-info', dot: 'bg-info', label: 'Idle' },
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface StatusBadgeProps {
|
|
11
|
-
status: string
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function StatusBadge({ status }: StatusBadgeProps) {
|
|
15
|
-
const style = STATUS_STYLES[status] ?? STATUS_STYLES['stopped']!
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<span className={`inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-[10px] font-medium ${style.bg} ${style.text}`}>
|
|
19
|
-
<span className={`h-1.5 w-1.5 rounded-full ${style.dot}`} />
|
|
20
|
-
{style.label}
|
|
21
|
-
</span>
|
|
22
|
-
)
|
|
23
|
-
}
|