Persistent nodedb (#780)

* Refactor  and consolitdate store imports

- Created a new index file in the core stores directory to export all stores from a single module.
- Updated imports to use consolidated store exports.

* Remove unnecessary import

* Update imports

* First steps to persist nodeDB

* Use named exports

* Change store import after merge

* Persistent nodeDB initial work

* Key mishmatch warning, new serialization handler

* Minor copilot changes

* Add NODEDB_RETENTION_NUM

* Updated tests

* Refactor PKI mismatch logic

* Clear persisted db on reset

* Only persist on featureFlag

* Mock featureFlag in tests

---------

Co-authored-by: philon- <philon-@users.noreply.github.com>
This commit is contained in:
Jeremy Gallant
2025-08-24 09:23:20 -04:00
committed by GitHub
co-authored by philon-
parent e33463b371
commit 68ec7ee5d8
43 changed files with 1335 additions and 476 deletions
@@ -8,7 +8,12 @@ import {
CommandList,
} from "@components/UI/Command.tsx";
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
import { useAppStore, useDevice, useDeviceStore } from "@core/stores";
import {
useAppStore,
useDevice,
useDeviceStore,
useNodeDB,
} from "@core/stores";
import { cn } from "@core/utils/cn.ts";
import { useNavigate } from "@tanstack/react-router";
import { useCommandState } from "cmdk";
@@ -65,7 +70,8 @@ export const CommandPalette = () => {
setSelectedDevice,
} = useAppStore();
const { getDevices } = useDeviceStore();
const { setDialogOpen, getNode, connection } = useDevice();
const { setDialogOpen, connection } = useDevice();
const { getNode, removeAllNodeErrors, removeAllNodes } = useNodeDB();
const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups",
});
@@ -201,6 +207,8 @@ export const CommandPalette = () => {
icon: TrashIcon,
action() {
connection?.resetNodes();
removeAllNodeErrors();
removeAllNodes(true);
},
},
{
@@ -217,6 +225,8 @@ export const CommandPalette = () => {
icon: FactoryIcon,
action() {
connection?.factoryResetDevice();
removeAllNodeErrors();
removeAllNodes();
},
},
{
@@ -10,7 +10,7 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useDevice } from "@core/stores";
import { useDevice, useNodeDB } from "@core/stores";
import { zodResolver } from "@hookform/resolvers/zod";
import { Protobuf } from "@meshtastic/core";
import { useForm } from "react-hook-form";
@@ -33,7 +33,8 @@ export const DeviceNameDialog = ({
onOpenChange,
}: DeviceNameDialogProps) => {
const { t } = useTranslation("dialog");
const { hardware, getNode, connection } = useDevice();
const { hardware, connection } = useDevice();
const { getNode } = useNodeDB();
const myNode = getNode(hardware.myNodeNum);
const defaultValues = {
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores";
import { useNodeDB } from "@core/stores";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next";
@@ -23,7 +23,7 @@ export const LocationResponseDialog = ({
onOpenChange,
}: LocationResponseDialogProps) => {
const { t } = useTranslation("dialog");
const { getNode } = useDevice();
const { getNode } = useNodeDB();
const from = getNode(location?.from ?? 0);
const longName =
@@ -27,7 +27,7 @@ import {
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
import { toast } from "@core/hooks/useToast.ts";
import { useAppStore, useDevice } from "@core/stores";
import { useAppStore, useDevice, useNodeDB } from "@core/stores";
import { cn } from "@core/utils/cn.ts";
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
@@ -55,7 +55,8 @@ export const NodeDetailsDialog = ({
onOpenChange,
}: NodeDetailsDialogProps) => {
const { t } = useTranslation("dialog");
const { setDialogOpen, connection, getNode } = useDevice();
const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB();
const navigate = useNavigate();
const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore();
const { updateFavorite } = useFavoriteNode();
@@ -1,3 +1,4 @@
import { Button } from "@components/UI/Button.tsx";
import {
Dialog,
DialogClose,
@@ -7,12 +8,11 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useDevice, useNodeDB } from "@core/stores";
import { fromByteArray } from "base64-js";
import { DownloadIcon, PrinterIcon } from "lucide-react";
import React from "react";
import { useTranslation } from "react-i18next";
import { useDevice } from "../../core/stores";
import { Button } from "../UI/Button.tsx";
export interface PkiBackupDialogProps {
open: boolean;
@@ -24,7 +24,8 @@ export const PkiBackupDialog = ({
onOpenChange,
}: PkiBackupDialogProps) => {
const { t } = useTranslation("dialog");
const { config, setDialogOpen, getMyNode } = useDevice();
const { config, setDialogOpen } = useDevice();
const { getMyNode } = useNodeDB();
const privateKey = config.security?.privateKey;
const publicKey = config.security?.publicKey;
@@ -1,4 +1,4 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { fireEvent, render, screen, act } from "@testing-library/react";
import type {
ButtonHTMLAttributes,
ClassAttributes,
@@ -89,7 +89,9 @@ describe("RebootDialog", () => {
render(<RebootDialog open onOpenChange={() => {}} />);
// Schedule non-OTA reboot
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
act(() => {
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
});
expect(rebootMock).toHaveBeenCalledWith(5);
expect(rebootOtaMock).not.toHaveBeenCalled();
@@ -97,7 +99,9 @@ describe("RebootDialog", () => {
rebootOtaMock.mockClear();
// Cancel scheduled
fireEvent.click(screen.getByTestId("cancelRebootBtn"));
act(() => {
fireEvent.click(screen.getByTestId("cancelRebootBtn"));
});
expect(rebootMock).toHaveBeenCalledWith(-1);
expect(rebootOtaMock).not.toHaveBeenCalled();
@@ -105,8 +109,12 @@ describe("RebootDialog", () => {
rebootOtaMock.mockClear();
// Schedule OTA reboot
fireEvent.click(screen.getByText(/reboot into ota mode/i));
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
act(() => {
fireEvent.click(screen.getByText(/reboot into ota mode/i));
});
act(() => {
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
});
expect(rebootOtaMock).toHaveBeenCalledWith(5);
expect(rebootMock).not.toHaveBeenCalled();
});
@@ -115,17 +123,23 @@ describe("RebootDialog", () => {
const onOpenChangeMock = vi.fn();
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
target: { value: "3" },
act(() => {
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
target: { value: "3" },
});
});
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
act(() => {
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
});
expect(rebootMock).toHaveBeenCalledWith(3);
expect(screen.getByText(/reboot has been scheduled/i)).toBeInTheDocument();
vi.advanceTimersByTime(3000);
act(() => {
vi.advanceTimersByTime(3000);
});
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
@@ -135,7 +149,9 @@ describe("RebootDialog", () => {
const onOpenChangeMock = vi.fn();
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
fireEvent.click(screen.getByRole("button", { name: /reboot now/i }));
act(() => {
fireEvent.click(screen.getByRole("button", { name: /reboot now/i }));
});
expect(rebootMock).toHaveBeenCalledWith(0);
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
@@ -148,9 +164,13 @@ describe("RebootDialog", () => {
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
act(() => {
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
});
vi.advanceTimersByTime(5000);
act(() => {
vi.advanceTimersByTime(5000);
});
expect(rebootMock).not.toHaveBeenCalled();
expect(rebootOtaMock).not.toHaveBeenCalled();
@@ -162,13 +182,19 @@ describe("RebootDialog", () => {
const onOpenChangeMock = vi.fn();
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
target: { value: "4" },
act(() => {
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
target: { value: "4" },
});
});
act(() => {
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
});
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
expect(rebootMock).toHaveBeenCalledWith(4);
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
act(() => {
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
});
expect(rebootMock).toHaveBeenCalledWith(-1);
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument();
});
@@ -1,4 +1,4 @@
import { DeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
import { CurrentDeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
import { render } from "@testing-library/react";
import { afterEach, beforeEach, expect, test, vi } from "vitest";
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
@@ -37,11 +37,6 @@ test("does not render dialog if no error exists for active chat", () => {
useDeviceStore.getState().addDevice(deviceId);
const currentDeviceState = useDeviceStore.getState().getDevice(deviceId);
if (!currentDeviceState) {
throw new Error("Device not found");
}
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
mockUseRefreshKeysDialog.mockReturnValue({
handleCloseDialog: vi.fn(),
@@ -49,9 +44,9 @@ test("does not render dialog if no error exists for active chat", () => {
});
const { container } = render(
<DeviceContext.Provider value={currentDeviceState}>
<CurrentDeviceContext.Provider value={{ deviceId }}>
<RefreshKeysDialog open onOpenChange={vi.fn()} />
</DeviceContext.Provider>,
</CurrentDeviceContext.Provider>,
);
expect(container.firstChild).toBeNull();
@@ -6,7 +6,7 @@ import {
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { useDevice, useMessageStore } from "@core/stores";
import { useMessageStore, useNodeDB } from "@core/stores";
import { LockKeyholeOpenIcon } from "lucide-react";
import { useTranslation } from "react-i18next";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
@@ -22,7 +22,8 @@ export const RefreshKeysDialog = ({
}: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog");
const { activeChat } = useMessageStore();
const { nodeErrors, getNode } = useDevice();
const { nodeErrors, getNode } = useNodeDB();
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
const nodeErrorNum = nodeErrors.get(activeChat);
@@ -1,9 +1,9 @@
import { useDevice, useMessageStore } from "@core/stores";
import { useDevice, useMessageStore, useNodeDB } from "@core/stores";
import { useCallback } from "react";
export function useRefreshKeysDialog() {
const { removeNode, setDialogOpen, clearNodeError, getNodeError } =
useDevice();
const { setDialogOpen } = useDevice();
const { removeNode, clearNodeError, getNodeError } = useNodeDB();
const { activeChat } = useMessageStore();
const handleCloseDialog = useCallback(() => {
@@ -9,7 +9,7 @@ import {
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Label } from "@components/UI/Label.tsx";
import { useAppStore, useDevice } from "@core/stores";
import { useAppStore, useDevice, useNodeDB } from "@core/stores";
import { useTranslation } from "react-i18next";
export interface RemoveNodeDialogProps {
@@ -22,7 +22,8 @@ export const RemoveNodeDialog = ({
onOpenChange,
}: RemoveNodeDialogProps) => {
const { t } = useTranslation("dialog");
const { connection, getNode, removeNode } = useDevice();
const { connection } = useDevice();
const { getNode, removeNode } = useNodeDB();
const { nodeNumToBeRemoved } = useAppStore();
const onSubmit = () => {
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores";
import { useNodeDB } from "@core/stores";
import type { Protobuf, Types } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next";
@@ -25,7 +25,7 @@ export const TracerouteResponseDialog = ({
onOpenChange,
}: TracerouteResponseDialogProps) => {
const { t } = useTranslation("dialog");
const { getNode } = useDevice();
const { getNode } = useNodeDB();
const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
@@ -36,7 +36,10 @@ const mockDevice = {
setDialogOpen: vi.fn(),
};
vi.mock("@core/stores/deviceStore", () => ({
vi.mock("@core/stores", () => ({
CurrentDeviceContext: {
_currentValue: { deviceId: 123 },
},
useDevice: () => ({
setDialogOpen: mockDevice.setDialogOpen,
}),
@@ -85,7 +88,7 @@ describe("useUnsafeRolesDialog", () => {
const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[0],
UNSAFE_ROLES[0]!,
);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
@@ -97,7 +100,7 @@ describe("useUnsafeRolesDialog", () => {
expect.any(Function),
);
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" });
const validationResult = await validationPromise;
@@ -111,9 +114,9 @@ describe("useUnsafeRolesDialog", () => {
it("should resolve with false when user dismisses the dialog", async () => {
const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[0],
UNSAFE_ROLES[0]!,
);
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "dismiss" });
const validationResult = await validationPromise;
@@ -128,9 +131,9 @@ describe("useUnsafeRolesDialog", () => {
const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[1],
UNSAFE_ROLES[1]!,
);
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" });
await validationPromise;
@@ -157,7 +160,7 @@ describe("useUnsafeRolesDialog", () => {
true,
);
const onHandler = (eventBus.on as Mock).mock.calls[0][1];
const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" });
const validationResult = await validationPromise;
@@ -1,6 +1,11 @@
import { Mono } from "@components/generic/Mono.tsx";
import { Button } from "@components/UI/Button.tsx";
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
import {
useAppStore,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core";
@@ -12,7 +17,9 @@ import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
export const BLE = ({ closeDialog }: TabElementProps) => {
const [connectionInProgress, setConnectionInProgress] = useState(false);
const [bleDevices, setBleDevices] = useState<BluetoothDevice[]>([]);
const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore();
const { t } = useTranslation();
@@ -29,11 +36,13 @@ export const BLE = ({ closeDialog }: TabElementProps) => {
const id = randId();
const transport = await TransportWebBluetooth.createFromDevice(bleDevice);
const device = addDevice(id);
const nodeDB = addNodeDB(id);
const connection = new MeshDevice(transport, id);
connection.configure();
setSelectedDevice(id);
device.addConnection(connection);
subscribeAll(device, connection, messageStore);
subscribeAll(device, connection, messageStore, nodeDB);
const HEARTBEAT_INTERVAL = 5 * 60 * 1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);
@@ -9,7 +9,10 @@ vi.mock("@core/stores", () => ({
useDeviceStore: vi.fn(() => ({
addDevice: vi.fn(() => ({ addConnection: vi.fn() })),
})),
useMessageStore: vi.fn()
useMessageStore: vi.fn(),
useNodeDBStore: vi.fn(() => ({
addNodeDB: vi.fn(),
})),
}));
vi.mock("@core/utils/randId.ts", () => ({
@@ -4,7 +4,12 @@ import { Input } from "@components/UI/Input.tsx";
import { Label } from "@components/UI/Label.tsx";
import { Switch } from "@components/UI/Switch.tsx";
import { Link } from "@components/UI/Typography/Link.tsx";
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
import {
useAppStore,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core";
@@ -25,6 +30,7 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const isURLHTTPS = location.protocol === "https:";
const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore();
@@ -56,11 +62,13 @@ export const HTTP = ({ closeDialog }: TabElementProps) => {
const id = randId();
const transport = await TransportHTTP.create(data.ip, data.tls);
const device = addDevice(id);
const nodeDB = addNodeDB(id);
const connection = new MeshDevice(transport, id);
connection.configure();
setSelectedDevice(id);
device.addConnection(connection);
subscribeAll(device, connection, messageStore);
subscribeAll(device, connection, messageStore, nodeDB);
closeDialog();
} catch (error) {
if (error instanceof Error) {
@@ -1,6 +1,11 @@
import { Mono } from "@components/generic/Mono.tsx";
import { Button } from "@components/UI/Button.tsx";
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
import {
useAppStore,
useDeviceStore,
useMessageStore,
useNodeDBStore,
} from "@core/stores";
import { subscribeAll } from "@core/subscriptions.ts";
import { randId } from "@core/utils/randId.ts";
import { MeshDevice } from "@meshtastic/core";
@@ -12,7 +17,9 @@ import type { TabElementProps } from "../../Dialog/NewDeviceDialog.tsx";
export const Serial = ({ closeDialog }: TabElementProps) => {
const [connectionInProgress, setConnectionInProgress] = useState(false);
const [serialPorts, setSerialPorts] = useState<SerialPort[]>([]);
const { addDevice } = useDeviceStore();
const { addNodeDB } = useNodeDBStore();
const messageStore = useMessageStore();
const { setSelectedDevice } = useAppStore();
const { t } = useTranslation();
@@ -34,12 +41,14 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
const onConnect = async (port: SerialPort) => {
const id = randId();
const device = addDevice(id);
const nodeDB = addNodeDB(id);
setSelectedDevice(id);
const transport = await TransportWebSerial.createFromPort(port);
const connection = new MeshDevice(transport, id);
connection.configure();
device.addConnection(connection);
subscribeAll(device, connection, messageStore);
subscribeAll(device, connection, messageStore, nodeDB);
const HEARTBEAT_INTERVAL = 5 * 60 * 1000;
connection.setHeartbeatInterval(HEARTBEAT_INTERVAL);
@@ -6,7 +6,12 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@components/UI/Tooltip.tsx";
import { MessageState, useDevice, useMessageStore } from "@core/stores";
import {
MessageState,
useDevice,
useMessageStore,
useNodeDB,
} from "@core/stores";
import type { Message } from "@core/stores/messageStore/types.ts";
import { cn } from "@core/utils/cn.ts";
import { type Protobuf, Types } from "@meshtastic/core";
@@ -47,7 +52,8 @@ interface MessageItemProps {
}
export const MessageItem = ({ message }: MessageItemProps) => {
const { config, getNode } = useDevice();
const { config } = useDevice();
const { getNode } = useNodeDB();
const { getMyNodeNum } = useMessageStore();
const { t, i18n } = useTranslation("messages");
@@ -1,6 +1,6 @@
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
import { mockDeviceStore } from "@core/stores/deviceStore/deviceStore.mock.ts";
import { useDevice } from "@core/stores";
import { mockNodeDBStore } from "@core/stores/nodeDBStore/nodeDBStore.mock.ts";
import { useNodeDB } from "@core/stores";
import { Protobuf } from "@meshtastic/core";
import { render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -64,9 +64,10 @@ describe("TraceRoute", () => {
]);
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(useDevice).mockReturnValue({
...mockDeviceStore,
vi.mocked(useNodeDB).mockReturnValue({
...mockNodeDBStore,
getNode: (nodeNum: number): Protobuf.Mesh.NodeInfo | undefined => {
return mockNodes.get(nodeNum);
},
@@ -1,4 +1,4 @@
import { useDevice } from "@core/stores";
import { useNodeDB } from "@core/stores";
import type { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useTranslation } from "react-i18next";
@@ -23,7 +23,7 @@ interface RoutePathProps {
}
const RoutePath = ({ title, from, to, path, snr }: RoutePathProps) => {
const { getNode } = useDevice();
const { getNode } = useNodeDB();
const { t } = useTranslation();
return (
+9 -9
View File
@@ -2,7 +2,13 @@ import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
import { Spinner } from "@components/UI/Spinner.tsx";
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
import { type Page, useAppStore, useDevice, useSidebar } from "@core/stores";
import {
type Page,
useAppStore,
useDevice,
useNodeDB,
useSidebar,
} from "@core/stores";
import { cn } from "@core/utils/cn.ts";
import { useLocation, useNavigate } from "@tanstack/react-router";
import {
@@ -62,14 +68,8 @@ const CollapseToggleButton = () => {
};
export const Sidebar = ({ children }: SidebarProps) => {
const {
hardware,
getNode,
getNodesLength,
metadata,
unreadCounts,
setDialogOpen,
} = useDevice();
const { hardware, metadata, unreadCounts, setDialogOpen } = useDevice();
const { getNode, getNodesLength } = useNodeDB();
const { setCommandPaletteOpen } = useAppStore();
const myNode = getNode(hardware.myNodeNum);
const { isCollapsed } = useSidebar();