diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 0f97c328..9cf14f15 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -22,14 +22,7 @@ export function App() { const device = getDevice(selectedDeviceId); return ( - // - {/* { - setConnectDialogOpen(open); - }} - /> */} diff --git a/apps/web/src/components/Dialog/NewDeviceDialog.tsx b/apps/web/src/components/Dialog/NewDeviceDialog.tsx deleted file mode 100644 index 2d102178..00000000 --- a/apps/web/src/components/Dialog/NewDeviceDialog.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import { BLE } from "@components/PageComponents/Connect/BLE.tsx"; -import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx"; -import { Serial } from "@components/PageComponents/Connect/Serial.tsx"; -import { - Dialog, - DialogClose, - DialogContent, - DialogHeader, - DialogTitle, -} from "@components/UI/Dialog.tsx"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx"; -import { - type BrowserFeature, - useBrowserFeatureDetection, -} from "@core/hooks/useBrowserFeatureDetection.ts"; -import { AlertCircle } from "lucide-react"; -import { Trans, useTranslation } from "react-i18next"; -import { Link } from "../UI/Typography/Link.tsx"; - -export interface TabElementProps { - closeDialog: () => void; -} - -export interface TabManifest { - id: "HTTP" | "BLE" | "Serial"; - label: string; - element: React.FC; - isDisabled: boolean; -} - -export interface NewDeviceProps { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -interface FeatureErrorProps { - missingFeatures: BrowserFeature[]; - tabId: "HTTP" | "BLE" | "Serial"; -} - -const errors: Record = { - "Web Bluetooth": { - href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", - i18nKey: "newDeviceDialog.validation.requiresWebBluetooth", - }, - "Web Serial": { - href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", - i18nKey: "newDeviceDialog.validation.requiresWebSerial", - }, - "Secure Context": { - href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", - i18nKey: "newDeviceDialog.validation.requiresSecureContext", - }, -}; - -const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => { - if (missingFeatures.length === 0) { - return null; - } - - const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context"); - const needsSecureContext = missingFeatures.includes("Secure Context"); - - const needsFeature = - tabId === "BLE" && browserFeatures.includes("Web Bluetooth") - ? "Web Bluetooth" - : tabId === "Serial" && browserFeatures.includes("Web Serial") - ? "Web Serial" - : undefined; - - return ( -
-
- -
-
- {needsFeature && ( - , - ]} - /> - )} - {needsFeature && needsSecureContext && " "} - {needsSecureContext && ( - 0 - ? "newDeviceDialog.validation.additionallyRequiresSecureContext" - : "newDeviceDialog.validation.requiresSecureContext" - } - components={{ - "0": ( - - ), - }} - /> - )} -
-
-
-
- ); -}; - -export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => { - const { t } = useTranslation("dialog"); - const { unsupported } = useBrowserFeatureDetection(); - - const tabs: TabManifest[] = [ - { - id: "HTTP", - label: t("newDeviceDialog.tabHttp"), - element: HTTP, - isDisabled: false, - }, - { - id: "BLE", - label: t("newDeviceDialog.tabBluetooth"), - element: BLE, - isDisabled: unsupported.includes("Web Bluetooth") || unsupported.includes("Secure Context"), - }, - { - id: "Serial", - label: t("newDeviceDialog.tabSerial"), - element: Serial, - isDisabled: unsupported.includes("Web Serial") || unsupported.includes("Secure Context"), - }, - ]; - - return ( - - - - - {t("newDeviceDialog.title")} - - - - {tabs.map((tab) => ( - - {tab.label} - - ))} - - {tabs.map((tab) => ( - -
- {tab.id !== "HTTP" && tab.isDisabled ? ( - - ) : ( - onOpenChange(false)} /> - )} -
-
- ))} -
-
-
- ); -}; diff --git a/apps/web/src/components/Form/createZodResolver.ts b/apps/web/src/components/Form/createZodResolver.ts index dd0448dd..1ecd03ff 100644 --- a/apps/web/src/components/Form/createZodResolver.ts +++ b/apps/web/src/components/Form/createZodResolver.ts @@ -1,5 +1,6 @@ import type { FieldError, + FieldErrors, FieldValues, Resolver, ResolverOptions, @@ -49,8 +50,8 @@ export function createZodResolver( } return { - values: {} as T, - errors, + values: {} as Record, + errors: errors as FieldErrors, }; }; } diff --git a/apps/web/src/components/PageComponents/Map/Layers/HeatmapLayer.tsx b/apps/web/src/components/PageComponents/Map/Layers/HeatmapLayer.tsx index b5e004ac..ce5d0f2c 100644 --- a/apps/web/src/components/PageComponents/Map/Layers/HeatmapLayer.tsx +++ b/apps/web/src/components/PageComponents/Map/Layers/HeatmapLayer.tsx @@ -11,9 +11,11 @@ export interface HeatmapLayerProps { id: string; filteredNodes: Protobuf.Mesh.NodeInfo[]; mode: HeatmapMode; + isVisible: boolean; } -export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) => { +export const HeatmapLayer = ({ id, filteredNodes, mode, isVisible }: HeatmapLayerProps) => { + if (!isVisible) return null; const data: FeatureCollection = useMemo(() => { const features: Feature[] = filteredNodes .filter((node) => hasPos(node.position)) diff --git a/apps/web/src/components/PageComponents/Map/Layers/SNRLayer.tsx b/apps/web/src/components/PageComponents/Map/Layers/SNRLayer.tsx index a8c4969a..3f40d3f6 100644 --- a/apps/web/src/components/PageComponents/Map/Layers/SNRLayer.tsx +++ b/apps/web/src/components/PageComponents/Map/Layers/SNRLayer.tsx @@ -31,7 +31,7 @@ export interface SNRTooltipProps { pos: { x: number; y: number }; snr: number; from: string; - to: string; + to: string | undefined; } type NeighborPlus = Protobuf.Mesh.Neighbor & { diff --git a/apps/web/src/components/generic/Filter/useFilterNode.test.ts b/apps/web/src/components/generic/Filter/useFilterNode.test.ts index 2f6e1f05..8106595c 100644 --- a/apps/web/src/components/generic/Filter/useFilterNode.test.ts +++ b/apps/web/src/components/generic/Filter/useFilterNode.test.ts @@ -13,6 +13,7 @@ function createMockNode(): Protobuf.Mesh.NodeInfo { viaMqtt: false, isFavorite: true, isIgnored: false, + isMuted: false, hopsAway: 2, isKeyManuallyVerified: false, user: { diff --git a/apps/web/src/components/generic/Table/index.test.tsx b/apps/web/src/components/generic/Table/index.test.tsx index 90fa0ea0..d8326dd3 100644 --- a/apps/web/src/components/generic/Table/index.test.tsx +++ b/apps/web/src/components/generic/Table/index.test.tsx @@ -112,17 +112,17 @@ describe("Generic Table", () => { expect(getRenderedOrder()).toEqual(["TST2", "TST4", "TST1", "TST3"]); // Click "Short Name" to sort asc - fireEvent.click(columnHeaders[0]); + fireEvent.click(columnHeaders[0]!); // TST2 is favorite, so it's first. Then TST1, TST3, TST4 alphabetically. expect(getRenderedOrder()).toEqual(["TST2", "TST1", "TST3", "TST4"]); // Click "Short Name" again to sort desc - fireEvent.click(columnHeaders[0]); + fireEvent.click(columnHeaders[0]!); // TST2 is favorite, so it's first. Then TST4, TST3, TST1 reverse alphabetically. expect(getRenderedOrder()).toEqual(["TST2", "TST4", "TST3", "TST1"]); // Click "Connection" to sort by hops asc - fireEvent.click(columnHeaders[2]); + fireEvent.click(columnHeaders[2]!); // TST2 is favorite (and also has 0 hops). Then sorted by hops: TST1 (1), TST4 (3), TST3 (4). expect(getRenderedOrder()).toEqual(["TST2", "TST1", "TST4", "TST3"]); }); diff --git a/apps/web/src/components/generic/Table/index.tsx b/apps/web/src/components/generic/Table/index.tsx index dafc32b8..579d93ab 100755 --- a/apps/web/src/components/generic/Table/index.tsx +++ b/apps/web/src/components/generic/Table/index.tsx @@ -64,6 +64,7 @@ export const Table = ({ headings, rows }: TableProps) => { const aCell = a.cells[columnIndex]; const bCell = b.cells[columnIndex]; + if (!aCell || !bCell) return 0; let aValue: string | number; let bValue: string | number; diff --git a/apps/web/src/core/hooks/useCopyToClipboard.ts b/apps/web/src/core/hooks/useCopyToClipboard.ts index 06a29e60..14a8e63f 100644 --- a/apps/web/src/core/hooks/useCopyToClipboard.ts +++ b/apps/web/src/core/hooks/useCopyToClipboard.ts @@ -6,7 +6,7 @@ interface UseCopyToClipboardProps { export function useCopyToClipboard({ timeout = 2000 }: UseCopyToClipboardProps = {}) { const [isCopied, setIsCopied] = useState(false); - const timeoutRef = useRef(null); + const timeoutRef = useRef | null>(null); useEffect(() => { return () => { diff --git a/apps/web/src/core/stores/deviceStore/deviceStore.mock.ts b/apps/web/src/core/stores/deviceStore/deviceStore.mock.ts index 5edbd4ec..1b3ab42a 100644 --- a/apps/web/src/core/stores/deviceStore/deviceStore.mock.ts +++ b/apps/web/src/core/stores/deviceStore/deviceStore.mock.ts @@ -16,6 +16,8 @@ export const mockDeviceStore: Device = { id: 0, myNodeNum: 123456, status: 5 as const, + connectionPhase: "disconnected", + connectionId: null, channels: new Map(), config: {} as Protobuf.LocalOnly.LocalConfig, moduleConfig: {} as Protobuf.LocalOnly.LocalModuleConfig, @@ -50,6 +52,8 @@ export const mockDeviceStore: Device = { neighborInfo: new Map(), setStatus: vi.fn(), + setConnectionPhase: vi.fn(), + setConnectionId: vi.fn(), setConfig: vi.fn(), setModuleConfig: vi.fn(), getEffectiveConfig: vi.fn(), diff --git a/apps/web/src/core/stores/deviceStore/index.ts b/apps/web/src/core/stores/deviceStore/index.ts index eb5fceb7..28ee7723 100644 --- a/apps/web/src/core/stores/deviceStore/index.ts +++ b/apps/web/src/core/stores/deviceStore/index.ts @@ -2,7 +2,7 @@ import { create, toBinary } from "@bufbuild/protobuf"; import { evictOldestEntries } from "@core/stores/utils/evictOldestEntries.ts"; import { createStorage } from "@core/stores/utils/indexDB.ts"; import { type MeshDevice, Protobuf, Types } from "@meshtastic/sdk"; -import { produce } from "immer"; +import { type Draft, produce } from "immer"; import { create as createStore, type StateCreator } from "zustand"; import { type PersistOptions, persist, subscribeWithSelector } from "zustand/middleware"; import type { @@ -476,7 +476,7 @@ function deviceFactory( produce((draft) => { const device = draft.devices.get(id); if (device) { - device.connection = connection; + device.connection = connection as unknown as Draft; } }), ); @@ -616,7 +616,7 @@ export const deviceStoreInitializer: StateCreator = (set, ge const device = deviceFactory(id, get, set); set( produce((draft) => { - draft.devices = new Map(draft.devices).set(id, device); + draft.devices = new Map(draft.devices).set(id, device as unknown as Draft); // Enforce retention limit evictOldestEntries(draft.devices, DEVICESTORE_RETENTION_NUM); @@ -737,7 +737,7 @@ const persistOptions: PersistOptions = { ); } } - draft.devices = rebuilt; + draft.devices = rebuilt as unknown as Map>; }), ); }, diff --git a/apps/web/src/core/utils/x25519.ts b/apps/web/src/core/utils/x25519.ts index 7e69f339..f3dd30da 100644 --- a/apps/web/src/core/utils/x25519.ts +++ b/apps/web/src/core/utils/x25519.ts @@ -5,9 +5,9 @@ export function getX25519PrivateKey(): Uint8Array { // scalar clamping for curve25519, according to // https://www.rfc-editor.org/rfc/rfc7748#section-5 - key[0] &= 248; - key[31] &= 127; - key[31] |= 64; + key[0]! &= 248; + key[31]! &= 127; + key[31]! |= 64; return key; } diff --git a/apps/web/src/routes.tsx b/apps/web/src/routes.tsx index 542b6df6..f61b656f 100644 --- a/apps/web/src/routes.tsx +++ b/apps/web/src/routes.tsx @@ -146,7 +146,7 @@ const connectionsRoute = createRoute({ component: Connections, }); -const routeTree = rootRoute.addChildren([ +export const routeTree = rootRoute.addChildren([ indexRoute, messagesRoute, messagesWithParamsRoute, diff --git a/apps/web/src/tests/test-utils.tsx b/apps/web/src/tests/test-utils.tsx index 108c43cd..65a27858 100644 --- a/apps/web/src/tests/test-utils.tsx +++ b/apps/web/src/tests/test-utils.tsx @@ -4,7 +4,7 @@ import type { ReactElement } from "react"; import "@app/i18n-config.ts"; import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; -import { routeTree } from "../routeTree.gen.ts"; +import { routeTree } from "../routes.tsx"; const Providers = () => { const memoryHistory = createMemoryHistory({ @@ -14,10 +14,14 @@ const Providers = () => { const router = createRouter({ routeTree, history: memoryHistory, + context: { + stores: {} as never, + i18n: {} as never, + }, }); return ( - + ); diff --git a/apps/web/src/validation/pskSchema.test.ts b/apps/web/src/validation/pskSchema.test.ts index 1bb7a633..1027241d 100644 --- a/apps/web/src/validation/pskSchema.test.ts +++ b/apps/web/src/validation/pskSchema.test.ts @@ -19,7 +19,7 @@ describe("stringSchema", () => { const result = stringSchema().safeParse(invalid); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.length); + expect(result.error.issues[0]!.message).toBe(msgs.length); } }); @@ -28,7 +28,7 @@ describe("stringSchema", () => { const result = stringSchema().safeParse("not_base64!"); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.format); + expect(result.error.issues[0]!.message).toBe(msgs.format); } }); @@ -37,7 +37,7 @@ describe("stringSchema", () => { const result = stringSchema().safeParse(""); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.required); + expect(result.error.issues[0]!.message).toBe(msgs.required); } }); @@ -75,7 +75,7 @@ describe("stringSchema", () => { const result = stringSchema().safeParse(valid); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.format); + expect(result.error.issues[0]!.message).toBe(msgs.format); } }); @@ -91,7 +91,7 @@ describe("stringSchema", () => { const result = stringSchema().safeParse(invalid); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.length); + expect(result.error.issues[0]!.message).toBe(msgs.length); } }); @@ -108,7 +108,7 @@ describe("stringSchema", () => { const result = bytesSchema().safeParse(invalid); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.length); + expect(result.error.issues[0]!.message).toBe(msgs.length); } }); @@ -123,7 +123,7 @@ describe("stringSchema", () => { const result = bytesSchema().safeParse(new Uint8Array(0)); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.required); + expect(result.error.issues[0]!.message).toBe(msgs.required); } }); @@ -167,7 +167,7 @@ describe("stringSchema", () => { const result = bytesSchema().safeParse(invalid); expect(result.success).toBe(false); if (!result.success) { - expect(result.error.issues[0].message).toBe(msgs.length); + expect(result.error.issues[0]!.message).toBe(msgs.length); } }); }); diff --git a/packages/sdk-react/package.json b/packages/sdk-react/package.json index 5d2c5fc6..0beebad2 100644 --- a/packages/sdk-react/package.json +++ b/packages/sdk-react/package.json @@ -56,7 +56,8 @@ "test": "vitest run" }, "dependencies": { - "@meshtastic/sdk": "workspace:*" + "@meshtastic/sdk": "workspace:*", + "better-result": "^1.0.0" }, "peerDependencies": { "react": "^18 || ^19" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd3ffcc0..84676df7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -343,6 +343,9 @@ importers: '@meshtastic/sdk': specifier: workspace:* version: link:../sdk + better-result: + specifier: ^1.0.0 + version: 1.0.1 devDependencies: '@testing-library/react': specifier: ^16.0.0