From e0961d02c42fbcd6065ed4ae8b780967ae40ded4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Tue, 1 Sep 2026 10:28:31 -0600 Subject: [PATCH 1/4] fix[installer](nginx): added keepalive instruction on agent manager grpc requests and enable ssl Co-authored-by: Yadian Llada Lopez --- installer/templates/front-end.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/installer/templates/front-end.go b/installer/templates/front-end.go index cba03a0be..bdc415062 100644 --- a/installer/templates/front-end.go +++ b/installer/templates/front-end.go @@ -72,21 +72,20 @@ server { location /agent.AgentService/ { grpc_pass grpcs://$utmstack_agent_manager_grpc; - grpc_ssl_verify off; grpc_read_timeout 900; grpc_send_timeout 900; + client_body_timeout 1h; + grpc_socket_keepalive on; } location /agent.PanelService/ { grpc_pass grpcs://$utmstack_agent_manager_grpc; - grpc_ssl_verify off; grpc_read_timeout 900; grpc_send_timeout 900; } location /agent.CollectorService/ { grpc_pass grpcs://$utmstack_agent_manager_grpc; - grpc_ssl_verify off; grpc_read_timeout 900; grpc_send_timeout 900; } @@ -94,14 +93,12 @@ server { # log-input's ingest, whose service lives in the SDK's "plugins" package. location /plugins.Integration/ { grpc_pass grpcs://$utmstack_log_input_grpc; - grpc_ssl_verify off; grpc_read_timeout 900; grpc_send_timeout 900; } location /agent.PingService/ { grpc_pass grpcs://$utmstack_agent_manager_grpc; - grpc_ssl_verify off; grpc_read_timeout 900; grpc_send_timeout 900; } From 041db56bc765b936fd3f160f58443e7f022aca07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Tue, 1 Sep 2026 14:40:09 -0600 Subject: [PATCH 2/4] fix[frontend](soar-flows): improved executing state glyph --- frontend/src/features/soar/components/ExecutionsView.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/features/soar/components/ExecutionsView.tsx b/frontend/src/features/soar/components/ExecutionsView.tsx index 0d7c07dd8..9b189b156 100644 --- a/frontend/src/features/soar/components/ExecutionsView.tsx +++ b/frontend/src/features/soar/components/ExecutionsView.tsx @@ -19,7 +19,7 @@ const STATUS_META: Record
From 227465af6011ab718a8a076a4ce183a22bce99aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Tue, 1 Sep 2026 15:52:51 -0600 Subject: [PATCH 3/4] fix[backend](soar_flows): added execution summary on nodes --- .../modules/soar/usecase/command_summary.go | 101 ++++++++++++++++++ backend/modules/soar/usecase/execution.go | 1 + 2 files changed, 102 insertions(+) create mode 100644 backend/modules/soar/usecase/command_summary.go diff --git a/backend/modules/soar/usecase/command_summary.go b/backend/modules/soar/usecase/command_summary.go new file mode 100644 index 000000000..ab8ec0dc3 --- /dev/null +++ b/backend/modules/soar/usecase/command_summary.go @@ -0,0 +1,101 @@ +package usecase + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "strings" + + "github.com/tidwall/gjson" + + notificationdomain "github.com/utmstack/utmstack/backend/modules/notifications/domain" + "github.com/utmstack/utmstack/backend/modules/soar/connectors" + "github.com/utmstack/utmstack/backend/modules/soar/domain" +) + +// CommandSummary fills `e.Command` for flow-origin node executions whose action +// does not live in the command column — http, mail, llm, notify, incident and +// conditional nodes store theirs in Params. It derives a short human-readable +// line so the command column always shows what the node was configured to do; +// the frontend clamps long lines with a tooltip for the full text. +// +// Params were already interpolated by the dispatcher, so a secret the user +// inlined in the node config appears here in plain text. `vars` (may be nil) +// re-applies secret masking to the derived line. Manual executions and rows +// that already carry a command (shell nodes) are left untouched. +func CommandSummary(ctx context.Context, vars connectors.VariableUsecase, e *domain.SoarExecution) { + if e.Origin != domain.ExecutionOriginFlow || strings.TrimSpace(e.Command) != "" { + return + } + summary := summarizeNodeAction(e.Executor, e.Params) + if summary == "" { + return + } + if vars != nil { + if masked, err := vars.MaskSecrets(ctx, summary); err == nil { + summary = masked + } + } + e.Command = summary +} + +// summarizeNodeAction renders one configured action as a single line. +func summarizeNodeAction(executor string, params json.RawMessage) string { + if len(params) == 0 { + return "" + } + g := gjson.ParseBytes(params) + switch executor { + case "http": + method := strings.ToUpper(strings.TrimSpace(g.Get("method").Str)) + if method == "" { + if g.Get("body").Exists() { + method = http.MethodPost + } else { + method = http.MethodGet + } + } + return method + " " + g.Get("url").Str + case "mail": + s := "mail to " + strings.TrimSpace(g.Get("to").Str) + if subj := strings.TrimSpace(g.Get("subject").Str); subj != "" { + s += " — " + subj + } + return s + case "llm_enrich", "llm_action": + if p := strings.TrimSpace(g.Get("prompt").Str); p != "" { + return "LLM: " + p + } + return "" + case "notify": + label := "notify (INFO)" + if g.Get("type").Str == string(notificationdomain.TypeWarning) { + label = "notify (WARNING)" + } + return label + ": " + g.Get("message").Str + case "incident": + return "open incident: " + g.Get("name").Str + case "conditional": + conds := g.Get("conditions").Array() + if len(conds) == 0 { + return "" + } + first := summarizeCondition(conds[0].Value()) + if len(conds) == 1 { + return "if: " + first + } + return fmt.Sprintf("if: %s AND %d more", first, len(conds)-1) + } + return "" +} + +// summarizeCondition renders one conditional predicate, e.g. `severity IS High`. +func summarizeCondition(v any) string { + b, err := json.Marshal(v) + if err != nil { + return "" + } + c := gjson.ParseBytes(b) + return strings.TrimSpace(c.Get("field").Str + " " + c.Get("operator").Str + " " + c.Get("value").String()) +} diff --git a/backend/modules/soar/usecase/execution.go b/backend/modules/soar/usecase/execution.go index ff81bdc00..9e97ba6a3 100644 --- a/backend/modules/soar/usecase/execution.go +++ b/backend/modules/soar/usecase/execution.go @@ -163,6 +163,7 @@ func (u *executionUsecase) List(ctx context.Context, f dto.ExecutionFilters) (*d items := make([]dto.ExecutionResponse, len(executions)) for i, e := range executions { + CommandSummary(ctx, u.vars, &e) items[i] = dto.ExecutionResponse{ ID: e.ID, Origin: e.Origin, From 4638ce46328947a14859ae39b1d98d5bf37c01d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20S=C3=A1nchez?= Date: Tue, 1 Sep 2026 15:54:10 -0600 Subject: [PATCH 4/4] fix[frontend](soar-flows): added command summary state on execution logs --- .../soar/components/ExecutionsView.tsx | 488 ++++++++++++++---- frontend/src/shared/i18n/locales/de.json | 5 +- frontend/src/shared/i18n/locales/en.json | 5 +- frontend/src/shared/i18n/locales/es.json | 5 +- frontend/src/shared/i18n/locales/fr.json | 5 +- frontend/src/shared/i18n/locales/it.json | 5 +- frontend/src/shared/i18n/locales/pt.json | 5 +- frontend/src/shared/i18n/locales/ru.json | 5 +- 8 files changed, 397 insertions(+), 126 deletions(-) diff --git a/frontend/src/features/soar/components/ExecutionsView.tsx b/frontend/src/features/soar/components/ExecutionsView.tsx index 9b189b156..5f51b186c 100644 --- a/frontend/src/features/soar/components/ExecutionsView.tsx +++ b/frontend/src/features/soar/components/ExecutionsView.tsx @@ -1,110 +1,209 @@ -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { AlertTriangle, CheckCircle2, Clock, Loader2, RefreshCw, Search, XCircle } from 'lucide-react' -import { cn } from '@/shared/lib/utils' -import { Button } from '@/shared/components/ui/button' -import { Input } from '@/shared/components/ui/input' -import { InfiniteScrollSentinel } from '@/shared/components/ui/infinite-scroll' -import { presetRange, resolveRange, TimeRangePicker, type TimeRange } from '@/shared/components/ui/time-range-picker' -import { useDateFormat } from '@/shared/lib/datetime' -import { datasourcesHttpService } from '@/features/datasources/services/datasources-http.service' -import { soarExecutionsService } from '../services/soar-executions.service' -import type { Execution, ExecutionOrigin, ExecutionStatus, ExecutionListQuery } from '../types/soar.types' - -const STATUSES: (ExecutionStatus | 'all')[] = ['all', 'EXECUTED', 'PENDING', 'WAITING', 'EXECUTING', 'FAILED', 'DEAD'] -const ORIGINS: (ExecutionOrigin | 'all')[] = ['all', 'FLOW', 'MANUAL'] -const COLS = '90px minmax(160px,1.2fr) minmax(180px,1.6fr) 120px 150px 60px' - -const STATUS_META: Record = { - EXECUTED: { icon: CheckCircle2, cls: 'text-emerald-500' }, - PENDING: { icon: Clock, cls: 'text-amber-500' }, - WAITING: { icon: Clock, cls: 'text-muted-foreground' }, - EXECUTING: { icon: Loader2, cls: 'text-sky-500 [&_svg]:animate-spin' }, - FAILED: { icon: XCircle, cls: 'text-red-500' }, - DEAD: { icon: AlertTriangle, cls: 'text-muted-foreground' }, -} +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + AlertTriangle, + CheckCircle2, + Clock, + Loader2, + RefreshCw, + Search, + XCircle, +} from "lucide-react"; +import { cn } from "@/shared/lib/utils"; +import { Button } from "@/shared/components/ui/button"; +import { Input } from "@/shared/components/ui/input"; +import { InfiniteScrollSentinel } from "@/shared/components/ui/infinite-scroll"; +import { + presetRange, + resolveRange, + TimeRangePicker, + type TimeRange, +} from "@/shared/components/ui/time-range-picker"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/shared/components/ui/tooltip"; +import { useDateFormat } from "@/shared/lib/datetime"; +import { datasourcesHttpService } from "@/features/datasources/services/datasources-http.service"; +import { soarExecutionsService } from "../services/soar-executions.service"; +import { soarFlowsService } from "../services/soar-flows.service"; +import type { + Execution, + ExecutionOrigin, + ExecutionStatus, + ExecutionListQuery, + Flow, + FlowNode, +} from "../types/soar.types"; + +const STATUSES: (ExecutionStatus | "all")[] = [ + "all", + "EXECUTED", + "PENDING", + "WAITING", + "EXECUTING", + "FAILED", + "DEAD", +]; +const ORIGINS: (ExecutionOrigin | "all")[] = ["all", "FLOW", "MANUAL"]; +const COLS = + "90px 100px minmax(160px,1.2fr) minmax(180px,1.6fr) 120px 150px 60px"; + +const STATUS_META: Record< + ExecutionStatus, + { icon: typeof CheckCircle2; cls: string } +> = { + EXECUTED: { icon: CheckCircle2, cls: "text-emerald-500" }, + PENDING: { icon: Clock, cls: "text-amber-500" }, + WAITING: { icon: Clock, cls: "text-muted-foreground" }, + EXECUTING: { icon: Loader2, cls: "text-sky-500 [&_svg]:animate-spin" }, + FAILED: { icon: XCircle, cls: "text-red-500" }, + DEAD: { icon: AlertTriangle, cls: "text-muted-foreground" }, +}; export function ExecutionsView() { - const { t } = useTranslation() - const df = useDateFormat() - const [search, setSearch] = useState('') - const [debounced, setDebounced] = useState('') - const [status, setStatus] = useState('all') - const [origin, setOrigin] = useState('FLOW') - const [agent, setAgent] = useState('') - const [agents, setAgents] = useState([]) - const [range, setRange] = useState(presetRange('7d')) - const [items, setItems] = useState([]) - const [total, setTotal] = useState(0) - const [page, setPage] = useState(0) - const [pageSize] = useState(50) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(false) + const { t } = useTranslation(); + const df = useDateFormat(); + const [search, setSearch] = useState(""); + const [debounced, setDebounced] = useState(""); + const [status, setStatus] = useState("all"); + const [origin, setOrigin] = useState("FLOW"); + const [agent, setAgent] = useState(""); + const [agents, setAgents] = useState([]); + const [range, setRange] = useState(presetRange("7d")); + const [items, setItems] = useState([]); + const [total, setTotal] = useState(0); + // Flows of the runs currently on screen, keyed by rulePath. Used to render + // each node's position in the flow's DAG (its ancestor chain) in the Node + // column — the flow itself carries no per-run state, only its shape. + const [runFlows, setRunFlows] = useState>({}); + const [page, setPage] = useState(0); + const [pageSize] = useState(50); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(false); useEffect(() => { const h = setTimeout(() => { - setDebounced(search.trim()) - setPage(0) - }, 300) - return () => clearTimeout(h) - }, [search]) + setDebounced(search.trim()); + setPage(0); + }, 300); + return () => clearTimeout(h); + }, [search]); // Same source as FlowEditor / InteractiveConsole — Execution.agent stores the datasource name. useEffect(() => { datasourcesHttpService - .list({ page: 1, size: 1000, kind: 'agent', sort: 'asset_name.asc' }) - .then((r) => setAgents((r.items ?? []).map((d) => d.name).filter(Boolean))) - .catch(() => {}) - }, []) + .list({ page: 1, size: 1000, kind: "agent", sort: "asset_name.asc" }) + .then((r) => + setAgents((r.items ?? []).map((d) => d.name).filter(Boolean)), + ) + .catch(() => {}); + }, []); const query = useMemo(() => { - const { from, to } = resolveRange(range) + const { from, to } = resolveRange(range); return { alertId: debounced || undefined, - status: status === 'all' ? undefined : status, - origin: origin === 'all' ? undefined : origin, + status: status === "all" ? undefined : status, + origin: origin === "all" ? undefined : origin, agent: agent || undefined, startedAtFrom: from ?? undefined, startedAtTo: to, page, size: pageSize, - } - }, [debounced, status, origin, agent, range, page, pageSize]) + }; + }, [debounced, status, origin, agent, range, page, pageSize]); const load = useCallback(() => { - setLoading(true) - setError(false) + setLoading(true); + setError(false); soarExecutionsService .list(query) .then((r) => { - setItems((prev) => (page === 0 ? (r.data ?? []) : [...prev, ...(r.data ?? [])])) - setTotal(r.total ?? 0) + setItems((prev) => + page === 0 ? (r.data ?? []) : [...prev, ...(r.data ?? [])], + ); + setTotal(r.total ?? 0); }) .catch(() => setError(true)) - .finally(() => setLoading(false)) - }, [query, page]) + .finally(() => setLoading(false)); + }, [query, page]); useEffect(() => { - load() - }, [load]) + load(); + }, [load]); + + // Fetch the flows behind the runs currently on screen so the Node column can + // show where each node sits in the DAG. One GET per unseen rulePath; a failure + // leaves the cell showing the bare node id (the flow may have been deleted). + const neededPaths = useMemo(() => { + const seen = new Set(); + for (const e of items) { + if ( + e.origin === "FLOW" && + e.rulePath && + !runFlows[e.rulePath] && + !seen.has(e.rulePath) + ) + seen.add(e.rulePath); + } + return [...seen]; + }, [items, runFlows]); + useEffect(() => { + if (neededPaths.length === 0) return; + let cancelled = false; + Promise.allSettled( + neededPaths.map(async (p) => { + const f = await soarFlowsService.get(p); + return { p, f }; + }), + ).then((results) => { + if (cancelled) return; + const found: Record = {}; + for (const res of results) { + if (res.status === "fulfilled") found[res.value.p] = res.value.f; + } + if (Object.keys(found).length > 0) + setRunFlows((prev) => ({ ...prev, ...found })); + }); + return () => { + cancelled = true; + }; + }, [neededPaths]); return (
- - setSearch(e.target.value)} placeholder={t('soar.executions.search')} className="w-[260px] pl-8" /> + + setSearch(e.target.value)} + placeholder={t("soar.executions.search")} + className="w-[260px] pl-8" + />
{STATUSES.map((s) => ( ))}
@@ -113,106 +212,271 @@ export function ExecutionsView() { ))}
{ - setRange(r) - setPage(0) + setRange(r); + setPage(0); }} align="right" /> -
-
-
{t('soar.executions.cols.status')}
-
{t('soar.executions.cols.flow')}
-
{t('soar.executions.cols.command')}
-
{t('soar.executions.cols.agent')}
-
{t('soar.executions.cols.date')}
-
{t('soar.executions.cols.retries')}
+
+
{t("soar.executions.cols.status")}
+
{t("soar.executions.cols.node")}
+
{t("soar.executions.cols.flow")}
+
{t("soar.executions.cols.command")}
+
{t("soar.executions.cols.agent")}
+
{t("soar.executions.cols.date")}
+
{t("soar.executions.cols.retries")}
{loading && items.length === 0 ? ( -
{t('soar.executions.loading')}
+
+ {" "} + {t("soar.executions.loading")} +
) : error ? (
- {t('soar.executions.loadError')} - + {" "} + {t("soar.executions.loadError")} +
) : items.length === 0 ? ( -
{t('soar.executions.empty')}
+
+ {t("soar.executions.empty")} +
) : ( <> - {items.map((e) => )} + {items.map((e) => ( + + ))} setPage((p) => p + 1)} hasMore={items.length < total} loading={loading} - endLabel={t('common.allLoaded', { count: total })} + endLabel={t("common.allLoaded", { count: total })} /> )}
- ) + ); } -function ExecutionRow({ e, df, t }: { e: Execution; df: ReturnType; t: ReturnType['t'] }) { - const meta = STATUS_META[e.status] - const Icon = meta?.icon ?? Clock +function ExecutionRow({ + e, + flow, + df, + t, +}: { + e: Execution; + flow?: Flow; + df: ReturnType; + t: ReturnType["t"]; +}) { + const meta = STATUS_META[e.status]; + const Icon = meta?.icon ?? Clock; // A manual run has no flow: what identifies it is who typed it. const source = - e.origin === 'MANUAL' - ? e.triggeredBy || t('soar.executions.manual') - : ((e.rulePath ?? '').split('/').pop() ?? '').replace(/\.ya?ml$/i, '') || '—' + e.origin === "MANUAL" + ? e.triggeredBy || t("soar.executions.manual") + : ((e.rulePath ?? "").split("/").pop() ?? "").replace(/\.ya?ml$/i, "") || + "—"; + + // Node column: the flow carries no per-run state, only its DAG shape — so the + // node's place in the run is its ancestor chain, read off the live flow. + const nodeLabel = + e.origin === "FLOW" && e.nodeId + ? [ancestorPath(e.nodeId, flow?.nodes ?? {}), e.nodeId] + .filter(Boolean) + .join(" ← ") + : e.origin === "MANUAL" + ? t("soar.executions.manual") + : "—"; + return ( -
-
+
+
{t(`soar.executionStatus.${e.status}`)}
+
+ {nodeLabel} +
-
{source}
- {e.alertId &&
{e.alertId}
} +
+ {source} +
+ {e.alertId && ( +
+ {e.alertId} +
+ )}
-
{e.command || '—'}
- {e.nonExecutionCause &&
{t(`soar.nonExecutionCause.${e.nonExecutionCause}`)}
} + + {e.nonExecutionCause && ( +
+ {t(`soar.nonExecutionCause.${e.nonExecutionCause}`)} +
+ )} +
+
+ {e.agent || "—"} +
+
+ {df.formatDateTime(e.startedAt)} +
+
+ {e.retries || 0}
-
{e.agent || '—'}
-
{df.formatDateTime(e.startedAt)}
-
{e.retries || 0}
- ) + ); +} + +// The command column is always filled for every node: flow executors that +// carry no shell command (http, mail, llm, notify, incident, conditional) get +// a derived action summary from the backend. Long values clamp to one line; +// the tooltip carries the full text. +function CommandCell({ text }: { text?: string }) { + const value = (text ?? "").trim(); + if (!value) return ; + return ( +
+ + +
+ {value} +
+
+ + {value} + +
+
+ ); +} + +// Walk the flow's DAG backward from `startId` and return the ancestor chain +// root → … → parent as ids. Bounded by a visited-set so cycles can't spin. +// Returns '' when the node isn't in this flow (renamed or deleted since the run). +function ancestorPath( + startId: string, + nodes: Record, +): string { + if (!nodes[startId]) return ""; + const chain: string[] = []; + const seen = new Set([startId]); + let cur: string | undefined = startId; + while (cur) { + const parents = parentIds(cur, nodes); + if (parents.length === 0) break; + const next = parents[0]; // arbitrary but stable: first declared parent + if (seen.has(next)) break; + seen.add(next); + chain.unshift(next); + cur = next; + } + return chain.join(" ← "); +} + +function parentIds(id: string, nodes: Record): string[] { + const out: string[] = []; + for (const [pid, n] of Object.entries(nodes)) { + if (pid === id) continue; + if ((n.onSuccess ?? []).includes(id) || (n.onError ?? []).includes(id)) + out.push(pid); + } + return out; } function Center({ children }: { children: React.ReactNode }) { - return
{children}
+ return ( +
+ {children} +
+ ); } diff --git a/frontend/src/shared/i18n/locales/de.json b/frontend/src/shared/i18n/locales/de.json index cf518a300..c3c148ebb 100644 --- a/frontend/src/shared/i18n/locales/de.json +++ b/frontend/src/shared/i18n/locales/de.json @@ -4815,10 +4815,11 @@ "cols": { "status": "Status", "flow": "Flow", - "command": "Befehl", + "command": "Befehl / Aktion", "agent": "Agent", "date": "Datum", - "retries": "Versuche" + "retries": "Versuche", + "node": "Knoten" }, "manual": "Interaktive Konsole" }, diff --git a/frontend/src/shared/i18n/locales/en.json b/frontend/src/shared/i18n/locales/en.json index 4c3d0999f..88f126600 100644 --- a/frontend/src/shared/i18n/locales/en.json +++ b/frontend/src/shared/i18n/locales/en.json @@ -5201,10 +5201,11 @@ "cols": { "status": "Status", "flow": "Flow", - "command": "Command", + "command": "Command / Action", "agent": "Agent", "date": "Date", - "retries": "Retries" + "retries": "Retries", + "node": "Node" }, "filters": { "source": "Source", diff --git a/frontend/src/shared/i18n/locales/es.json b/frontend/src/shared/i18n/locales/es.json index 36f44d0a5..7a60a9f82 100644 --- a/frontend/src/shared/i18n/locales/es.json +++ b/frontend/src/shared/i18n/locales/es.json @@ -4937,10 +4937,11 @@ "cols": { "status": "Estado", "flow": "Flujo", - "command": "Comando", + "command": "Comando / Acción", "agent": "Agente", "date": "Fecha", - "retries": "Reintentos" + "retries": "Reintentos", + "node": "Nodo" }, "filters": { "source": "Origen", diff --git a/frontend/src/shared/i18n/locales/fr.json b/frontend/src/shared/i18n/locales/fr.json index 7641219b8..f9a88213e 100644 --- a/frontend/src/shared/i18n/locales/fr.json +++ b/frontend/src/shared/i18n/locales/fr.json @@ -4815,10 +4815,11 @@ "cols": { "status": "Statut", "flow": "Flux", - "command": "Commande", + "command": "Commande / Action", "agent": "Agent", "date": "Date", - "retries": "Tentatives" + "retries": "Tentatives", + "node": "Nœud" }, "filters": { "source": "Source", diff --git a/frontend/src/shared/i18n/locales/it.json b/frontend/src/shared/i18n/locales/it.json index 2474db90a..df6743b58 100644 --- a/frontend/src/shared/i18n/locales/it.json +++ b/frontend/src/shared/i18n/locales/it.json @@ -4815,10 +4815,11 @@ "cols": { "status": "Stato", "flow": "Flusso", - "command": "Comando", + "command": "Comando / Acción", "agent": "Agente", "date": "Data", - "retries": "Tentativi" + "retries": "Tentativi", + "node": "Nodo" }, "filters": { "source": "Origine", diff --git a/frontend/src/shared/i18n/locales/pt.json b/frontend/src/shared/i18n/locales/pt.json index 453a59448..446c7e58c 100644 --- a/frontend/src/shared/i18n/locales/pt.json +++ b/frontend/src/shared/i18n/locales/pt.json @@ -4937,10 +4937,11 @@ "cols": { "status": "Status", "flow": "Fluxo", - "command": "Comando", + "command": "Comando / Ação", "agent": "Agente", "date": "Data", - "retries": "Tentativas" + "retries": "Tentativas", + "node": "Nó" }, "filters": { "source": "Origem", diff --git a/frontend/src/shared/i18n/locales/ru.json b/frontend/src/shared/i18n/locales/ru.json index 90fb5bfdb..8f136028f 100644 --- a/frontend/src/shared/i18n/locales/ru.json +++ b/frontend/src/shared/i18n/locales/ru.json @@ -4607,10 +4607,11 @@ "cols": { "status": "Статус", "flow": "Поток", - "command": "Команда", + "command": "Команда / Действие", "agent": "Агент", "date": "Дата", - "retries": "Попытки" + "retries": "Попытки", + "node": "Узел" }, "manual": "Интерактивная консоль" },