From 20318a6ca071ad1534af451fba38df74e7f2dc22 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Thu, 9 Apr 2026 11:38:09 -0500 Subject: [PATCH 1/2] Upgrade to Zustand 5.x --- scripts/build_cgo.sh | 1 + ui/package-lock.json | 21 ++++---- ui/package.json | 2 +- ui/src/hooks/stores.ts | 7 +-- .../devices.$id.settings.general._index.tsx | 10 ++-- .../routes/devices.$id.settings.network.tsx | 52 +++++++++---------- 6 files changed, 46 insertions(+), 47 deletions(-) diff --git a/scripts/build_cgo.sh b/scripts/build_cgo.sh index 868c59e04..0330abd4a 100755 --- a/scripts/build_cgo.sh +++ b/scripts/build_cgo.sh @@ -23,6 +23,7 @@ msg_info "▶ Generating UI index" ./ui_index.gen.sh msg_info "▶ Building native library" +git config --global --add safe.directory "/build/internal/*" VERBOSE=1 cmake -B "${BUILD_DIR}" \ -DCMAKE_SYSTEM_PROCESSOR=armv7l \ -DCMAKE_SYSTEM_NAME=Linux \ diff --git a/ui/package-lock.json b/ui/package-lock.json index 3553c7814..678622a44 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -40,7 +40,7 @@ "tslog": "^4.10.2", "usehooks-ts": "^3.1.1", "validator": "^13.15.23", - "zustand": "^4.5.2" + "zustand": "^5.0.12" }, "devDependencies": { "@inlang/cli": "^3.0.12", @@ -5511,20 +5511,18 @@ } }, "node_modules/zustand": { - "version": "4.5.7", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", - "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.12.tgz", + "integrity": "sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==", "license": "MIT", - "dependencies": { - "use-sync-external-store": "^1.2.2" - }, "engines": { - "node": ">=12.7.0" + "node": ">=12.20.0" }, "peerDependencies": { - "@types/react": ">=16.8", + "@types/react": ">=18.0.0", "immer": ">=9.0.6", - "react": ">=16.8" + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" }, "peerDependenciesMeta": { "@types/react": { @@ -5535,6 +5533,9 @@ }, "react": { "optional": true + }, + "use-sync-external-store": { + "optional": true } } } diff --git a/ui/package.json b/ui/package.json index b2b8506eb..1052c616f 100644 --- a/ui/package.json +++ b/ui/package.json @@ -60,7 +60,7 @@ "tslog": "^4.10.2", "usehooks-ts": "^3.1.1", "validator": "^13.15.23", - "zustand": "^4.5.2" + "zustand": "^5.0.12" }, "devDependencies": { "@inlang/cli": "^3.0.12", diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index 23399f6ec..83dd801ac 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -829,7 +829,9 @@ export interface NetworkState { ipv6_gateway?: string; dhcp_lease?: DhcpLease; hostname?: string; +} +interface NetworkStateStore extends NetworkState { setNetworkState: (state: NetworkState) => void; setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void; setDhcpLeaseExpiry: (expiry: Date) => void; @@ -881,7 +883,7 @@ export interface NetworkSettings { time_sync_http_urls?: string[]; } -export const useNetworkStateStore = create((set, get) => ({ +export const useNetworkStateStore = create((set, get) => ({ setNetworkState: (state: NetworkState) => set(state), setDhcpLease: (lease: NetworkState["dhcp_lease"]) => set({ dhcp_lease: lease }), setDhcpLeaseExpiry: (expiry: Date) => { @@ -891,8 +893,7 @@ export const useNetworkStateStore = create((set, get) => ({ return; } - lease.lease_expiry = expiry; - set({ dhcp_lease: lease }); + set({ dhcp_lease: { ...lease, lease_expiry: expiry } }); }, })); diff --git a/ui/src/routes/devices.$id.settings.general._index.tsx b/ui/src/routes/devices.$id.settings.general._index.tsx index 78d790e67..2a40fdc3c 100644 --- a/ui/src/routes/devices.$id.settings.general._index.tsx +++ b/ui/src/routes/devices.$id.settings.general._index.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo } from "react"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import { useDeviceUiNavigation } from "@hooks/useAppNavigation"; +import { useShallow } from "zustand/shallow"; import { useDeviceStore } from "@hooks/stores"; import { Button } from "@components/Button"; import Checkbox from "@components/Checkbox"; @@ -17,11 +18,10 @@ export default function SettingsGeneralRoute() { const { send } = useJsonRpc(); const { navigateTo } = useDeviceUiNavigation(); const [autoUpdate, setAutoUpdate] = useState(true); - const currentVersions = useDeviceStore(state => { - const { appVersion, systemVersion } = state; - if (!appVersion || !systemVersion) return null; - return { appVersion, systemVersion }; - }); + const { appVersion, systemVersion } = useDeviceStore( + useShallow(state => ({ appVersion: state.appVersion, systemVersion: state.systemVersion })), + ); + const currentVersions = appVersion && systemVersion ? { appVersion, systemVersion } : null; useEffect(() => { send("getAutoUpdateState", {}, (resp: JsonRpcResponse) => { diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index c417ce0c2..30923a926 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -8,6 +8,7 @@ import validator from "validator"; import PublicIPCard from "@components/PublicIPCard"; import TailscaleCard from "@components/TailscaleCard"; import { NetworkSettings, NetworkState, useNetworkStateStore, useRTCStore } from "@hooks/stores"; +import { useShallow } from "zustand/shallow"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import AutoHeight from "@components/AutoHeight"; import { Button } from "@components/Button"; @@ -33,32 +34,13 @@ dayjs.extend(relativeTime); const isLLDPAvailable = false; // LLDP is not supported yet -const resolveOnRtcReady = () => { - return new Promise(resolve => { - // Check if RTC is already connected - const currentState = useRTCStore.getState(); - if (currentState.rpcDataChannel?.readyState === "open") { - // Already connected, fetch data immediately - return resolve(void 0); - } - - // Not connected yet, subscribe to state changes - const unsubscribe = useRTCStore.subscribe(state => { - if (state.rpcDataChannel?.readyState === "open") { - unsubscribe(); // Clean up subscription - return resolve(void 0); - } - }); - }); -}; - export function LifeTimeLabel({ lifetime }: Readonly<{ lifetime: string }>) { const [remaining, setRemaining] = useState(null); - // rrecalculate remaining time every 30 seconds + // recalculate remaining time every 30 seconds useEffect(() => { - // schedule immediate initial update - setInterval(() => setRemaining(dayjs(lifetime).fromNow()), 0); + // immediate initial update + setRemaining(dayjs(lifetime).fromNow()); const interval = setInterval(() => { setRemaining(dayjs(lifetime).fromNow()); @@ -85,7 +67,16 @@ const NonCustomDomainOptions = ["dhcp", "local"]; export default function SettingsNetworkRoute() { const { send } = useJsonRpc(); - const networkState = useNetworkStateStore(state => state); + const networkState = useNetworkStateStore( + useShallow(state => ({ + mac_address: state.mac_address, + hostname: state.hostname, + dhcp_lease: state.dhcp_lease, + ipv6_addresses: state.ipv6_addresses, + ipv6_link_local: state.ipv6_link_local, + ipv6_gateway: state.ipv6_gateway, + })), + ); const setNetworkState = useNetworkStateStore(state => state.setNetworkState); // Some input needs direct state management. Mostly options that open more details @@ -164,8 +155,6 @@ export default function SettingsNetworkRoute() { mode: "onBlur", defaultValues: async () => { - // Ensure data channel is ready, before fetching network data from the device - await resolveOnRtcReady(); const { settings } = await fetchNetworkData(); return settings; }, @@ -215,7 +204,6 @@ export default function SettingsNetworkRoute() { } catch (error) { console.error("Failed to fetch network data:", error); } - notifications.success(m.network_dhcp_lease_renew_success()); } }); }, @@ -270,7 +258,11 @@ export default function SettingsNetworkRoute() { }); } - if (dirty.ipv4_static?.dns && dirty.ipv4_static.dns.length > 0 && dirty.ipv4_static.dns.every(dirty => dirty)) { + if ( + dirty.ipv4_static?.dns && + dirty.ipv4_static.dns.length > 0 && + dirty.ipv4_static.dns.every(dirty => dirty) + ) { changes.push({ label: m.network_ipv4_dns(), from: initialSettingsRef.current?.ipv4_static?.dns.join(", ").toString() ?? "", @@ -302,7 +294,11 @@ export default function SettingsNetworkRoute() { }); } - if (dirty.ipv6_static?.dns && dirty.ipv6_static.dns.length > 0 && dirty.ipv6_static.dns.every(dirty => dirty)) { + if ( + dirty.ipv6_static?.dns && + dirty.ipv6_static.dns.length > 0 && + dirty.ipv6_static.dns.every(dirty => dirty) + ) { changes.push({ label: m.network_ipv6_dns(), from: initialSettingsRef.current?.ipv6_static?.dns.join(", ").toString() ?? "", From 2381c2c682bbdc21f92cf708ed1d471155a3c90e Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 10 Apr 2026 18:51:52 -0500 Subject: [PATCH 2/2] Address CoPilot comments. --- scripts/build_cgo.sh | 2 +- ui/src/routes/devices.$id.settings.network.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build_cgo.sh b/scripts/build_cgo.sh index 0330abd4a..2f14bdcf5 100755 --- a/scripts/build_cgo.sh +++ b/scripts/build_cgo.sh @@ -23,7 +23,7 @@ msg_info "▶ Generating UI index" ./ui_index.gen.sh msg_info "▶ Building native library" -git config --global --add safe.directory "/build/internal/*" +git config --global --add safe.directory "$BUILD_DIR/_deps/lvgl-src" VERBOSE=1 cmake -B "${BUILD_DIR}" \ -DCMAKE_SYSTEM_PROCESSOR=armv7l \ -DCMAKE_SYSTEM_NAME=Linux \ diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 30923a926..9b223478d 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -7,7 +7,7 @@ import validator from "validator"; import PublicIPCard from "@components/PublicIPCard"; import TailscaleCard from "@components/TailscaleCard"; -import { NetworkSettings, NetworkState, useNetworkStateStore, useRTCStore } from "@hooks/stores"; +import { NetworkSettings, NetworkState, useNetworkStateStore } from "@hooks/stores"; import { useShallow } from "zustand/shallow"; import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import AutoHeight from "@components/AutoHeight";