From d08c1dc591b4a0467eebb060f79c44cdd973c49c Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Tue, 10 Nov 2026 14:37:46 +0100 Subject: [PATCH 1/4] feat(monitoring): add live trace page Administrators cannot tell which firewall layer blocks a client. The Monitoring > Live trace page traces one client IP for a limited time and shows the blocked connections with layer and rule, the DNS queries and the live connections with rates. Uses the new ns.livetrace API. Assisted-by: Claude Code:claude-fable-5-2 --- src/components/standalone/SideMenu.vue | 4 + .../monitoring/livetrace/LiveTrace.vue | 1017 +++++++++++++++++ src/i18n/en.json | 71 +- src/i18n/it.json | 71 +- src/router/index.ts | 5 + .../standalone/monitoring/LiveTraceView.vue | 21 + 6 files changed, 1187 insertions(+), 2 deletions(-) create mode 100644 src/components/standalone/monitoring/livetrace/LiveTrace.vue create mode 100644 src/views/standalone/monitoring/LiveTraceView.vue diff --git a/src/components/standalone/SideMenu.vue b/src/components/standalone/SideMenu.vue index f565fb5ff..6d8b4981a 100644 --- a/src/components/standalone/SideMenu.vue +++ b/src/components/standalone/SideMenu.vue @@ -44,6 +44,10 @@ const navigation: Ref = ref([ name: 'standalone.real_time_monitor.title', to: 'monitoring/real-time-monitoring' }, + { + name: 'standalone.live_trace.title', + to: 'monitoring/live-trace' + }, { name: 'standalone.netify_informatics.title', to: 'monitoring/netify-informatics' diff --git a/src/components/standalone/monitoring/livetrace/LiveTrace.vue b/src/components/standalone/monitoring/livetrace/LiveTrace.vue new file mode 100644 index 000000000..4d981f451 --- /dev/null +++ b/src/components/standalone/monitoring/livetrace/LiveTrace.vue @@ -0,0 +1,1017 @@ + + + + + diff --git a/src/i18n/en.json b/src/i18n/en.json index da8f6f56d..4d44d1a59 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -473,7 +473,10 @@ "cannot_retrieve_allowlist_urls": "Cannot retrieve allowlists", "cannot_create_custom_feed": "Cannot create custom feed", "cannot_edit_custom_feed": "Cannot edit custom feed", - "cannot_delete_custom_feed": "Cannot delete custom feed" + "cannot_delete_custom_feed": "Cannot delete custom feed", + "invalid_duration": "Invalid duration", + "session_busy": "Another live trace session is already running", + "collector_not_started": "The live trace collector could not be started" }, "ne_text_input": { "show_password": "Show password", @@ -2669,6 +2672,72 @@ "no_data_available": "No data available", "live_flows": "Live flows" }, + "live_trace": { + "title": "Live trace", + "description": "Trace the traffic of a single client for a limited time. Start a session, ask the user to retry the connection, then check which part of the firewall blocks the traffic.", + "client_ip": "Client IP address", + "port_n": "port {port}", + "filter_helper": "Ping is not traced when port or protocol is set", + "duration": "Duration", + "minutes": "{n} minute | {n} minutes", + "start": "Start", + "stop": "Stop", + "error": "Cannot run live trace", + "failure": "Live trace session stopped unexpectedly", + "running": "Tracing traffic of {ip}", + "remaining": "Time remaining: {time}", + "last_session_results": "Results of the last session for {ip}", + "truncated": "Some traffic was not traced", + "truncated_description": "The session reached its limits. Filter by port or protocol to narrow the trace.", + "no_traffic_last_session": "No traffic seen for {ip} during the last session", + "blocked_traffic": "Blocked traffic", + "blocked_detected": "{n} blocked connection detected | {n} blocked connections detected", + "no_blocked_traffic": "No blocked traffic yet", + "no_blocked_traffic_description": "Ask the user to retry the connection", + "no_blocked_traffic_last_session": "No blocked traffic during the last session", + "last_seen": "Last seen", + "protocol": "Protocol", + "source": "Source", + "destination": "Destination", + "blocked_by": "Blocked by", + "interfaces": "Interfaces", + "attempts": "Attempts", + "layer_firewall": "Firewall rule", + "layer_threat_shield_ip": "Threat Shield IP (banIP)", + "layer_dpi": "DPI", + "layer_ips": "IPS (Snort)", + "layer_dns_filter": "DNS filter", + "layer_mac_binding": "MAC binding", + "layer_routing": "Routing", + "no_route": "No route to destination", + "private_to_wan": "Private destination sent to WAN: missing route or tunnel down", + "default_policy": "Default firewall policy: the interface is not in any zone", + "dns_queries": "DNS queries", + "no_dns": "No DNS queries", + "result": "Result", + "count": "Count", + "blocked_by_dns_filter": "Blocked by DNS filter", + "connections": "Connections", + "allowed_connections": "Allowed connections", + "application": "Application", + "no_connections": "No connections", + "no_connections_found": "No connections found", + "filter_connections": "Filter connections", + "download_rate": "Download rate", + "upload_rate": "Upload rate", + "total_traffic": "Total traffic", + "no_reply": "No reply", + "blocked_by_dpi": "Blocked by DPI", + "zone_policy": "Default policy of zone {zone}", + "ip_list": "Address in list {name}", + "not_traced": "Not traced", + "dns_success": "Success", + "dns_not_found": "Domain not found", + "dns_server_failure": "Server failure", + "dns_refused": "Refused", + "go_to_rules": "Go to Firewall rules", + "show_details": "Show details" + }, "flows": { "subtitle": "View real-time network connections, each row represents an active flow with live transfer rates and total traffic.", "application": "Application", diff --git a/src/i18n/it.json b/src/i18n/it.json index 2227a0aa0..e9907a3d7 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -470,7 +470,10 @@ "cannot_delete_custom_feed": "Impossibile eliminare il feed personalizzato", "cannot_retrieve_allowlist_urls": "Impossibile recuperare le allowlist", "allowlist_url_not_found": "Feed personalizzato non trovato", - "url_already_present": "Questo URL è già usato da un'altra allowlist" + "url_already_present": "Questo URL è già usato da un'altra allowlist", + "invalid_duration": "Durata non valida", + "session_busy": "Un'altra sessione di traccia live è già in corso", + "collector_not_started": "Impossibile avviare la raccolta dei dati della traccia live" }, "login": { "sign_in": "Accedi", @@ -3124,6 +3127,72 @@ "few_apps_warning": "Abbonati per sbloccare il rilevamento DPI completo", "few_apps_warning_description": "I flussi in tempo reale includono solo applicazioni e protocolli standard, quindi parte del traffico potrebbe apparire come \"Sconosciuto\". Abbonati per rilevare oltre 2600 applicazioni.", "blocked": "Bloccato" + }, + "live_trace": { + "title": "Traccia live", + "description": "Traccia il traffico di un singolo client per un tempo limitato. Avvia una sessione, chiedi all'utente di riprovare la connessione, poi verifica quale parte del firewall blocca il traffico.", + "client_ip": "Indirizzo IP del client", + "port_n": "porta {port}", + "filter_helper": "Il ping non viene tracciato se porta o protocollo sono impostati", + "duration": "Durata", + "minutes": "{n} minuto | {n} minuti", + "start": "Avvia", + "stop": "Ferma", + "error": "Impossibile eseguire la traccia live", + "failure": "La sessione di traccia live si è interrotta in modo imprevisto", + "running": "Traccia del traffico di {ip} in corso", + "remaining": "Tempo rimanente: {time}", + "last_session_results": "Risultati dell'ultima sessione per {ip}", + "truncated": "Parte del traffico non è stata tracciata", + "truncated_description": "La sessione ha raggiunto i suoi limiti. Filtra per porta o protocollo per restringere la traccia.", + "no_traffic_last_session": "Nessun traffico rilevato per {ip} durante l'ultima sessione", + "blocked_traffic": "Traffico bloccato", + "blocked_detected": "{n} connessione bloccata rilevata | {n} connessioni bloccate rilevate", + "no_blocked_traffic": "Nessun traffico bloccato finora", + "no_blocked_traffic_description": "Chiedi all'utente di riprovare la connessione", + "no_blocked_traffic_last_session": "Nessun traffico bloccato durante l'ultima sessione", + "last_seen": "Ultima occorrenza", + "protocol": "Protocollo", + "source": "Sorgente", + "destination": "Destinazione", + "blocked_by": "Bloccato da", + "interfaces": "Interfacce", + "attempts": "Tentativi", + "layer_firewall": "Regola firewall", + "layer_threat_shield_ip": "Threat Shield IP (banIP)", + "layer_dpi": "DPI", + "layer_ips": "IPS (Snort)", + "layer_dns_filter": "Filtro DNS", + "layer_mac_binding": "Binding MAC", + "layer_routing": "Routing", + "no_route": "Nessuna rotta verso la destinazione", + "private_to_wan": "Destinazione privata inviata alla WAN: rotta mancante o tunnel non attivo", + "default_policy": "Policy predefinita del firewall: l'interfaccia non appartiene ad alcuna zona", + "dns_queries": "Query DNS", + "no_dns": "Nessuna query DNS", + "result": "Risultato", + "count": "Conteggio", + "blocked_by_dns_filter": "Bloccato dal filtro DNS", + "connections": "Connessioni", + "allowed_connections": "Connessioni consentite", + "application": "Applicazione", + "no_connections": "Nessuna connessione", + "no_connections_found": "Nessuna connessione trovata", + "filter_connections": "Filtra connessioni", + "download_rate": "Velocità download", + "upload_rate": "Velocità upload", + "total_traffic": "Traffico totale", + "no_reply": "Nessuna risposta", + "blocked_by_dpi": "Bloccato dal DPI", + "zone_policy": "Policy predefinita della zona {zone}", + "ip_list": "Indirizzo nella lista {name}", + "not_traced": "Non tracciato", + "dns_success": "Riuscita", + "dns_not_found": "Dominio non trovato", + "dns_server_failure": "Errore del server", + "dns_refused": "Rifiutata", + "go_to_rules": "Vai a Regole firewall", + "show_details": "Mostra dettagli" } }, "controller": { diff --git a/src/router/index.ts b/src/router/index.ts index 8fa201edd..889f03390 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -187,6 +187,11 @@ const standaloneRoutes = [ name: 'RealTimeMonitoring', component: () => import('../views/standalone/monitoring/RealTimeMonitoringView.vue') }, + { + path: 'monitoring/live-trace', + name: 'LiveTrace', + component: () => import('../views/standalone/monitoring/LiveTraceView.vue') + }, { path: 'monitoring/netify-informatics', name: 'NetifyInformatics', diff --git a/src/views/standalone/monitoring/LiveTraceView.vue b/src/views/standalone/monitoring/LiveTraceView.vue new file mode 100644 index 000000000..17205099f --- /dev/null +++ b/src/views/standalone/monitoring/LiveTraceView.vue @@ -0,0 +1,21 @@ + + + + + From 3af20f8c8eb061004a2a6d7490cfb38e18a4cf49 Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 12 Nov 2026 16:39:26 +0100 Subject: [PATCH 2/4] feat(monitoring): always show live trace details Allowed connections now always list one row per connection with its source port, so the "Show details" switch and the count column are gone. DNS queries move below the connections table. Assisted-by: Claude Code:Sonnet 5.5 --- .../monitoring/livetrace/LiveTrace.vue | 97 +++++++------------ src/i18n/en.json | 3 +- src/i18n/it.json | 3 +- 3 files changed, 39 insertions(+), 64 deletions(-) diff --git a/src/components/standalone/monitoring/livetrace/LiveTrace.vue b/src/components/standalone/monitoring/livetrace/LiveTrace.vue index 4d981f451..789ed998c 100644 --- a/src/components/standalone/monitoring/livetrace/LiveTrace.vue +++ b/src/components/standalone/monitoring/livetrace/LiveTrace.vue @@ -27,7 +27,6 @@ import { NeTableHeadCell, NeTableRow, NeTextInput, - NeToggle, NeTooltip, useItemPagination, type NeDropdownFilterV2Option, @@ -391,13 +390,10 @@ const totalDownload = computed(() => ) const totalUpload = computed(() => filteredConnections.value.reduce((sum, c) => sum + c.upload, 0)) -const showDetails = ref(false) -// one row per connection when details are shown +// one row per connection const allowedRows = computed<(Allowed & { source_port?: string })[]>(() => (data.value?.allowed ?? []).flatMap((a) => - showDetails.value && a.source_ports?.length - ? a.source_ports.map((port) => ({ ...a, source_port: port })) - : [a] + a.source_ports?.length ? a.source_ports.map((port) => ({ ...a, source_port: port })) : [a] ) ) @@ -429,15 +425,7 @@ const { currentPage: allowedPage, paginatedItems: paginatedAllowed } = useItemPa { itemsPerPage: allowedPageSize } ) watch( - [ - filter, - applicationFilter, - protocolFilter, - sourceFilter, - destinationFilter, - statusFilter, - showDetails - ], + [filter, applicationFilter, protocolFilter, sourceFilter, destinationFilter, statusFilter], () => { if (currentPage.value != 1) { currentPage.value = 1 @@ -765,40 +753,6 @@ const ruleDetails = (rule: FirewallRule): [string, string][] => [ - -
- {{ t('standalone.live_trace.dns_queries') }} - - - - {{ t('common.name') }} - {{ t('common.type') }} - {{ t('standalone.live_trace.result') }} - {{ t('standalone.live_trace.count') }} - - - - - {{ item.name }} - - - {{ item.type }} - - - - {{ dnsResult(item.result) }} - - {{ t('standalone.live_trace.blocked_by_dns_filter') }} - - - - - {{ item.count }} - - - - -
@@ -851,11 +805,6 @@ const ruleDetails = (rule: FirewallRule): [string, string][] => [ {{ t('common.reset_filters') }} - diff --git a/src/i18n/en.json b/src/i18n/en.json index 4d44d1a59..35543463e 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2735,8 +2735,7 @@ "dns_not_found": "Domain not found", "dns_server_failure": "Server failure", "dns_refused": "Refused", - "go_to_rules": "Go to Firewall rules", - "show_details": "Show details" + "go_to_rules": "Go to Firewall rules" }, "flows": { "subtitle": "View real-time network connections, each row represents an active flow with live transfer rates and total traffic.", diff --git a/src/i18n/it.json b/src/i18n/it.json index e9907a3d7..0d0d5584e 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -3191,8 +3191,7 @@ "dns_not_found": "Dominio non trovato", "dns_server_failure": "Errore del server", "dns_refused": "Rifiutata", - "go_to_rules": "Vai a Regole firewall", - "show_details": "Mostra dettagli" + "go_to_rules": "Vai a Regole firewall" } }, "controller": { From 7bd4f5d5b7573824142df9724e6f09ed3e0da6ac Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 12 Nov 2026 16:51:18 +0100 Subject: [PATCH 3/4] fix(monitoring): align live trace with standards Make the Go to Firewall rules link reachable from the keyboard, and reword the page description and empty states by the Nebula writing guidelines. Validate the IP address and port with valibot and the shared validators, clear a stale start error when stopping, type the ubus calls, and use hierarchical query keys and full i18n keys. Assisted-by: Claude Code:Opus 5.5 --- .../monitoring/livetrace/LiveTrace.vue | 132 ++++++++++++------ src/i18n/en.json | 9 +- src/i18n/it.json | 9 +- 3 files changed, 99 insertions(+), 51 deletions(-) diff --git a/src/components/standalone/monitoring/livetrace/LiveTrace.vue b/src/components/standalone/monitoring/livetrace/LiveTrace.vue index 789ed998c..ce67babc1 100644 --- a/src/components/standalone/monitoring/livetrace/LiveTrace.vue +++ b/src/components/standalone/monitoring/livetrace/LiveTrace.vue @@ -7,6 +7,8 @@ import { computed, ref, watch } from 'vue' import { useI18n } from 'vue-i18n' import { useMutation, useQuery, useQueryClient } from '@tanstack/vue-query' +import { isAxiosError } from 'axios' +import * as v from 'valibot' import { byteFormat1024, getAxiosErrorMessage, @@ -34,9 +36,8 @@ import { } from '@nethesis/vue-components' import { faBan, faShieldHalved, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' -import type { AxiosError } from 'axios' import { ubusCall, ValidationError } from '@/lib/standalone/ubus' -import { MessageBag } from '@/lib/validation' +import { MessageBag, validateIpAddress, validatePort } from '@/lib/validation' import { formatRate } from '@/composables/useFlows' import { getStandaloneRoutePrefix } from '@/lib/router' import type { FirewallRule, RuleHost } from '@/stores/standalone/firewall' @@ -142,7 +143,7 @@ const layerLabels: Record = { } const ip = ref('') -const duration = ref(durationOptions[1]!.id) +const duration = ref('120') const port = ref('') const protocol = ref('any') const filter = ref('') @@ -153,23 +154,28 @@ const destinationFilter = ref([]) const statusFilter = ref([]) const validationBag = ref(new MessageBag()) +const STATUS_KEY = ['livetrace', 'status'] + // poll every 2s while running, the poll that sees running=false is the final status const { data, error, isPending } = useQuery({ - queryKey: ['livetrace', 'status'], - queryFn: async () => (await ubusCall<{ data: Status }>('ns.livetrace', 'status')).data, - refetchInterval: (query) => (query.state.data?.running ? 2000 : false) + queryKey: STATUS_KEY, + queryFn: () => ubusCall<{ data: Status }>('ns.livetrace', 'status'), + select: (res) => res.data, + refetchInterval: (query) => (query.state.data?.data.running ? 2000 : false) }) -const refreshStatus = () => queryClient.invalidateQueries({ queryKey: ['livetrace', 'status'] }) +const refreshStatus = () => queryClient.invalidateQueries({ queryKey: STATUS_KEY }) const start = useMutation({ - mutationFn: () => - ubusCall('ns.livetrace', 'start', { - ip: ip.value, + mutationFn: () => { + const portValue = String(port.value).trim() + return ubusCall<{ data: unknown }>('ns.livetrace', 'start', { + ip: ip.value.trim(), duration: Number(duration.value), - ...(String(port.value).trim() ? { port: Number(port.value) } : {}), + ...(portValue ? { port: Number(portValue) } : {}), ...(protocol.value != 'any' ? { protocol: protocol.value } : {}) - }), + }) + }, onMutate: () => { validationBag.value.clear() stop.reset() @@ -183,37 +189,64 @@ const start = useMutation({ } }) -// the backend treats port 0 as no filter, reject it here -function startTrace() { +const stop = useMutation({ + mutationFn: () => ubusCall<{ data: unknown }>('ns.livetrace', 'stop'), + onMutate: () => start.reset(), + onSuccess: refreshStatus +}) + +// the backend treats port 0 as no filter, an empty port means no filter +const formSchema = v.object({ + ip: v.pipe( + v.string(), + v.check((value) => validateIpAddress(value.trim()).valid, 'error.invalid_ip_address') + ), + port: v.pipe( + v.string(), + v.check( + (value) => value.trim() == '' || (/^\d+$/.test(value.trim()) && validatePort(value).valid), + 'error.invalid_port' + ) + ) +}) + +function validate(): boolean { validationBag.value.clear() - const value = String(port.value).trim() - const number = Number(value) - if (value && !(Number.isInteger(number) && number >= 1 && number <= 65535)) { - validationBag.value.set('port', 'error.invalid_port') - return + const result = v.safeParse(formSchema, { ip: ip.value, port: String(port.value) }) + if (!result.success) { + for (const issue of result.issues) { + const field = issue.path?.[0]?.key + if (typeof field == 'string' && !validationBag.value.has(field)) { + validationBag.value.set(field, issue.message) + } + } } - start.mutate() + return result.success } -// drop the stale port error once the value changes -watch(port, () => validationBag.value.delete('port')) +function startTrace() { + if (validate()) { + start.mutate() + } +} -const stop = useMutation({ - mutationFn: () => ubusCall('ns.livetrace', 'stop'), - onSuccess: refreshStatus -}) +// drop stale errors once the value changes +watch(ip, () => validationBag.value.delete('ip')) +watch(port, () => validationBag.value.delete('port')) const actionError = computed(() => start.error.value instanceof ValidationError ? null : (start.error.value ?? stop.error.value) ) // start error codes with their own message +const errorKeys: Record = { + session_busy: 'error.session_busy', + collector_not_started: 'error.collector_not_started' +} const errorDescription = computed(() => { - const e = (error.value ?? actionError.value) as AxiosError<{ message?: string }> | null - const code = e?.response?.data?.message ?? '' - return ['session_busy', 'collector_not_started'].includes(code) - ? t(`error.${code}`) - : t(getAxiosErrorMessage(e)) + const e = error.value ?? actionError.value + const code = isAxiosError<{ message?: string }>(e) ? (e.response?.data?.message ?? '') : '' + return t(errorKeys[code] ?? getAxiosErrorMessage(e)) }) const hasResults = computed( @@ -495,7 +528,7 @@ const isUserRule = (item: Blocked) => !/^(?:drop|reject)_(?:from|to)_/.test(item.chain ?? '') const { data: firewallRules } = useQuery({ - queryKey: ['livetrace-firewall-rules'], + queryKey: ['livetrace', 'firewall-rules'], queryFn: async () => { const responses = await Promise.all( ['list-forward-rules', 'list-input-rules', 'list-output-rules'].map((method) => @@ -718,14 +751,20 @@ const ruleDetails = (rule: FirewallRule): [string, string][] => [
{{ value }}
- - {{ t('standalone.live_trace.go_to_rules') }} - + + {{ t('standalone.live_trace.go_to_rules') }} + + @@ -814,9 +853,9 @@ const ruleDetails = (rule: FirewallRule): [string, string][] => [ @@ -958,7 +997,14 @@ const ruleDetails = (rule: FirewallRule): [string, string][] => [
{{ t('standalone.live_trace.dns_queries') }} - + {{ t('common.name') }} diff --git a/src/i18n/en.json b/src/i18n/en.json index 35543463e..f3bc80544 100644 --- a/src/i18n/en.json +++ b/src/i18n/en.json @@ -2674,7 +2674,7 @@ }, "live_trace": { "title": "Live trace", - "description": "Trace the traffic of a single client for a limited time. Start a session, ask the user to retry the connection, then check which part of the firewall blocks the traffic.", + "description": "Trace the traffic of a single client for a limited time. Start a session, ask the user to retry the connection, then check what blocks the traffic.", "client_ip": "Client IP address", "port_n": "port {port}", "filter_helper": "Ping is not traced when port or protocol is set", @@ -2693,7 +2693,7 @@ "blocked_traffic": "Blocked traffic", "blocked_detected": "{n} blocked connection detected | {n} blocked connections detected", "no_blocked_traffic": "No blocked traffic yet", - "no_blocked_traffic_description": "Ask the user to retry the connection", + "no_blocked_traffic_description": "Ask the user to retry the connection.", "no_blocked_traffic_last_session": "No blocked traffic during the last session", "last_seen": "Last seen", "protocol": "Protocol", @@ -2713,14 +2713,15 @@ "private_to_wan": "Private destination sent to WAN: missing route or tunnel down", "default_policy": "Default firewall policy: the interface is not in any zone", "dns_queries": "DNS queries", - "no_dns": "No DNS queries", + "no_dns": "No DNS queries yet", + "no_dns_last_session": "No DNS queries during the last session", "result": "Result", "count": "Count", "blocked_by_dns_filter": "Blocked by DNS filter", "connections": "Connections", "allowed_connections": "Allowed connections", "application": "Application", - "no_connections": "No connections", + "no_connections": "No connections yet", "no_connections_found": "No connections found", "filter_connections": "Filter connections", "download_rate": "Download rate", diff --git a/src/i18n/it.json b/src/i18n/it.json index 0d0d5584e..523b1f2c8 100644 --- a/src/i18n/it.json +++ b/src/i18n/it.json @@ -3130,7 +3130,7 @@ }, "live_trace": { "title": "Traccia live", - "description": "Traccia il traffico di un singolo client per un tempo limitato. Avvia una sessione, chiedi all'utente di riprovare la connessione, poi verifica quale parte del firewall blocca il traffico.", + "description": "Traccia il traffico di un singolo client per un tempo limitato. Avvia una sessione, chiedi all'utente di riprovare la connessione, poi verifica cosa blocca il traffico.", "client_ip": "Indirizzo IP del client", "port_n": "porta {port}", "filter_helper": "Il ping non viene tracciato se porta o protocollo sono impostati", @@ -3149,7 +3149,7 @@ "blocked_traffic": "Traffico bloccato", "blocked_detected": "{n} connessione bloccata rilevata | {n} connessioni bloccate rilevate", "no_blocked_traffic": "Nessun traffico bloccato finora", - "no_blocked_traffic_description": "Chiedi all'utente di riprovare la connessione", + "no_blocked_traffic_description": "Chiedi all'utente di riprovare la connessione.", "no_blocked_traffic_last_session": "Nessun traffico bloccato durante l'ultima sessione", "last_seen": "Ultima occorrenza", "protocol": "Protocollo", @@ -3169,14 +3169,15 @@ "private_to_wan": "Destinazione privata inviata alla WAN: rotta mancante o tunnel non attivo", "default_policy": "Policy predefinita del firewall: l'interfaccia non appartiene ad alcuna zona", "dns_queries": "Query DNS", - "no_dns": "Nessuna query DNS", + "no_dns": "Nessuna query DNS finora", + "no_dns_last_session": "Nessuna query DNS durante l'ultima sessione", "result": "Risultato", "count": "Conteggio", "blocked_by_dns_filter": "Bloccato dal filtro DNS", "connections": "Connessioni", "allowed_connections": "Connessioni consentite", "application": "Applicazione", - "no_connections": "Nessuna connessione", + "no_connections": "Nessuna connessione finora", "no_connections_found": "Nessuna connessione trovata", "filter_connections": "Filtra connessioni", "download_rate": "Velocità download", From b848c032ae605b4453e25330a8a9efd7b6dfcb0a Mon Sep 17 00:00:00 2001 From: Giacomo Sanchietti Date: Thu, 12 Nov 2026 17:04:38 +0100 Subject: [PATCH 4/4] fix(monitoring): open live trace rule details with keyboard The rule tooltip listened for focus on its wrapper, so focusing the rule name never opened it and the Go to Firewall rules link was out of reach. A button that opens it on hover or click works with Enter. Assisted-by: Claude Code:claude-opus-5-5 --- .../standalone/monitoring/livetrace/LiveTrace.vue | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/standalone/monitoring/livetrace/LiveTrace.vue b/src/components/standalone/monitoring/livetrace/LiveTrace.vue index ce67babc1..b12dab319 100644 --- a/src/components/standalone/monitoring/livetrace/LiveTrace.vue +++ b/src/components/standalone/monitoring/livetrace/LiveTrace.vue @@ -735,11 +735,15 @@ const ruleDetails = (rule: FirewallRule): [string, string][] => [ {{ layerLabels[item.layer] ?? item.layer }}

- + +