@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.
Files changed (127) hide show
  1. package/dist/assets/index-BoVZEQ1j.js +598 -0
  2. package/dist/assets/index-DwSc8ann.css +1 -0
  3. package/{index.html → dist/index.html} +3 -1
  4. package/dist/server/addon.d.ts +11 -0
  5. package/dist/server/addon.js +50 -0
  6. package/dist/server/addon.js.map +1 -0
  7. package/package.json +4 -1
  8. package/src/App.tsx +0 -71
  9. package/src/components/addons/AddonCard.tsx +0 -355
  10. package/src/components/addons/AddonUploadZone.tsx +0 -69
  11. package/src/components/addons/CapabilityBadge.tsx +0 -55
  12. package/src/components/addons/CapabilityMap.tsx +0 -133
  13. package/src/components/addons/UpdatesList.tsx +0 -108
  14. package/src/components/agents/AgentCard.tsx +0 -281
  15. package/src/components/agents/AgentLogs.tsx +0 -231
  16. package/src/components/agents/ProcessList.tsx +0 -127
  17. package/src/components/agents/ProcessTree.tsx +0 -369
  18. package/src/components/agents/TaskList.tsx +0 -68
  19. package/src/components/cameras/CameraCard.tsx +0 -60
  20. package/src/components/cameras/LiveEventsPanel.tsx +0 -91
  21. package/src/components/cameras/ProviderSection.tsx +0 -50
  22. package/src/components/cameras/StreamArea.tsx +0 -107
  23. package/src/components/cameras/tabs/AddonsTab.tsx +0 -113
  24. package/src/components/cameras/tabs/CameraEventsTab.tsx +0 -129
  25. package/src/components/cameras/tabs/PipelineTab.tsx +0 -118
  26. package/src/components/cameras/tabs/StreamsTab.tsx +0 -114
  27. package/src/components/dashboard/BlockPicker.tsx +0 -54
  28. package/src/components/dashboard/BlockWrapper.tsx +0 -97
  29. package/src/components/dashboard/DashboardGrid.tsx +0 -160
  30. package/src/components/dashboard/block-registry.ts +0 -15
  31. package/src/components/dashboard/blocks/PipelineStagesBlock.tsx +0 -39
  32. package/src/components/dashboard/blocks/StorageBlock.tsx +0 -66
  33. package/src/components/dashboard/blocks/SystemStatusBlock.tsx +0 -67
  34. package/src/components/dashboard/blocks/index.ts +0 -32
  35. package/src/components/device/DeviceHeader.tsx +0 -116
  36. package/src/components/device/FloatingPanel.tsx +0 -132
  37. package/src/components/device/FloatingPanelManager.tsx +0 -167
  38. package/src/components/device/PanelContent.tsx +0 -196
  39. package/src/components/device/QuickConfigWizard.tsx +0 -507
  40. package/src/components/device/tabs/DetectionConfigTab.tsx +0 -96
  41. package/src/components/device/tabs/EventsTab.tsx +0 -19
  42. package/src/components/device/tabs/LogsTab.tsx +0 -22
  43. package/src/components/device/tabs/OverviewTab.tsx +0 -104
  44. package/src/components/device/tabs/ProviderSettingsTab.tsx +0 -34
  45. package/src/components/device/tabs/RecordingTab.tsx +0 -47
  46. package/src/components/device/tabs/ReplTab.tsx +0 -153
  47. package/src/components/device/tabs/TrackTrailTab.tsx +0 -49
  48. package/src/components/device/tabs/ZonesTab.tsx +0 -98
  49. package/src/components/device/zone-editor/ZoneCanvas.tsx +0 -354
  50. package/src/components/device/zone-editor/ZoneForm.tsx +0 -128
  51. package/src/components/device/zone-editor/ZoneList.tsx +0 -150
  52. package/src/components/form-builder/FormBuilder.tsx +0 -135
  53. package/src/components/form-builder/FormField.tsx +0 -732
  54. package/src/components/form-builder/ModelSelector.tsx +0 -239
  55. package/src/components/integrations/AddDeviceDialog.tsx +0 -205
  56. package/src/components/integrations/CompactDeviceCard.tsx +0 -35
  57. package/src/components/integrations/DeviceCard.tsx +0 -29
  58. package/src/components/integrations/DeviceDiscoveryStep.tsx +0 -105
  59. package/src/components/integrations/DeviceGrid.tsx +0 -79
  60. package/src/components/integrations/DeviceGroupHeader.tsx +0 -17
  61. package/src/components/integrations/DiscoveredDeviceCard.tsx +0 -26
  62. package/src/components/integrations/IntegrationCard.tsx +0 -40
  63. package/src/components/integrations/IntegrationWizard.tsx +0 -172
  64. package/src/components/integrations/ProviderConfigForm.tsx +0 -89
  65. package/src/components/integrations/ProviderPicker.tsx +0 -91
  66. package/src/components/integrations/SnapshotPopover.tsx +0 -68
  67. package/src/components/metrics/AgentLoad.tsx +0 -105
  68. package/src/components/metrics/IntegrationUsage.tsx +0 -73
  69. package/src/components/metrics/PipelineStatus.tsx +0 -74
  70. package/src/components/metrics/ProcessResources.tsx +0 -123
  71. package/src/components/pipeline/PhaseSettings.tsx +0 -131
  72. package/src/components/shared/CapabilityBadges.tsx +0 -30
  73. package/src/components/shared/ProviderIcon.tsx +0 -42
  74. package/src/components/shared/StatusBadge.tsx +0 -23
  75. package/src/components/shared/WebRtcPlayer.tsx +0 -211
  76. package/src/components/timeline/EventMarker.tsx +0 -32
  77. package/src/components/timeline/TimelineBar.tsx +0 -131
  78. package/src/components/ui/ConfirmDialog.tsx +0 -115
  79. package/src/components/ui/ToastContainer.tsx +0 -92
  80. package/src/contexts/auth-context.tsx +0 -91
  81. package/src/hooks/useBackendClient.ts +0 -6
  82. package/src/hooks/useTheme.ts +0 -1
  83. package/src/i18n/en.json +0 -164
  84. package/src/i18n/index.ts +0 -29
  85. package/src/i18n/it.json +0 -164
  86. package/src/index.css +0 -63
  87. package/src/layouts/AddonPageLoader.tsx +0 -120
  88. package/src/layouts/AppLayout.tsx +0 -254
  89. package/src/layouts/ProtectedRoute.tsx +0 -25
  90. package/src/lib/addon-page-context.ts +0 -29
  91. package/src/lib/backend.ts +0 -16
  92. package/src/main.tsx +0 -21
  93. package/src/pages/AccessDenied.tsx +0 -22
  94. package/src/pages/Cameras.tsx +0 -127
  95. package/src/pages/Dashboard.tsx +0 -6
  96. package/src/pages/DeviceDetail.tsx +0 -175
  97. package/src/pages/IntegrationDetail.tsx +0 -222
  98. package/src/pages/Integrations.tsx +0 -333
  99. package/src/pages/Login.tsx +0 -106
  100. package/src/pages/Metrics.tsx +0 -18
  101. package/src/pages/PipelineConfig.tsx +0 -282
  102. package/src/pages/Showroom.tsx +0 -351
  103. package/src/pages/Timeline.tsx +0 -269
  104. package/src/pages/system/Addons.tsx +0 -396
  105. package/src/pages/system/Agents.tsx +0 -362
  106. package/src/pages/system/Logs.tsx +0 -131
  107. package/src/pages/system/Models.tsx +0 -102
  108. package/src/pages/system/Processes.tsx +0 -129
  109. package/src/pages/system/Repl.tsx +0 -148
  110. package/src/pages/system/Settings.tsx +0 -168
  111. package/src/pages/system/Users.tsx +0 -174
  112. package/src/server/addon.ts +0 -54
  113. package/src/types/config-ui.ts +0 -28
  114. package/src/types/dashboard.ts +0 -39
  115. package/tsconfig.json +0 -29
  116. package/tsconfig.server.json +0 -16
  117. package/tsup.config.ts +0 -20
  118. package/vite.config.ts +0 -68
  119. /package/{public → dist}/brand/logo-dark.svg +0 -0
  120. /package/{public → dist}/brand/logo-horizontal-dark.svg +0 -0
  121. /package/{public → dist}/brand/logo-horizontal-light.svg +0 -0
  122. /package/{public → dist}/brand/logo-light.svg +0 -0
  123. /package/{public → dist}/brand/logo-wide-dark.svg +0 -0
  124. /package/{public → dist}/brand/logo-wide-light.svg +0 -0
  125. /package/{public → dist}/favicon.svg +0 -0
  126. /package/{public → dist}/vendor/react-jsx-runtime.mjs +0 -0
  127. /package/{public → dist}/vendor/react.mjs +0 -0
@@ -1,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
- }