@camstack/addon-admin-ui 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-DjELGD4R.css +1 -0
- package/dist/assets/index-w55PwKyu.js +598 -0
- package/{index.html → dist/index.html} +3 -1
- package/dist/server/addon.d.ts +11 -0
- package/dist/server/addon.js +50 -0
- package/dist/server/addon.js.map +1 -0
- package/package.json +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,362 +0,0 @@
|
|
|
1
|
-
import { useState, useMemo } from "react";
|
|
2
|
-
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import {
|
|
4
|
-
Bot,
|
|
5
|
-
Cpu,
|
|
6
|
-
ListTodo,
|
|
7
|
-
WifiOff,
|
|
8
|
-
TreePine,
|
|
9
|
-
ScrollText,
|
|
10
|
-
X,
|
|
11
|
-
FlaskConical,
|
|
12
|
-
Square,
|
|
13
|
-
} from "lucide-react";
|
|
14
|
-
import { useBackendClient } from "../../hooks/useBackendClient";
|
|
15
|
-
import { ProcessTree } from "../../components/agents/ProcessTree";
|
|
16
|
-
import { AgentLogs } from "../../components/agents/AgentLogs";
|
|
17
|
-
import type {
|
|
18
|
-
ProcessTreeEntry,
|
|
19
|
-
SubProcess,
|
|
20
|
-
} from "../../components/agents/ProcessTree";
|
|
21
|
-
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
// Helpers
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
|
|
26
|
-
function normaliseProcessTreeEntry(
|
|
27
|
-
raw: Record<string, unknown>,
|
|
28
|
-
): ProcessTreeEntry {
|
|
29
|
-
const info = (raw.info ?? raw) as Record<string, unknown>;
|
|
30
|
-
const status = (raw.status ?? {}) as Record<string, unknown>;
|
|
31
|
-
return {
|
|
32
|
-
id: String(info.id ?? raw.id ?? ""),
|
|
33
|
-
name: String(info.name ?? raw.name ?? ""),
|
|
34
|
-
pid: Number(raw.pid ?? 0),
|
|
35
|
-
state: String(raw.state ?? "running"),
|
|
36
|
-
cpuPercent: Number(status.cpuPercent ?? raw.cpuPercent ?? 0),
|
|
37
|
-
memoryPercent: Number(status.memoryPercent ?? raw.memoryPercent ?? 0),
|
|
38
|
-
memoryMB: Number(info.memoryMB ?? raw.memoryMB ?? 0),
|
|
39
|
-
isHub: Boolean(raw.isHub ?? false),
|
|
40
|
-
platform: String(info.platform ?? raw.platform ?? ""),
|
|
41
|
-
arch: String(info.arch ?? raw.arch ?? ""),
|
|
42
|
-
host: String(info.host ?? raw.host ?? ""),
|
|
43
|
-
capabilities:
|
|
44
|
-
(info.capabilities as string[]) ?? (raw.capabilities as string[]) ?? [],
|
|
45
|
-
installedAddons:
|
|
46
|
-
(info.installedAddons as string[]) ??
|
|
47
|
-
(raw.installedAddons as string[]) ??
|
|
48
|
-
[],
|
|
49
|
-
pythonRuntimes:
|
|
50
|
-
(info.pythonRuntimes as string[]) ??
|
|
51
|
-
(raw.pythonRuntimes as string[]) ??
|
|
52
|
-
[],
|
|
53
|
-
connectedSince: Number(raw.connectedSince ?? Date.now()),
|
|
54
|
-
subProcesses: normaliseSubProcesses(raw.subProcesses),
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function normaliseSubProcesses(raw: unknown): SubProcess[] {
|
|
59
|
-
if (!Array.isArray(raw)) return [];
|
|
60
|
-
return raw.map((p: Record<string, unknown>) => ({
|
|
61
|
-
pid: Number(p.pid ?? 0),
|
|
62
|
-
name: String(p.name ?? ""),
|
|
63
|
-
command: String(p.command ?? ""),
|
|
64
|
-
state: (p.state as SubProcess["state"]) ?? "running",
|
|
65
|
-
cpuPercent: Number(p.cpuPercent ?? 0),
|
|
66
|
-
memoryRss: Number(p.memoryRss ?? 0),
|
|
67
|
-
uptimeSeconds: Number(p.uptimeSeconds ?? 0),
|
|
68
|
-
}));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Stat Card
|
|
73
|
-
// ---------------------------------------------------------------------------
|
|
74
|
-
|
|
75
|
-
function StatCard({
|
|
76
|
-
icon: Icon,
|
|
77
|
-
label,
|
|
78
|
-
value,
|
|
79
|
-
color,
|
|
80
|
-
}: {
|
|
81
|
-
icon: React.ElementType;
|
|
82
|
-
label: string;
|
|
83
|
-
value: number | string;
|
|
84
|
-
color: string;
|
|
85
|
-
}) {
|
|
86
|
-
return (
|
|
87
|
-
<div className="rounded-xl border border-border bg-surface px-4 py-3 flex items-center gap-3">
|
|
88
|
-
<div
|
|
89
|
-
className={`flex-shrink-0 h-9 w-9 rounded-lg flex items-center justify-center ${color}`}
|
|
90
|
-
>
|
|
91
|
-
<Icon className="h-4 w-4" />
|
|
92
|
-
</div>
|
|
93
|
-
<div>
|
|
94
|
-
<div className="text-xl font-bold text-foreground leading-none">
|
|
95
|
-
{value}
|
|
96
|
-
</div>
|
|
97
|
-
<div className="text-[10px] text-foreground-subtle mt-0.5">{label}</div>
|
|
98
|
-
</div>
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ---------------------------------------------------------------------------
|
|
104
|
-
// Tab selector
|
|
105
|
-
// ---------------------------------------------------------------------------
|
|
106
|
-
|
|
107
|
-
type Tab = "tree" | "logs";
|
|
108
|
-
|
|
109
|
-
function TabButton({
|
|
110
|
-
tab,
|
|
111
|
-
active,
|
|
112
|
-
icon: Icon,
|
|
113
|
-
label,
|
|
114
|
-
onClick,
|
|
115
|
-
}: {
|
|
116
|
-
tab: Tab;
|
|
117
|
-
active: boolean;
|
|
118
|
-
icon: React.ElementType;
|
|
119
|
-
label: string;
|
|
120
|
-
onClick: () => void;
|
|
121
|
-
}) {
|
|
122
|
-
return (
|
|
123
|
-
<button
|
|
124
|
-
type="button"
|
|
125
|
-
onClick={onClick}
|
|
126
|
-
className={`inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 text-xs font-medium transition-colors ${
|
|
127
|
-
active
|
|
128
|
-
? "bg-primary/10 text-primary border border-primary/30"
|
|
129
|
-
: "bg-surface text-foreground-subtle border border-border hover:text-foreground hover:bg-primary/5"
|
|
130
|
-
}`}
|
|
131
|
-
>
|
|
132
|
-
<Icon className="h-3.5 w-3.5" />
|
|
133
|
-
{label}
|
|
134
|
-
</button>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// ---------------------------------------------------------------------------
|
|
139
|
-
// Page
|
|
140
|
-
// ---------------------------------------------------------------------------
|
|
141
|
-
|
|
142
|
-
export function AgentsPage() {
|
|
143
|
-
const client = useBackendClient();
|
|
144
|
-
const queryClient = useQueryClient();
|
|
145
|
-
const [activeTab, setActiveTab] = useState<Tab>("tree");
|
|
146
|
-
const [selectedAgentName, setSelectedAgentName] = useState<string | null>(
|
|
147
|
-
null,
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
// Test worker state
|
|
151
|
-
const { data: testWorkerStatus } = useQuery({
|
|
152
|
-
queryKey: ["agents", "testWorker", "status"],
|
|
153
|
-
queryFn: () => client.trpc.agents.isTestWorkerRunning.query(),
|
|
154
|
-
refetchInterval: 5000,
|
|
155
|
-
});
|
|
156
|
-
const testWorkerRunning = testWorkerStatus?.running ?? false;
|
|
157
|
-
|
|
158
|
-
const startTestWorker = useMutation({
|
|
159
|
-
mutationFn: () => client.trpc.agents.startTestWorker.mutate(),
|
|
160
|
-
onSuccess: () => {
|
|
161
|
-
queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
162
|
-
queryClient.invalidateQueries({ queryKey: ["agent"] });
|
|
163
|
-
},
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
const stopTestWorker = useMutation({
|
|
167
|
-
mutationFn: () => client.trpc.agents.stopTestWorker.mutate(),
|
|
168
|
-
onSuccess: () => {
|
|
169
|
-
queryClient.invalidateQueries({ queryKey: ["agents"] });
|
|
170
|
-
queryClient.invalidateQueries({ queryKey: ["agent"] });
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
// Fetch process tree via the agent.processTree endpoint
|
|
175
|
-
const { data: rawProcessTree = [], isLoading: loadingTree } = useQuery({
|
|
176
|
-
queryKey: ["agent", "processTree"],
|
|
177
|
-
queryFn: () => client.trpc.agent.processTree.query(),
|
|
178
|
-
refetchInterval: 5000,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// Also fetch the classic agents list for stats
|
|
182
|
-
const { data: rawAgents = [] } = useQuery({
|
|
183
|
-
queryKey: ["agents", "list"],
|
|
184
|
-
queryFn: () => client.listAgents(),
|
|
185
|
-
refetchInterval: 5000,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
const treeEntries: ProcessTreeEntry[] = useMemo(
|
|
189
|
-
() =>
|
|
190
|
-
(rawProcessTree as unknown as Record<string, unknown>[]).map(
|
|
191
|
-
normaliseProcessTreeEntry,
|
|
192
|
-
),
|
|
193
|
-
[rawProcessTree],
|
|
194
|
-
);
|
|
195
|
-
|
|
196
|
-
// Compute stats
|
|
197
|
-
const agentCount = treeEntries.length;
|
|
198
|
-
const totalSubProcesses = treeEntries.reduce(
|
|
199
|
-
(sum, e) => sum + e.subProcesses.length,
|
|
200
|
-
0,
|
|
201
|
-
);
|
|
202
|
-
const totalAddons = treeEntries.reduce(
|
|
203
|
-
(sum, e) => sum + e.installedAddons.length,
|
|
204
|
-
0,
|
|
205
|
-
);
|
|
206
|
-
|
|
207
|
-
// Build filter options for logs
|
|
208
|
-
const agentNames = useMemo(
|
|
209
|
-
() => treeEntries.filter((e) => !e.isHub).map((e) => e.name),
|
|
210
|
-
[treeEntries],
|
|
211
|
-
);
|
|
212
|
-
const addonNames = useMemo(() => {
|
|
213
|
-
const set = new Set<string>();
|
|
214
|
-
for (const entry of treeEntries) {
|
|
215
|
-
for (const addon of entry.installedAddons) {
|
|
216
|
-
set.add(addon);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return [...set].sort();
|
|
220
|
-
}, [treeEntries]);
|
|
221
|
-
|
|
222
|
-
return (
|
|
223
|
-
<div className="p-6 space-y-6">
|
|
224
|
-
{/* Header */}
|
|
225
|
-
<div className="flex items-start justify-between gap-4">
|
|
226
|
-
<div>
|
|
227
|
-
<h1 className="text-lg font-semibold text-foreground">
|
|
228
|
-
Agents & Process Tree
|
|
229
|
-
</h1>
|
|
230
|
-
<p className="text-xs text-foreground-subtle mt-0.5">
|
|
231
|
-
Distributed processing agents, sub-processes, and system logs.
|
|
232
|
-
</p>
|
|
233
|
-
</div>
|
|
234
|
-
<div className="shrink-0">
|
|
235
|
-
{testWorkerRunning ? (
|
|
236
|
-
<button
|
|
237
|
-
type="button"
|
|
238
|
-
onClick={() => stopTestWorker.mutate()}
|
|
239
|
-
disabled={stopTestWorker.isPending}
|
|
240
|
-
className="inline-flex items-center gap-1.5 rounded-lg border border-danger/30 bg-danger/10 text-danger px-3 py-1.5 text-xs font-medium hover:bg-danger/20 transition-colors disabled:opacity-50"
|
|
241
|
-
>
|
|
242
|
-
<Square className="h-3.5 w-3.5" />
|
|
243
|
-
{stopTestWorker.isPending ? "Stopping..." : "Stop Test Agent"}
|
|
244
|
-
</button>
|
|
245
|
-
) : (
|
|
246
|
-
<button
|
|
247
|
-
type="button"
|
|
248
|
-
onClick={() => startTestWorker.mutate()}
|
|
249
|
-
disabled={startTestWorker.isPending}
|
|
250
|
-
className="inline-flex items-center gap-1.5 rounded-lg border border-border bg-surface text-foreground-subtle px-3 py-1.5 text-xs font-medium hover:text-foreground hover:bg-primary/5 transition-colors disabled:opacity-50"
|
|
251
|
-
>
|
|
252
|
-
<FlaskConical className="h-3.5 w-3.5" />
|
|
253
|
-
{startTestWorker.isPending ? "Starting..." : "Start Test Agent"}
|
|
254
|
-
</button>
|
|
255
|
-
)}
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
|
|
259
|
-
{/* Overview bar */}
|
|
260
|
-
<div className="grid grid-cols-1 sm:grid-cols-4 gap-3">
|
|
261
|
-
<StatCard
|
|
262
|
-
icon={Bot}
|
|
263
|
-
label="Agents online"
|
|
264
|
-
value={agentCount}
|
|
265
|
-
color="bg-green-500/10 text-green-400"
|
|
266
|
-
/>
|
|
267
|
-
<StatCard
|
|
268
|
-
icon={Cpu}
|
|
269
|
-
label="Sub-processes"
|
|
270
|
-
value={totalSubProcesses}
|
|
271
|
-
color="bg-blue-500/10 text-blue-400"
|
|
272
|
-
/>
|
|
273
|
-
<StatCard
|
|
274
|
-
icon={ListTodo}
|
|
275
|
-
label="Installed addons"
|
|
276
|
-
value={totalAddons}
|
|
277
|
-
color="bg-purple-500/10 text-purple-400"
|
|
278
|
-
/>
|
|
279
|
-
<StatCard
|
|
280
|
-
icon={Bot}
|
|
281
|
-
label="Capabilities"
|
|
282
|
-
value={
|
|
283
|
-
[...new Set(treeEntries.flatMap((e) => e.capabilities))].length
|
|
284
|
-
}
|
|
285
|
-
color="bg-cyan-500/10 text-cyan-400"
|
|
286
|
-
/>
|
|
287
|
-
</div>
|
|
288
|
-
|
|
289
|
-
{/* Tabs */}
|
|
290
|
-
<div className="flex items-center gap-2">
|
|
291
|
-
<TabButton
|
|
292
|
-
tab="tree"
|
|
293
|
-
active={activeTab === "tree"}
|
|
294
|
-
icon={TreePine}
|
|
295
|
-
label="Process Tree"
|
|
296
|
-
onClick={() => setActiveTab("tree")}
|
|
297
|
-
/>
|
|
298
|
-
<TabButton
|
|
299
|
-
tab="logs"
|
|
300
|
-
active={activeTab === "logs"}
|
|
301
|
-
icon={ScrollText}
|
|
302
|
-
label="Logs"
|
|
303
|
-
onClick={() => setActiveTab("logs")}
|
|
304
|
-
/>
|
|
305
|
-
</div>
|
|
306
|
-
|
|
307
|
-
{/* Tab content */}
|
|
308
|
-
{activeTab === "tree" && (
|
|
309
|
-
<>
|
|
310
|
-
{agentCount === 0 && !loadingTree && (
|
|
311
|
-
<div className="flex flex-col items-center justify-center rounded-xl border border-border bg-surface py-12 text-foreground-subtle">
|
|
312
|
-
<WifiOff className="h-8 w-8 mb-2 opacity-40" />
|
|
313
|
-
<div className="text-sm">No agents connected</div>
|
|
314
|
-
<div className="text-xs mt-1 opacity-70">
|
|
315
|
-
Run{" "}
|
|
316
|
-
<code className="font-mono bg-border px-1 rounded">
|
|
317
|
-
camstack --agent --hub wss://<host>:4443
|
|
318
|
-
</code>{" "}
|
|
319
|
-
to connect one.
|
|
320
|
-
</div>
|
|
321
|
-
</div>
|
|
322
|
-
)}
|
|
323
|
-
<ProcessTree
|
|
324
|
-
entries={treeEntries}
|
|
325
|
-
isLoading={loadingTree}
|
|
326
|
-
selectedAgentName={selectedAgentName}
|
|
327
|
-
onSelectAgent={(name) => {
|
|
328
|
-
setSelectedAgentName(name);
|
|
329
|
-
setActiveTab("logs");
|
|
330
|
-
}}
|
|
331
|
-
/>
|
|
332
|
-
</>
|
|
333
|
-
)}
|
|
334
|
-
|
|
335
|
-
{activeTab === "logs" && (
|
|
336
|
-
<>
|
|
337
|
-
{selectedAgentName && (
|
|
338
|
-
<div className="flex items-center gap-2 text-xs">
|
|
339
|
-
<span className="text-foreground-subtle">Filtered to agent:</span>
|
|
340
|
-
<span className="font-semibold text-foreground">
|
|
341
|
-
{selectedAgentName}
|
|
342
|
-
</span>
|
|
343
|
-
<button
|
|
344
|
-
type="button"
|
|
345
|
-
onClick={() => setSelectedAgentName(null)}
|
|
346
|
-
className="inline-flex items-center gap-1 rounded-lg border border-border bg-surface px-2 py-1 text-xs text-foreground-subtle hover:text-foreground hover:bg-primary/5 transition-colors"
|
|
347
|
-
>
|
|
348
|
-
<X className="h-3 w-3" />
|
|
349
|
-
Clear selection
|
|
350
|
-
</button>
|
|
351
|
-
</div>
|
|
352
|
-
)}
|
|
353
|
-
<AgentLogs
|
|
354
|
-
agentNames={agentNames}
|
|
355
|
-
addonNames={addonNames}
|
|
356
|
-
preselectedAgent={selectedAgentName ?? undefined}
|
|
357
|
-
/>
|
|
358
|
-
</>
|
|
359
|
-
)}
|
|
360
|
-
</div>
|
|
361
|
-
);
|
|
362
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { useState, useEffect, useRef, useMemo } from 'react'
|
|
2
|
-
import { useQuery } from '@tanstack/react-query'
|
|
3
|
-
import { Pause, Play } from 'lucide-react'
|
|
4
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
5
|
-
|
|
6
|
-
type LogLevel = 'all' | 'debug' | 'info' | 'warn' | 'error'
|
|
7
|
-
|
|
8
|
-
const LEVEL_STYLES: Record<string, { badge: string; text: string }> = {
|
|
9
|
-
debug: { badge: 'bg-foreground-subtle/10 text-foreground-subtle', text: 'text-foreground-subtle' },
|
|
10
|
-
info: { badge: 'bg-info/10 text-info', text: 'text-info' },
|
|
11
|
-
warn: { badge: 'bg-warning/10 text-warning', text: 'text-warning' },
|
|
12
|
-
error: { badge: 'bg-danger/10 text-danger', text: 'text-danger' },
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function LogsPage() {
|
|
16
|
-
const client = useBackendClient()
|
|
17
|
-
const [levelFilter, setLevelFilter] = useState<LogLevel>('all')
|
|
18
|
-
const [paused, setPaused] = useState(false)
|
|
19
|
-
const topRef = useRef<HTMLDivElement>(null)
|
|
20
|
-
|
|
21
|
-
const { data: logsData, isLoading, isError } = useQuery({
|
|
22
|
-
queryKey: ['logs', levelFilter],
|
|
23
|
-
queryFn: () =>
|
|
24
|
-
client.getLogs(
|
|
25
|
-
levelFilter !== 'all'
|
|
26
|
-
? { level: levelFilter as 'debug' | 'info' | 'warn' | 'error', limit: 500 }
|
|
27
|
-
: { limit: 500 },
|
|
28
|
-
),
|
|
29
|
-
refetchInterval: paused ? false : 3000,
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const rawLogs = (logsData ?? []) as unknown as Array<Record<string, unknown>>
|
|
33
|
-
|
|
34
|
-
// Reverse so newest logs appear at the top
|
|
35
|
-
const logs = useMemo(() => [...rawLogs].reverse(), [rawLogs])
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
if (!paused) {
|
|
39
|
-
topRef.current?.scrollIntoView({ behavior: 'smooth' })
|
|
40
|
-
}
|
|
41
|
-
}, [logs, paused])
|
|
42
|
-
|
|
43
|
-
function formatTs(ts: unknown): string {
|
|
44
|
-
if (!ts) return '—'
|
|
45
|
-
const d = new Date(typeof ts === 'number' ? ts : String(ts))
|
|
46
|
-
return d.toLocaleTimeString('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', fractionalSecondDigits: 3 })
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return (
|
|
50
|
-
<div className="p-6 space-y-4">
|
|
51
|
-
<div className="flex items-center justify-between">
|
|
52
|
-
<h1 className="text-lg font-semibold text-foreground">Logs</h1>
|
|
53
|
-
<div className="flex items-center gap-2">
|
|
54
|
-
<select
|
|
55
|
-
value={levelFilter}
|
|
56
|
-
onChange={(e) => setLevelFilter(e.target.value as LogLevel)}
|
|
57
|
-
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"
|
|
58
|
-
>
|
|
59
|
-
<option value="all">All levels</option>
|
|
60
|
-
<option value="debug">Debug</option>
|
|
61
|
-
<option value="info">Info</option>
|
|
62
|
-
<option value="warn">Warn</option>
|
|
63
|
-
<option value="error">Error</option>
|
|
64
|
-
</select>
|
|
65
|
-
<button
|
|
66
|
-
onClick={() => setPaused((p) => !p)}
|
|
67
|
-
className={`inline-flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
|
68
|
-
paused
|
|
69
|
-
? 'border-primary/30 bg-primary/10 text-primary hover:bg-primary/20'
|
|
70
|
-
: 'border-border bg-surface text-foreground-subtle hover:text-foreground'
|
|
71
|
-
}`}
|
|
72
|
-
>
|
|
73
|
-
{paused ? <Play className="h-3.5 w-3.5" /> : <Pause className="h-3.5 w-3.5" />}
|
|
74
|
-
{paused ? 'Resume' : 'Pause'}
|
|
75
|
-
</button>
|
|
76
|
-
</div>
|
|
77
|
-
</div>
|
|
78
|
-
|
|
79
|
-
{isLoading && (
|
|
80
|
-
<div className="text-xs text-foreground-subtle animate-pulse">Loading...</div>
|
|
81
|
-
)}
|
|
82
|
-
|
|
83
|
-
{isError && (
|
|
84
|
-
<div className="text-xs text-danger">Failed to load</div>
|
|
85
|
-
)}
|
|
86
|
-
|
|
87
|
-
{!isLoading && !isError && logs.length === 0 && (
|
|
88
|
-
<div className="text-xs text-foreground-subtle">No data</div>
|
|
89
|
-
)}
|
|
90
|
-
|
|
91
|
-
{logs.length > 0 && (
|
|
92
|
-
<div className="rounded-lg border border-border bg-surface overflow-hidden">
|
|
93
|
-
<div className="max-h-[calc(100vh-220px)] overflow-y-auto">
|
|
94
|
-
<div ref={topRef} />
|
|
95
|
-
<table className="w-full text-xs">
|
|
96
|
-
<thead className="sticky top-0">
|
|
97
|
-
<tr>
|
|
98
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border w-36">Time</th>
|
|
99
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border w-16">Level</th>
|
|
100
|
-
<th className="text-left px-3 py-2 text-foreground-subtle font-medium bg-surface border-b border-border">Message</th>
|
|
101
|
-
</tr>
|
|
102
|
-
</thead>
|
|
103
|
-
<tbody>
|
|
104
|
-
{logs.map((entry, i) => {
|
|
105
|
-
const level = String(entry.level ?? 'info').toLowerCase()
|
|
106
|
-
const style = LEVEL_STYLES[level] ?? LEVEL_STYLES['info']!
|
|
107
|
-
const message = String(entry.message ?? entry.msg ?? entry.text ?? '')
|
|
108
|
-
return (
|
|
109
|
-
<tr key={i} className="hover:bg-primary/5">
|
|
110
|
-
<td className="px-3 py-1.5 text-foreground-subtle border-b border-border font-mono whitespace-nowrap">
|
|
111
|
-
{formatTs(entry.timestamp ?? entry.ts ?? entry.time)}
|
|
112
|
-
</td>
|
|
113
|
-
<td className="px-3 py-1.5 border-b border-border">
|
|
114
|
-
<span className={`inline-flex rounded-full px-2 py-0.5 text-[10px] font-medium uppercase ${style.badge}`}>
|
|
115
|
-
{level}
|
|
116
|
-
</span>
|
|
117
|
-
</td>
|
|
118
|
-
<td className={`px-3 py-1.5 border-b border-border font-mono ${style.text} break-all`}>
|
|
119
|
-
{message}
|
|
120
|
-
</td>
|
|
121
|
-
</tr>
|
|
122
|
-
)
|
|
123
|
-
})}
|
|
124
|
-
</tbody>
|
|
125
|
-
</table>
|
|
126
|
-
</div>
|
|
127
|
-
</div>
|
|
128
|
-
)}
|
|
129
|
-
</div>
|
|
130
|
-
)
|
|
131
|
-
}
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import { useQuery } from '@tanstack/react-query'
|
|
2
|
-
import { Info, Package } from 'lucide-react'
|
|
3
|
-
import { useBackendClient } from '../../hooks/useBackendClient'
|
|
4
|
-
|
|
5
|
-
// ---------------------------------------------------------------------------
|
|
6
|
-
// Types
|
|
7
|
-
// ---------------------------------------------------------------------------
|
|
8
|
-
|
|
9
|
-
interface AddonInfo {
|
|
10
|
-
id: string
|
|
11
|
-
name?: string
|
|
12
|
-
description?: string
|
|
13
|
-
enabled?: boolean
|
|
14
|
-
[key: string]: unknown
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
// ---------------------------------------------------------------------------
|
|
18
|
-
// Main page
|
|
19
|
-
// ---------------------------------------------------------------------------
|
|
20
|
-
|
|
21
|
-
export function ModelsPage() {
|
|
22
|
-
const client = useBackendClient()
|
|
23
|
-
|
|
24
|
-
const { data: addons = [], isLoading } = useQuery<AddonInfo[]>({
|
|
25
|
-
queryKey: ['models', 'bridge-addons'],
|
|
26
|
-
queryFn: async () => {
|
|
27
|
-
const result = await client.bridgeListAddons()
|
|
28
|
-
return (result as AddonInfo[]) ?? []
|
|
29
|
-
},
|
|
30
|
-
})
|
|
31
|
-
|
|
32
|
-
const visionAddons = addons.filter(
|
|
33
|
-
(a) => a.id?.includes('vision') || a.id?.includes('detection') || a.id?.includes('model'),
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<div className="p-6 space-y-6 max-w-5xl mx-auto">
|
|
38
|
-
{/* Header */}
|
|
39
|
-
<div className="flex items-start justify-between">
|
|
40
|
-
<div>
|
|
41
|
-
<h1 className="text-lg font-bold text-foreground">Models</h1>
|
|
42
|
-
<p className="text-xs text-foreground-subtle mt-0.5">
|
|
43
|
-
ML model management for detection and recognition.
|
|
44
|
-
</p>
|
|
45
|
-
</div>
|
|
46
|
-
</div>
|
|
47
|
-
|
|
48
|
-
{/* Info banner */}
|
|
49
|
-
<div className="rounded-lg border border-info/30 bg-info/5 px-4 py-3 flex items-start gap-3">
|
|
50
|
-
<Info className="h-4 w-4 text-info shrink-0 mt-0.5" />
|
|
51
|
-
<div className="text-xs text-foreground leading-relaxed">
|
|
52
|
-
<p className="font-medium mb-1">Model management has moved to addon settings</p>
|
|
53
|
-
<p className="text-foreground-subtle">
|
|
54
|
-
ML models are now managed through individual vision addons. Go to{' '}
|
|
55
|
-
<span className="font-medium text-foreground">System → Addons</span> and configure the
|
|
56
|
-
vision/detection addon to download and manage models.
|
|
57
|
-
</p>
|
|
58
|
-
</div>
|
|
59
|
-
</div>
|
|
60
|
-
|
|
61
|
-
{/* Related addons list */}
|
|
62
|
-
<div className="rounded-lg border border-border overflow-hidden">
|
|
63
|
-
<div className="px-4 py-2.5 border-b border-border bg-surface/40 flex items-center">
|
|
64
|
-
<h2 className="text-xs font-semibold text-foreground uppercase tracking-wide">Related Addons</h2>
|
|
65
|
-
</div>
|
|
66
|
-
|
|
67
|
-
{isLoading ? (
|
|
68
|
-
<div className="px-4 py-8 text-center text-xs text-foreground-subtle">Loading addons...</div>
|
|
69
|
-
) : visionAddons.length === 0 ? (
|
|
70
|
-
<div className="px-4 py-8 text-center text-xs text-foreground-subtle">
|
|
71
|
-
{addons.length === 0
|
|
72
|
-
? 'No addons registered. Install a vision addon to enable model management.'
|
|
73
|
-
: 'No vision/detection addons found. Install one to manage ML models.'}
|
|
74
|
-
</div>
|
|
75
|
-
) : (
|
|
76
|
-
<div className="divide-y divide-border/50">
|
|
77
|
-
{visionAddons.map((addon) => (
|
|
78
|
-
<div key={addon.id} className="px-4 py-3 flex items-center gap-3 hover:bg-surface/30 transition-colors">
|
|
79
|
-
<Package className="h-4 w-4 text-foreground-subtle shrink-0" />
|
|
80
|
-
<div className="flex-1 min-w-0">
|
|
81
|
-
<div className="text-xs font-medium text-foreground">{addon.name ?? addon.id}</div>
|
|
82
|
-
{addon.description && (
|
|
83
|
-
<div className="text-[10px] text-foreground-subtle mt-0.5">{String(addon.description)}</div>
|
|
84
|
-
)}
|
|
85
|
-
</div>
|
|
86
|
-
<span
|
|
87
|
-
className={`text-[10px] rounded px-1.5 py-0.5 font-medium ${
|
|
88
|
-
addon.enabled
|
|
89
|
-
? 'bg-success/10 text-success border border-success/30'
|
|
90
|
-
: 'bg-border/40 text-foreground-subtle'
|
|
91
|
-
}`}
|
|
92
|
-
>
|
|
93
|
-
{addon.enabled ? 'Enabled' : 'Disabled'}
|
|
94
|
-
</span>
|
|
95
|
-
</div>
|
|
96
|
-
))}
|
|
97
|
-
</div>
|
|
98
|
-
)}
|
|
99
|
-
</div>
|
|
100
|
-
</div>
|
|
101
|
-
)
|
|
102
|
-
}
|