From 44ac1295d5ed82e33f2281c915a3a80bedfbcd1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:54:33 -0300 Subject: [PATCH 1/2] Add FlowGenie monitoring dashboard Add a new FlowGenie monitoring UI and supporting chart components. Introduces MonitoringDashboard.vue and index export, plus chart components (Bar/Doughnut/HorizontalBar/Line, ChartFrame) and chartTheme.js. Registers a new /agents/monitoring route (with package guard) and updates routes export. Update HeaderBar, Sidebar and LogContainer to surface the monitoring view and adjust tab/visibility logic. Enhance AgentSessionDetail with robust key generation and label formatting for events, MCP servers and collections. Small refactor for link class handling in Sidebar. --- .../AgentSessionDetail/AgentSessionDetail.vue | 71 ++- .../components/Logs/HeaderBar/HeaderBar.vue | 14 +- .../Logs/LogContainer/LogContainer.vue | 3 + .../Logs/Monitoring/MonitoringDashboard.vue | 594 ++++++++++++++++++ .../logs/components/Logs/Monitoring/index.js | 2 + .../logs/components/Logs/Sidebar/Sidebar.vue | 45 +- .../logs/components/Logs/charts/BarChart.vue | 38 ++ .../components/Logs/charts/ChartFrame.vue | 18 + .../components/Logs/charts/DoughnutChart.vue | 38 ++ .../Logs/charts/HorizontalBarChart.vue | 38 ++ .../logs/components/Logs/charts/LineChart.vue | 38 ++ .../logs/components/Logs/charts/chartTheme.js | 93 +++ .../js/admin/logs/components/Logs/routes.js | 13 + 13 files changed, 985 insertions(+), 20 deletions(-) create mode 100644 resources/js/admin/logs/components/Logs/Monitoring/MonitoringDashboard.vue create mode 100644 resources/js/admin/logs/components/Logs/Monitoring/index.js create mode 100644 resources/js/admin/logs/components/Logs/charts/BarChart.vue create mode 100644 resources/js/admin/logs/components/Logs/charts/ChartFrame.vue create mode 100644 resources/js/admin/logs/components/Logs/charts/DoughnutChart.vue create mode 100644 resources/js/admin/logs/components/Logs/charts/HorizontalBarChart.vue create mode 100644 resources/js/admin/logs/components/Logs/charts/LineChart.vue create mode 100644 resources/js/admin/logs/components/Logs/charts/chartTheme.js diff --git a/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue b/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue index 175622802c..d516c772c5 100644 --- a/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue +++ b/resources/js/admin/logs/components/Logs/AgentSessionDetail/AgentSessionDetail.vue @@ -111,7 +111,7 @@
@@ -361,11 +361,11 @@
{{ $t('MCP Servers') }}
- {{ server }} + {{ formatMcpServerLabel(server) }}
@@ -374,13 +374,13 @@
{{ $t('Collections') }}
- {{ collection.name }} + {{ formatCollectionLabel(collection) }}
@@ -708,6 +708,61 @@ export default { this.$set(this.expandedEvents, index, !this.expandedEvents[index]); }, + getEventKey(event, index) { + const id = event.id ?? event.call_id ?? event.response_id; + if (typeof id === 'string' || typeof id === 'number') { + return String(id); + } + const type = event._type || event.type || 'event'; + const timestamp = event.timestamp || event.created_at || ''; + return `${type}-${timestamp}-${index}`; + }, + + getMcpServerKey(server, index) { + if (typeof server === 'string' || typeof server === 'number') { + return `mcp-${server}-${index}`; + } + if (server && typeof server === 'object') { + const name = server.name || server.id || 'server'; + const type = server.type || server.server_type || ''; + return `mcp-${name}-${type}-${index}`; + } + return `mcp-server-${index}`; + }, + + formatMcpServerLabel(server) { + if (typeof server === 'string' || typeof server === 'number') { + return String(server); + } + if (server && typeof server === 'object') { + return server.name || server.id || JSON.stringify(server); + } + return String(server ?? ''); + }, + + getCollectionKey(collection, index) { + if (typeof collection === 'string' || typeof collection === 'number') { + return `collection-${collection}-${index}`; + } + if (collection && typeof collection === 'object') { + const id = collection.uuid || collection.id; + if (typeof id === 'string' || typeof id === 'number') { + return `collection-${id}`; + } + } + return `collection-${index}`; + }, + + formatCollectionLabel(collection) { + if (typeof collection === 'string' || typeof collection === 'number') { + return String(collection); + } + if (collection && typeof collection === 'object') { + return collection.name || collection.uuid || collection.id || JSON.stringify(collection); + } + return String(collection ?? ''); + }, + isGuardrailEvent(event) { const type = event._type || event.type; return type === 'guardrail_evaluation_event'; diff --git a/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue index 6878c1c6fc..a7fcc7add8 100644 --- a/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue +++ b/resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue @@ -30,7 +30,7 @@
-
+
+
+
+
+ +
+
+ +
+
+ +
+ {{ $t('Loading...') }} +
+ + +
+ + + + + diff --git a/resources/js/admin/logs/components/Logs/Monitoring/index.js b/resources/js/admin/logs/components/Logs/Monitoring/index.js new file mode 100644 index 0000000000..baa481f302 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/Monitoring/index.js @@ -0,0 +1,2 @@ +// eslint-disable-next-line import/prefer-default-export +export { default as MonitoringDashboard } from './MonitoringDashboard.vue'; diff --git a/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue index a2276bbff7..6d947d71ca 100644 --- a/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue +++ b/resources/js/admin/logs/components/Logs/Sidebar/Sidebar.vue @@ -104,7 +104,7 @@ :visible="isAgentsActive" > - FlowGenie Agents logs + {{ $t('Monitoring') }} + + + {{ $t('FlowGenie Agents logs') }}
@@ -153,13 +170,25 @@ export default { isAgentsActive() { return this.$route.path.startsWith("/agents"); }, + isAgentsLogsActive() { + return this.$route.path === '/agents/design' || this.$route.path === '/agents/execution'; + }, + isMonitoringActive() { + return this.$route.path.startsWith('/agents/monitoring'); + }, emailLinkClasses() { - return this.isEmailActive - ? "tw-bg-blue-50 tw-font-semibold tw-text-blue-500" - : "tw-font-medium tw-text-zinc-700 hover:tw-bg-zinc-50"; + return this.linkClasses(this.isEmailActive); + }, + agentsLogsLinkClasses() { + return this.linkClasses(this.isAgentsLogsActive); }, - agentsLinkClasses() { - return this.isAgentsActive + monitoringLinkClasses() { + return this.linkClasses(this.isMonitoringActive); + }, + }, + methods: { + linkClasses(isActive) { + return isActive ? "tw-bg-blue-50 tw-font-semibold tw-text-blue-500" : "tw-font-medium tw-text-zinc-700 hover:tw-bg-zinc-50"; }, diff --git a/resources/js/admin/logs/components/Logs/charts/BarChart.vue b/resources/js/admin/logs/components/Logs/charts/BarChart.vue new file mode 100644 index 0000000000..fd46f8cb06 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/charts/BarChart.vue @@ -0,0 +1,38 @@ + diff --git a/resources/js/admin/logs/components/Logs/charts/ChartFrame.vue b/resources/js/admin/logs/components/Logs/charts/ChartFrame.vue new file mode 100644 index 0000000000..e010c4054f --- /dev/null +++ b/resources/js/admin/logs/components/Logs/charts/ChartFrame.vue @@ -0,0 +1,18 @@ + + + diff --git a/resources/js/admin/logs/components/Logs/charts/DoughnutChart.vue b/resources/js/admin/logs/components/Logs/charts/DoughnutChart.vue new file mode 100644 index 0000000000..5691a36bbd --- /dev/null +++ b/resources/js/admin/logs/components/Logs/charts/DoughnutChart.vue @@ -0,0 +1,38 @@ + diff --git a/resources/js/admin/logs/components/Logs/charts/HorizontalBarChart.vue b/resources/js/admin/logs/components/Logs/charts/HorizontalBarChart.vue new file mode 100644 index 0000000000..5ddcfa75b7 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/charts/HorizontalBarChart.vue @@ -0,0 +1,38 @@ + diff --git a/resources/js/admin/logs/components/Logs/charts/LineChart.vue b/resources/js/admin/logs/components/Logs/charts/LineChart.vue new file mode 100644 index 0000000000..fa8a6674e9 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/charts/LineChart.vue @@ -0,0 +1,38 @@ + diff --git a/resources/js/admin/logs/components/Logs/charts/chartTheme.js b/resources/js/admin/logs/components/Logs/charts/chartTheme.js new file mode 100644 index 0000000000..8ad3a3fa92 --- /dev/null +++ b/resources/js/admin/logs/components/Logs/charts/chartTheme.js @@ -0,0 +1,93 @@ +export const CHART_COLORS = { + pass: '#0CA442', + redact: '#EC8E00', + block: '#EC5962', + info: '#2773F3', + muted: '#728092', + completed: '#0CA442', + error: '#EC5962', +}; + +export const doughnutOptions = { + legend: { + display: true, + position: 'bottom', + }, + maintainAspectRatio: false, + responsive: true, + cutoutPercentage: 60, + layout: { + padding: 4, + }, +}; + +export const horizontalBarOptions = (valueSuffix = '') => ({ + legend: { + display: false, + }, + maintainAspectRatio: false, + responsive: true, + layout: { + padding: { + right: 12, + }, + }, + scales: { + xAxes: [{ + ticks: { + beginAtZero: true, + callback: (value) => `${value}${valueSuffix}`, + }, + }], + yAxes: [{ + gridLines: { + display: false, + }, + afterFit: (scale) => { + scale.width = Math.min(scale.width, 140); + }, + }], + }, +}); + +export const stackedBarOptions = { + legend: { + display: true, + position: 'bottom', + }, + maintainAspectRatio: false, + responsive: true, + layout: { + padding: 4, + }, + scales: { + xAxes: [{ stacked: true }], + yAxes: [{ + stacked: true, + ticks: { beginAtZero: true }, + }], + }, +}; + +export const lineOptions = { + legend: { + display: true, + position: 'bottom', + }, + maintainAspectRatio: false, + responsive: true, + layout: { + padding: 4, + }, + scales: { + yAxes: [{ + ticks: { beginAtZero: true }, + }], + }, +}; + +export const chartBoxStyles = { + width: '100%', + height: '220px', + position: 'relative', +}; diff --git a/resources/js/admin/logs/components/Logs/routes.js b/resources/js/admin/logs/components/Logs/routes.js index 398d462193..85443ea7d3 100644 --- a/resources/js/admin/logs/components/Logs/routes.js +++ b/resources/js/admin/logs/components/Logs/routes.js @@ -1,4 +1,5 @@ import { LogTable } from "./LogTable"; +import { MonitoringDashboard } from "./Monitoring"; export default {}; @@ -70,6 +71,18 @@ export const routes = [ path: "/agents", redirect: "/agents/design", }, + { + name: "logs.agents.monitoring", + path: "/agents/monitoring", + component: MonitoringDashboard, + beforeEnter: (to, from, next) => { + if (!hasAiPackage()) { + next(hasEmailPackage() ? "/email/errors" : "/"); + } else { + next(); + } + }, + }, { name: "logs.agents", path: "/agents/:logType", From d9ca75dcf2ca89badf8d6effc4d7061b069f1ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Agust=C3=ADn=20Busso?= <90727999+agustinbusso@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:44:48 -0300 Subject: [PATCH 2/2] Add monitoring i18n keys and use $t Make monitoring UI strings translatable and add English entries. Moved range/execution options into computed properties so labels use this.$t, updated KPI labels to call $t, and replaced inline empty-message references with $t. Added many monitoring-related keys to resources/lang/en.json. Also fixed a small casing change for the LogContainer fallback title ('FlowGenie Agents logs'). --- .../Logs/LogContainer/LogContainer.vue | 2 +- .../Logs/Monitoring/MonitoringDashboard.vue | 57 ++++++------ resources/lang/en.json | 93 ++++++++++++++++++- 3 files changed, 122 insertions(+), 30 deletions(-) diff --git a/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue index caa62040e7..491155762b 100644 --- a/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue +++ b/resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue @@ -102,7 +102,7 @@ export default { design: 'FlowGenie Studio Logs', execution: 'Runtime Logs', }; - return agentTitles[this.logType] ?? 'FlowGenie Agents Logs'; + return agentTitles[this.logType] ?? 'FlowGenie Agents logs'; } const titles = { diff --git a/resources/js/admin/logs/components/Logs/Monitoring/MonitoringDashboard.vue b/resources/js/admin/logs/components/Logs/Monitoring/MonitoringDashboard.vue index c304d2337c..dd45679a2c 100644 --- a/resources/js/admin/logs/components/Logs/Monitoring/MonitoringDashboard.vue +++ b/resources/js/admin/logs/components/Logs/Monitoring/MonitoringDashboard.vue @@ -12,7 +12,7 @@ : 'tw-text-zinc-700 hover:tw-bg-zinc-50'" @click="setExecutionType(option.value)" > - {{ $t(option.label) }} + {{ option.label }}
@@ -26,7 +26,7 @@ : 'tw-text-zinc-700 hover:tw-bg-zinc-50'" @click="setRange(option.value)" > - {{ $t(option.label) }} + {{ option.label }}
@@ -52,7 +52,7 @@ {{ kpi.value }}
- {{ $t(kpi.label) }} + {{ kpi.label }}
@@ -76,7 +76,7 @@ v-else class="tw-text-sm tw-text-zinc-500" > - {{ $t(emptyGuardrails) }} + {{ $t('No guardrail evaluations in this period.') }}

@@ -94,7 +94,7 @@ v-else class="tw-text-sm tw-text-zinc-500" > - {{ $t(emptyBlocks) }} + {{ $t('No blocked sessions in this period.') }}

@@ -132,7 +132,7 @@ v-else class="tw-text-sm tw-text-zinc-500" > - {{ $t(emptyGuardrails) }} + {{ $t('No guardrail evaluations in this period.') }}

@@ -168,7 +168,7 @@ v-else class="tw-text-sm tw-text-zinc-500" > - {{ $t(emptyTokens) }} + {{ $t('No token usage in this period.') }}

@@ -186,7 +186,7 @@ v-else class="tw-text-sm tw-text-zinc-500" > - {{ $t(emptyTokens) }} + {{ $t('No token usage in this period.') }}

@@ -259,7 +259,7 @@ v-else class="tw-text-sm tw-text-zinc-500" > - {{ $t(emptyBlocks) }} + {{ $t('No blocked sessions in this period.') }}

@@ -316,36 +316,37 @@ export default { range: '7d', executionType: 'all', monitoring: EMPTY_MONITORING(), - emptyGuardrails: 'No guardrail evaluations in this period.', - emptyBlocks: 'No blocked sessions in this period.', - emptyTokens: 'No token usage in this period.', doughnutOptions, stackedBarOptions, lineOptions, percentBarOptions: horizontalBarOptions('%'), countBarOptions: horizontalBarOptions(), - rangeOptions: [ - { value: '7d', label: 'Last 7 days' }, - { value: '30d', label: 'Last 30 days' }, - { value: '90d', label: 'Last 90 days' }, - ], - executionOptions: [ - { value: 'all', label: 'All' }, - { value: 'design', label: 'Studio' }, - { value: 'execution', label: 'Runtime' }, - ], }; }, computed: { + rangeOptions() { + return [ + { value: '7d', label: this.$t('Last 7 days') }, + { value: '30d', label: this.$t('Last 30 days') }, + { value: '90d', label: this.$t('Last 90 days') }, + ]; + }, + executionOptions() { + return [ + { value: 'all', label: this.$t('All') }, + { value: 'design', label: this.$t('Studio') }, + { value: 'execution', label: this.$t('Runtime') }, + ]; + }, kpiCards() { const kpis = this.monitoring.kpis || {}; return [ - { label: 'Sessions', value: this.formatCount(kpis.sessions), toneClass: 'tw-text-zinc-900' }, - { label: 'Block rate', value: this.formatRate(kpis.block_rate), toneClass: 'tw-text-red-600' }, - { label: 'Redact rate', value: this.formatRate(kpis.redact_rate), toneClass: 'tw-text-amber-600' }, - { label: 'Error rate', value: this.formatRate(kpis.error_rate), toneClass: 'tw-text-zinc-900' }, - { label: 'Tokens', value: this.formatTokens(kpis.tokens), toneClass: 'tw-text-zinc-900' }, - { label: 'Avg duration', value: this.formatDuration(kpis.avg_duration_ms), toneClass: 'tw-text-zinc-900' }, + { label: this.$t('Sessions'), value: this.formatCount(kpis.sessions), toneClass: 'tw-text-zinc-900' }, + { label: this.$t('Block rate'), value: this.formatRate(kpis.block_rate), toneClass: 'tw-text-red-600' }, + { label: this.$t('Redact rate'), value: this.formatRate(kpis.redact_rate), toneClass: 'tw-text-amber-600' }, + { label: this.$t('Error rate'), value: this.formatRate(kpis.error_rate), toneClass: 'tw-text-zinc-900' }, + { label: this.$t('Tokens'), value: this.formatTokens(kpis.tokens), toneClass: 'tw-text-zinc-900' }, + { label: this.$t('Avg duration'), value: this.formatDuration(kpis.avg_duration_ms), toneClass: 'tw-text-zinc-900' }, ]; }, hasOutcomes() { diff --git a/resources/lang/en.json b/resources/lang/en.json index 257686e0f1..e6a75a84e0 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -2695,5 +2695,96 @@ "Your PMQL search could not be completed.": "Your PMQL search could not be completed.", "Your profile was saved.": "Your profile was saved.", "Zoom In": "Zoom In", - "Zoom Out": "Zoom Out" + "Zoom Out": "Zoom Out", + "After tool": "After tool", + "Agent": "Agent", + "Arguments": "Arguments", + "Avg duration": "Avg duration", + "Before response": "Before response", + "Before tool": "Before tool", + "Block": "Block", + "Block rate": "Block rate", + "Block rate by FlowGenie": "Block rate by FlowGenie", + "Blocked sessions": "Blocked sessions", + "Blocked vs redacted sessions": "Blocked vs redacted sessions", + "Blocks": "Blocks", + "Connected": "Connected", + "Connection": "Connection", + "Date Email": "Date Email", + "Email From": "Email From", + "Error Code": "Error Code", + "Error rate": "Error rate", + "Execution Time": "Execution Time", + "Export to CSV": "Export to CSV", + "Files Attached": "Files Attached", + "FlowGenie": "FlowGenie", + "FlowGenie Agents logs": "FlowGenie Agents logs", + "FlowGenie Monitoring": "FlowGenie Monitoring", + "FlowGenie Name": "FlowGenie Name", + "Guardrail": "Guardrail", + "Guardrail outcomes": "Guardrail outcomes", + "Guardrails": "Guardrails", + "Input": "Input", + "Instructions": "Instructions", + "LLM": "LLM", + "LLM Calls": "LLM Calls", + "Last 30 days": "Last 30 days", + "Last 7 days": "Last 7 days", + "Last 90 days": "Last 90 days", + "Loading session details...": "Loading session details...", + "MCP Servers": "MCP Servers", + "Max Turns": "Max Turns", + "Model": "Model", + "Model Request": "Model Request", + "Model Settings": "Model Settings", + "Monitoring": "Monitoring", + "No": "No", + "No blocked sessions in this period.": "No blocked sessions in this period.", + "No events recorded for this session": "No events recorded for this session", + "No guardrail evaluations in this period.": "No guardrail evaluations in this period.", + "No sessions in this period.": "No sessions in this period.", + "No token usage in this period.": "No token usage in this period.", + "No violations in this period.": "No violations in this period.", + "Node": "Node", + "On input": "On input", + "On output": "On output", + "Pass": "Pass", + "Passed": "Passed", + "Phase": "Phase", + "Policy": "Policy", + "Processing": "Processing", + "Processing Started": "Processing Started", + "Properties": "Properties", + "Reasoning": "Reasoning", + "Redact": "Redact", + "Redact rate": "Redact rate", + "Redacted": "Redacted", + "Redacted sessions": "Redacted sessions", + "Request Config": "Request Config", + "Request ID": "Request ID", + "Response": "Response", + "Runtime": "Runtime", + "Session Details": "Session Details", + "Session ID": "Session ID", + "Session status": "Session status", + "Sessions": "Sessions", + "Sessions per day": "Sessions per day", + "Studio": "Studio", + "Thinking": "Thinking", + "Tokens Used": "Tokens Used", + "Tokens by FlowGenie": "Tokens by FlowGenie", + "Tokens in vs out": "Tokens in vs out", + "Tool": "Tool", + "Tool Call": "Tool Call", + "Tool Calls": "Tool Calls", + "Tools": "Tools", + "Top users by blocks": "Top users by blocks", + "Total Emails": "Total Emails", + "Total Matches": "Total Matches", + "Total Tokens": "Total Tokens", + "Usage": "Usage", + "User Email": "User Email", + "Violations": "Violations", + "Violations by kind": "Violations by kind", + "Where it fired": "Where it fired" }