diff --git a/changelog.d/tsk-br5bqw-hw-panels-live-refresh.md b/changelog.d/tsk-br5bqw-hw-panels-live-refresh.md
new file mode 100644
index 000000000..05263b660
--- /dev/null
+++ b/changelog.d/tsk-br5bqw-hw-panels-live-refresh.md
@@ -0,0 +1,2 @@
+### Changed
+- Wi-Fi and Bluetooth settings panels now auto-refresh every 10 seconds while open, showing live device/AP changes without reopening Settings.
\ No newline at end of file
diff --git a/desktop/src/apps/SettingsApp/BluetoothPanel.test.tsx b/desktop/src/apps/SettingsApp/BluetoothPanel.test.tsx
index c187fe4ae..bddc7b575 100644
--- a/desktop/src/apps/SettingsApp/BluetoothPanel.test.tsx
+++ b/desktop/src/apps/SettingsApp/BluetoothPanel.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from "vitest";
-import { render, screen, waitFor } from "@testing-library/react";
+import { render, screen, waitFor, act } from "@testing-library/react";
import { BluetoothPanel } from "./BluetoothPanel";
function json(obj: unknown, status = 200) {
@@ -61,4 +61,38 @@ describe("BluetoothPanel", () => {
render();
expect(await screen.findByRole("alert")).toBeInTheDocument();
});
+
+ it("refreshes devices every 10s and clears interval on unmount", async () => {
+ vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] });
+ const fetchSpy = vi.spyOn(globalThis, "fetch");
+ fetchSpy
+ .mockResolvedValueOnce(
+ json({
+ available: true,
+ adapters: [{ path: "/org/bluez/hci0", address: "AA:BB:CC:DD:EE:FF", powered: true }],
+ devices: [{ path: "/org/bluez/hci0/dev_11_22_33_44_55_66", address: "11:22:33:44:55:66", name: "FirstDevice", paired: true, connected: true, trusted: true }],
+ })
+ )
+ .mockResolvedValueOnce(
+ json({
+ available: true,
+ adapters: [{ path: "/org/bluez/hci0", address: "AA:BB:CC:DD:EE:FF", powered: true }],
+ devices: [{ path: "/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF", address: "AA:BB:CC:DD:EE:FF", name: "SecondDevice", paired: true, connected: true, trusted: true }],
+ })
+ );
+ const { unmount } = render();
+ await waitFor(() => expect(screen.getByText("FirstDevice")).toBeInTheDocument(), { timeout: 3000 });
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ vi.advanceTimersByTime(10_000);
+ });
+ await waitFor(() => expect(screen.getByText("SecondDevice")).toBeInTheDocument(), { timeout: 3000 });
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ unmount();
+ await act(async () => {
+ vi.advanceTimersByTime(10_000);
+ });
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ vi.useRealTimers();
+ }, 15000);
});
diff --git a/desktop/src/apps/SettingsApp/BluetoothPanel.tsx b/desktop/src/apps/SettingsApp/BluetoothPanel.tsx
index 24f992ba5..fec553cbe 100644
--- a/desktop/src/apps/SettingsApp/BluetoothPanel.tsx
+++ b/desktop/src/apps/SettingsApp/BluetoothPanel.tsx
@@ -7,10 +7,9 @@ export function BluetoothPanel() {
const [devices, setDevices] = useState<{ path: string; address: string; name: string | null; paired: boolean; connected: boolean; trusted: boolean }[]>([]);
const [error, setError] = useState(null);
const mounted = useRef(true);
- useEffect(() => () => { mounted.current = false; }, []);
useEffect(() => {
- (async () => {
+ const load = async () => {
try {
const res = await fetch("/api/hardware/bluetooth", { credentials: "include" });
const body = await res.json().catch(() => null);
@@ -26,10 +25,17 @@ export function BluetoothPanel() {
if (Array.isArray(body.devices)) {
setDevices(body.devices);
}
+ setError(null);
} catch {
if (mounted.current) setError("Could not reach taOS.");
}
- })();
+ };
+ load();
+ const id = setInterval(load, 10_000);
+ return () => {
+ mounted.current = false;
+ clearInterval(id);
+ };
}, []);
return (
diff --git a/desktop/src/apps/SettingsApp/WifiPanel.test.tsx b/desktop/src/apps/SettingsApp/WifiPanel.test.tsx
index 7613fab29..9c910f0ac 100644
--- a/desktop/src/apps/SettingsApp/WifiPanel.test.tsx
+++ b/desktop/src/apps/SettingsApp/WifiPanel.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, afterEach } from "vitest";
-import { render, screen, waitFor } from "@testing-library/react";
+import { render, screen, waitFor, act } from "@testing-library/react";
import { WifiPanel } from "./WifiPanel";
function json(obj: unknown, status = 200) {
@@ -56,4 +56,40 @@ describe("WifiPanel", () => {
expect(await screen.findByRole("alert")).toHaveTextContent("Could not load Wi-Fi state.");
expect(screen.queryByRole("list", { name: "Wi-Fi networks" })).not.toBeInTheDocument();
});
+
+it("refreshes access points every 10s and clears interval on unmount", async () => {
+ vi.useFakeTimers({ toFake: ["setInterval", "clearInterval"] });
+ const fetchSpy = vi.spyOn(globalThis, "fetch");
+ fetchSpy
+ .mockResolvedValueOnce(
+ json({
+ available: true,
+ wifi_enabled: true,
+ devices: [],
+ access_points: [{ ssid: "FirstAP", strength: 50, frequency: 2412, secured: true, active: false }],
+ })
+ )
+ .mockResolvedValueOnce(
+ json({
+ available: true,
+ wifi_enabled: true,
+ devices: [],
+ access_points: [{ ssid: "SecondAP", strength: 75, frequency: 2437, secured: true, active: true }],
+ })
+ );
+ const { unmount } = render();
+ await waitFor(() => expect(screen.getByText("FirstAP")).toBeInTheDocument(), { timeout: 3000 });
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ await act(async () => {
+ vi.advanceTimersByTime(10_000);
+ });
+ await waitFor(() => expect(screen.getByText("SecondAP")).toBeInTheDocument(), { timeout: 3000 });
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ unmount();
+ await act(async () => {
+ vi.advanceTimersByTime(10_000);
+ });
+ expect(fetchSpy).toHaveBeenCalledTimes(2);
+ vi.useRealTimers();
+ }, 15000);
});
\ No newline at end of file
diff --git a/desktop/src/apps/SettingsApp/WifiPanel.tsx b/desktop/src/apps/SettingsApp/WifiPanel.tsx
index 4175057e7..9fc79a8f0 100644
--- a/desktop/src/apps/SettingsApp/WifiPanel.tsx
+++ b/desktop/src/apps/SettingsApp/WifiPanel.tsx
@@ -8,10 +8,9 @@ export function WifiPanel() {
const [accessPoints, setAccessPoints] = useState<{ ssid: string; strength: number; frequency: number; secured: boolean; active: boolean }[]>([]);
const [error, setError] = useState(null);
const mounted = useRef(true);
- useEffect(() => () => { mounted.current = false; }, []);
useEffect(() => {
- (async () => {
+ const load = async () => {
try {
const res = await fetch("/api/hardware/wifi", { credentials: "include" });
const body = await res.json().catch(() => null);
@@ -25,10 +24,17 @@ export function WifiPanel() {
if (body.available && body.wifi_enabled && Array.isArray(body.access_points)) {
setAccessPoints(body.access_points);
}
+ setError(null);
} catch {
if (mounted.current) setError("Could not reach taOS.");
}
- })();
+ };
+ load();
+ const id = setInterval(load, 10_000);
+ return () => {
+ mounted.current = false;
+ clearInterval(id);
+ };
}, []);
return (