Browse Source

Merge pull request #213 from bmv437/feature/enforce-biome

Update biome, fix and enforce recommended rules
pull/255/head
Hunter Thornsberry 2 years ago
committed by GitHub
parent
commit
0dddf6ad2f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      .vscode/settings.json
  2. 4
      biome.json
  3. 5
      package.json
  4. 2
      pnpm-lock.yaml
  5. 4
      postcss.config.cjs
  6. 4
      src/components/CommandPalette.tsx
  7. 2
      src/components/DeviceSelector.tsx
  8. 2
      src/components/Dialog/DialogManager.tsx
  9. 36
      src/components/Dialog/QRDialog.tsx
  10. 2
      src/components/Dialog/RebootDialog.tsx
  11. 4
      src/components/Dialog/RemoveNodeDialog.tsx
  12. 2
      src/components/Dialog/ShutdownDialog.tsx
  13. 21
      src/components/Form/DynamicForm.tsx
  14. 15
      src/components/Form/DynamicFormField.tsx
  15. 2
      src/components/Form/FormInput.tsx
  16. 4
      src/components/Form/FormSelect.tsx
  17. 4
      src/components/Form/FormToggle.tsx
  18. 52
      src/components/PageComponents/Channel.tsx
  19. 2
      src/components/PageComponents/Config/LoRa.tsx
  20. 2
      src/components/PageComponents/Connect/BLE.tsx
  21. 14
      src/components/PageComponents/Connect/HTTP.tsx
  22. 31
      src/components/PageComponents/Connect/Serial.tsx
  23. 5
      src/components/PageComponents/Messages/ChannelChat.tsx
  24. 6
      src/components/PageComponents/ModuleConfig/DetectionSensor.tsx
  25. 32
      src/components/PageComponents/ModuleConfig/MQTT.tsx
  26. 2
      src/components/PageComponents/ModuleConfig/NeighborInfo.tsx
  27. 3
      src/components/PageComponents/ModuleConfig/Paxcounter.tsx
  28. 2
      src/components/PageLayout.tsx
  29. 21
      src/components/Sidebar.tsx
  30. 10
      src/components/Toaster.tsx
  31. 2
      src/components/UI/Button.tsx
  32. 2
      src/components/UI/Toast.tsx
  33. 19
      src/components/generic/Table/index.tsx
  34. 36
      src/core/hooks/useToast.ts
  35. 9
      src/core/stores/appStore.ts
  36. 4
      src/core/stores/deviceStore.ts
  37. 2
      src/core/subscriptions.ts
  38. 2
      src/core/utils/cn.ts
  39. 5
      src/pages/Config/DeviceConfig.tsx
  40. 2
      src/pages/Config/ModuleConfig.tsx
  41. 36
      src/pages/Map.tsx
  42. 60
      src/pages/Nodes.tsx
  43. 8
      src/validation/config/position.ts
  44. 2
      src/validation/moduleConfig/mqtt.ts
  45. 4
      vite.config.ts

9
.vscode/settings.json

@ -1,4 +1,7 @@
{ {
"editor.defaultFormatter": "biomejs.biome", "editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true "editor.codeActionsOnSave": {
} "quickfix.biome": "explicit"
},
"editor.formatOnSave": true
}

4
biome.json

@ -1,5 +1,5 @@
{ {
"$schema": "https://biomejs.dev/schemas/1.6.3/schema.json", "$schema": "https://biomejs.dev/schemas/1.7.3/schema.json",
"organizeImports": { "organizeImports": {
"enabled": true "enabled": true
}, },
@ -20,7 +20,7 @@
"linter": { "linter": {
"enabled": true, "enabled": true,
"rules": { "rules": {
"all": true "recommended": true
} }
} }
} }

5
package.json

@ -6,8 +6,9 @@
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
"build": "tsc && vite build", "build": "tsc && pnpm check && vite build ",
"check": "biome check .", "check": "biome check .",
"check:fix": "pnpm check --write",
"preview": "vite preview", "preview": "vite preview",
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)" "package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ $(ls ./dist/output/)"
}, },
@ -60,7 +61,7 @@
"zustand": "4.5.2" "zustand": "4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.6.3", "@biomejs/biome": "^1.8.1",
"@buf/meshtastic_protobufs.bufbuild_es": "1.8.0-20240325205556-b11811405eea.2", "@buf/meshtastic_protobufs.bufbuild_es": "1.8.0-20240325205556-b11811405eea.2",
"@types/chrome": "^0.0.263", "@types/chrome": "^0.0.263",
"@types/node": "^20.11.30", "@types/node": "^20.11.30",

2
pnpm-lock.yaml

@ -122,7 +122,7 @@ dependencies:
devDependencies: devDependencies:
'@biomejs/biome': '@biomejs/biome':
specifier: ^1.6.3 specifier: ^1.8.1
version: 1.8.1 version: 1.8.1
'@buf/meshtastic_protobufs.bufbuild_es': '@buf/meshtastic_protobufs.bufbuild_es':
specifier: 1.8.0-20240325205556-b11811405eea.2 specifier: 1.8.0-20240325205556-b11811405eea.2

4
postcss.config.cjs

@ -1,6 +1,6 @@
module.exports = { module.exports = {
plugins: { plugins: {
tailwindcss: {}, tailwindcss: {},
autoprefixer: {} autoprefixer: {},
} },
}; };

4
src/components/CommandPalette.tsx

