@burdenoff/microfe-vibecontrols 2026.529.14 → 2026.529.16
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/components/agents/ConnectionLogPanel.js +83 -68
- package/dist/components/agents/ConnectionLogPanel.js.map +1 -1
- package/dist/components/logs/LogViewer.js +1 -1
- package/dist/components/logs/LogViewer.js.map +1 -1
- package/dist/generated/wspace-operations.js +807 -769
- package/dist/generated/wspace-operations.js.map +1 -1
- package/dist/generated/wspace-types.js.map +1 -1
- package/dist/index.js +10 -10
- package/dist/pages/SessionDetailsPage.js +120 -112
- package/dist/pages/SessionDetailsPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,32 +1,32 @@
|
|
|
1
|
-
import { useGetAgentConnectionEventsQuery as e } from "../../generated/wspace-operations.js";
|
|
2
|
-
import { useCallback as
|
|
3
|
-
import { jsx as
|
|
1
|
+
import { useGetAgentConnectionEventsQuery as e, useGetSessionConnectionEventsQuery as t } from "../../generated/wspace-operations.js";
|
|
2
|
+
import { useCallback as n, useMemo as r, useState as i } from "react";
|
|
3
|
+
import { jsx as a, jsxs as o } from "react/jsx-runtime";
|
|
4
4
|
//#region src/components/agents/ConnectionLogPanel.tsx
|
|
5
|
-
var
|
|
5
|
+
var s = {
|
|
6
6
|
info: "text-status-info-text",
|
|
7
7
|
warn: "text-status-warning-text",
|
|
8
8
|
error: "text-status-danger-text",
|
|
9
9
|
debug: "text-fg-muted"
|
|
10
|
-
},
|
|
10
|
+
}, c = {
|
|
11
11
|
info: "bg-status-info-bg",
|
|
12
12
|
warn: "bg-status-warning-bg",
|
|
13
13
|
error: "bg-status-danger-bg",
|
|
14
14
|
debug: "bg-bg-muted"
|
|
15
15
|
};
|
|
16
|
-
function
|
|
16
|
+
function l(e) {
|
|
17
17
|
try {
|
|
18
18
|
return new Date(e).toISOString().replace("T", " ").replace("Z", "");
|
|
19
19
|
} catch {
|
|
20
20
|
return e;
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
function
|
|
23
|
+
function u(e) {
|
|
24
24
|
return e.map((e) => {
|
|
25
25
|
let t = e.detailsJson ? `\n details: ${e.detailsJson}` : "";
|
|
26
26
|
return `[${e.timestamp}] #${e.sequence} ${e.level.toUpperCase().padEnd(5)} ${e.step.padEnd(28)} ${e.message}${t}`;
|
|
27
27
|
}).join("\n");
|
|
28
28
|
}
|
|
29
|
-
function
|
|
29
|
+
function d(e, t, n) {
|
|
30
30
|
return JSON.stringify({
|
|
31
31
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
32
32
|
agentId: t,
|
|
@@ -43,19 +43,28 @@ function u(e, t, n) {
|
|
|
43
43
|
}))
|
|
44
44
|
}, null, 2);
|
|
45
45
|
}
|
|
46
|
-
function
|
|
46
|
+
function f(e, t, n) {
|
|
47
47
|
let r = new Blob([n], { type: t }), i = URL.createObjectURL(r), a = document.createElement("a");
|
|
48
48
|
a.href = i, a.download = e, document.body.appendChild(a), a.click(), document.body.removeChild(a), URL.revokeObjectURL(i);
|
|
49
49
|
}
|
|
50
|
-
function
|
|
51
|
-
let
|
|
50
|
+
function p({ agentId: p, sessionId: m, agentName: h, sessionName: g }) {
|
|
51
|
+
let _ = !!p, v = (p ?? m) || "", y = _ ? h : g, b = e({
|
|
52
|
+
variables: {
|
|
53
|
+
agentId: p ?? "",
|
|
54
|
+
limit: 200
|
|
55
|
+
},
|
|
56
|
+
pollInterval: 3e3,
|
|
57
|
+
fetchPolicy: "cache-and-network",
|
|
58
|
+
skip: !p
|
|
59
|
+
}), x = t({
|
|
52
60
|
variables: {
|
|
53
|
-
|
|
61
|
+
sessionId: m ?? "",
|
|
54
62
|
limit: 200
|
|
55
63
|
},
|
|
56
64
|
pollInterval: 3e3,
|
|
57
|
-
fetchPolicy: "cache-and-network"
|
|
58
|
-
|
|
65
|
+
fetchPolicy: "cache-and-network",
|
|
66
|
+
skip: !m
|
|
67
|
+
}), { data: S, loading: C, error: w, refetch: T } = _ ? b : x, [E, D] = i("none"), O = r(() => (_ ? b.data?.vibecontrolsAgentConnectionEvents ?? [] : x.data?.vibecontrolsSessionConnectionEvents ?? []).map((e) => ({
|
|
59
68
|
id: e.id,
|
|
60
69
|
timestamp: e.timestamp,
|
|
61
70
|
sequence: e.sequence,
|
|
@@ -63,89 +72,95 @@ function f({ agentId: f, agentName: p }) {
|
|
|
63
72
|
level: e.level,
|
|
64
73
|
message: e.message,
|
|
65
74
|
detailsJson: e.detailsJson ?? null
|
|
66
|
-
})), [
|
|
67
|
-
|
|
75
|
+
})), [
|
|
76
|
+
_,
|
|
77
|
+
b.data,
|
|
78
|
+
x.data
|
|
79
|
+
]), k = n(async (e) => {
|
|
80
|
+
let t = e === "text" ? u(O) : d(O, v, y);
|
|
68
81
|
try {
|
|
69
|
-
await navigator.clipboard.writeText(t),
|
|
82
|
+
await navigator.clipboard.writeText(t), D(e), setTimeout(() => D("none"), 1500);
|
|
70
83
|
} catch {
|
|
71
|
-
|
|
84
|
+
f(`${_ ? "agent" : "session"}-${v}-connection-log.${e === "text" ? "txt" : "json"}`, e === "text" ? "text/plain" : "application/json", t);
|
|
72
85
|
}
|
|
73
86
|
}, [
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
87
|
+
O,
|
|
88
|
+
v,
|
|
89
|
+
y,
|
|
90
|
+
_
|
|
91
|
+
]), A = n((e) => {
|
|
92
|
+
let t = e === "text" ? u(O) : d(O, v, y), n = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
93
|
+
f(`${_ ? "agent" : "session"}-${y ?? v}-connection-log-${n}.${e === "text" ? "txt" : "json"}`, e === "text" ? "text/plain" : "application/json", t);
|
|
80
94
|
}, [
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
95
|
+
O,
|
|
96
|
+
v,
|
|
97
|
+
y,
|
|
98
|
+
_
|
|
84
99
|
]);
|
|
85
|
-
return /* @__PURE__ */
|
|
100
|
+
return /* @__PURE__ */ o("div", {
|
|
86
101
|
className: "flex flex-col h-full",
|
|
87
102
|
children: [
|
|
88
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ o("div", {
|
|
89
104
|
className: "flex items-center justify-between p-4 border-b border-border-default",
|
|
90
|
-
children: [/* @__PURE__ */
|
|
105
|
+
children: [/* @__PURE__ */ o("div", { children: [/* @__PURE__ */ a("h3", {
|
|
91
106
|
className: "text-fg-default font-semibold text-base",
|
|
92
107
|
children: "Connection Log"
|
|
93
|
-
}), /* @__PURE__ */
|
|
108
|
+
}), /* @__PURE__ */ a("p", {
|
|
94
109
|
className: "text-fg-muted text-xs mt-0.5",
|
|
95
|
-
children:
|
|
96
|
-
})] }), /* @__PURE__ */
|
|
110
|
+
children: C && O.length === 0 ? "Loading…" : `${O.length} event${O.length === 1 ? "" : "s"} · auto-refresh every 3s`
|
|
111
|
+
})] }), /* @__PURE__ */ o("div", {
|
|
97
112
|
className: "flex gap-2",
|
|
98
113
|
children: [
|
|
99
|
-
/* @__PURE__ */
|
|
114
|
+
/* @__PURE__ */ a("button", {
|
|
100
115
|
type: "button",
|
|
101
|
-
onClick: () =>
|
|
116
|
+
onClick: () => T(),
|
|
102
117
|
className: "px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default",
|
|
103
118
|
children: "Refresh"
|
|
104
119
|
}),
|
|
105
|
-
/* @__PURE__ */
|
|
120
|
+
/* @__PURE__ */ a("button", {
|
|
106
121
|
type: "button",
|
|
107
|
-
onClick: () =>
|
|
122
|
+
onClick: () => k("text"),
|
|
108
123
|
className: "px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default",
|
|
109
|
-
children:
|
|
124
|
+
children: E === "text" ? "Copied!" : "Copy text"
|
|
110
125
|
}),
|
|
111
|
-
/* @__PURE__ */
|
|
126
|
+
/* @__PURE__ */ a("button", {
|
|
112
127
|
type: "button",
|
|
113
|
-
onClick: () =>
|
|
128
|
+
onClick: () => k("json"),
|
|
114
129
|
className: "px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default",
|
|
115
|
-
children:
|
|
130
|
+
children: E === "json" ? "Copied!" : "Copy JSON"
|
|
116
131
|
}),
|
|
117
|
-
/* @__PURE__ */
|
|
132
|
+
/* @__PURE__ */ a("button", {
|
|
118
133
|
type: "button",
|
|
119
|
-
onClick: () =>
|
|
134
|
+
onClick: () => A("text"),
|
|
120
135
|
className: "px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary",
|
|
121
136
|
children: "Export .txt"
|
|
122
137
|
}),
|
|
123
|
-
/* @__PURE__ */
|
|
138
|
+
/* @__PURE__ */ a("button", {
|
|
124
139
|
type: "button",
|
|
125
|
-
onClick: () =>
|
|
140
|
+
onClick: () => A("json"),
|
|
126
141
|
className: "px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary",
|
|
127
142
|
children: "Export .json"
|
|
128
143
|
})
|
|
129
144
|
]
|
|
130
145
|
})]
|
|
131
146
|
}),
|
|
132
|
-
|
|
147
|
+
w && /* @__PURE__ */ o("div", {
|
|
133
148
|
className: "m-4 p-3 rounded bg-status-danger-bg text-status-danger-text text-sm",
|
|
134
|
-
children: ["Failed to load events: ",
|
|
149
|
+
children: ["Failed to load events: ", w.message]
|
|
135
150
|
}),
|
|
136
|
-
/* @__PURE__ */
|
|
151
|
+
/* @__PURE__ */ o("div", {
|
|
137
152
|
className: "flex-1 overflow-auto p-4",
|
|
138
|
-
children: [
|
|
153
|
+
children: [O.length === 0 && !C && /* @__PURE__ */ o("div", {
|
|
139
154
|
className: "text-fg-muted text-sm text-center py-12",
|
|
140
155
|
children: [
|
|
141
156
|
"No connection events recorded for this agent yet.",
|
|
142
|
-
/* @__PURE__ */
|
|
157
|
+
/* @__PURE__ */ a("br", {}),
|
|
143
158
|
"Events appear here every time the platform probes, registers, or syncs the agent."
|
|
144
159
|
]
|
|
145
|
-
}), /* @__PURE__ */
|
|
160
|
+
}), /* @__PURE__ */ a("ol", {
|
|
146
161
|
className: "space-y-2",
|
|
147
|
-
children:
|
|
148
|
-
let t =
|
|
162
|
+
children: O.map((e) => {
|
|
163
|
+
let t = s[e.level] ?? s.info, n = c[e.level] ?? c.info, r = (() => {
|
|
149
164
|
if (!e.detailsJson) return null;
|
|
150
165
|
try {
|
|
151
166
|
return JSON.stringify(JSON.parse(e.detailsJson), null, 2);
|
|
@@ -153,39 +168,39 @@ function f({ agentId: f, agentName: p }) {
|
|
|
153
168
|
return e.detailsJson;
|
|
154
169
|
}
|
|
155
170
|
})();
|
|
156
|
-
return /* @__PURE__ */
|
|
171
|
+
return /* @__PURE__ */ a("li", {
|
|
157
172
|
className: "rounded border border-border-default bg-bg-subtle",
|
|
158
|
-
children: /* @__PURE__ */
|
|
173
|
+
children: /* @__PURE__ */ o("div", {
|
|
159
174
|
className: "flex items-start gap-3 p-3",
|
|
160
|
-
children: [/* @__PURE__ */
|
|
175
|
+
children: [/* @__PURE__ */ a("div", {
|
|
161
176
|
className: `px-2 py-0.5 rounded text-[10px] uppercase font-mono ${n} ${t} shrink-0`,
|
|
162
177
|
children: e.level
|
|
163
|
-
}), /* @__PURE__ */
|
|
178
|
+
}), /* @__PURE__ */ o("div", {
|
|
164
179
|
className: "flex-1 min-w-0",
|
|
165
180
|
children: [
|
|
166
|
-
/* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ o("div", {
|
|
167
182
|
className: "flex items-center gap-2 text-xs text-fg-muted font-mono",
|
|
168
183
|
children: [
|
|
169
|
-
/* @__PURE__ */
|
|
170
|
-
/* @__PURE__ */
|
|
171
|
-
/* @__PURE__ */
|
|
172
|
-
/* @__PURE__ */
|
|
173
|
-
/* @__PURE__ */
|
|
184
|
+
/* @__PURE__ */ o("span", { children: ["#", e.sequence] }),
|
|
185
|
+
/* @__PURE__ */ a("span", { children: "·" }),
|
|
186
|
+
/* @__PURE__ */ a("span", { children: l(e.timestamp) }),
|
|
187
|
+
/* @__PURE__ */ a("span", { children: "·" }),
|
|
188
|
+
/* @__PURE__ */ a("span", {
|
|
174
189
|
className: "text-fg-default font-medium",
|
|
175
190
|
children: e.step
|
|
176
191
|
})
|
|
177
192
|
]
|
|
178
193
|
}),
|
|
179
|
-
/* @__PURE__ */
|
|
194
|
+
/* @__PURE__ */ a("div", {
|
|
180
195
|
className: "text-sm text-fg-default mt-1 break-words",
|
|
181
196
|
children: e.message
|
|
182
197
|
}),
|
|
183
|
-
r && /* @__PURE__ */
|
|
198
|
+
r && /* @__PURE__ */ o("details", {
|
|
184
199
|
className: "mt-2",
|
|
185
|
-
children: [/* @__PURE__ */
|
|
200
|
+
children: [/* @__PURE__ */ a("summary", {
|
|
186
201
|
className: "text-xs text-fg-muted cursor-pointer hover:text-fg-default",
|
|
187
202
|
children: "Details"
|
|
188
|
-
}), /* @__PURE__ */
|
|
203
|
+
}), /* @__PURE__ */ a("pre", {
|
|
189
204
|
className: "mt-2 p-2 rounded bg-bg-default text-xs text-fg-default overflow-x-auto whitespace-pre-wrap break-words",
|
|
190
205
|
children: r
|
|
191
206
|
})]
|
|
@@ -201,6 +216,6 @@ function f({ agentId: f, agentName: p }) {
|
|
|
201
216
|
});
|
|
202
217
|
}
|
|
203
218
|
//#endregion
|
|
204
|
-
export {
|
|
219
|
+
export { p as ConnectionLogPanel };
|
|
205
220
|
|
|
206
221
|
//# sourceMappingURL=ConnectionLogPanel.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConnectionLogPanel.js","names":[],"sources":["../../../src/components/agents/ConnectionLogPanel.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport { useGetAgentConnectionEventsQuery } from '@/generated/wspace-operations';\n\ninterface Props {\n agentId: string;\n agentName?: string;\n}\n\ninterface EventRow {\n id: string;\n timestamp: string;\n sequence: number;\n step: string;\n level: string;\n message: string;\n detailsJson?: string | null;\n}\n\nconst LEVEL_COLOR: Record<string, string> = {\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-danger-text',\n debug: 'text-fg-muted',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n info: 'bg-status-info-bg',\n warn: 'bg-status-warning-bg',\n error: 'bg-status-danger-bg',\n debug: 'bg-bg-muted',\n};\n\nfunction formatTimestamp(iso: string): string {\n try {\n return new Date(iso).toISOString().replace('T', ' ').replace('Z', '');\n } catch {\n return iso;\n }\n}\n\nfunction eventsAsText(rows: EventRow[]): string {\n return rows\n .map((e) => {\n const detail = e.detailsJson ? `\\n details: ${e.detailsJson}` : '';\n return `[${e.timestamp}] #${e.sequence} ${e.level.toUpperCase().padEnd(5)} ${e.step.padEnd(28)} ${e.message}${detail}`;\n })\n .join('\\n');\n}\n\nfunction eventsAsJson(rows: EventRow[], agentId: string, agentName?: string): string {\n return JSON.stringify(\n {\n exportedAt: new Date().toISOString(),\n agentId,\n agentName,\n count: rows.length,\n events: rows.map((e) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n details: e.detailsJson ? JSON.parse(e.detailsJson) : null,\n })),\n },\n null,\n 2,\n );\n}\n\nfunction downloadBlob(filename: string, mime: string, body: string): void {\n const blob = new Blob([body], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\nexport function ConnectionLogPanel({ agentId, agentName }: Props) {\n // Poll every 3s while the panel is mounted so the operator sees new\n // events appear as the svc records them — no manual refresh needed\n // while debugging \"stuck\" agents.\n const { data, loading, error, refetch } = useGetAgentConnectionEventsQuery({\n variables: { agentId, limit: 200 },\n pollInterval: 3000,\n fetchPolicy: 'cache-and-network',\n });\n const [copied, setCopied] = useState<'none' | 'text' | 'json'>('none');\n\n const events: EventRow[] = useMemo(() => {\n const raw = data?.vibecontrolsAgentConnectionEvents ?? [];\n return raw.map((e: { id: string; timestamp: string; sequence: number; step: string; level: string; message: string; detailsJson?: string | null }) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n detailsJson: e.detailsJson ?? null,\n }));\n }, [data]);\n\n const handleCopy = useCallback(\n async (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, agentId, agentName);\n try {\n await navigator.clipboard.writeText(body);\n setCopied(mode);\n setTimeout(() => setCopied('none'), 1500);\n } catch {\n /* clipboard denied — fall back to download */\n downloadBlob(`agent-${agentId}-connection-log.${mode === 'text' ? 'txt' : 'json'}`, mode === 'text' ? 'text/plain' : 'application/json', body);\n }\n },\n [events, agentId, agentName],\n );\n\n const handleDownload = useCallback(\n (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, agentId, agentName);\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n downloadBlob(\n `agent-${agentName ?? agentId}-connection-log-${stamp}.${mode === 'text' ? 'txt' : 'json'}`,\n mode === 'text' ? 'text/plain' : 'application/json',\n body,\n );\n },\n [events, agentId, agentName],\n );\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center justify-between p-4 border-b border-border-default\">\n <div>\n <h3 className=\"text-fg-default font-semibold text-base\">Connection Log</h3>\n <p className=\"text-fg-muted text-xs mt-0.5\">\n {loading && events.length === 0\n ? 'Loading…'\n : `${events.length} event${events.length === 1 ? '' : 's'} · auto-refresh every 3s`}\n </p>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'text' ? 'Copied!' : 'Copy text'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'json' ? 'Copied!' : 'Copy JSON'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .txt\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .json\n </button>\n </div>\n </div>\n\n {error && (\n <div className=\"m-4 p-3 rounded bg-status-danger-bg text-status-danger-text text-sm\">\n Failed to load events: {error.message}\n </div>\n )}\n\n <div className=\"flex-1 overflow-auto p-4\">\n {events.length === 0 && !loading && (\n <div className=\"text-fg-muted text-sm text-center py-12\">\n No connection events recorded for this agent yet.\n <br />\n Events appear here every time the platform probes, registers, or syncs the agent.\n </div>\n )}\n <ol className=\"space-y-2\">\n {events.map((e) => {\n const levelColor = LEVEL_COLOR[e.level] ?? LEVEL_COLOR.info;\n const levelBg = LEVEL_BG[e.level] ?? LEVEL_BG.info;\n const details = (() => {\n if (!e.detailsJson) return null;\n try {\n return JSON.stringify(JSON.parse(e.detailsJson), null, 2);\n } catch {\n return e.detailsJson;\n }\n })();\n return (\n <li\n key={e.id}\n className=\"rounded border border-border-default bg-bg-subtle\"\n >\n <div className=\"flex items-start gap-3 p-3\">\n <div className={`px-2 py-0.5 rounded text-[10px] uppercase font-mono ${levelBg} ${levelColor} shrink-0`}>\n {e.level}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 text-xs text-fg-muted font-mono\">\n <span>#{e.sequence}</span>\n <span>·</span>\n <span>{formatTimestamp(e.timestamp)}</span>\n <span>·</span>\n <span className=\"text-fg-default font-medium\">{e.step}</span>\n </div>\n <div className=\"text-sm text-fg-default mt-1 break-words\">{e.message}</div>\n {details && (\n <details className=\"mt-2\">\n <summary className=\"text-xs text-fg-muted cursor-pointer hover:text-fg-default\">\n Details\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-bg-default text-xs text-fg-default overflow-x-auto whitespace-pre-wrap break-words\">\n {details}\n </pre>\n </details>\n )}\n </div>\n </div>\n </li>\n );\n })}\n </ol>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ConnectionLogPanel.js","names":[],"sources":["../../../src/components/agents/ConnectionLogPanel.tsx"],"sourcesContent":["import { useCallback, useMemo, useState } from 'react';\nimport {\n useGetAgentConnectionEventsQuery,\n useGetSessionConnectionEventsQuery,\n} from '@/generated/wspace-operations';\n\ninterface Props {\n /** Pass agentId XOR sessionId. */\n agentId?: string;\n sessionId?: string;\n /** Friendly label shown in the export filename. */\n agentName?: string;\n sessionName?: string;\n}\n\ninterface EventRow {\n id: string;\n timestamp: string;\n sequence: number;\n step: string;\n level: string;\n message: string;\n detailsJson?: string | null;\n}\n\nconst LEVEL_COLOR: Record<string, string> = {\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-danger-text',\n debug: 'text-fg-muted',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n info: 'bg-status-info-bg',\n warn: 'bg-status-warning-bg',\n error: 'bg-status-danger-bg',\n debug: 'bg-bg-muted',\n};\n\nfunction formatTimestamp(iso: string): string {\n try {\n return new Date(iso).toISOString().replace('T', ' ').replace('Z', '');\n } catch {\n return iso;\n }\n}\n\nfunction eventsAsText(rows: EventRow[]): string {\n return rows\n .map((e) => {\n const detail = e.detailsJson ? `\\n details: ${e.detailsJson}` : '';\n return `[${e.timestamp}] #${e.sequence} ${e.level.toUpperCase().padEnd(5)} ${e.step.padEnd(28)} ${e.message}${detail}`;\n })\n .join('\\n');\n}\n\nfunction eventsAsJson(rows: EventRow[], agentId: string, agentName?: string): string {\n return JSON.stringify(\n {\n exportedAt: new Date().toISOString(),\n agentId,\n agentName,\n count: rows.length,\n events: rows.map((e) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n details: e.detailsJson ? JSON.parse(e.detailsJson) : null,\n })),\n },\n null,\n 2,\n );\n}\n\nfunction downloadBlob(filename: string, mime: string, body: string): void {\n const blob = new Blob([body], { type: mime });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.appendChild(a);\n a.click();\n document.body.removeChild(a);\n URL.revokeObjectURL(url);\n}\n\nexport function ConnectionLogPanel({ agentId, sessionId, agentName, sessionName }: Props) {\n // Poll every 3s while the panel is mounted so the operator sees new\n // events appear as the svc records them. The same panel powers both\n // the AgentDetails \"Connection Log\" tab (pass agentId) and the\n // SessionDetails \"Connection Log\" tab (pass sessionId). Apollo\n // skips the query whose id is missing.\n const isAgent = !!agentId;\n const subjectId = (agentId ?? sessionId) || '';\n const subjectLabel = isAgent ? agentName : sessionName;\n\n const agentQuery = useGetAgentConnectionEventsQuery({\n variables: { agentId: agentId ?? '', limit: 200 },\n pollInterval: 3000,\n fetchPolicy: 'cache-and-network',\n skip: !agentId,\n });\n const sessionQuery = useGetSessionConnectionEventsQuery({\n variables: { sessionId: sessionId ?? '', limit: 200 },\n pollInterval: 3000,\n fetchPolicy: 'cache-and-network',\n skip: !sessionId,\n });\n const { data, loading, error, refetch } = isAgent ? agentQuery : sessionQuery;\n const [copied, setCopied] = useState<'none' | 'text' | 'json'>('none');\n\n const events: EventRow[] = useMemo(() => {\n const rawList: Array<{\n id: string;\n timestamp: string;\n sequence: number;\n step: string;\n level: string;\n message: string;\n detailsJson?: string | null;\n }> = isAgent\n ? (agentQuery.data?.vibecontrolsAgentConnectionEvents ?? [])\n : (sessionQuery.data?.vibecontrolsSessionConnectionEvents ?? []);\n return rawList.map((e) => ({\n id: e.id,\n timestamp: e.timestamp,\n sequence: e.sequence,\n step: e.step,\n level: e.level,\n message: e.message,\n detailsJson: e.detailsJson ?? null,\n }));\n }, [isAgent, agentQuery.data, sessionQuery.data]);\n\n const handleCopy = useCallback(\n async (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, subjectId, subjectLabel);\n try {\n await navigator.clipboard.writeText(body);\n setCopied(mode);\n setTimeout(() => setCopied('none'), 1500);\n } catch {\n const filename = `${isAgent ? 'agent' : 'session'}-${subjectId}-connection-log.${\n mode === 'text' ? 'txt' : 'json'\n }`;\n downloadBlob(filename, mode === 'text' ? 'text/plain' : 'application/json', body);\n }\n },\n [events, subjectId, subjectLabel, isAgent],\n );\n\n const handleDownload = useCallback(\n (mode: 'text' | 'json') => {\n const body = mode === 'text' ? eventsAsText(events) : eventsAsJson(events, subjectId, subjectLabel);\n const stamp = new Date().toISOString().replace(/[:.]/g, '-');\n const prefix = isAgent ? 'agent' : 'session';\n downloadBlob(\n `${prefix}-${subjectLabel ?? subjectId}-connection-log-${stamp}.${mode === 'text' ? 'txt' : 'json'}`,\n mode === 'text' ? 'text/plain' : 'application/json',\n body,\n );\n },\n [events, subjectId, subjectLabel, isAgent],\n );\n\n return (\n <div className=\"flex flex-col h-full\">\n <div className=\"flex items-center justify-between p-4 border-b border-border-default\">\n <div>\n <h3 className=\"text-fg-default font-semibold text-base\">Connection Log</h3>\n <p className=\"text-fg-muted text-xs mt-0.5\">\n {loading && events.length === 0\n ? 'Loading…'\n : `${events.length} event${events.length === 1 ? '' : 's'} · auto-refresh every 3s`}\n </p>\n </div>\n <div className=\"flex gap-2\">\n <button\n type=\"button\"\n onClick={() => refetch()}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n Refresh\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'text' ? 'Copied!' : 'Copy text'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleCopy('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-bg-subtle hover:bg-bg-muted text-fg-default border border-border-default\"\n >\n {copied === 'json' ? 'Copied!' : 'Copy JSON'}\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('text')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .txt\n </button>\n <button\n type=\"button\"\n onClick={() => handleDownload('json')}\n className=\"px-3 py-1.5 text-xs rounded bg-action-primary hover:bg-action-primary-hover text-action-primary-text border border-action-primary\"\n >\n Export .json\n </button>\n </div>\n </div>\n\n {error && (\n <div className=\"m-4 p-3 rounded bg-status-danger-bg text-status-danger-text text-sm\">\n Failed to load events: {error.message}\n </div>\n )}\n\n <div className=\"flex-1 overflow-auto p-4\">\n {events.length === 0 && !loading && (\n <div className=\"text-fg-muted text-sm text-center py-12\">\n No connection events recorded for this agent yet.\n <br />\n Events appear here every time the platform probes, registers, or syncs the agent.\n </div>\n )}\n <ol className=\"space-y-2\">\n {events.map((e) => {\n const levelColor = LEVEL_COLOR[e.level] ?? LEVEL_COLOR.info;\n const levelBg = LEVEL_BG[e.level] ?? LEVEL_BG.info;\n const details = (() => {\n if (!e.detailsJson) return null;\n try {\n return JSON.stringify(JSON.parse(e.detailsJson), null, 2);\n } catch {\n return e.detailsJson;\n }\n })();\n return (\n <li\n key={e.id}\n className=\"rounded border border-border-default bg-bg-subtle\"\n >\n <div className=\"flex items-start gap-3 p-3\">\n <div className={`px-2 py-0.5 rounded text-[10px] uppercase font-mono ${levelBg} ${levelColor} shrink-0`}>\n {e.level}\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2 text-xs text-fg-muted font-mono\">\n <span>#{e.sequence}</span>\n <span>·</span>\n <span>{formatTimestamp(e.timestamp)}</span>\n <span>·</span>\n <span className=\"text-fg-default font-medium\">{e.step}</span>\n </div>\n <div className=\"text-sm text-fg-default mt-1 break-words\">{e.message}</div>\n {details && (\n <details className=\"mt-2\">\n <summary className=\"text-xs text-fg-muted cursor-pointer hover:text-fg-default\">\n Details\n </summary>\n <pre className=\"mt-2 p-2 rounded bg-bg-default text-xs text-fg-default overflow-x-auto whitespace-pre-wrap break-words\">\n {details}\n </pre>\n </details>\n )}\n </div>\n </div>\n </li>\n );\n })}\n </ol>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;AAyBA,IAAM,IAAsC;CAC1C,MAAM;CACN,MAAM;CACN,OAAO;CACP,OAAO;CACR,EAEK,IAAmC;CACvC,MAAM;CACN,MAAM;CACN,OAAO;CACP,OAAO;CACR;AAED,SAAS,EAAgB,GAAqB;AAC5C,KAAI;AACF,SAAO,IAAI,KAAK,EAAI,CAAC,aAAa,CAAC,QAAQ,KAAK,IAAI,CAAC,QAAQ,KAAK,GAAG;SAC/D;AACN,SAAO;;;AAIX,SAAS,EAAa,GAA0B;AAC9C,QAAO,EACJ,KAAK,MAAM;EACV,IAAM,IAAS,EAAE,cAAc,kBAAkB,EAAE,gBAAgB;AACnE,SAAO,IAAI,EAAE,UAAU,KAAK,EAAE,SAAS,GAAG,EAAE,MAAM,aAAa,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,KAAK,OAAO,GAAG,CAAC,GAAG,EAAE,UAAU;GAC9G,CACD,KAAK,KAAK;;AAGf,SAAS,EAAa,GAAkB,GAAiB,GAA4B;AACnF,QAAO,KAAK,UACV;EACE,6BAAY,IAAI,MAAM,EAAC,aAAa;EACpC;EACA;EACA,OAAO,EAAK;EACZ,QAAQ,EAAK,KAAK,OAAO;GACvB,IAAI,EAAE;GACN,WAAW,EAAE;GACb,UAAU,EAAE;GACZ,MAAM,EAAE;GACR,OAAO,EAAE;GACT,SAAS,EAAE;GACX,SAAS,EAAE,cAAc,KAAK,MAAM,EAAE,YAAY,GAAG;GACtD,EAAE;EACJ,EACD,MACA,EACD;;AAGH,SAAS,EAAa,GAAkB,GAAc,GAAoB;CACxE,IAAM,IAAO,IAAI,KAAK,CAAC,EAAK,EAAE,EAAE,MAAM,GAAM,CAAC,EACvC,IAAM,IAAI,gBAAgB,EAAK,EAC/B,IAAI,SAAS,cAAc,IAAI;AAMrC,CALA,EAAE,OAAO,GACT,EAAE,WAAW,GACb,SAAS,KAAK,YAAY,EAAE,EAC5B,EAAE,OAAO,EACT,SAAS,KAAK,YAAY,EAAE,EAC5B,IAAI,gBAAgB,EAAI;;AAG1B,SAAgB,EAAmB,EAAE,YAAS,cAAW,cAAW,kBAAsB;CAMxF,IAAM,IAAU,CAAC,CAAC,GACZ,KAAa,KAAW,MAAc,IACtC,IAAe,IAAU,IAAY,GAErC,IAAa,EAAiC;EAClD,WAAW;GAAE,SAAS,KAAW;GAAI,OAAO;GAAK;EACjD,cAAc;EACd,aAAa;EACb,MAAM,CAAC;EACR,CAAC,EACI,IAAe,EAAmC;EACtD,WAAW;GAAE,WAAW,KAAa;GAAI,OAAO;GAAK;EACrD,cAAc;EACd,aAAa;EACb,MAAM,CAAC;EACR,CAAC,EACI,EAAE,SAAM,YAAS,UAAO,eAAY,IAAU,IAAa,GAC3D,CAAC,GAAQ,KAAa,EAAmC,OAAO,EAEhE,IAAqB,SASpB,IACA,EAAW,MAAM,qCAAqC,EAAE,GACxD,EAAa,MAAM,uCAAuC,EAAE,EAClD,KAAK,OAAO;EACzB,IAAI,EAAE;EACN,WAAW,EAAE;EACb,UAAU,EAAE;EACZ,MAAM,EAAE;EACR,OAAO,EAAE;EACT,SAAS,EAAE;EACX,aAAa,EAAE,eAAe;EAC/B,EAAE,EACF;EAAC;EAAS,EAAW;EAAM,EAAa;EAAK,CAAC,EAE3C,IAAa,EACjB,OAAO,MAA0B;EAC/B,IAAM,IAAO,MAAS,SAAS,EAAa,EAAO,GAAG,EAAa,GAAQ,GAAW,EAAa;AACnG,MAAI;AAGF,GAFA,MAAM,UAAU,UAAU,UAAU,EAAK,EACzC,EAAU,EAAK,EACf,iBAAiB,EAAU,OAAO,EAAE,KAAK;UACnC;AAIN,KAHiB,GAAG,IAAU,UAAU,UAAU,GAAG,EAAU,kBAC7D,MAAS,SAAS,QAAQ,UAEL,MAAS,SAAS,eAAe,oBAAoB,EAAK;;IAGrF;EAAC;EAAQ;EAAW;EAAc;EAAQ,CAC3C,EAEK,IAAiB,GACpB,MAA0B;EACzB,IAAM,IAAO,MAAS,SAAS,EAAa,EAAO,GAAG,EAAa,GAAQ,GAAW,EAAa,EAC7F,qBAAQ,IAAI,MAAM,EAAC,aAAa,CAAC,QAAQ,SAAS,IAAI;AAE5D,IACE,GAFa,IAAU,UAAU,UAEvB,GAAG,KAAgB,EAAU,kBAAkB,EAAM,GAAG,MAAS,SAAS,QAAQ,UAC5F,MAAS,SAAS,eAAe,oBACjC,EACD;IAEH;EAAC;EAAQ;EAAW;EAAc;EAAQ,CAC3C;AAED,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,OAAD,EAAA,UAAA,CACE,kBAAC,MAAD;KAAI,WAAU;eAA0C;KAAmB,CAAA,EAC3E,kBAAC,KAAD;KAAG,WAAU;eACV,KAAW,EAAO,WAAW,IAC1B,aACA,GAAG,EAAO,OAAO,QAAQ,EAAO,WAAW,IAAI,KAAK,IAAI;KAC1D,CAAA,CACA,EAAA,CAAA,EACN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,GAAS;OACxB,WAAU;iBACX;OAEQ,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,OAAO;OACjC,WAAU;iBAET,MAAW,SAAS,YAAY;OAC1B,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAW,OAAO;OACjC,WAAU;iBAET,MAAW,SAAS,YAAY;OAC1B,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAe,OAAO;OACrC,WAAU;iBACX;OAEQ,CAAA;MACT,kBAAC,UAAD;OACE,MAAK;OACL,eAAe,EAAe,OAAO;OACrC,WAAU;iBACX;OAEQ,CAAA;MACL;OACF;;GAEL,KACC,kBAAC,OAAD;IAAK,WAAU;cAAf,CAAqF,2BAC3D,EAAM,QAC1B;;GAGR,kBAAC,OAAD;IAAK,WAAU;cAAf,CACG,EAAO,WAAW,KAAK,CAAC,KACvB,kBAAC,OAAD;KAAK,WAAU;eAAf;MAAyD;MAEvD,kBAAC,MAAD,EAAM,CAAA;;MAEF;QAER,kBAAC,MAAD;KAAI,WAAU;eACX,EAAO,KAAK,MAAM;MACjB,IAAM,IAAa,EAAY,EAAE,UAAU,EAAY,MACjD,IAAU,EAAS,EAAE,UAAU,EAAS,MACxC,WAAiB;AACrB,WAAI,CAAC,EAAE,YAAa,QAAO;AAC3B,WAAI;AACF,eAAO,KAAK,UAAU,KAAK,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE;eACnD;AACN,eAAO,EAAE;;UAET;AACJ,aACE,kBAAC,MAAD;OAEE,WAAU;iBAEV,kBAAC,OAAD;QAAK,WAAU;kBAAf,CACE,kBAAC,OAAD;SAAK,WAAW,uDAAuD,EAAQ,GAAG,EAAW;mBAC1F,EAAE;SACC,CAAA,EACN,kBAAC,OAAD;SAAK,WAAU;mBAAf;UACE,kBAAC,OAAD;WAAK,WAAU;qBAAf;YACE,kBAAC,QAAD,EAAA,UAAA,CAAM,KAAE,EAAE,SAAgB,EAAA,CAAA;YAC1B,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA;YACd,kBAAC,QAAD,EAAA,UAAO,EAAgB,EAAE,UAAU,EAAQ,CAAA;YAC3C,kBAAC,QAAD,EAAA,UAAM,KAAQ,CAAA;YACd,kBAAC,QAAD;aAAM,WAAU;uBAA+B,EAAE;aAAY,CAAA;YACzD;;UACN,kBAAC,OAAD;WAAK,WAAU;qBAA4C,EAAE;WAAc,CAAA;UAC1E,KACC,kBAAC,WAAD;WAAS,WAAU;qBAAnB,CACE,kBAAC,WAAD;YAAS,WAAU;sBAA6D;YAEtE,CAAA,EACV,kBAAC,OAAD;YAAK,WAAU;sBACZ;YACG,CAAA,CACE;;UAER;WACF;;OACH,EA5BE,EAAE,GA4BJ;OAEP;KACC,CAAA,CACD;;GACF"}
|
|
@@ -203,7 +203,7 @@ var h = {
|
|
|
203
203
|
style: { maxHeight: C ? "300px" : void 0 },
|
|
204
204
|
children: G.length === 0 ? /* @__PURE__ */ f("div", {
|
|
205
205
|
className: "flex h-full min-h-[200px] items-center justify-center text-text-tertiary",
|
|
206
|
-
children:
|
|
206
|
+
children: _ ? N ? `${B("logs.subscriptionError", "Log stream error")}: ${N}` : z ? B("logs.connecting", "Connecting to log stream…") : B("logs.waitingForEntries", "Waiting for log entries…") : B("logs.connectToAgent", "Connect to an agent to view logs")
|
|
207
207
|
}) : G.map((e, t) => /* @__PURE__ */ p("div", {
|
|
208
208
|
className: `flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${g[e.level] ?? ""}`,
|
|
209
209
|
children: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"LogViewer.js","names":[],"sources":["../../../src/components/logs/LogViewer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type FC } from 'react';\nimport { Download, Pause, Play, RefreshCw, Search, Trash2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useVibecontrolsAgentLogStreamSubscription } from '@/generated/wspace-operations';\n\ninterface LogEntry {\n timestamp: string;\n level: 'debug' | 'info' | 'warn' | 'error';\n source: string;\n message: string;\n metadata?: Record<string, unknown>;\n}\n\ninterface LogViewerProps {\n agentId: string;\n /** Canonical CLI profile name on the remote agent. */\n profile: string;\n level?: 'debug' | 'info' | 'warn' | 'error';\n source?: string;\n maxEntries?: number;\n className?: string;\n compact?: boolean;\n}\n\nfunction getEntryKey(entry: LogEntry): string {\n return `${entry.timestamp}:${entry.level}:${entry.source}:${entry.message}`;\n}\n\nconst LEVEL_COLORS: Record<string, string> = {\n debug: 'text-text-tertiary',\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-error-text',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n debug: 'bg-bg-tertiary/30',\n info: 'bg-status-info-bg/10',\n warn: 'bg-status-warning-bg/10',\n error: 'bg-status-error-bg/10',\n};\n\nexport const LogViewer: FC<LogViewerProps> = ({\n agentId,\n profile,\n level: initialLevel = 'info',\n source: initialSource,\n maxEntries = 1000,\n className = '',\n compact = false,\n}) => {\n const { t } = useI18n();\n const [entries, setEntries] = useState<LogEntry[]>([]);\n const [paused, setPaused] = useState(false);\n const [level, setLevel] = useState(initialLevel);\n const [searchQuery, setSearchQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n const autoScrollRef = useRef(true);\n\n // GraphQL subscription — the svc polls the agent server-side every\n // ~2s and pushes only new entries here. Replaces the previous client\n // setInterval + fetch, which was firing infinitely because\n // `tr` recreated on every render mutated fetchLogs' identity, which\n // re-ran the useEffect, which re-armed the interval, which called\n // fetchLogs immediately. The subscription has no client polling at\n // all — Apollo opens one WS, the svc owns the cadence.\n const {\n data: streamData,\n error: subscriptionError,\n loading: streamLoading,\n } = useVibecontrolsAgentLogStreamSubscription({\n variables: {\n agentId,\n profile,\n level,\n source: initialSource,\n },\n skip: !agentId || paused,\n shouldResubscribe: true,\n });\n\n const tr = (key: string, fallback: string) => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const handleScroll = useCallback(() => {\n if (!containerRef.current) {\n return;\n }\n\n const element = containerRef.current;\n autoScrollRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 50;\n }, []);\n\n const scrollToBottom = useCallback(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n }, []);\n\n // Drain incoming subscription batches into the entries list. Each\n // payload is the NEW entries since the last server-side tick; merge\n // by key so re-deliveries (e.g. on reconnect backfill) don't dupe.\n useEffect(() => {\n const batch = streamData?.vibecontrolsAgentLogStream;\n if (!batch || batch.length === 0) return;\n const incoming: LogEntry[] = batch.map((row: { timestamp: string; level: string; source?: string | null; message: string; metadataJson?: string | null }) => {\n let metadata: Record<string, unknown> | undefined;\n if (row.metadataJson) {\n try {\n metadata = JSON.parse(row.metadataJson) as Record<string, unknown>;\n } catch {\n metadata = undefined;\n }\n }\n return {\n timestamp: row.timestamp,\n level: (row.level as LogEntry['level']) ?? 'info',\n source: row.source ?? '',\n message: row.message,\n metadata,\n };\n });\n setEntries((current) => {\n const merged = new Map<string, LogEntry>();\n for (const entry of current) merged.set(getEntryKey(entry), entry);\n for (const entry of incoming) merged.set(getEntryKey(entry), entry);\n return Array.from(merged.values())\n .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())\n .slice(-maxEntries);\n });\n setErrorMessage(null);\n if (autoScrollRef.current) {\n requestAnimationFrame(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n });\n }\n }, [streamData, maxEntries]);\n\n useEffect(() => {\n if (subscriptionError) {\n setErrorMessage(subscriptionError.message);\n }\n }, [subscriptionError]);\n\n const connected = !subscriptionError && !streamLoading;\n const isRefreshing = streamLoading;\n\n // Reset the entries list when the agent / filter set actually changes.\n useEffect(() => {\n setEntries([]);\n setErrorMessage(null);\n }, [agentId, profile, level, initialSource]);\n\n // Manual refresh: no-op since the subscription pushes continuously.\n // Kept as a callback so the existing \"Refresh\" button doesn't have to\n // be conditionally hidden. Wraps in setEntries([]) so the operator's\n // intent of \"give me a fresh window\" is honoured — the next push\n // backfills.\n const fetchLogs = useCallback(() => {\n setEntries([]);\n }, []);\n\n const filteredEntries = searchQuery\n ? entries.filter(\n (entry) =>\n entry.message.toLowerCase().includes(searchQuery.toLowerCase()) ||\n entry.source.toLowerCase().includes(searchQuery.toLowerCase())\n )\n : entries;\n\n const handleClear = () => setEntries([]);\n\n const handleExport = () => {\n const blob = new Blob([filteredEntries.map((entry) => JSON.stringify(entry)).join('\\n')], {\n type: 'application/x-ndjson',\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `agent-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.jsonl`;\n anchor.click();\n URL.revokeObjectURL(url);\n };\n\n const formatTime = (timestamp: string) => {\n try {\n const date = new Date(timestamp);\n return (\n date.toLocaleTimeString('en-US', {\n hour12: false,\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }) +\n '.' +\n String(date.getMilliseconds()).padStart(3, '0')\n );\n } catch {\n return timestamp;\n }\n };\n\n return (\n <div\n className={`flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${className}`}\n >\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2\">\n <div className=\"mr-2 flex items-center gap-1.5\">\n <div\n className={`size-2 rounded-full ${connected ? 'animate-pulse bg-status-success-bg' : 'bg-status-error-bg'}`}\n />\n <span className=\"text-xs text-text-tertiary\">\n {connected ? tr('logs.live', 'Live') : tr('logs.disconnected', 'Disconnected')}\n </span>\n </div>\n\n <select\n value={level}\n onChange={(event) => setLevel(event.target.value as LogEntry['level'])}\n className=\"rounded border border-border-secondary bg-bg-tertiary px-2 py-1 text-xs text-text-primary\"\n aria-label={tr('logs.levelFilter', 'Log level filter')}\n >\n <option value=\"debug\">DEBUG+</option>\n <option value=\"info\">INFO+</option>\n <option value=\"warn\">WARN+</option>\n <option value=\"error\">ERROR</option>\n </select>\n\n <div className=\"relative min-w-[120px] max-w-xs flex-1\">\n <Search\n size={12}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary\"\n />\n <input\n aria-label=\"control\"\n type=\"text\"\n placeholder={tr('logs.filterLogs', 'Filter logs...')}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full rounded border border-border-secondary bg-bg-tertiary py-1 pl-6 pr-2 text-xs text-text-primary placeholder:text-text-tertiary\"\n />\n </div>\n\n <div className=\"ml-auto flex items-center gap-1\">\n <span className=\"text-xs text-text-tertiary\">\n {filteredEntries.length} {tr('logs.entries', 'entries')}\n </span>\n <button\n type=\"button\"\n onClick={() => void fetchLogs()}\n disabled={isRefreshing}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />\n </button>\n <button\n type=\"button\"\n onClick={() => setPaused((current) => !current)}\n className={`rounded p-1 transition-colors hover:bg-bg-tertiary ${paused ? 'text-status-warning-text' : 'text-text-tertiary'}`}\n title={paused ? tr('logs.resume', 'Resume') : tr('logs.pause', 'Pause')}\n >\n {paused ? <Play size={14} /> : <Pause size={14} />}\n </button>\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.clear', 'Clear')}\n >\n <Trash2 size={14} />\n </button>\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.export', 'Export')}\n >\n <Download size={14} />\n </button>\n </div>\n </div>\n\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text\"\n >\n {errorMessage}\n </div>\n ) : null}\n\n <div\n ref={containerRef}\n onScroll={handleScroll}\n className={`overflow-y-auto font-mono text-xs leading-relaxed ${compact ? 'max-h-[300px]' : 'min-h-[400px] flex-1'}`}\n style={{ maxHeight: compact ? '300px' : undefined }}\n >\n {filteredEntries.length === 0 ? (\n <div className=\"flex h-full min-h-[200px] items-center justify-center text-text-tertiary\">\n {connected\n ? tr('logs.waitingForEntries', 'Waiting for log entries...')\n : tr('logs.connectToAgent', 'Connect to an agent to view logs')}\n </div>\n ) : (\n filteredEntries.map((entry, index) => (\n <div\n key={`${entry.timestamp}-${entry.source}-${index}`}\n className={`flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${LEVEL_BG[entry.level] ?? ''}`}\n >\n <span className=\"w-[85px] shrink-0 text-text-tertiary\">\n {formatTime(entry.timestamp)}\n </span>\n <span\n className={`w-[42px] shrink-0 uppercase font-semibold ${LEVEL_COLORS[entry.level] ?? 'text-text-primary'}`}\n >\n {entry.level}\n </span>\n <span\n className=\"w-[100px] shrink-0 truncate text-text-secondary\"\n title={entry.source}\n >\n [{entry.source}]\n </span>\n <span className=\"flex-1 break-all text-text-primary\">\n {entry.message}\n {entry.metadata && Object.keys(entry.metadata).length > 0 ? (\n <span className=\"ml-2 text-text-tertiary\">{JSON.stringify(entry.metadata)}</span>\n ) : null}\n </span>\n </div>\n ))\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"LogViewer.js","names":[],"sources":["../../../src/components/logs/LogViewer.tsx"],"sourcesContent":["import { useCallback, useEffect, useRef, useState, type FC } from 'react';\nimport { Download, Pause, Play, RefreshCw, Search, Trash2 } from 'lucide-react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { useVibecontrolsAgentLogStreamSubscription } from '@/generated/wspace-operations';\n\ninterface LogEntry {\n timestamp: string;\n level: 'debug' | 'info' | 'warn' | 'error';\n source: string;\n message: string;\n metadata?: Record<string, unknown>;\n}\n\ninterface LogViewerProps {\n agentId: string;\n /** Canonical CLI profile name on the remote agent. */\n profile: string;\n level?: 'debug' | 'info' | 'warn' | 'error';\n source?: string;\n maxEntries?: number;\n className?: string;\n compact?: boolean;\n}\n\nfunction getEntryKey(entry: LogEntry): string {\n return `${entry.timestamp}:${entry.level}:${entry.source}:${entry.message}`;\n}\n\nconst LEVEL_COLORS: Record<string, string> = {\n debug: 'text-text-tertiary',\n info: 'text-status-info-text',\n warn: 'text-status-warning-text',\n error: 'text-status-error-text',\n};\n\nconst LEVEL_BG: Record<string, string> = {\n debug: 'bg-bg-tertiary/30',\n info: 'bg-status-info-bg/10',\n warn: 'bg-status-warning-bg/10',\n error: 'bg-status-error-bg/10',\n};\n\nexport const LogViewer: FC<LogViewerProps> = ({\n agentId,\n profile,\n level: initialLevel = 'info',\n source: initialSource,\n maxEntries = 1000,\n className = '',\n compact = false,\n}) => {\n const { t } = useI18n();\n const [entries, setEntries] = useState<LogEntry[]>([]);\n const [paused, setPaused] = useState(false);\n const [level, setLevel] = useState(initialLevel);\n const [searchQuery, setSearchQuery] = useState('');\n const [errorMessage, setErrorMessage] = useState<string | null>(null);\n const containerRef = useRef<HTMLDivElement>(null);\n const autoScrollRef = useRef(true);\n\n // GraphQL subscription — the svc polls the agent server-side every\n // ~2s and pushes only new entries here. Replaces the previous client\n // setInterval + fetch, which was firing infinitely because\n // `tr` recreated on every render mutated fetchLogs' identity, which\n // re-ran the useEffect, which re-armed the interval, which called\n // fetchLogs immediately. The subscription has no client polling at\n // all — Apollo opens one WS, the svc owns the cadence.\n const {\n data: streamData,\n error: subscriptionError,\n loading: streamLoading,\n } = useVibecontrolsAgentLogStreamSubscription({\n variables: {\n agentId,\n profile,\n level,\n source: initialSource,\n },\n skip: !agentId || paused,\n shouldResubscribe: true,\n });\n\n const tr = (key: string, fallback: string) => {\n const value = t(key);\n return value === key ? fallback : value;\n };\n\n const handleScroll = useCallback(() => {\n if (!containerRef.current) {\n return;\n }\n\n const element = containerRef.current;\n autoScrollRef.current = element.scrollHeight - element.scrollTop - element.clientHeight < 50;\n }, []);\n\n const scrollToBottom = useCallback(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n }, []);\n\n // Drain incoming subscription batches into the entries list. Each\n // payload is the NEW entries since the last server-side tick; merge\n // by key so re-deliveries (e.g. on reconnect backfill) don't dupe.\n useEffect(() => {\n const batch = streamData?.vibecontrolsAgentLogStream;\n if (!batch || batch.length === 0) return;\n const incoming: LogEntry[] = batch.map((row: { timestamp: string; level: string; source?: string | null; message: string; metadataJson?: string | null }) => {\n let metadata: Record<string, unknown> | undefined;\n if (row.metadataJson) {\n try {\n metadata = JSON.parse(row.metadataJson) as Record<string, unknown>;\n } catch {\n metadata = undefined;\n }\n }\n return {\n timestamp: row.timestamp,\n level: (row.level as LogEntry['level']) ?? 'info',\n source: row.source ?? '',\n message: row.message,\n metadata,\n };\n });\n setEntries((current) => {\n const merged = new Map<string, LogEntry>();\n for (const entry of current) merged.set(getEntryKey(entry), entry);\n for (const entry of incoming) merged.set(getEntryKey(entry), entry);\n return Array.from(merged.values())\n .sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime())\n .slice(-maxEntries);\n });\n setErrorMessage(null);\n if (autoScrollRef.current) {\n requestAnimationFrame(() => {\n if (containerRef.current) {\n containerRef.current.scrollTop = containerRef.current.scrollHeight;\n }\n });\n }\n }, [streamData, maxEntries]);\n\n useEffect(() => {\n if (subscriptionError) {\n setErrorMessage(subscriptionError.message);\n }\n }, [subscriptionError]);\n\n const connected = !subscriptionError && !streamLoading;\n const isRefreshing = streamLoading;\n\n // Reset the entries list when the agent / filter set actually changes.\n useEffect(() => {\n setEntries([]);\n setErrorMessage(null);\n }, [agentId, profile, level, initialSource]);\n\n // Manual refresh: no-op since the subscription pushes continuously.\n // Kept as a callback so the existing \"Refresh\" button doesn't have to\n // be conditionally hidden. Wraps in setEntries([]) so the operator's\n // intent of \"give me a fresh window\" is honoured — the next push\n // backfills.\n const fetchLogs = useCallback(() => {\n setEntries([]);\n }, []);\n\n const filteredEntries = searchQuery\n ? entries.filter(\n (entry) =>\n entry.message.toLowerCase().includes(searchQuery.toLowerCase()) ||\n entry.source.toLowerCase().includes(searchQuery.toLowerCase())\n )\n : entries;\n\n const handleClear = () => setEntries([]);\n\n const handleExport = () => {\n const blob = new Blob([filteredEntries.map((entry) => JSON.stringify(entry)).join('\\n')], {\n type: 'application/x-ndjson',\n });\n const url = URL.createObjectURL(blob);\n const anchor = document.createElement('a');\n anchor.href = url;\n anchor.download = `agent-logs-${new Date().toISOString().slice(0, 19).replace(/:/g, '-')}.jsonl`;\n anchor.click();\n URL.revokeObjectURL(url);\n };\n\n const formatTime = (timestamp: string) => {\n try {\n const date = new Date(timestamp);\n return (\n date.toLocaleTimeString('en-US', {\n hour12: false,\n hour: '2-digit',\n minute: '2-digit',\n second: '2-digit',\n }) +\n '.' +\n String(date.getMilliseconds()).padStart(3, '0')\n );\n } catch {\n return timestamp;\n }\n };\n\n return (\n <div\n className={`flex flex-col overflow-hidden rounded-lg border border-border-primary bg-bg-elevated ${className}`}\n >\n <div className=\"flex flex-wrap items-center gap-2 border-b border-border-primary bg-bg-secondary/50 px-3 py-2\">\n <div className=\"mr-2 flex items-center gap-1.5\">\n <div\n className={`size-2 rounded-full ${connected ? 'animate-pulse bg-status-success-bg' : 'bg-status-error-bg'}`}\n />\n <span className=\"text-xs text-text-tertiary\">\n {connected ? tr('logs.live', 'Live') : tr('logs.disconnected', 'Disconnected')}\n </span>\n </div>\n\n <select\n value={level}\n onChange={(event) => setLevel(event.target.value as LogEntry['level'])}\n className=\"rounded border border-border-secondary bg-bg-tertiary px-2 py-1 text-xs text-text-primary\"\n aria-label={tr('logs.levelFilter', 'Log level filter')}\n >\n <option value=\"debug\">DEBUG+</option>\n <option value=\"info\">INFO+</option>\n <option value=\"warn\">WARN+</option>\n <option value=\"error\">ERROR</option>\n </select>\n\n <div className=\"relative min-w-[120px] max-w-xs flex-1\">\n <Search\n size={12}\n className=\"absolute left-2 top-1/2 -translate-y-1/2 text-text-tertiary\"\n />\n <input\n aria-label=\"control\"\n type=\"text\"\n placeholder={tr('logs.filterLogs', 'Filter logs...')}\n value={searchQuery}\n onChange={(event) => setSearchQuery(event.target.value)}\n className=\"w-full rounded border border-border-secondary bg-bg-tertiary py-1 pl-6 pr-2 text-xs text-text-primary placeholder:text-text-tertiary\"\n />\n </div>\n\n <div className=\"ml-auto flex items-center gap-1\">\n <span className=\"text-xs text-text-tertiary\">\n {filteredEntries.length} {tr('logs.entries', 'entries')}\n </span>\n <button\n type=\"button\"\n onClick={() => void fetchLogs()}\n disabled={isRefreshing}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary disabled:opacity-50\"\n title={tr('common.refresh', 'Refresh')}\n >\n <RefreshCw size={14} className={isRefreshing ? 'animate-spin' : ''} />\n </button>\n <button\n type=\"button\"\n onClick={() => setPaused((current) => !current)}\n className={`rounded p-1 transition-colors hover:bg-bg-tertiary ${paused ? 'text-status-warning-text' : 'text-text-tertiary'}`}\n title={paused ? tr('logs.resume', 'Resume') : tr('logs.pause', 'Pause')}\n >\n {paused ? <Play size={14} /> : <Pause size={14} />}\n </button>\n <button\n type=\"button\"\n onClick={handleClear}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.clear', 'Clear')}\n >\n <Trash2 size={14} />\n </button>\n <button\n type=\"button\"\n onClick={handleExport}\n className=\"rounded p-1 text-text-tertiary transition-colors hover:bg-bg-tertiary\"\n title={tr('logs.export', 'Export')}\n >\n <Download size={14} />\n </button>\n </div>\n </div>\n\n {errorMessage ? (\n <div\n role=\"alert\"\n className=\"border-b border-border-primary bg-status-error-bg/10 px-3 py-2 text-xs text-status-error-text\"\n >\n {errorMessage}\n </div>\n ) : null}\n\n <div\n ref={containerRef}\n onScroll={handleScroll}\n className={`overflow-y-auto font-mono text-xs leading-relaxed ${compact ? 'max-h-[300px]' : 'min-h-[400px] flex-1'}`}\n style={{ maxHeight: compact ? '300px' : undefined }}\n >\n {filteredEntries.length === 0 ? (\n <div className=\"flex h-full min-h-[200px] items-center justify-center text-text-tertiary\">\n {!agentId\n ? tr('logs.connectToAgent', 'Connect to an agent to view logs')\n : errorMessage\n ? `${tr('logs.subscriptionError', 'Log stream error')}: ${errorMessage}`\n : streamLoading\n ? tr('logs.connecting', 'Connecting to log stream…')\n : tr('logs.waitingForEntries', 'Waiting for log entries…')}\n </div>\n ) : (\n filteredEntries.map((entry, index) => (\n <div\n key={`${entry.timestamp}-${entry.source}-${index}`}\n className={`flex gap-2 border-b border-border-primary/30 px-3 py-0.5 hover:bg-bg-tertiary/50 ${LEVEL_BG[entry.level] ?? ''}`}\n >\n <span className=\"w-[85px] shrink-0 text-text-tertiary\">\n {formatTime(entry.timestamp)}\n </span>\n <span\n className={`w-[42px] shrink-0 uppercase font-semibold ${LEVEL_COLORS[entry.level] ?? 'text-text-primary'}`}\n >\n {entry.level}\n </span>\n <span\n className=\"w-[100px] shrink-0 truncate text-text-secondary\"\n title={entry.source}\n >\n [{entry.source}]\n </span>\n <span className=\"flex-1 break-all text-text-primary\">\n {entry.message}\n {entry.metadata && Object.keys(entry.metadata).length > 0 ? (\n <span className=\"ml-2 text-text-tertiary\">{JSON.stringify(entry.metadata)}</span>\n ) : null}\n </span>\n </div>\n ))\n )}\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|