From da7d16f62e8ef6a16c5918cd52d732b38d809959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Ant=C3=B4nio=20Cardoso?= Date: Sat, 25 Jul 2026 11:29:18 -0300 Subject: [PATCH] frontend: store: Gate network and platform polling by FetchType Refcount SystemNetworkType and PlatformType like other system-info polls, subscribe from Networking widgets / Network tab / HealthTray, and keep a stable iface-name list so App tray widgets do not rebuild on every speed update. --- core/frontend/src/App.vue | 19 ++--- .../components/common/DevicePathHelper.vue | 8 +- .../src/components/health/HealthTrayMenu.vue | 6 +- .../components/system-information/Network.vue | 18 +++-- .../vehiclesetup/overview/VehicleInfo.vue | 8 +- core/frontend/src/store/system-information.ts | 77 +++++++++++++------ core/frontend/src/widgets/Networking.vue | 8 +- 7 files changed, 96 insertions(+), 48 deletions(-) diff --git a/core/frontend/src/App.vue b/core/frontend/src/App.vue index 43c3434cc4..7d22cf239c 100644 --- a/core/frontend/src/App.vue +++ b/core/frontend/src/App.vue @@ -52,8 +52,8 @@ !['docker', 'lo', 'veth'].some((prefix) => iface.name.startsWith(prefix)), + // Use the stable name list so speed polls do not rebuild this computed every 2s + const extra_interfaces = system_information.network_interface_names.filter( + (name) => !['docker', 'lo', 'veth'].some((prefix) => name.startsWith(prefix)), ) - for (const iface of extra_interfaces) { + for (const name of extra_interfaces) { widgets.push({ // eslint-disable-next-line @typescript-eslint/no-explicit-any component: Networking as any, props: { - interface: iface.name, + interface: name, }, - name: `${iface.name} Networking`, + name: `${name} Networking`, }) } return widgets diff --git a/core/frontend/src/components/common/DevicePathHelper.vue b/core/frontend/src/components/common/DevicePathHelper.vue index ec5c02e130..2cbf2bcd67 100644 --- a/core/frontend/src/components/common/DevicePathHelper.vue +++ b/core/frontend/src/components/common/DevicePathHelper.vue @@ -39,9 +39,11 @@ import navigator_image from '@/assets/img/devicePathHelper/navigator.svg' import raspberry_pi3_image from '@/assets/img/devicePathHelper/rpi3b.svg' import raspberry_pi4_image from '@/assets/img/devicePathHelper/rpi4b.svg' import raspberry_pi5_image from '@/assets/img/devicePathHelper/rpi5.svg' -import system_information from '@/store/system-information' +import system_information, { FetchType } from '@/store/system-information' import { Dictionary } from '@/types/common' +const FETCH_TYPES = [FetchType.PlatformType] + enum BoardType { Rpi4B = 'Rpi4B', Rpi3B = 'Rpi3B', @@ -180,6 +182,7 @@ export default Vue.extend({ }, }, mounted() { + system_information.subscribeSystemInformation(FETCH_TYPES) // Wait for svg element to be loaded to set object let id = 0 const name = `.${this.svgName}${this.inline ? '-inline' : ''}` @@ -199,6 +202,9 @@ export default Vue.extend({ } }, 500) }, + beforeDestroy() { + system_information.unsubscribeSystemInformation(FETCH_TYPES) + }, methods: { updateImgObjectFromElement(element: HTMLEmbedElement) { this.imgObject = element?.getSVGDocument() diff --git a/core/frontend/src/components/health/HealthTrayMenu.vue b/core/frontend/src/components/health/HealthTrayMenu.vue index a2fe4f330b..afd6356ace 100644 --- a/core/frontend/src/components/health/HealthTrayMenu.vue +++ b/core/frontend/src/components/health/HealthTrayMenu.vue @@ -145,7 +145,11 @@ import { RaspberryEventType } from '@/types/system-information/platform' import { Disk } from '@/types/system-information/system' import mavlink_store_get from '@/utils/mavlink' -const FETCH_TYPES = [FetchType.SystemTemperatureType, FetchType.SystemDiskType] +const FETCH_TYPES = [ + FetchType.SystemTemperatureType, + FetchType.SystemDiskType, + FetchType.PlatformType, +] export default Vue.extend({ name: 'HealthTrayMenu', diff --git a/core/frontend/src/components/system-information/Network.vue b/core/frontend/src/components/system-information/Network.vue index d6ef8f0017..b80322f978 100644 --- a/core/frontend/src/components/system-information/Network.vue +++ b/core/frontend/src/components/system-information/Network.vue @@ -24,26 +24,28 @@ import Vue from 'vue' import NetworkCard from '@/components/system-information/NetworkCard.vue' -import system_information from '@/store/system-information' +import system_information, { FetchType } from '@/store/system-information' import { Network } from '@/types/system-information/system' +const FETCH_TYPES = [FetchType.SystemNetworkType] + export default Vue.extend({ name: 'Network', components: { NetworkCard, }, - data() { - return { - timer: 0, - } - }, computed: { networks(): Network[] { - return system_information.system?.network.sort((first, second) => first.name.localeCompare(second.name)) ?? [] + // Copy before sort — do not mutate the Vuex network array in place. + const networks = system_information.system?.network ?? [] + return [...networks].sort((first, second) => first.name.localeCompare(second.name)) }, }, + mounted() { + system_information.subscribeSystemInformation(FETCH_TYPES) + }, beforeDestroy() { - clearInterval(this.timer) + system_information.unsubscribeSystemInformation(FETCH_TYPES) }, }) diff --git a/core/frontend/src/components/vehiclesetup/overview/VehicleInfo.vue b/core/frontend/src/components/vehiclesetup/overview/VehicleInfo.vue index fd7bd2e5c8..f988f5dd30 100755 --- a/core/frontend/src/components/vehiclesetup/overview/VehicleInfo.vue +++ b/core/frontend/src/components/vehiclesetup/overview/VehicleInfo.vue @@ -35,7 +35,9 @@ import { fetchCurrentBoard, fetchFirmwareInfo } from '@/components/autopilot/Aut import { OneMoreTime } from '@/one-more-time' import autopilot_data from '@/store/autopilot' import autopilot from '@/store/autopilot_manager' -import system_information from '@/store/system-information' +import system_information, { FetchType } from '@/store/system-information' + +const FETCH_TYPES = [FetchType.PlatformType] export default Vue.extend({ name: 'VehicleInfo', @@ -64,8 +66,12 @@ export default Vue.extend({ }, }, mounted() { + system_information.subscribeSystemInformation(FETCH_TYPES) this.fetch_firmware_info_task.setAction(fetchCurrentBoard) this.fetch_current_board_task.setAction(fetchFirmwareInfo) }, + beforeDestroy() { + system_information.unsubscribeSystemInformation(FETCH_TYPES) + }, }) diff --git a/core/frontend/src/store/system-information.ts b/core/frontend/src/store/system-information.ts index 12b4524a22..36e88521ac 100644 --- a/core/frontend/src/store/system-information.ts +++ b/core/frontend/src/store/system-information.ts @@ -43,6 +43,36 @@ const system_information_subscribers: Partial> = { [FetchType.SystemMemoryType]: 0, [FetchType.SystemDiskType]: 0, [FetchType.SystemTemperatureType]: 0, + [FetchType.SystemNetworkType]: 0, + [FetchType.PlatformType]: 0, +} + +function subscribedSystemFetchTypes(): FetchType[] { + return (Object.keys(system_information_subscribers) as FetchType[]) + .filter((type) => type !== FetchType.PlatformType && (system_information_subscribers[type] ?? 0) > 0) +} + +function hasSystemSubscribers(): boolean { + return subscribedSystemFetchTypes().length > 0 +} + +/** Return a new name list only when the set of interface names changed (keeps App tray stable). */ +function nextNetworkInterfaceNames(previous: string[], networks: Network[] | undefined): string[] | undefined { + const names = networks?.map(({ name }) => name) ?? [] + const same_set = previous.length === names.length + && previous.every((name) => names.includes(name)) + && names.every((name) => previous.includes(name)) + return same_set ? undefined : names +} + +function resumeOrStart(task: OneMoreTime): void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const state = task as any + if (state.isPaused) { + task.resume() + } else if (!state.isRunning && !state.timeoutId) { + task.start() + } } @Module({ @@ -67,12 +97,11 @@ class SystemInformationStore extends VuexModule { serial: Serial | null = null - fetchPlatformTask = new OneMoreTime( - { delay: 5000 }, - ) + // Stable interface name list for App tray widgets — updated only when the set of names changes. + network_interface_names: string[] = [] - fetchSystemNetworkTask = new OneMoreTime( - { delay: 2000 }, + fetchPlatformTask = new OneMoreTime( + { delay: 5000, autostart: false }, ) fetchSubscribedSystemInformationTask = new OneMoreTime( @@ -113,6 +142,10 @@ class SystemInformationStore extends VuexModule { @Mutation updateSystem(system: System): void { this.system = system + const names = nextNetworkInterfaceNames(this.network_interface_names, system.network) + if (names) { + this.network_interface_names = names + } } @Mutation @@ -158,6 +191,10 @@ class SystemInformationStore extends VuexModule { } } this.system.network = networks + const names = nextNetworkInterfaceNames(this.network_interface_names, networks) + if (names) { + this.network_interface_names = names + } } } @@ -208,16 +245,9 @@ class SystemInformationStore extends VuexModule { await this.fetchSystemInformation(FetchType.SystemType) } - @Action - async fetchNetworkInformation(): Promise { - await this.fetchSystemInformation(FetchType.SystemNetworkType) - } - @Action async fetchSubscribedSystemInformation(): Promise { - const fetches = (Object.keys(system_information_subscribers) as FetchType[]) - .filter((type) => (system_information_subscribers[type] ?? 0) > 0) - .map((type) => this.fetchSystemInformation(type)) + const fetches = subscribedSystemFetchTypes().map((type) => this.fetchSystemInformation(type)) if (fetches.length === 0) { return } @@ -232,12 +262,12 @@ class SystemInformationStore extends VuexModule { } system_information_subscribers[type]! += 1 }) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const task = this.fetchSubscribedSystemInformationTask as any - if (task.isPaused) { - this.fetchSubscribedSystemInformationTask.resume() - } else if (!task.isRunning && !task.timeoutId) { - this.fetchSubscribedSystemInformationTask.start() + + if (hasSystemSubscribers()) { + resumeOrStart(this.fetchSubscribedSystemInformationTask) + } + if ((system_information_subscribers[FetchType.PlatformType] ?? 0) > 0) { + resumeOrStart(this.fetchPlatformTask) } } @@ -249,11 +279,13 @@ class SystemInformationStore extends VuexModule { } system_information_subscribers[type] = Math.max(0, system_information_subscribers[type]! - 1) }) - const has_subscribers = (Object.keys(system_information_subscribers) as FetchType[]) - .some((type) => (system_information_subscribers[type] ?? 0) > 0) - if (!has_subscribers) { + + if (!hasSystemSubscribers()) { this.fetchSubscribedSystemInformationTask.stop() } + if ((system_information_subscribers[FetchType.PlatformType] ?? 0) === 0) { + this.fetchPlatformTask.stop() + } } @Action @@ -343,7 +375,6 @@ const system_information: SystemInformationStore = getModule(SystemInformationSt system_information.fetchSystem() system_information.fetchPlatformTask.setAction(system_information.fetchPlatform) -system_information.fetchSystemNetworkTask.setAction(system_information.fetchNetworkInformation) system_information.fetchSubscribedSystemInformationTask.setAction(system_information.fetchSubscribedSystemInformation) // Vuex store modules are a poor fit for WebSocket callbacks; keep sockets outside the store and diff --git a/core/frontend/src/widgets/Networking.vue b/core/frontend/src/widgets/Networking.vue index 590a16b978..38a4c6c5b1 100644 --- a/core/frontend/src/widgets/Networking.vue +++ b/core/frontend/src/widgets/Networking.vue @@ -29,9 +29,11 @@ import axios from 'axios' import Vue from 'vue' -import system_information from '@/store/system-information' +import system_information, { FetchType } from '@/store/system-information' import { formatBandwidth } from '@/utils/networking' +const FETCH_TYPES = [FetchType.SystemNetworkType] + export default Vue.extend({ name: 'ETh0Widget', props: { @@ -42,7 +44,6 @@ export default Vue.extend({ }, data() { return { - timer: 0, image: '', arrowLeft: '', arrowRight: '', @@ -59,10 +60,11 @@ export default Vue.extend({ }, }, async mounted() { + system_information.subscribeSystemInformation(FETCH_TYPES) this.loadImages() }, beforeDestroy() { - clearInterval(this.timer) + system_information.unsubscribeSystemInformation(FETCH_TYPES) }, methods: { formatBandwidth(bytesPerSecond: number): string {