@ -19,7 +19,7 @@ import {
LayersIcon, LayersIcon,
LayoutIcon, LayoutIcon,
LinkIcon, LinkIcon,
LucideIcon, type LucideIcon,
MapIcon, MapIcon,
MessageSquareIcon, MessageSquareIcon,
MoonIcon, MoonIcon,
@ -350,7 +350,7 @@ export const CommandPalette = (): JSX.Element => {
window.addEventListener("keydown", handleKeydown); window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown); return () => window.removeEventListener("keydown", handleKeydown);
}, []); }, [setCommandPaletteOpen]);
return ( return (
<CommandDialog <CommandDialog

2
src/components/DeviceSelector.tsx

@ -9,8 +9,8 @@ import {
LanguagesIcon, LanguagesIcon,
MoonIcon, MoonIcon,
PlusIcon, PlusIcon,
SunIcon,
SearchIcon, SearchIcon,
SunIcon,
} from "lucide-react"; } from "lucide-react";
export const DeviceSelector = (): JSX.Element => { export const DeviceSelector = (): JSX.Element => {

2
src/components/Dialog/DialogManager.tsx

@ -1,9 +1,9 @@
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js";
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js"; import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.js";
import { ImportDialog } from "@components/Dialog/ImportDialog.js"; import { ImportDialog } from "@components/Dialog/ImportDialog.js";
import { QRDialog } from "@components/Dialog/QRDialog.js"; import { QRDialog } from "@components/Dialog/QRDialog.js";
import { RebootDialog } from "@components/Dialog/RebootDialog.js"; import { RebootDialog } from "@components/Dialog/RebootDialog.js";
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js"; import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.js";
import { RemoveNodeDialog } from "@app/components/Dialog/RemoveNodeDialog.js"
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
export const DialogManager = (): JSX.Element => { export const DialogManager = (): JSX.Element => {

36
src/components/Dialog/QRDialog.tsx

@ -9,10 +9,10 @@ import {
} from "@components/UI/Dialog.js"; } from "@components/UI/Dialog.js";
import { Input } from "@components/UI/Input.js"; import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js"; import { Label } from "@components/UI/Label.js";
import { Protobuf, Types } from "@meshtastic/js"; import { Protobuf, type Types } from "@meshtastic/js";
import { fromByteArray } from "base64-js"; import { fromByteArray } from "base64-js";
import { ClipboardIcon } from "lucide-react"; import { ClipboardIcon } from "lucide-react";
import { useEffect, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { QRCode } from "react-qrcode-logo"; import { QRCode } from "react-qrcode-logo";
export interface QRDialogProps { export interface QRDialogProps {
@ -32,7 +32,7 @@ export const QRDialog = ({
const [qrCodeUrl, setQrCodeUrl] = useState<string>(""); const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>(); const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
const allChannels = Array.from(channels.values()); const allChannels = useMemo(() => Array.from(channels.values()), [channels]);
useEffect(() => { useEffect(() => {
const channelsToEncode = allChannels const channelsToEncode = allChannels
@ -50,8 +50,10 @@ export const QRDialog = ({
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_"); .replace(/\//g, "_");
setQrCodeUrl(`https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`); setQrCodeUrl(
}, [channels, selectedChannels, qrCodeAdd, loraConfig]); `https://meshtastic.org/e/#${base64}${qrCodeAdd ? "?add=true" : ""}`,
);
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@ -97,18 +99,26 @@ export const QRDialog = ({
</div> </div>
<div className="flex justify-center"> <div className="flex justify-center">
<button <button
type="button" type="button"
className={ "border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") } className={`border-black border-t border-l border-b rounded-l h-10 px-7 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
qrCodeAdd
? "focus:ring-green-800 bg-green-800 text-white"
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
}`}
onClick={() => setQrCodeAdd(true)} onClick={() => setQrCodeAdd(true)}
> >
Add Channels Add Channels
</button> </button>
<button <button
type="button" type="button"
className={ "border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 " + (!qrCodeAdd ? "focus:ring-green-800 bg-green-800 text-white" : "focus:ring-slate-400 bg-slate-400 hover:bg-green-600") } className={`border-black border-t border-r border-b rounded-r h-10 px-4 py-2 text-sm font-medium focus:outline-none focus:ring-2 focus:ring-offset-2 ${
!qrCodeAdd
? "focus:ring-green-800 bg-green-800 text-white"
: "focus:ring-slate-400 bg-slate-400 hover:bg-green-600"
}`}
onClick={() => setQrCodeAdd(false)} onClick={() => setQrCodeAdd(false)}
> >
Replace Channels Replace Channels
</button> </button>
</div> </div>
</div> </div>

2
src/components/Dialog/RebootDialog.tsx

@ -37,7 +37,7 @@ export const RebootDialog = ({
<Input <Input
type="number" type="number"
value={time} value={time}
onChange={(e) => setTime(parseInt(e.target.value))} onChange={(e) => setTime(Number.parseInt(e.target.value))}
action={{ action={{
icon: ClockIcon, icon: ClockIcon,
onClick() { onClick() {

4
src/components/Dialog/RemoveNodeDialog.tsx

@ -44,7 +44,9 @@ export const RemoveNodeDialog = ({
</form> </form>
</div> </div>
<DialogFooter> <DialogFooter>
<Button variant="destructive" onClick={() => onSubmit()}>Remove</Button> <Button variant="destructive" onClick={() => onSubmit()}>
Remove
</Button>
</DialogFooter> </DialogFooter>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

2
src/components/Dialog/ShutdownDialog.tsx

@ -38,7 +38,7 @@ export const ShutdownDialog = ({
<Input <Input
type="number" type="number"
value={time} value={time}
onChange={(e) => setTime(parseInt(e.target.value))} onChange={(e) => setTime(Number.parseInt(e.target.value))}
suffix="Minutes" suffix="Minutes"
/> />
<Button <Button

21
src/components/Form/DynamicForm.tsx

@ -1,17 +1,17 @@
import { import {
DynamicFormField, DynamicFormField,
FieldProps, type FieldProps,
} from "@components/Form/DynamicFormField.js"; } from "@components/Form/DynamicFormField.js";
import { FieldWrapper } from "@components/Form/FormWrapper.js"; import { FieldWrapper } from "@components/Form/FormWrapper.js";
import { Button } from "@components/UI/Button.js"; import { Button } from "@components/UI/Button.js";
import { H4 } from "@components/UI/Typography/H4.js"; import { H4 } from "@components/UI/Typography/H4.js";
import { Subtle } from "@components/UI/Typography/Subtle.js"; import { Subtle } from "@components/UI/Typography/Subtle.js";
import { import {
Control, type Control,
DefaultValues, type DefaultValues,
FieldValues, type FieldValues,
Path, type Path,
SubmitHandler, type SubmitHandler,
useForm, useForm,
} from "react-hook-form"; } from "react-hook-form";
@ -26,7 +26,7 @@ export interface BaseFormBuilderProps<T> {
disabledBy?: DisabledBy<T>[]; disabledBy?: DisabledBy<T>[];
label: string; label: string;
description?: string; description?: string;
properties?: {}; properties?: Record<string, unknown>;
} }
export interface GenericFormElementProps<T extends FieldValues, Y> { export interface GenericFormElementProps<T extends FieldValues, Y> {
@ -94,9 +94,12 @@ export function DynamicForm<T extends FieldValues>({
</div> </div>
{fieldGroup.fields.map((field) => ( {fieldGroup.fields.map((field) => (
<FieldWrapper label={field.label} description={field.description}> <FieldWrapper
key={field.label}
label={field.label}
description={field.description}
>
<DynamicFormField <DynamicFormField
key={field.label}
field={field} field={field}
control={control} control={control}
disabled={isDisabled(field.disabledBy)} disabled={isDisabled(field.disabledBy)}

15
src/components/Form/DynamicFormField.tsx

@ -1,6 +1,15 @@
import { GenericInput, InputFieldProps } from "@components/Form/FormInput.js"; import {
import { SelectFieldProps, SelectInput } from "@components/Form/FormSelect.js"; GenericInput,
import { ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.js"; type InputFieldProps,
} from "@components/Form/FormInput.js";
import {
type SelectFieldProps,
SelectInput,
} from "@components/Form/FormSelect.js";
import {
type ToggleFieldProps,
ToggleInput,
} from "@components/Form/FormToggle.js";
import type { Control, FieldValues } from "react-hook-form"; import type { Control, FieldValues } from "react-hook-form";
export type FieldProps<T> = export type FieldProps<T> =

2
src/components/Form/FormInput.tsx

@ -4,7 +4,7 @@ import type {
} from "@components/Form/DynamicForm.js"; } from "@components/Form/DynamicForm.js";
import { Input } from "@components/UI/Input.js"; import { Input } from "@components/UI/Input.js";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import { Controller, FieldValues } from "react-hook-form"; import { Controller, type FieldValues } from "react-hook-form";
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> { export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
type: "text" | "number" | "password"; type: "text" | "number" | "password";

4
src/components/Form/FormSelect.tsx

@ -9,7 +9,7 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from "@components/UI/Select.js"; } from "@components/UI/Select.js";
import { Controller, FieldValues } from "react-hook-form"; import { Controller, type FieldValues } from "react-hook-form";
export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> { export interface SelectFieldProps<T> extends BaseFormBuilderProps<T> {
type: "select" | "multiSelect"; type: "select" | "multiSelect";
@ -40,7 +40,7 @@ export function SelectInput<T extends FieldValues>({
: []; : [];
return ( return (
<Select <Select
onValueChange={(e) => onChange(parseInt(e))} onValueChange={(e) => onChange(Number.parseInt(e))}
disabled={disabled} disabled={disabled}
value={value?.toString()} value={value?.toString()}
{...remainingProperties} {...remainingProperties}

4
src/components/Form/FormToggle.tsx

@ -3,8 +3,8 @@ import type {
GenericFormElementProps, GenericFormElementProps,
} from "@components/Form/DynamicForm.js"; } from "@components/Form/DynamicForm.js";
import { Switch } from "@components/UI/Switch.js"; import { Switch } from "@components/UI/Switch.js";
import { ChangeEvent } from "react"; import type { ChangeEvent } from "react";
import { Controller, FieldValues } from "react-hook-form"; import { Controller, type FieldValues } from "react-hook-form";
export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> { export interface ToggleFieldProps<T> extends BaseFormBuilderProps<T> {
type: "toggle"; type: "toggle";

52
src/components/PageComponents/Channel.tsx

@ -1,4 +1,4 @@
import type{ ChannelValidation } from "@app/validation/channel.js"; import type { ChannelValidation } from "@app/validation/channel.js";
import { DynamicForm } from "@components/Form/DynamicForm.js"; import { DynamicForm } from "@components/Form/DynamicForm.js";
import { useToast } from "@core/hooks/useToast.js"; import { useToast } from "@core/hooks/useToast.js";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
@ -20,8 +20,12 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
...data.settings, ...data.settings,
psk: toByteArray(data.settings.psk ?? ""), psk: toByteArray(data.settings.psk ?? ""),
moduleSettings: { moduleSettings: {
positionPrecision: data.settings.positionEnabled ? data.settings.preciseLocation ? 32 : data.settings.positionPrecision : 0, positionPrecision: data.settings.positionEnabled
} ? data.settings.preciseLocation
? 32
: data.settings.positionPrecision
: 0,
},
}, },
}); });
connection?.setChannel(channel).then(() => { connection?.setChannel(channel).then(() => {
@ -43,9 +47,16 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
settings: { settings: {
...channel?.settings, ...channel?.settings,
psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)), psk: fromByteArray(channel?.settings?.psk ?? new Uint8Array(0)),
positionEnabled: channel?.settings?.moduleSettings?.positionPrecision != undefined && channel?.settings?.moduleSettings?.positionPrecision > 0, positionEnabled:
preciseLocation: channel?.settings?.moduleSettings?.positionPrecision == 32, channel?.settings?.moduleSettings?.positionPrecision !==
positionPrecision: channel?.settings?.moduleSettings?.positionPrecision == undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision undefined &&
channel?.settings?.moduleSettings?.positionPrecision > 0,
preciseLocation:
channel?.settings?.moduleSettings?.positionPrecision === 32,
positionPrecision:
channel?.settings?.moduleSettings?.positionPrecision === undefined
? 10
: channel?.settings?.moduleSettings?.positionPrecision,
}, },
}, },
}} }}
@ -111,9 +122,32 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
description: description:
"If not sharing precise location, position shared on channel will be accurate within this distance", "If not sharing precise location, position shared on channel will be accurate within this distance",
properties: { properties: {
enumValue: config.display?.units == 0 ? enumValue:
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } : config.display?.units === 0
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 } ? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
}, },
}, },
], ],

2
src/components/PageComponents/Config/LoRa.tsx

@ -41,7 +41,7 @@ export const LoRa = (): JSX.Element => {
label: "Hop Limit", label: "Hop Limit",
description: "Maximum number of hops", description: "Maximum number of hops",
properties: { properties: {
enumValue: {1:1, 2:2, 3:3, 4:4, 5:5, 6:6, 7:7} enumValue: { 1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7 },
}, },
}, },
{ {

2
src/components/PageComponents/Connect/BLE.tsx

@ -1,4 +1,4 @@
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog"; import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.js"; import { Button } from "@components/UI/Button.js";
import { Mono } from "@components/generic/Mono.js"; import { Mono } from "@components/generic/Mono.js";
import { useAppStore } from "@core/stores/appStore.js"; import { useAppStore } from "@core/stores/appStore.js";

14
src/components/PageComponents/Connect/HTTP.tsx

@ -1,5 +1,4 @@
import React, { useState } from "react"; import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.js"; import { Button } from "@components/UI/Button.js";
import { Input } from "@components/UI/Input.js"; import { Input } from "@components/UI/Input.js";
import { Label } from "@components/UI/Label.js"; import { Label } from "@components/UI/Label.js";
@ -9,6 +8,7 @@ import { useDeviceStore } from "@core/stores/deviceStore.js";
import { subscribeAll } from "@core/subscriptions.js"; import { subscribeAll } from "@core/subscriptions.js";
import { randId } from "@core/utils/randId.js"; import { randId } from "@core/utils/randId.js";
import { HttpConnection } from "@meshtastic/js"; import { HttpConnection } from "@meshtastic/js";
import { useState } from "react";
import { Controller, useForm, useWatch } from "react-hook-form"; import { Controller, useForm, useWatch } from "react-hook-form";
export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => { export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
@ -20,7 +20,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
}>({ }>({
defaultValues: { defaultValues: {
ip: ["client.meshtastic.org", "localhost"].includes( ip: ["client.meshtastic.org", "localhost"].includes(
window.location.hostname window.location.hostname,
) )
? "meshtastic.local" ? "meshtastic.local"
: window.location.hostname, : window.location.hostname,
@ -38,7 +38,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
const onSubmit = handleSubmit(async (data) => { const onSubmit = handleSubmit(async (data) => {
setConnectionInProgress(true); setConnectionInProgress(true);
const id = randId(); const id = randId();
const device = addDevice(id); const device = addDevice(id);
const connection = new HttpConnection(id); const connection = new HttpConnection(id);
@ -75,7 +75,9 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
<Switch <Switch
// label="Use TLS" // label="Use TLS"
// description="Description" // description="Description"
disabled={location.protocol === "https:" || connectionInProgress} disabled={
location.protocol === "https:" || connectionInProgress
}
checked={value} checked={value}
{...rest} {...rest}
/> />
@ -84,7 +86,7 @@ export const HTTP = ({ closeDialog }: TabElementProps): JSX.Element => {
/> />
</div> </div>
<Button type="submit" disabled={connectionInProgress}> <Button type="submit" disabled={connectionInProgress}>
<span>{connectionInProgress ? 'Connecting...' : 'Connect' }</span> <span>{connectionInProgress ? "Connecting..." : "Connect"}</span>
</Button> </Button>
</form> </form>
); );

31
src/components/PageComponents/Connect/Serial.tsx

@ -1,4 +1,4 @@
import { TabElementProps } from "@app/components/Dialog/NewDeviceDialog"; import type { TabElementProps } from "@app/components/Dialog/NewDeviceDialog";
import { Button } from "@components/UI/Button.js"; import { Button } from "@components/UI/Button.js";
import { Mono } from "@components/generic/Mono.js"; import { Mono } from "@components/generic/Mono.js";
import { useAppStore } from "@core/stores/appStore.js"; import { useAppStore } from "@core/stores/appStore.js";
@ -48,19 +48,22 @@ export const Serial = ({ closeDialog }: TabElementProps): JSX.Element => {
return ( return (
<div className="flex w-full flex-col gap-2 p-4"> <div className="flex w-full flex-col gap-2 p-4">
<div className="flex h-48 flex-col gap-2 overflow-y-auto"> <div className="flex h-48 flex-col gap-2 overflow-y-auto">
{serialPorts.map((port, index) => ( {serialPorts.map((port, index) => {
<Button const { usbProductId, usbVendorId } = port.getInfo();
key={index} return (
disabled={port.readable !== null} <Button
onClick={async () => { key={`${usbVendorId ?? "UNK"}-${usbProductId ?? "UNK"}-${index}`}
await onConnect(port); disabled={port.readable !== null}
}} onClick={async () => {
> await onConnect(port);
{`# ${index} - ${port.getInfo().usbVendorId ?? "UNK"} - ${ }}
port.getInfo().usbProductId ?? "UNK" >
}`} {`# ${index} - ${usbVendorId ?? "UNK"} - ${
</Button> usbProductId ?? "UNK"
))} }`}
</Button>
);
})}
{serialPorts.length === 0 && ( {serialPorts.length === 0 && (
<Mono className="m-auto select-none">No devices paired yet.</Mono> <Mono className="m-auto select-none">No devices paired yet.</Mono>
)} )}

5
src/components/PageComponents/Messages/ChannelChat.tsx

@ -1,5 +1,8 @@
import { Subtle } from "@app/components/UI/Typography/Subtle.js"; import { Subtle } from "@app/components/UI/Typography/Subtle.js";
import { MessageWithState, useDevice } from "@app/core/stores/deviceStore.js"; import {
type MessageWithState,
useDevice,
} from "@app/core/stores/deviceStore.js";
import { Message } from "@components/PageComponents/Messages/Message.js"; import { Message } from "@components/PageComponents/Messages/Message.js";
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js"; import { MessageInput } from "@components/PageComponents/Messages/MessageInput.js";
import type { Types } from "@meshtastic/js"; import type { Types } from "@meshtastic/js";

6
src/components/PageComponents/ModuleConfig/DetectionSensor.tsx

@ -38,9 +38,9 @@ export const DetectionSensor = (): JSX.Element => {
label: "Minimum Broadcast Seconds", label: "Minimum Broadcast Seconds",
description: description:
"The interval in seconds of how often we can send a message to the mesh when a state change is detected", "The interval in seconds of how often we can send a message to the mesh when a state change is detected",
properties: { properties: {
suffix: "Seconds", suffix: "Seconds",
}, },
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",

32
src/components/PageComponents/ModuleConfig/MQTT.tsx

@ -70,7 +70,8 @@ export const MQTT = (): JSX.Element => {
type: "toggle", type: "toggle",
name: "encryptionEnabled", name: "encryptionEnabled",
label: "Encryption Enabled", label: "Encryption Enabled",
description: "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.", description:
"Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data.",
disabledBy: [ disabledBy: [
{ {
fieldName: "enabled", fieldName: "enabled",
@ -157,9 +158,32 @@ export const MQTT = (): JSX.Element => {
description: description:
"Position shared will be accurate within this distance", "Position shared will be accurate within this distance",
properties: { properties: {
enumValue: config.display?.units == 0 ? enumValue:
{ "Within 23 km":10, "Within 12 km":11, "Within 5.8 km":12, "Within 2.9 km":13, "Within 1.5 km":14, "Within 700 m":15, "Within 350 m":16, "Within 200 m":17, "Within 90 m":18, "Within 50 m":19 } : config.display?.units === 0
{ "Within 15 miles":10, "Within 7.3 miles":11, "Within 3.6 miles":12, "Within 1.8 miles":13, "Within 0.9 miles":14, "Within 0.5 miles":15, "Within 0.2 miles":16, "Within 600 feet":17, "Within 300 feet":18, "Within 150 feet":19 } ? {
"Within 23 km": 10,
"Within 12 km": 11,
"Within 5.8 km": 12,
"Within 2.9 km": 13,
"Within 1.5 km": 14,
"Within 700 m": 15,
"Within 350 m": 16,
"Within 200 m": 17,
"Within 90 m": 18,
"Within 50 m": 19,
}
: {
"Within 15 miles": 10,
"Within 7.3 miles": 11,
"Within 3.6 miles": 12,
"Within 1.8 miles": 13,
"Within 0.9 miles": 14,
"Within 0.5 miles": 15,
"Within 0.2 miles": 16,
"Within 600 feet": 17,
"Within 300 feet": 18,
"Within 150 feet": 19,
},
}, },
disabledBy: [ disabledBy: [
{ {

2
src/components/PageComponents/ModuleConfig/NeighborInfo.tsx

@ -36,7 +36,7 @@ export const NeighborInfo = (): JSX.Element => {
type: "number", type: "number",
name: "updateInterval", name: "updateInterval",
label: "Update Interval", label: "Update Interval",
description: description:
"Interval in seconds of how often we should try to send our Neighbor Info to the mesh", "Interval in seconds of how often we should try to send our Neighbor Info to the mesh",
properties: { properties: {
suffix: "Seconds", suffix: "Seconds",

3
src/components/PageComponents/ModuleConfig/Paxcounter.tsx

@ -36,7 +36,8 @@ export const Paxcounter = (): JSX.Element => {
type: "number", type: "number",
name: "paxcounterUpdateInterval", name: "paxcounterUpdateInterval",
label: "Update Interval (seconds)", label: "Update Interval (seconds)",
description: "How long to wait between sending paxcounter packets", description:
"How long to wait between sending paxcounter packets",
properties: { properties: {
suffix: "Seconds", suffix: "Seconds",
}, },

2
src/components/PageLayout.tsx

@ -1,5 +1,5 @@
import { cn } from "@app/core/utils/cn.js"; import { cn } from "@app/core/utils/cn.js";
import { AlignLeftIcon, LucideIcon } from "lucide-react"; import { AlignLeftIcon, type LucideIcon } from "lucide-react";
export interface PageLayoutProps { export interface PageLayoutProps {
label: string; label: string;

21
src/components/Sidebar.tsx

@ -4,16 +4,16 @@ import { Subtle } from "@components/UI/Typography/Subtle.js";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
import type { Page } from "@core/stores/deviceStore.js"; import type { Page } from "@core/stores/deviceStore.js";
import { import {
BatteryMediumIcon,
CpuIcon,
EditIcon, EditIcon,
LayersIcon, LayersIcon,
LucideIcon, type LucideIcon,
MapIcon, MapIcon,
MessageSquareIcon, MessageSquareIcon,
SettingsIcon, SettingsIcon,
UsersIcon, UsersIcon,
ZapIcon, ZapIcon,
BatteryMediumIcon,
CpuIcon
} from "lucide-react"; } from "lucide-react";
export interface SidebarProps { export interface SidebarProps {
@ -79,16 +79,18 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
</div> </div>
<div className="px-8 pb-6"> <div className="px-8 pb-6">
<div className="flex items-center"> <div className="flex items-center">
<BatteryMediumIcon size={24} viewBox={'0 0 28 24'}/> <BatteryMediumIcon size={24} viewBox={"0 0 28 24"} />
<Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle> <Subtle>{myNode?.deviceMetrics?.batteryLevel ?? "UNK"}%</Subtle>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<ZapIcon size={24} viewBox={'0 0 36 24'}/> <ZapIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts</Subtle> <Subtle>
{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts
</Subtle>
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<CpuIcon size={24} viewBox={'0 0 36 24'}/> <CpuIcon size={24} viewBox={"0 0 36 24"} />
<Subtle>v{ myMetadata?.firmwareVersion ?? "UNK"}</Subtle> <Subtle>v{myMetadata?.firmwareVersion ?? "UNK"}</Subtle>
</div> </div>
</div> </div>
@ -109,4 +111,3 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
</div> </div>
); );
}; };

10
src/components/Toaster.tsx

@ -17,8 +17,14 @@ export function Toaster() {
{toasts.map(({ id, title, description, action, ...props }) => ( {toasts.map(({ id, title, description, action, ...props }) => (
<Toast key={id} {...props}> <Toast key={id} {...props}>
<div className="grid gap-1"> <div className="grid gap-1">
{title && <ToastTitle className="dark:text-white">{title}</ToastTitle>} {title && (
{description && <ToastDescription className="dark:text-white-400">{description}</ToastDescription>} <ToastTitle className="dark:text-white">{title}</ToastTitle>
)}
{description && (
<ToastDescription className="dark:text-white-400">
{description}
</ToastDescription>
)}
</div> </div>
{action} {action}
<ToastClose /> <ToastClose />

2
src/components/UI/Button.tsx

@ -1,4 +1,4 @@
import { VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react"; import * as React from "react";
import { cn } from "@core/utils/cn.js"; import { cn } from "@core/utils/cn.js";

2
src/components/UI/Toast.tsx

@ -1,5 +1,5 @@
import * as ToastPrimitives from "@radix-ui/react-toast"; import * as ToastPrimitives from "@radix-ui/react-toast";
import { VariantProps, cva } from "class-variance-authority"; import { type VariantProps, cva } from "class-variance-authority";
import { X } from "lucide-react"; import { X } from "lucide-react";
import * as React from "react"; import * as React from "react";

19
src/components/generic/Table/index.tsx

@ -34,8 +34,12 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
// Custom comparison for 'Last Heard' column // Custom comparison for 'Last Heard' column
if (sortColumn === "Last Heard") { if (sortColumn === "Last Heard") {
const aTimestamp = a[columnIndex].props.timestamp ? a[columnIndex].props.timestamp : 0; const aTimestamp = a[columnIndex].props.timestamp
const bTimestamp = b[columnIndex].props.timestamp ? b[columnIndex].props.timestamp : 0; ? a[columnIndex].props.timestamp
: 0;
const bTimestamp = b[columnIndex].props.timestamp
? b[columnIndex].props.timestamp
: 0;
if (aTimestamp < bTimestamp) { if (aTimestamp < bTimestamp) {
return sortOrder === "asc" ? -1 : 1; return sortOrder === "asc" ? -1 : 1;
@ -70,11 +74,18 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
: "" : ""
}`} }`}
onClick={() => heading.sortable && headingSort(heading.title)} onClick={() => heading.sortable && headingSort(heading.title)}
onKeyUp={() => heading.sortable && headingSort(heading.title)}
> >
<div className="flex gap-2"> <div className="flex gap-2">
{heading.title} {heading.title}
{sortColumn === heading.title && ( {sortColumn === heading.title && (
<>{sortOrder === "asc" ? <ChevronUpIcon size={16} /> : <ChevronDownIcon size={16} />}</> <>
{sortOrder === "asc" ? (
<ChevronUpIcon size={16} />
) : (
<ChevronDownIcon size={16} />
)}
</>
)} )}
</div> </div>
</th> </th>
@ -83,9 +94,11 @@ export const Table = ({ headings, rows }: TableProps): JSX.Element => {
</thead> </thead>
<tbody> <tbody>
{sortedRows.map((row, index) => ( {sortedRows.map((row, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
<tr key={index}> <tr key={index}>
{row.map((item, index) => ( {row.map((item, index) => (
<td <td
// biome-ignore lint/suspicious/noArrayIndexKey: OK because column order never changes.
key={index} key={index}
className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2" className="whitespace-nowrap py-2 text-sm text-textSecondary first:pl-2"
> >

36
src/core/hooks/useToast.ts

@ -1,4 +1,4 @@
import { ReactNode, useEffect, useState } from "react"; import { type ReactNode, useSyncExternalStore } from "react";
import type { ToastActionElement, ToastProps } from "@components/UI/Toast.js"; import type { ToastActionElement, ToastProps } from "@components/UI/Toast.js";
@ -92,9 +92,9 @@ export const reducer = (state: State, action: Action): State => {
if (toastId) { if (toastId) {
addToRemoveQueue(toastId); addToRemoveQueue(toastId);
} else { } else {
state.toasts.forEach((toast) => { for (const toast of state.toasts) {
addToRemoveQueue(toast.id); addToRemoveQueue(toast.id);
}); }
} }
return { return {
@ -130,9 +130,9 @@ let memoryState: State = { toasts: [] };
function dispatch(action: Action) { function dispatch(action: Action) {
memoryState = reducer(memoryState, action); memoryState = reducer(memoryState, action);
listeners.forEach((listener) => { for (const listener of listeners) {
listener(memoryState); listener(memoryState);
}); }
} }
type Toast = Omit<ToasterToast, "id">; type Toast = Omit<ToasterToast, "id">;
@ -166,18 +166,22 @@ function toast({ ...props }: Toast) {
}; };
} }
const subscribe = (listener: () => void) => {
listeners.push(listener);
return function unsubscribe() {
const index = listeners.indexOf(listener);
if (index > -1) {
listeners.splice(index, 1);
}
};
};
const getState = () => {
return memoryState;
};
function useToast() { function useToast() {
const [state, setState] = useState<State>(memoryState); const state = useSyncExternalStore(subscribe, getState);
useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
return { return {
...state, ...state,

9
src/core/stores/appStore.ts

@ -50,7 +50,10 @@ export const useAppStore = create<AppState>()((set) => ({
currentPage: "messages", currentPage: "messages",
rasterSources: [], rasterSources: [],
commandPaletteOpen: false, commandPaletteOpen: false,
darkMode: (localStorage.getItem('theme-dark') !== null ? (localStorage.getItem('theme-dark') === 'true' ? true : false) : window.matchMedia("(prefers-color-scheme: dark)").matches), darkMode:
localStorage.getItem("theme-dark") !== null
? localStorage.getItem("theme-dark") === "true"
: window.matchMedia("(prefers-color-scheme: dark)").matches,
accent: "orange", accent: "orange",
connectDialogOpen: false, connectDialogOpen: false,
nodeNumToBeRemoved: 0, nodeNumToBeRemoved: 0,
@ -96,7 +99,7 @@ export const useAppStore = create<AppState>()((set) => ({
); );
}, },
setDarkMode: (enabled: boolean) => { setDarkMode: (enabled: boolean) => {
localStorage.setItem('theme-dark', enabled.toString()); localStorage.setItem("theme-dark", enabled.toString());
set( set(
produce<AppState>((draft) => { produce<AppState>((draft) => {
draft.darkMode = enabled; draft.darkMode = enabled;
@ -105,7 +108,7 @@ export const useAppStore = create<AppState>()((set) => ({
}, },
setNodeNumToBeRemoved: (nodeNum) => setNodeNumToBeRemoved: (nodeNum) =>
set((state) => ({ set((state) => ({
nodeNumToBeRemoved: nodeNum nodeNumToBeRemoved: nodeNum,
})), })),
setAccent(color) { setAccent(color) {
set( set(

4
src/core/stores/deviceStore.ts

@ -506,8 +506,8 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
return; return;
} }
device.nodes.delete(nodeNum); device.nodes.delete(nodeNum);
}) }),
) );
}, },
setMessageState: ( setMessageState: (
type: "direct" | "broadcast", type: "direct" | "broadcast",

2
src/core/subscriptions.ts

@ -1,5 +1,5 @@
import type { Device } from "@core/stores/deviceStore.js"; import type { Device } from "@core/stores/deviceStore.js";
import { Protobuf, Types } from "@meshtastic/js"; import { Protobuf, type Types } from "@meshtastic/js";
export const subscribeAll = ( export const subscribeAll = (
device: Device, device: Device,

2
src/core/utils/cn.ts

@ -1,4 +1,4 @@
import { ClassValue, clsx } from "clsx"; import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {

5
src/pages/Config/DeviceConfig.tsx

@ -53,10 +53,7 @@ export const DeviceConfig = (): JSX.Element => {
<Tabs defaultValue="Device"> <Tabs defaultValue="Device">
<TabsList> <TabsList>
{tabs.map((tab) => ( {tabs.map((tab) => (
<TabsTrigger <TabsTrigger key={tab.label} value={tab.label}>
key={tab.label}
value={tab.label}
>
{tab.label} {tab.label}
</TabsTrigger> </TabsTrigger>
))} ))}

2
src/pages/Config/ModuleConfig.tsx

@ -5,11 +5,11 @@ import { Audio } from "@components/PageComponents/ModuleConfig/Audio.js";
import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js"; import { CannedMessage } from "@components/PageComponents/ModuleConfig/CannedMessage.js";
import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js"; import { ExternalNotification } from "@components/PageComponents/ModuleConfig/ExternalNotification.js";
import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js"; import { MQTT } from "@components/PageComponents/ModuleConfig/MQTT.js";
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js"; import { RangeTest } from "@components/PageComponents/ModuleConfig/RangeTest.js";
import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js"; import { Serial } from "@components/PageComponents/ModuleConfig/Serial.js";
import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js"; import { StoreForward } from "@components/PageComponents/ModuleConfig/StoreForward.js";
import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js"; import { Telemetry } from "@components/PageComponents/ModuleConfig/Telemetry.js";
import { Paxcounter } from "@components/PageComponents/ModuleConfig/Paxcounter.js";
import { import {
Tabs, Tabs,
TabsContent, TabsContent,

36
src/pages/Map.tsx

@ -14,7 +14,7 @@ import {
ZoomInIcon, ZoomInIcon,
ZoomOutIcon, ZoomOutIcon,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Marker, useMap } from "react-map-gl"; import { Marker, useMap } from "react-map-gl";
import MapGl from "react-map-gl/maplibre"; import MapGl from "react-map-gl/maplibre";
@ -27,7 +27,7 @@ export const MapPage = (): JSX.Element => {
const allNodes = Array.from(nodes.values()); const allNodes = Array.from(nodes.values());
const getBBox = () => { const getBBox = useCallback(() => {
if (!map) { if (!map) {
return; return;
} }
@ -64,7 +64,7 @@ export const MapPage = (): JSX.Element => {
if (center) { if (center) {
map.easeTo(center); map.easeTo(center);
} }
}; }, [allNodes, map]);
useEffect(() => { useEffect(() => {
map?.on("zoom", () => { map?.on("zoom", () => {
@ -128,7 +128,11 @@ export const MapPage = (): JSX.Element => {
attributionControl={false} attributionControl={false}
renderWorldCopies={false} renderWorldCopies={false}
maxPitch={0} maxPitch={0}
style = {{filter: darkMode ? 'brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)' : ''}} style={{
filter: darkMode
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
: "",
}}
dragRotate={false} dragRotate={false}
touchZoomRotate={false} touchZoomRotate={false}
initialViewState={{ initialViewState={{
@ -161,21 +165,19 @@ export const MapPage = (): JSX.Element => {
key={node.num} key={node.num}
longitude={node.position.longitudeI / 1e7} longitude={node.position.longitudeI / 1e7}
latitude={node.position.latitudeI / 1e7} latitude={node.position.latitudeI / 1e7}
style = {{filter: darkMode ? 'invert(1)' : ''}} style={{ filter: darkMode ? "invert(1)" : "" }}
anchor="bottom" anchor="bottom"
onClick={() => {
map?.easeTo({
zoom: 12,
center: [
(node.position?.longitudeI ?? 0) / 1e7,
(node.position?.latitudeI ?? 0) / 1e7,
],
});
}}
> >
<div <div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5"
onClick={() => {
map?.easeTo({
zoom: 12,
center: [
(node.position?.longitudeI ?? 0) / 1e7,
(node.position?.latitudeI ?? 0) / 1e7,
],
});
}}
>
<Hashicon value={node.num.toString()} size={22} /> <Hashicon value={node.num.toString()} size={22} />
<Subtle className={cn(zoom < 12 && "hidden")}> <Subtle className={cn(zoom < 12 && "hidden")}>
{node.user?.longName} {node.user?.longName}

60
src/pages/Nodes.tsx

@ -1,15 +1,15 @@
import { useAppStore } from "@app/core/stores/appStore";
import { Sidebar } from "@components/Sidebar.js"; import { Sidebar } from "@components/Sidebar.js";
import { Button } from "@components/UI/Button.js";
import { Mono } from "@components/generic/Mono.js"; import { Mono } from "@components/generic/Mono.js";
import { Table } from "@components/generic/Table/index.js"; import { Table } from "@components/generic/Table/index.js";
import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js"; import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react"; import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { base16 } from "rfc4648";
import { Button } from "@components/UI/Button.js";
import { TrashIcon } from "lucide-react"; import { TrashIcon } from "lucide-react";
import { useAppStore } from "@app/core/stores/appStore"; import { Fragment } from "react";
import { base16 } from "rfc4648";
export interface DeleteNoteDialogProps { export interface DeleteNoteDialogProps {
open: boolean; open: boolean;
@ -40,8 +40,8 @@ export const NodesPage = (): JSX.Element => {
{ title: "Remove", type: "normal", sortable: false }, { title: "Remove", type: "normal", sortable: false },
]} ]}
rows={filteredNodes.map((node) => [ rows={filteredNodes.map((node) => [
<Hashicon size={24} value={node.num.toString()} />, <Hashicon key="icon" size={24} value={node.num.toString()} />,
<h1> <h1 key="header">
{node.user?.longName ?? {node.user?.longName ??
(node.user?.macaddr (node.user?.macaddr
? `Meshtastic ${base16 ? `Meshtastic ${base16
@ -50,34 +50,46 @@ export const NodesPage = (): JSX.Element => {
: `UNK: ${node.num}`)} : `UNK: ${node.num}`)}
</h1>, </h1>,
<Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>, <Mono key="model">
<Mono> {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
</Mono>,
<Mono key="addr">
{base16 {base16
.stringify(node.user?.macaddr ?? []) .stringify(node.user?.macaddr ?? [])
.match(/.{1,2}/g) .match(/.{1,2}/g)
?.join(":") ?? "UNK"} ?.join(":") ?? "UNK"}
</Mono>, </Mono>,
node.lastHeard === 0 ? ( <Fragment key="lastHeard">
<p>Never</p> {node.lastHeard === 0 ? (
) : ( <p>Never</p>
<TimeAgo timestamp={node.lastHeard * 1000} /> ) : (
), <TimeAgo timestamp={node.lastHeard * 1000} />
<Mono> )}
</Fragment>,
<Mono key="snr">
{node.snr}db/ {node.snr}db/
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/ {Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
{(node.snr + 10) * 5}raw {(node.snr + 10) * 5}raw
</Mono>, </Mono>,
<Mono> <Mono key="hops">
{node.lastHeard != 0 ? {node.lastHeard !== 0
(node.viaMqtt === false && node.hopsAway === 0 ? node.viaMqtt === false && node.hopsAway === 0
? "Direct": node.hopsAway.toString() + " hops away") ? "Direct"
: `${node.hopsAway.toString()} hops away`
: "-"} : "-"}
{node.viaMqtt === true? ", via MQTT": ""} {node.viaMqtt === true ? ", via MQTT" : ""}
</Mono>, </Mono>,
<Button variant="destructive" onClick={() => { <Button
setNodeNumToBeRemoved(node.num); key="remove"
setDialogOpen("nodeRemoval", true) variant="destructive"
}}><TrashIcon />Remove</Button> onClick={() => {
setNodeNumToBeRemoved(node.num);
setDialogOpen("nodeRemoval", true);
}}
>
<TrashIcon />
Remove
</Button>,
])} ])}
/> />
</div> </div>

8
src/validation/config/position.ts

@ -2,10 +2,14 @@ import type { Message } from "@bufbuild/protobuf";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator"; import { IsArray, IsBoolean, IsEnum, IsInt } from "class-validator";
const DeprecatedPositionValidationFields = ['gpsEnabled', 'gpsAttemptTime']; const DeprecatedPositionValidationFields = ["gpsEnabled", "gpsAttemptTime"];
export class PositionValidation export class PositionValidation
implements Omit<Protobuf.Config.Config_PositionConfig, keyof Message | typeof DeprecatedPositionValidationFields[number]> implements
Omit<
Protobuf.Config.Config_PositionConfig,
keyof Message | (typeof DeprecatedPositionValidationFields)[number]
>
{ {
@IsInt() @IsInt()
positionBroadcastSecs: number; positionBroadcastSecs: number;

2
src/validation/moduleConfig/mqtt.ts

@ -1,6 +1,6 @@
import type { Message } from "@bufbuild/protobuf"; import type { Message } from "@bufbuild/protobuf";
import type { Protobuf } from "@meshtastic/js"; import type { Protobuf } from "@meshtastic/js";
import { IsBoolean, IsString, Length, IsNumber } from "class-validator"; import { IsBoolean, IsNumber, IsString, Length } from "class-validator";
export class MqttValidation export class MqttValidation
implements implements

4
vite.config.ts

@ -1,5 +1,5 @@
import { execSync } from "child_process"; import { execSync } from "node:child_process";
import { resolve } from "path"; import { resolve } from "node:path";
import { visualizer } from "rollup-plugin-visualizer"; import { visualizer } from "rollup-plugin-visualizer";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
import EnvironmentPlugin from "vite-plugin-environment"; import EnvironmentPlugin from "vite-plugin-environment";

Loading…
Cancel
Save