@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,108 +0,0 @@
|
|
|
1
|
-
import { ArrowRight, RotateCcw, Zap } from 'lucide-react'
|
|
2
|
-
|
|
3
|
-
// ---------------------------------------------------------------------------
|
|
4
|
-
// Types — inferred from tRPC update.listUpdates return type
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
|
-
import type { BackendClient } from '@camstack/sdk'
|
|
8
|
-
|
|
9
|
-
export type UpdateEntry = Awaited<ReturnType<BackendClient['trpc']['update']['listUpdates']['query']>>[number]
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// UpdatesList
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
interface UpdatesListProps {
|
|
16
|
-
updates: readonly UpdateEntry[]
|
|
17
|
-
onUpdate: (name: string) => void
|
|
18
|
-
onUpdateAll: () => void
|
|
19
|
-
onCheckUpdates: () => void
|
|
20
|
-
isChecking: boolean
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function UpdatesList({ updates, onUpdate, onUpdateAll, onCheckUpdates, isChecking }: UpdatesListProps) {
|
|
24
|
-
return (
|
|
25
|
-
<div className="space-y-4">
|
|
26
|
-
{/* Toolbar */}
|
|
27
|
-
<div className="flex items-center justify-between gap-3">
|
|
28
|
-
<button
|
|
29
|
-
type="button"
|
|
30
|
-
onClick={onCheckUpdates}
|
|
31
|
-
disabled={isChecking}
|
|
32
|
-
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"
|
|
33
|
-
>
|
|
34
|
-
<RotateCcw className={`h-3.5 w-3.5 ${isChecking ? 'animate-spin' : ''}`} />
|
|
35
|
-
{isChecking ? 'Checking…' : 'Check for updates'}
|
|
36
|
-
</button>
|
|
37
|
-
|
|
38
|
-
{updates.length > 0 && (
|
|
39
|
-
<button
|
|
40
|
-
type="button"
|
|
41
|
-
onClick={onUpdateAll}
|
|
42
|
-
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"
|
|
43
|
-
>
|
|
44
|
-
Update all ({updates.length})
|
|
45
|
-
</button>
|
|
46
|
-
)}
|
|
47
|
-
</div>
|
|
48
|
-
|
|
49
|
-
{updates.length === 0 && !isChecking && (
|
|
50
|
-
<div className="rounded-lg border border-border bg-surface py-10 text-center text-xs text-foreground-subtle">
|
|
51
|
-
All packages are up to date
|
|
52
|
-
</div>
|
|
53
|
-
)}
|
|
54
|
-
|
|
55
|
-
{updates.length > 0 && (
|
|
56
|
-
<div className="space-y-2">
|
|
57
|
-
{updates.map((update) => (
|
|
58
|
-
<div
|
|
59
|
-
key={update.name}
|
|
60
|
-
className="rounded-lg border border-border bg-surface px-4 py-3 flex items-start justify-between gap-4"
|
|
61
|
-
>
|
|
62
|
-
<div className="flex items-start gap-3 min-w-0">
|
|
63
|
-
{/* Icon letter */}
|
|
64
|
-
<div className="flex-shrink-0 h-8 w-8 rounded-lg bg-primary/10 flex items-center justify-center">
|
|
65
|
-
<span className="text-xs font-bold text-primary uppercase">
|
|
66
|
-
{update.name.charAt(0)}
|
|
67
|
-
</span>
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<div className="min-w-0">
|
|
71
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
72
|
-
<span className="text-sm font-semibold text-foreground">{update.name}</span>
|
|
73
|
-
{update.requiresRestart ? (
|
|
74
|
-
<span className="inline-flex items-center gap-1 rounded-full bg-warning/10 text-warning px-2 py-0.5 text-[10px] font-medium">
|
|
75
|
-
<RotateCcw className="h-2.5 w-2.5" />
|
|
76
|
-
Restart
|
|
77
|
-
</span>
|
|
78
|
-
) : (
|
|
79
|
-
<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">
|
|
80
|
-
<Zap className="h-2.5 w-2.5" />
|
|
81
|
-
Hot-swap
|
|
82
|
-
</span>
|
|
83
|
-
)}
|
|
84
|
-
</div>
|
|
85
|
-
|
|
86
|
-
{/* Version diff */}
|
|
87
|
-
<div className="flex items-center gap-1.5 mt-1 text-[10px]">
|
|
88
|
-
<span className="text-foreground-subtle font-mono">v{update.currentVersion}</span>
|
|
89
|
-
<ArrowRight className="h-3 w-3 text-foreground-subtle" />
|
|
90
|
-
<span className="text-green-400 font-mono font-medium">v{update.latestVersion}</span>
|
|
91
|
-
</div>
|
|
92
|
-
</div>
|
|
93
|
-
</div>
|
|
94
|
-
|
|
95
|
-
<button
|
|
96
|
-
type="button"
|
|
97
|
-
onClick={() => onUpdate(update.name)}
|
|
98
|
-
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"
|
|
99
|
-
>
|
|
100
|
-
Update
|
|
101
|
-
</button>
|
|
102
|
-
</div>
|
|
103
|
-
))}
|
|
104
|
-
</div>
|
|
105
|
-
)}
|
|
106
|
-
</div>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
@@ -1,281 +0,0 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import {
|
|
3
|
-
ChevronDown,
|
|
4
|
-
ChevronUp,
|
|
5
|
-
Cpu,
|
|
6
|
-
HardDrive,
|
|
7
|
-
Monitor,
|
|
8
|
-
Layers,
|
|
9
|
-
Activity,
|
|
10
|
-
Server,
|
|
11
|
-
} from 'lucide-react'
|
|
12
|
-
import { TaskList } from './TaskList'
|
|
13
|
-
import { ProcessList } from './ProcessList'
|
|
14
|
-
import type { AgentTask } from './TaskList'
|
|
15
|
-
import type { ProcessEntry } from './ProcessList'
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Types
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export interface AgentRegistrationInfo {
|
|
22
|
-
id: string
|
|
23
|
-
name: string
|
|
24
|
-
capabilities: string[]
|
|
25
|
-
host: string
|
|
26
|
-
port: number
|
|
27
|
-
platform: string
|
|
28
|
-
arch: string
|
|
29
|
-
cpuModel?: string
|
|
30
|
-
cpuCores: number
|
|
31
|
-
memoryMB: number
|
|
32
|
-
gpuModel?: string
|
|
33
|
-
pythonRuntimes: string[]
|
|
34
|
-
httpPort: number
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface AgentRuntimeStatus {
|
|
38
|
-
activeCameras: number
|
|
39
|
-
cpuPercent: number
|
|
40
|
-
memoryPercent: number
|
|
41
|
-
fps: Record<string, number>
|
|
42
|
-
errors: string[]
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface ConnectedAgent {
|
|
46
|
-
info: AgentRegistrationInfo
|
|
47
|
-
status?: AgentRuntimeStatus
|
|
48
|
-
connectedSince: number
|
|
49
|
-
isHub?: boolean
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ---------------------------------------------------------------------------
|
|
53
|
-
// Sub-components
|
|
54
|
-
// ---------------------------------------------------------------------------
|
|
55
|
-
|
|
56
|
-
function StatusDot({ online }: { online: boolean }) {
|
|
57
|
-
return (
|
|
58
|
-
<span
|
|
59
|
-
className={`inline-block h-2.5 w-2.5 rounded-full shrink-0 ${online ? 'bg-green-400' : 'bg-red-400'}`}
|
|
60
|
-
title={online ? 'Online' : 'Offline'}
|
|
61
|
-
/>
|
|
62
|
-
)
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function Bar({ percent, color = 'bg-primary' }: { percent: number; color?: string }) {
|
|
66
|
-
return (
|
|
67
|
-
<div className="h-1.5 w-full rounded-full bg-border overflow-hidden">
|
|
68
|
-
<div
|
|
69
|
-
className={`h-full rounded-full transition-all ${color}`}
|
|
70
|
-
style={{ width: `${Math.min(100, percent)}%` }}
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const CAPABILITY_COLORS: Record<string, string> = {
|
|
77
|
-
decode: 'bg-blue-500/10 text-blue-400',
|
|
78
|
-
decoder: 'bg-blue-500/10 text-blue-400',
|
|
79
|
-
detect: 'bg-green-500/10 text-green-400',
|
|
80
|
-
detector: 'bg-green-500/10 text-green-400',
|
|
81
|
-
record: 'bg-orange-500/10 text-orange-400',
|
|
82
|
-
recorder: 'bg-orange-500/10 text-orange-400',
|
|
83
|
-
transcode: 'bg-purple-500/10 text-purple-400',
|
|
84
|
-
transcoder: 'bg-purple-500/10 text-purple-400',
|
|
85
|
-
restream: 'bg-cyan-500/10 text-cyan-400',
|
|
86
|
-
restreamer: 'bg-cyan-500/10 text-cyan-400',
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const RUNTIME_COLORS: Record<string, string> = {
|
|
90
|
-
onnx: 'bg-yellow-500/10 text-yellow-400',
|
|
91
|
-
coreml: 'bg-pink-500/10 text-pink-400',
|
|
92
|
-
pytorch: 'bg-orange-500/10 text-orange-400',
|
|
93
|
-
openvino:'bg-blue-500/10 text-blue-400',
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function CapBadge({ cap }: { cap: string }) {
|
|
97
|
-
const lower = cap.toLowerCase()
|
|
98
|
-
const color = CAPABILITY_COLORS[lower] ?? 'bg-primary/10 text-primary'
|
|
99
|
-
return (
|
|
100
|
-
<span className={`inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-medium ${color}`}>
|
|
101
|
-
{cap}
|
|
102
|
-
</span>
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function RuntimeBadge({ runtime }: { runtime: string }) {
|
|
107
|
-
const lower = runtime.toLowerCase()
|
|
108
|
-
const color = RUNTIME_COLORS[lower] ?? 'bg-foreground-subtle/10 text-foreground-subtle'
|
|
109
|
-
return (
|
|
110
|
-
<span className={`inline-flex rounded-md px-1.5 py-0.5 text-[10px] font-medium ${color}`}>
|
|
111
|
-
{runtime}
|
|
112
|
-
</span>
|
|
113
|
-
)
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function formatUptime(connectedSince: number): string {
|
|
117
|
-
const seconds = Math.floor((Date.now() - connectedSince) / 1000)
|
|
118
|
-
if (seconds < 60) return `${seconds}s`
|
|
119
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m`
|
|
120
|
-
return `${Math.floor(seconds / 3600)}h`
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// AgentCard
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
|
|
127
|
-
interface AgentCardProps {
|
|
128
|
-
agent: ConnectedAgent
|
|
129
|
-
processes: ProcessEntry[]
|
|
130
|
-
tasks: AgentTask[]
|
|
131
|
-
defaultExpanded?: boolean
|
|
132
|
-
online?: boolean
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function AgentCard({ agent, processes, tasks, defaultExpanded = false, online = true }: AgentCardProps) {
|
|
136
|
-
const [expanded, setExpanded] = useState(defaultExpanded)
|
|
137
|
-
const { info, status } = agent
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<div className={`rounded-xl border border-border bg-surface overflow-hidden transition-opacity ${!online ? 'opacity-50' : ''}`}>
|
|
141
|
-
{/* Header */}
|
|
142
|
-
<button
|
|
143
|
-
type="button"
|
|
144
|
-
className="w-full flex items-start justify-between gap-3 px-4 py-3 hover:bg-primary/5 transition-colors text-left"
|
|
145
|
-
onClick={() => setExpanded((e) => !e)}
|
|
146
|
-
>
|
|
147
|
-
<div className="flex items-center gap-2.5 min-w-0">
|
|
148
|
-
<StatusDot online={online} />
|
|
149
|
-
<Server className="h-4 w-4 shrink-0 text-foreground-subtle" />
|
|
150
|
-
<div className="min-w-0">
|
|
151
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
152
|
-
<span className="text-sm font-semibold text-foreground truncate">{info.name}</span>
|
|
153
|
-
{agent.isHub && (
|
|
154
|
-
<span className="rounded-full bg-primary/10 text-primary px-2 py-0.5 text-[10px] font-medium shrink-0">HUB</span>
|
|
155
|
-
)}
|
|
156
|
-
{!online && (
|
|
157
|
-
<span className="rounded-full bg-foreground-subtle/10 text-foreground-subtle px-2 py-0.5 text-[10px] shrink-0">Offline</span>
|
|
158
|
-
)}
|
|
159
|
-
</div>
|
|
160
|
-
<div className="flex items-center gap-2 mt-0.5 text-[10px] text-foreground-subtle flex-wrap">
|
|
161
|
-
<span className="font-mono">{info.host}:{info.port}</span>
|
|
162
|
-
<span>{info.platform}/{info.arch}</span>
|
|
163
|
-
{status && (
|
|
164
|
-
<>
|
|
165
|
-
<span>CPU {status.cpuPercent.toFixed(0)}%</span>
|
|
166
|
-
<span>MEM {status.memoryPercent.toFixed(0)}%</span>
|
|
167
|
-
</>
|
|
168
|
-
)}
|
|
169
|
-
</div>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
172
|
-
|
|
173
|
-
<div className="flex items-center gap-2 shrink-0">
|
|
174
|
-
{/* task count badge */}
|
|
175
|
-
{tasks.length > 0 && (
|
|
176
|
-
<span className="rounded-full bg-info/10 text-info px-2 py-0.5 text-[10px] font-medium">
|
|
177
|
-
{tasks.length} task{tasks.length !== 1 ? 's' : ''}
|
|
178
|
-
</span>
|
|
179
|
-
)}
|
|
180
|
-
{/* runtime badges */}
|
|
181
|
-
{info.pythonRuntimes.slice(0, 2).map((rt) => (
|
|
182
|
-
<RuntimeBadge key={rt} runtime={rt} />
|
|
183
|
-
))}
|
|
184
|
-
<span className="text-[10px] text-foreground-subtle">up {formatUptime(agent.connectedSince)}</span>
|
|
185
|
-
{expanded ? <ChevronUp className="h-4 w-4 text-foreground-subtle" /> : <ChevronDown className="h-4 w-4 text-foreground-subtle" />}
|
|
186
|
-
</div>
|
|
187
|
-
</button>
|
|
188
|
-
|
|
189
|
-
{/* Load bars (always visible when online) */}
|
|
190
|
-
{status && !expanded && (
|
|
191
|
-
<div className="px-4 pb-3 space-y-1">
|
|
192
|
-
<Bar percent={status.cpuPercent} color="bg-blue-400" />
|
|
193
|
-
<Bar percent={status.memoryPercent} color="bg-purple-400" />
|
|
194
|
-
</div>
|
|
195
|
-
)}
|
|
196
|
-
|
|
197
|
-
{/* Expanded content */}
|
|
198
|
-
{expanded && (
|
|
199
|
-
<div className="border-t border-border divide-y divide-border">
|
|
200
|
-
{/* Hardware details */}
|
|
201
|
-
<div className="px-4 py-3 grid grid-cols-2 gap-x-4 gap-y-1.5 text-[10px] text-foreground-subtle">
|
|
202
|
-
<div className="flex items-center gap-1.5">
|
|
203
|
-
<Cpu className="h-3 w-3 shrink-0" />
|
|
204
|
-
<span className="truncate">{info.cpuModel ?? `${info.cpuCores} cores`}</span>
|
|
205
|
-
</div>
|
|
206
|
-
<div className="flex items-center gap-1.5">
|
|
207
|
-
<HardDrive className="h-3 w-3 shrink-0" />
|
|
208
|
-
<span>{Math.round(info.memoryMB / 1024)} GB RAM</span>
|
|
209
|
-
</div>
|
|
210
|
-
<div className="flex items-center gap-1.5">
|
|
211
|
-
<Monitor className="h-3 w-3 shrink-0" />
|
|
212
|
-
<span>{info.platform}/{info.arch}</span>
|
|
213
|
-
</div>
|
|
214
|
-
{info.gpuModel && (
|
|
215
|
-
<div className="flex items-center gap-1.5">
|
|
216
|
-
<Layers className="h-3 w-3 shrink-0" />
|
|
217
|
-
<span className="truncate">{info.gpuModel}</span>
|
|
218
|
-
</div>
|
|
219
|
-
)}
|
|
220
|
-
</div>
|
|
221
|
-
|
|
222
|
-
{/* Load bars */}
|
|
223
|
-
{status && (
|
|
224
|
-
<div className="px-4 py-3 space-y-2">
|
|
225
|
-
<div className="flex items-center justify-between text-[10px] text-foreground-subtle mb-1">
|
|
226
|
-
<span className="flex items-center gap-1">
|
|
227
|
-
<Activity className="h-3 w-3" />
|
|
228
|
-
{status.activeCameras} active camera{status.activeCameras !== 1 ? 's' : ''}
|
|
229
|
-
</span>
|
|
230
|
-
<span>CPU {status.cpuPercent.toFixed(0)}% / MEM {status.memoryPercent.toFixed(0)}%</span>
|
|
231
|
-
</div>
|
|
232
|
-
<Bar percent={status.cpuPercent} color="bg-blue-400" />
|
|
233
|
-
<Bar percent={status.memoryPercent} color="bg-purple-400" />
|
|
234
|
-
</div>
|
|
235
|
-
)}
|
|
236
|
-
|
|
237
|
-
{/* Capabilities */}
|
|
238
|
-
<div className="px-4 py-3 space-y-2">
|
|
239
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">Capabilities</div>
|
|
240
|
-
<div className="flex flex-wrap gap-1.5">
|
|
241
|
-
{info.capabilities.map((cap) => (
|
|
242
|
-
<CapBadge key={cap} cap={cap} />
|
|
243
|
-
))}
|
|
244
|
-
{info.capabilities.length === 0 && (
|
|
245
|
-
<span className="text-[10px] text-foreground-subtle italic">None registered</span>
|
|
246
|
-
)}
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
{/* Runtimes */}
|
|
251
|
-
{info.pythonRuntimes.length > 0 && (
|
|
252
|
-
<div className="px-4 py-3 space-y-2">
|
|
253
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">Runtimes</div>
|
|
254
|
-
<div className="flex flex-wrap gap-1.5">
|
|
255
|
-
{info.pythonRuntimes.map((rt) => (
|
|
256
|
-
<RuntimeBadge key={rt} runtime={rt} />
|
|
257
|
-
))}
|
|
258
|
-
</div>
|
|
259
|
-
</div>
|
|
260
|
-
)}
|
|
261
|
-
|
|
262
|
-
{/* Active Tasks */}
|
|
263
|
-
<div className="px-4 py-3 space-y-2">
|
|
264
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">
|
|
265
|
-
Active Tasks ({tasks.length})
|
|
266
|
-
</div>
|
|
267
|
-
<TaskList tasks={tasks} />
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
{/* Processes */}
|
|
271
|
-
<div className="px-4 py-3 space-y-2">
|
|
272
|
-
<div className="text-[10px] font-medium text-foreground-subtle uppercase tracking-wide">
|
|
273
|
-
Processes ({processes.length})
|
|
274
|
-
</div>
|
|
275
|
-
<ProcessList processes={processes} />
|
|
276
|
-
</div>
|
|
277
|
-
</div>
|
|
278
|
-
)}
|
|
279
|
-
</div>
|
|
280
|
-
)
|
|
281
|
-
}
|
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useMemo, useCallback } from 'react'
|
|
2
|
-
import { useQuery } from '@tanstack/react-query'
|
|
3
|
-
import { ArrowDownToLine, Filter } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Types
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
type LogLevel = 'all' | 'debug' | 'info' | 'warn' | 'error'
|
|
11
|
-
|
|
12
|
-
interface AgentLogsProps {
|
|
13
|
-
/** List of agent names/IDs for the filter dropdown */
|
|
14
|
-
agentNames: readonly string[]
|
|
15
|
-
/** List of addon names for the filter dropdown */
|
|
16
|
-
addonNames: readonly string[]
|
|
17
|
-
/** Pre-selected agent name from clicking an agent in the tree */
|
|
18
|
-
preselectedAgent?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
// Styles
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
|
|
25
|
-
const LEVEL_STYLES: Record<string, { badge: string; text: string }> = {
|
|
26
|
-
debug: { badge: 'bg-foreground-subtle/10 text-foreground-subtle', text: 'text-foreground-subtle' },
|
|
27
|
-
info: { badge: 'bg-info/10 text-info', text: 'text-info' },
|
|
28
|
-
warn: { badge: 'bg-warning/10 text-warning', text: 'text-warning' },
|
|
29
|
-
error: { badge: 'bg-danger/10 text-danger', text: 'text-danger' },
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// Component
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
export function AgentLogs({ agentNames, addonNames, preselectedAgent }: AgentLogsProps) {
|
|
37
|
-
const client = useBackendClient()
|
|
38
|
-
const [levelFilter, setLevelFilter] = useState<LogLevel>('all')
|
|
39
|
-
const [agentFilter, setAgentFilter] = useState<string>(preselectedAgent ?? 'all')
|
|
40
|
-
const [addonFilter, setAddonFilter] = useState<string>('all')
|
|
41
|
-
|
|
42
|
-
// Sync agentFilter when preselectedAgent changes externally
|
|
43
|
-
const prevPreselected = useRef(preselectedAgent)
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
if (preselectedAgent !== prevPreselected.current) {
|
|
46
|
-
setAgentFilter(preselectedAgent ?? 'all')
|
|
47
|
-
prevPreselected.current = preselectedAgent
|
|
48
|
-
}
|
|
49
|
-
}, [preselectedAgent])
|
|
50
|
-
const [autoScroll, setAutoScroll] = useState(true)
|
|
51
|
-
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
|
52
|
-
// bottomRef removed — auto-scroll goes to top (newest first)
|
|
53
|
-
|
|
54
|
-
// Build scope filters from agent + addon selections
|
|
55
|
-
const scopeFilter = useMemo(() => {
|
|
56
|
-
const scopes: string[] = []
|
|
57
|
-
if (agentFilter !== 'all') scopes.push(agentFilter)
|
|
58
|
-
if (addonFilter !== 'all') scopes.push(addonFilter)
|
|
59
|
-
return scopes.length > 0 ? scopes : undefined
|
|
60
|
-
}, [agentFilter, addonFilter])
|
|
61
|
-
|
|
62
|
-
// Always fetch live -- no pause concept
|
|
63
|
-
const { data: logsData, isLoading, isError } = useQuery({
|
|
64
|
-
queryKey: ['agent-logs', levelFilter, agentFilter, addonFilter],
|
|
65
|
-
queryFn: () =>
|
|
66
|
-
client.getLogs({
|
|
67
|
-
...(levelFilter !== 'all' ? { level: levelFilter as 'debug' | 'info' | 'warn' | 'error' } : {}),
|
|
68
|
-
limit: 300,
|
|
69
|
-
...(scopeFilter ? { scope: scopeFilter } : {}),
|
|
70
|
-
} as Parameters<typeof client.getLogs>[0]),
|
|
71
|
-
refetchInterval: 3000,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const rawLogs = (logsData ?? []) as unknown as Array<Record<string, unknown>>
|
|
75
|
-
|
|
76
|
-
// Newest first — most recent logs at the top
|
|
77
|
-
const logs = useMemo(() => [...rawLogs].reverse(), [rawLogs])
|
|
78
|
-
|
|
79
|
-
// Auto-scroll to top when new logs arrive (newest is at top)
|
|
80
|
-
useEffect(() => {
|
|
81
|
-
if (autoScroll) {
|
|
82
|
-
scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
83
|
-
}
|
|
84
|
-
}, [logs, autoScroll])
|
|
85
|
-
|
|
86
|
-
// Detect user scroll to auto-disable auto-scroll when scrolling down
|
|
87
|
-
const handleScroll = useCallback(() => {
|
|
88
|
-
const container = scrollContainerRef.current
|
|
89
|
-
if (!container) return
|
|
90
|
-
const isAtTop = container.scrollTop < 40
|
|
91
|
-
setAutoScroll(isAtTop)
|
|
92
|
-
}, [])
|
|
93
|
-
|
|
94
|
-
function formatTs(ts: unknown): string {
|
|
95
|
-
if (!ts) return '--'
|
|
96
|
-
const d = new Date(typeof ts === 'number' ? ts : String(ts))
|
|
97
|
-
return d.toLocaleTimeString('en-GB', {
|
|
98
|
-
hour12: false,
|
|
99
|
-
hour: '2-digit',
|
|
100
|
-
minute: '2-digit',
|
|
101
|
-
second: '2-digit',
|
|
102
|
-
fractionalSecondDigits: 3,
|
|
103
|
-
})
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<div className="space-y-3">
|
|
108
|
-
{/* Filters bar */}
|
|
109
|
-
<div className="flex items-center gap-2 flex-wrap">
|
|
110
|
-
<Filter className="h-3.5 w-3.5 text-foreground-subtle shrink-0" />
|
|
111
|
-
|
|
112
|
-
<select
|
|
113
|
-
value={agentFilter}
|
|
114
|
-
onChange={(e) => setAgentFilter(e.target.value)}
|
|
115
|
-
className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
116
|
-
>
|
|
117
|
-
<option value="all">All agents</option>
|
|
118
|
-
<option value="Hub">Hub</option>
|
|
119
|
-
{agentNames.map((name) => (
|
|
120
|
-
<option key={name} value={name}>{name}</option>
|
|
121
|
-
))}
|
|
122
|
-
</select>
|
|
123
|
-
|
|
124
|
-
<select
|
|
125
|
-
value={addonFilter}
|
|
126
|
-
onChange={(e) => setAddonFilter(e.target.value)}
|
|
127
|
-
className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
128
|
-
>
|
|
129
|
-
<option value="all">All addons</option>
|
|
130
|
-
{addonNames.map((name) => (
|
|
131
|
-
<option key={name} value={name}>{name}</option>
|
|
132
|
-
))}
|
|
133
|
-
</select>
|
|
134
|
-
|
|
135
|
-
<select
|
|
136
|
-
value={levelFilter}
|
|
137
|
-
onChange={(e) => setLevelFilter(e.target.value as LogLevel)}
|
|
138
|
-
className="rounded-lg border border-border bg-surface text-xs text-foreground px-2 py-1.5 focus:outline-none focus:ring-1 focus:ring-primary"
|
|
139
|
-
>
|
|
140
|
-
<option value="all">All levels</option>
|
|
141
|
-
<option value="debug">Debug</option>
|
|
142
|
-
<option value="info">Info</option>
|
|
143
|
-
<option value="warn">Warn</option>
|
|
144
|
-
<option value="error">Error</option>
|
|
145
|
-
</select>
|
|
146
|
-
|
|
147
|
-
<button
|
|
148
|
-
type="button"
|
|
149
|
-
onClick={() => {
|
|
150
|
-
const next = !autoScroll
|
|
151
|
-
setAutoScroll(next)
|
|
152
|
-
if (next) {
|
|
153
|
-
scrollContainerRef.current?.scrollTo({ top: 0, behavior: 'smooth' })
|
|
154
|
-
}
|
|
155
|
-
}}
|
|
156
|
-
className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
|
157
|
-
autoScroll
|
|
158
|
-
? 'border-primary/30 bg-primary/10 text-primary'
|
|
159
|
-
: 'border-border bg-surface text-foreground-subtle hover:text-foreground'
|
|
160
|
-
}`}
|
|
161
|
-
>
|
|
162
|
-
<ArrowDownToLine className="h-3.5 w-3.5" />
|
|
163
|
-
Auto-scroll: {autoScroll ? 'ON' : 'OFF'}
|
|
164
|
-
</button>
|
|
165
|
-
</div>
|
|
166
|
-
|
|
167
|
-
{/* Log display */}
|
|
168
|
-
{isLoading && (
|
|
169
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading logs...</div>
|
|
170
|
-
)}
|
|
171
|
-
|
|
172
|
-
{isError && (
|
|
173
|
-
<div className="text-xs text-danger">Failed to load logs</div>
|
|
174
|
-
)}
|
|
175
|
-
|
|
176
|
-
{!isLoading && !isError && logs.length === 0 && (
|
|
177
|
-
<div className="text-xs text-foreground-subtle italic">No logs match the current filters</div>
|
|
178
|
-
)}
|
|
179
|
-
|
|
180
|
-
{logs.length > 0 && (
|
|
181
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
182
|
-
<div
|
|
183
|
-
ref={scrollContainerRef}
|
|
184
|
-
onScroll={handleScroll}
|
|
185
|
-
className="max-h-[400px] overflow-y-auto"
|
|
186
|
-
>
|
|
187
|
-
<table className="w-full text-[10px]">
|
|
188
|
-
<thead className="sticky top-0">
|
|
189
|
-
<tr>
|
|
190
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-24">Time</th>
|
|
191
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-14">Level</th>
|
|
192
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border w-28">Scope</th>
|
|
193
|
-
<th className="text-left px-2.5 py-1.5 text-foreground-subtle font-medium bg-surface border-b border-border">Message</th>
|
|
194
|
-
</tr>
|
|
195
|
-
</thead>
|
|
196
|
-
<tbody>
|
|
197
|
-
{logs.map((entry, i) => {
|
|
198
|
-
const level = String(entry.level ?? 'info').toLowerCase()
|
|
199
|
-
const style = LEVEL_STYLES[level] ?? LEVEL_STYLES['info']!
|
|
200
|
-
const message = String(entry.message ?? entry.msg ?? entry.text ?? '')
|
|
201
|
-
const scope = Array.isArray(entry.scope)
|
|
202
|
-
? (entry.scope as string[]).join(' > ')
|
|
203
|
-
: String(entry.scope ?? '')
|
|
204
|
-
return (
|
|
205
|
-
<tr key={i} className="hover:bg-primary/5">
|
|
206
|
-
<td className="px-2.5 py-1 text-foreground-subtle border-b border-border font-mono whitespace-nowrap">
|
|
207
|
-
{formatTs(entry.timestamp ?? entry.ts ?? entry.time)}
|
|
208
|
-
</td>
|
|
209
|
-
<td className="px-2.5 py-1 border-b border-border">
|
|
210
|
-
<span className={`inline-flex rounded-full px-1.5 py-0.5 text-[9px] font-medium uppercase ${style.badge}`}>
|
|
211
|
-
{level}
|
|
212
|
-
</span>
|
|
213
|
-
</td>
|
|
214
|
-
<td className="px-2.5 py-1 border-b border-border text-foreground-subtle font-mono truncate max-w-[180px]" title={scope}>
|
|
215
|
-
{scope || '--'}
|
|
216
|
-
</td>
|
|
217
|
-
<td className={`px-2.5 py-1 border-b border-border font-mono ${style.text} break-all`}>
|
|
218
|
-
{message}
|
|
219
|
-
</td>
|
|
220
|
-
</tr>
|
|
221
|
-
)
|
|
222
|
-
})}
|
|
223
|
-
</tbody>
|
|
224
|
-
</table>
|
|
225
|
-
{/* newest-first: no bottom anchor needed */}
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
</div>
|
|
230
|
-
)
|
|
231
|
-
}
|