From 5b0861eaac424c5320b29abdbac769686738066c Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Wed, 29 Jul 2026 16:10:54 +0200 Subject: [PATCH 1/2] fix: removing error notifications --- src/StandaloneApp.vue | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/StandaloneApp.vue b/src/StandaloneApp.vue index 16d264bce..3f5a81ee4 100644 --- a/src/StandaloneApp.vue +++ b/src/StandaloneApp.vue @@ -8,13 +8,12 @@ import StandaloneAppShell from '@/components/standalone/StandaloneAppShell.vue' import StandaloneAppLogin from '@/components/standalone/StandaloneAppLogin.vue' import { TOKEN_REFRESH_INTERVAL, useLoginStore } from '@/stores/standalone/standaloneLogin' import { onMounted, ref } from 'vue' -import axios, { type AxiosRequestConfig, CanceledError } from 'axios' +import axios, { type AxiosRequestConfig } from 'axios' import { getStandaloneApiEndpoint, isStandaloneMode } from './lib/config' import { useUnitsStore } from './stores/controller/units' import { useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { getPreference } from '@nethesis/vue-components' -import { useNotificationsStore } from './stores/notifications' import { UnauthorizedAction, useSudoStore } from '@/stores/standalone/sudo.ts' import AskSudoPasswordModal from '@/components/standalone/AskSudoPasswordModal.vue' import WizardShell from './views/standalone/wizard/WizardShell.vue' @@ -23,7 +22,6 @@ import { VueQueryDevtools } from '@tanstack/vue-query-devtools' const loginStore = useLoginStore() const unitsStore = useUnitsStore() -const notificationsStore = useNotificationsStore() const { locale } = useI18n({ useScope: 'global' }) const route = useRoute() const sudoStore = useSudoStore() @@ -140,30 +138,6 @@ function configureAxios() { } }, 200) }) - } else { - // show error notification only if error is not caused from cancellation - // and if it isn't a validation error - // and if it isn't caused by one of the update endpoints because of the system rebooting - // and if it isn't caused by a net::ERR_CONNECTION_REFUSED when applying a migration (probably because of nginx restarting or the machine's ip addresses changing) - if ( - !(error instanceof CanceledError) && - !error.response?.data?.data?.validation?.errors?.length && - !( - error.config.url.includes('/ubus/call') && - (JSON.parse(error.config.data)?.method === 'install-uploaded-image' || - JSON.parse(error.config.data)?.method === 'update-system') && - // if the error is caused by a system reboot, the response will not have a payload (since it's caused by a net::ERR_CONNECTION_REFUSED) - (!error.response || !error.response.data) - ) && - !( - error.config.url.includes('/ubus/call') && - (JSON.parse(error.config.data)?.path === 'ns.migration' || - JSON.parse(error.config.data)?.method === 'upload') && - (!error.response || !error.response.data) - ) - ) { - notificationsStore.createNotificationFromAxiosError(error) - } } return Promise.reject(error) } From 78185b83d28460734720c86a36085f18c432eded Mon Sep 17 00:00:00 2001 From: Tommaso Bailetti Date: Tue, 4 Aug 2026 11:50:11 +0200 Subject: [PATCH 2/2] fix: adjusted output --- src/StandaloneApp.vue | 11 +++++ src/lib/__tests__/axiosErrorCommand.spec.ts | 48 +++++++++++++++++++++ src/lib/axiosErrorCommand.ts | 27 ++++++++++++ src/stores/notifications.ts | 16 +++---- 4 files changed, 91 insertions(+), 11 deletions(-) create mode 100644 src/lib/__tests__/axiosErrorCommand.spec.ts create mode 100644 src/lib/axiosErrorCommand.ts diff --git a/src/StandaloneApp.vue b/src/StandaloneApp.vue index 3f5a81ee4..0d9c49827 100644 --- a/src/StandaloneApp.vue +++ b/src/StandaloneApp.vue @@ -14,6 +14,7 @@ import { useUnitsStore } from './stores/controller/units' import { useRoute } from 'vue-router' import { useI18n } from 'vue-i18n' import { getPreference } from '@nethesis/vue-components' +import { getUbusReproductionCommand } from '@/lib/axiosErrorCommand' import { UnauthorizedAction, useSudoStore } from '@/stores/standalone/sudo.ts' import AskSudoPasswordModal from '@/components/standalone/AskSudoPasswordModal.vue' import WizardShell from './views/standalone/wizard/WizardShell.vue' @@ -92,6 +93,16 @@ function configureAxios() { console.error('[interceptor]', error.response.data.message) } + if (error.config) { + const ubusCommand = getUbusReproductionCommand(error.config) + + if (ubusCommand) { + console.error('[interceptor] reproduce this call on the unit with:') + // logged on its own so it's easy to select and copy + console.error(ubusCommand) + } + } + if (error.response?.status == 401) { if (isStandaloneMode()) { if (error.response?.data?.message !== 'incorrect Username or Password') { diff --git a/src/lib/__tests__/axiosErrorCommand.spec.ts b/src/lib/__tests__/axiosErrorCommand.spec.ts new file mode 100644 index 000000000..13f26ef4f --- /dev/null +++ b/src/lib/__tests__/axiosErrorCommand.spec.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { getUbusReproductionCommand } from '@/lib/axiosErrorCommand' + +describe('getUbusReproductionCommand', () => { + it('builds the rpcd command for a ns.* ubus call with a payload', () => { + const command = getUbusReproductionCommand({ + url: 'https://unit.example/api/ubus/call', + data: JSON.stringify({ path: 'ns.dhcp', method: 'get-config', payload: { config: 'dhcp' } }) + }) + + expect(command).toBe(`echo '{"config":"dhcp"}' | /usr/libexec/rpcd/ns.dhcp call get-config`) + }) + + it('omits the echo when the payload is empty', () => { + const command = getUbusReproductionCommand({ + url: 'https://unit.example/api/ubus/call', + data: JSON.stringify({ path: 'ns.dhcp', method: 'get-config', payload: {} }) + }) + + expect(command).toBe('/usr/libexec/rpcd/ns.dhcp call get-config') + }) + + it('returns undefined for a non-ubus request', () => { + const command = getUbusReproductionCommand({ + url: 'https://unit.example/api/login', + data: JSON.stringify({ username: 'root', password: 'secret' }) + }) + + expect(command).toBeUndefined() + }) + + it('returns undefined for a ubus call whose path is not ns.*', () => { + const command = getUbusReproductionCommand({ + url: 'https://unit.example/api/ubus/call', + data: JSON.stringify({ path: 'uci', method: 'get', payload: { config: 'network' } }) + }) + + expect(command).toBeUndefined() + }) + + it('returns undefined when config.data is missing', () => { + const command = getUbusReproductionCommand({ + url: 'https://unit.example/api/ubus/call' + }) + + expect(command).toBeUndefined() + }) +}) diff --git a/src/lib/axiosErrorCommand.ts b/src/lib/axiosErrorCommand.ts new file mode 100644 index 000000000..9c1c976ba --- /dev/null +++ b/src/lib/axiosErrorCommand.ts @@ -0,0 +1,27 @@ +// Copyright (C) 2024 Nethesis S.r.l. +// SPDX-License-Identifier: GPL-3.0-or-later + +import { type AxiosRequestConfig } from 'axios' +import { isEmpty } from 'lodash-es' + +// builds the on-device rpcd command to reproduce a failed ns.* ubus call (e.g. over SSH), +// returns undefined for anything else since the curl equivalent would require the auth token +export function getUbusReproductionCommand(config: AxiosRequestConfig): string | undefined { + if (!config.url?.includes('/ubus/call') || typeof config.data !== 'string') { + return undefined + } + + const { path, method, payload } = JSON.parse(config.data) + + if (!/^ns\..+/.test(path)) { + return undefined + } + + let command = '' + + if (!isEmpty(payload)) { + command += `echo '${JSON.stringify(payload)}' | ` + } + command += `/usr/libexec/rpcd/${path} call ${method}` + return command +} diff --git a/src/stores/notifications.ts b/src/stores/notifications.ts index 4d583f8d5..19a53862c 100644 --- a/src/stores/notifications.ts +++ b/src/stores/notifications.ts @@ -8,8 +8,8 @@ import { type NeNotification } from '@nethesis/vue-components' import { useI18n } from 'vue-i18n' import { useLoginStore as useControllerLoginStore } from '@/stores/controller/controllerLogin' import { useLoginStore as useStandaloneLoginStore } from '@/stores/standalone/standaloneLogin' -import { isEmpty } from 'lodash-es' import { isStandaloneMode } from '@/lib/config' +import { getUbusReproductionCommand } from '@/lib/axiosErrorCommand' const NOTIFICATIONS_LIMIT = 30 const DEFAULT_NOTIFICATION_TIMEOUT = 5000 @@ -163,17 +163,11 @@ export const useNotificationsStore = defineStore('notifications', () => { } const copyUbusApiCommandToClipboard = (notification: NeNotification) => { - const inputData = JSON.parse(notification.payload.config.data) - const ubusPath = inputData.path - const ubusMethod = inputData.method - const inputPayload = inputData.payload - let command = `` - - if (!isEmpty(inputPayload)) { - command += `echo '${JSON.stringify(inputPayload)}' | ` + const command = getUbusReproductionCommand(notification.payload.config) + + if (command) { + navigator.clipboard.writeText(command) } - command += `/usr/libexec/rpcd/${ubusPath} call ${ubusMethod}` - navigator.clipboard.writeText(command) } const showErrorDetails = (notification: NeNotification) => {