@burdenoff/microfe-vibecontrols 2026.529.18 → 2026.530.1
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.map +1 -1
- package/dist/components/audit/AuditLogStats.js +12 -9
- package/dist/components/audit/AuditLogStats.js.map +1 -1
- package/dist/components/logs/LogViewer.js.map +1 -1
- package/dist/components/sessions/TerminalPanel.js.map +1 -1
- package/dist/components/settings/AdvancedSection.js +91 -98
- package/dist/components/settings/AdvancedSection.js.map +1 -1
- package/dist/pages/ActionsPage.js +10 -4
- package/dist/pages/ActionsPage.js.map +1 -1
- package/dist/pages/AgentsPage.js +1 -0
- package/dist/pages/AgentsPage.js.map +1 -1
- package/dist/pages/AnalyticsPage.js +19 -12
- package/dist/pages/AnalyticsPage.js.map +1 -1
- package/dist/pages/CatalogPage.js +18 -10
- package/dist/pages/CatalogPage.js.map +1 -1
- package/dist/pages/ComponentsPage.js +26 -15
- package/dist/pages/ComponentsPage.js.map +1 -1
- package/dist/pages/GettingStartedPage.js +168 -159
- package/dist/pages/GettingStartedPage.js.map +1 -1
- package/dist/pages/HealthPage.js +216 -183
- package/dist/pages/HealthPage.js.map +1 -1
- package/dist/pages/OverviewPage.js +255 -250
- package/dist/pages/OverviewPage.js.map +1 -1
- package/dist/pages/SettingsPage.js +3 -0
- package/dist/pages/SettingsPage.js.map +1 -1
- package/dist/pages/TemplatesPage.js +18 -10
- package/dist/pages/TemplatesPage.js.map +1 -1
- package/dist/pages/TunnelsLandingPage.js +2 -0
- package/dist/pages/TunnelsLandingPage.js.map +1 -1
- package/dist/pages/VibeAuditPage.js +24 -18
- package/dist/pages/VibeAuditPage.js.map +1 -1
- package/dist/pages/VibeCalendarPage.js +10 -6
- package/dist/pages/VibeCalendarPage.js.map +1 -1
- package/dist/pages/VibeDeckPage.js +51 -43
- package/dist/pages/VibeDeckPage.js.map +1 -1
- package/dist/pages/VibesPage.js +1 -0
- package/dist/pages/VibesPage.js.map +1 -1
- package/dist/pages/ai/AIPage.js +52 -45
- package/dist/pages/ai/AIPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
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":"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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 =\n 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 =\n 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 key={e.id} className=\"rounded border border-border-default bg-bg-subtle\">\n <div className=\"flex items-start gap-3 p-3\">\n <div\n className={`px-2 py-0.5 rounded text-[10px] uppercase font-mono ${levelBg} ${levelColor} shrink-0`}\n >\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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"}
|
|
@@ -5,7 +5,10 @@ import { Clock as n, Shield as r } from "lucide-react";
|
|
|
5
5
|
import { jsx as i, jsxs as a } from "react/jsx-runtime";
|
|
6
6
|
//#region src/components/audit/AuditLogStats.tsx
|
|
7
7
|
function o({ stats: o, loading: s }) {
|
|
8
|
-
let { t: c } = t()
|
|
8
|
+
let { t: c } = t(), l = (e, t) => {
|
|
9
|
+
let n = c(e);
|
|
10
|
+
return n === e ? t : n;
|
|
11
|
+
};
|
|
9
12
|
if (s || !o) return /* @__PURE__ */ i("div", {
|
|
10
13
|
className: "mb-6 grid grid-cols-2 gap-4 md:grid-cols-4",
|
|
11
14
|
children: [...[
|
|
@@ -15,34 +18,34 @@ function o({ stats: o, loading: s }) {
|
|
|
15
18
|
,
|
|
16
19
|
]].map((e, t) => /* @__PURE__ */ i("div", { className: "h-20 rounded-lg bg-bg-secondary/50 animate-pulse" }, t))
|
|
17
20
|
});
|
|
18
|
-
let
|
|
21
|
+
let u = o.logsByStatus.find((e) => e.status === "SUCCESS")?.count ?? 0, d = o.logsByStatus.find((e) => e.status === "ERROR")?.count ?? 0, f = o.logsByStatus.find((e) => e.status === "FAILURE")?.count ?? 0;
|
|
19
22
|
return /* @__PURE__ */ a("div", {
|
|
20
23
|
className: "grid grid-cols-2 md:grid-cols-4 gap-4 mb-6",
|
|
21
24
|
children: [
|
|
22
25
|
/* @__PURE__ */ i(e, {
|
|
23
|
-
title: "Total Logs",
|
|
26
|
+
title: l("audit.stats.totalLogs", "Total Logs"),
|
|
24
27
|
value: o.totalLogs.toLocaleString(),
|
|
25
28
|
icon: /* @__PURE__ */ i(r, { size: 16 })
|
|
26
29
|
}),
|
|
27
30
|
/* @__PURE__ */ i(e, {
|
|
28
|
-
title: "Successful",
|
|
29
|
-
value:
|
|
31
|
+
title: l("audit.stats.successful", "Successful"),
|
|
32
|
+
value: u.toLocaleString(),
|
|
30
33
|
icon: /* @__PURE__ */ i(r, {
|
|
31
34
|
size: 16,
|
|
32
35
|
className: "text-status-success-text"
|
|
33
36
|
})
|
|
34
37
|
}),
|
|
35
38
|
/* @__PURE__ */ i(e, {
|
|
36
|
-
title: "Errors / Failures",
|
|
37
|
-
value: `${
|
|
39
|
+
title: l("audit.stats.errorsFailures", "Errors / Failures"),
|
|
40
|
+
value: `${d + f}`,
|
|
38
41
|
icon: /* @__PURE__ */ i(r, {
|
|
39
42
|
size: 16,
|
|
40
43
|
className: "text-status-error-text"
|
|
41
44
|
})
|
|
42
45
|
}),
|
|
43
46
|
/* @__PURE__ */ i(e, {
|
|
44
|
-
title: "Retention",
|
|
45
|
-
value: o.retentionPolicy.retentionDays === -1 ? "Unlimited" : `${o.retentionPolicy.retentionDays} days`,
|
|
47
|
+
title: l("audit.stats.retention", "Retention"),
|
|
48
|
+
value: o.retentionPolicy.retentionDays === -1 ? l("audit.stats.unlimited", "Unlimited") : `${o.retentionPolicy.retentionDays} ${l("audit.stats.days", "days")}`,
|
|
46
49
|
icon: /* @__PURE__ */ i(n, { size: 16 })
|
|
47
50
|
})
|
|
48
51
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuditLogStats.js","names":[],"sources":["../../../src/components/audit/AuditLogStats.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Shield, Clock } from 'lucide-react';\nimport { StatCard } from '@/components/shared';\nimport type { GetVibeAuditLogStatsQuery } from '@/generated/wspace-operations';\n\ninterface AuditLogStatsProps {\n stats: GetVibeAuditLogStatsQuery['vibeAuditLogStats'] | undefined;\n loading: boolean;\n}\n\nexport function AuditLogStats({ stats, loading }: AuditLogStatsProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n if (loading || !stats) {\n return (\n <div className=\"mb-6 grid grid-cols-2 gap-4 md:grid-cols-4\">\n {[...Array(4)].map((_, i) => (\n <div key={i} className=\"h-20 rounded-lg bg-bg-secondary/50 animate-pulse\" />\n ))}\n </div>\n );\n }\n\n const successCount = stats.logsByStatus.find((s) => s.status === 'SUCCESS')?.count ?? 0;\n const errorCount = stats.logsByStatus.find((s) => s.status === 'ERROR')?.count ?? 0;\n const failureCount = stats.logsByStatus.find((s) => s.status === 'FAILURE')?.count ?? 0;\n\n return (\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-6\">\n <StatCard\n title
|
|
1
|
+
{"version":3,"file":"AuditLogStats.js","names":[],"sources":["../../../src/components/audit/AuditLogStats.tsx"],"sourcesContent":["import { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport { Shield, Clock } from 'lucide-react';\nimport { StatCard } from '@/components/shared';\nimport type { GetVibeAuditLogStatsQuery } from '@/generated/wspace-operations';\n\ninterface AuditLogStatsProps {\n stats: GetVibeAuditLogStatsQuery['vibeAuditLogStats'] | undefined;\n loading: boolean;\n}\n\nexport function AuditLogStats({ stats, loading }: AuditLogStatsProps) {\n const { t } = useI18n();\n const tr = (key: string, fallback: string) => {\n const v = t(key);\n return v === key ? fallback : v;\n };\n if (loading || !stats) {\n return (\n <div className=\"mb-6 grid grid-cols-2 gap-4 md:grid-cols-4\">\n {[...Array(4)].map((_, i) => (\n <div key={i} className=\"h-20 rounded-lg bg-bg-secondary/50 animate-pulse\" />\n ))}\n </div>\n );\n }\n\n const successCount = stats.logsByStatus.find((s) => s.status === 'SUCCESS')?.count ?? 0;\n const errorCount = stats.logsByStatus.find((s) => s.status === 'ERROR')?.count ?? 0;\n const failureCount = stats.logsByStatus.find((s) => s.status === 'FAILURE')?.count ?? 0;\n\n return (\n <div className=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-6\">\n <StatCard\n title={tr('audit.stats.totalLogs', 'Total Logs')}\n value={stats.totalLogs.toLocaleString()}\n icon={<Shield size={16} />}\n />\n <StatCard\n title={tr('audit.stats.successful', 'Successful')}\n value={successCount.toLocaleString()}\n icon={<Shield size={16} className=\"text-status-success-text\" />}\n />\n <StatCard\n title={tr('audit.stats.errorsFailures', 'Errors / Failures')}\n value={`${errorCount + failureCount}`}\n icon={<Shield size={16} className=\"text-status-error-text\" />}\n />\n <StatCard\n title={tr('audit.stats.retention', 'Retention')}\n value={\n stats.retentionPolicy.retentionDays === -1\n ? tr('audit.stats.unlimited', 'Unlimited')\n : `${stats.retentionPolicy.retentionDays} ${tr('audit.stats.days', 'days')}`\n }\n icon={<Clock size={16} />}\n />\n </div>\n );\n}\n"],"mappings":";;;;;;AAUA,SAAgB,EAAc,EAAE,UAAO,cAA+B;CACpE,IAAM,EAAE,SAAM,GAAS,EACjB,KAAM,GAAa,MAAqB;EAC5C,IAAM,IAAI,EAAE,EAAI;AAChB,SAAO,MAAM,IAAM,IAAW;;AAEhC,KAAI,KAAW,CAAC,EACd,QACE,kBAAC,OAAD;EAAK,WAAU;YACZ,CAAC,GAAG;;;;;GAAQ,CAAC,CAAC,KAAK,GAAG,MACrB,kBAAC,OAAD,EAAa,WAAU,oDAAqD,EAAlE,EAAkE,CAC5E;EACE,CAAA;CAIV,IAAM,IAAe,EAAM,aAAa,MAAM,MAAM,EAAE,WAAW,UAAU,EAAE,SAAS,GAChF,IAAa,EAAM,aAAa,MAAM,MAAM,EAAE,WAAW,QAAQ,EAAE,SAAS,GAC5E,IAAe,EAAM,aAAa,MAAM,MAAM,EAAE,WAAW,UAAU,EAAE,SAAS;AAEtF,QACE,kBAAC,OAAD;EAAK,WAAU;YAAf;GACE,kBAAC,GAAD;IACE,OAAO,EAAG,yBAAyB,aAAa;IAChD,OAAO,EAAM,UAAU,gBAAgB;IACvC,MAAM,kBAAC,GAAD,EAAQ,MAAM,IAAM,CAAA;IAC1B,CAAA;GACF,kBAAC,GAAD;IACE,OAAO,EAAG,0BAA0B,aAAa;IACjD,OAAO,EAAa,gBAAgB;IACpC,MAAM,kBAAC,GAAD;KAAQ,MAAM;KAAI,WAAU;KAA6B,CAAA;IAC/D,CAAA;GACF,kBAAC,GAAD;IACE,OAAO,EAAG,8BAA8B,oBAAoB;IAC5D,OAAO,GAAG,IAAa;IACvB,MAAM,kBAAC,GAAD;KAAQ,MAAM;KAAI,WAAU;KAA2B,CAAA;IAC7D,CAAA;GACF,kBAAC,GAAD;IACE,OAAO,EAAG,yBAAyB,YAAY;IAC/C,OACE,EAAM,gBAAgB,kBAAkB,KACpC,EAAG,yBAAyB,YAAY,GACxC,GAAG,EAAM,gBAAgB,cAAc,GAAG,EAAG,oBAAoB,OAAO;IAE9E,MAAM,kBAAC,GAAD,EAAO,MAAM,IAAM,CAAA;IACzB,CAAA;GACE"}
|
|
@@ -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 {!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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"}
|
|
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(\n (row: {\n timestamp: string;\n level: string;\n source?: string | null;\n message: string;\n metadataJson?: string | null;\n }) => {\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 );\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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"}
|