Merge remote-tracking branch 'meshtastic-remote/master' into fix/static-ip-display
This commit is contained in:
@@ -27,6 +27,12 @@ jobs:
|
|||||||
- name: Package Output
|
- name: Package Output
|
||||||
run: pnpm package
|
run: pnpm package
|
||||||
|
|
||||||
|
- name: Archive compressed build
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build
|
||||||
|
path: dist/build.tar
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
|||||||
+5
-3
@@ -23,7 +23,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^1.10.0",
|
"@bufbuild/protobuf": "^1.10.0",
|
||||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||||
"@meshtastic/js": "2.3.7-0",
|
"@meshtastic/js": "2.3.7-5",
|
||||||
|
"@noble/curves": "^1.5.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.0",
|
"@radix-ui/react-accordion": "^1.2.0",
|
||||||
"@radix-ui/react-checkbox": "^1.1.0",
|
"@radix-ui/react-checkbox": "^1.1.0",
|
||||||
"@radix-ui/react-dialog": "^1.1.1",
|
"@radix-ui/react-dialog": "^1.1.1",
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"crypto-random-string": "^5.0.0",
|
"crypto-random-string": "^5.0.0",
|
||||||
"immer": "^10.1.1",
|
"immer": "^10.1.1",
|
||||||
"lucide-react": "^0.363.0",
|
"lucide-react": "^0.363.0",
|
||||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
"mapbox-gl": "^3.6.0",
|
||||||
"maplibre-gl": "4.1.2",
|
"maplibre-gl": "4.1.2",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
@@ -59,11 +60,12 @@
|
|||||||
"tailwind-merge": "^2.3.0",
|
"tailwind-merge": "^2.3.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"timeago-react": "^3.0.6",
|
"timeago-react": "^3.0.6",
|
||||||
|
"vite-plugin-node-polyfills": "^0.22.0",
|
||||||
"zustand": "4.5.2"
|
"zustand": "4.5.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^1.8.2",
|
"@biomejs/biome": "^1.8.2",
|
||||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240613143006-244927bc441a.1",
|
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1",
|
||||||
"@types/chrome": "^0.0.263",
|
"@types/chrome": "^0.0.263",
|
||||||
"@types/node": "^20.14.9",
|
"@types/node": "^20.14.9",
|
||||||
"@types/react": "^18.3.3",
|
"@types/react": "^18.3.3",
|
||||||
|
|||||||
Generated
+788
-20
File diff suppressed because it is too large
Load Diff
@@ -200,10 +200,17 @@ export const CommandPalette = (): JSX.Element => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Factory Reset",
|
label: "Factory Reset Device",
|
||||||
icon: FactoryIcon,
|
icon: FactoryIcon,
|
||||||
action() {
|
action() {
|
||||||
connection?.factoryReset();
|
connection?.factoryResetDevice();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Factory Reset Config",
|
||||||
|
icon: FactoryIcon,
|
||||||
|
action() {
|
||||||
|
connection?.factoryResetConfig();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Button } from "@components/UI/Button.js";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog.js";
|
||||||
|
|
||||||
|
export interface PkiRegenerateDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: () => void;
|
||||||
|
onSubmit: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PkiRegenerateDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
onSubmit,
|
||||||
|
}: PkiRegenerateDialogProps): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Regenerate Key pair?</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Are you sure you want to regenerate key pair?
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="destructive" onClick={() => onSubmit()}>
|
||||||
|
Regenerate
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -23,6 +23,7 @@ interface DisabledBy<T> {
|
|||||||
|
|
||||||
export interface BaseFormBuilderProps<T> {
|
export interface BaseFormBuilderProps<T> {
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
|
disabled?: boolean;
|
||||||
disabledBy?: DisabledBy<T>[];
|
disabledBy?: DisabledBy<T>[];
|
||||||
label: string;
|
label: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -62,11 +63,16 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
defaultValues: defaultValues,
|
defaultValues: defaultValues,
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDisabled = (disabledBy?: DisabledBy<T>[]): boolean => {
|
const isDisabled = (
|
||||||
|
disabledBy?: DisabledBy<T>[],
|
||||||
|
disabled?: boolean,
|
||||||
|
): boolean => {
|
||||||
|
if (disabled) return true;
|
||||||
if (!disabledBy) return false;
|
if (!disabledBy) return false;
|
||||||
|
|
||||||
return disabledBy.some((field) => {
|
return disabledBy.some((field) => {
|
||||||
const value = getValues(field.fieldName);
|
const value = getValues(field.fieldName);
|
||||||
|
if (value === "always") return true;
|
||||||
if (typeof value === "boolean") return field.invert ? value : !value;
|
if (typeof value === "boolean") return field.invert ? value : !value;
|
||||||
if (typeof value === "number")
|
if (typeof value === "number")
|
||||||
return field.invert
|
return field.invert
|
||||||
@@ -109,7 +115,7 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
<DynamicFormField
|
<DynamicFormField
|
||||||
field={field}
|
field={field}
|
||||||
control={control}
|
control={control}
|
||||||
disabled={isDisabled(field.disabledBy)}
|
disabled={isDisabled(field.disabledBy, field.disabled)}
|
||||||
/>
|
/>
|
||||||
</FieldWrapper>
|
</FieldWrapper>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -4,11 +4,14 @@ 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 type { ChangeEventHandler } from "react";
|
||||||
import { Controller, type 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";
|
||||||
|
inputChange?: ChangeEventHandler;
|
||||||
properties?: {
|
properties?: {
|
||||||
|
value?: string;
|
||||||
prefix?: string;
|
prefix?: string;
|
||||||
suffix?: string;
|
suffix?: string;
|
||||||
step?: number;
|
step?: number;
|
||||||
@@ -33,13 +36,14 @@ export function GenericInput<T extends FieldValues>({
|
|||||||
type={field.type}
|
type={field.type}
|
||||||
step={field.properties?.step}
|
step={field.properties?.step}
|
||||||
value={field.type === "number" ? Number.parseFloat(value) : value}
|
value={field.type === "number" ? Number.parseFloat(value) : value}
|
||||||
onChange={(e) =>
|
onChange={(e) => {
|
||||||
|
if (field.inputChange) field.inputChange(e);
|
||||||
onChange(
|
onChange(
|
||||||
field.type === "number"
|
field.type === "number"
|
||||||
? Number.parseFloat(e.target.value)
|
? Number.parseFloat(e.target.value)
|
||||||
: e.target.value,
|
: e.target.value,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
{...field.properties}
|
{...field.properties}
|
||||||
{...rest}
|
{...rest}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { Controller, type FieldValues } from "react-hook-form";
|
|||||||
|
|
||||||
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
||||||
type: "passwordGenerator";
|
type: "passwordGenerator";
|
||||||
|
hide?: boolean;
|
||||||
|
bits?: { text: string; value: string; key: string }[];
|
||||||
devicePSKBitCount: number;
|
devicePSKBitCount: number;
|
||||||
inputChange: ChangeEventHandler;
|
inputChange: ChangeEventHandler;
|
||||||
selectChange: (event: string) => void;
|
selectChange: (event: string) => void;
|
||||||
@@ -17,6 +19,7 @@ export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
|
|||||||
export function PasswordGenerator<T extends FieldValues>({
|
export function PasswordGenerator<T extends FieldValues>({
|
||||||
control,
|
control,
|
||||||
field,
|
field,
|
||||||
|
disabled,
|
||||||
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
|
}: GenericFormElementProps<T, PasswordGeneratorProps<T>>) {
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
@@ -24,7 +27,9 @@ export function PasswordGenerator<T extends FieldValues>({
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, ...rest } }) => (
|
render={({ field: { value, ...rest } }) => (
|
||||||
<Generator
|
<Generator
|
||||||
|
hide={field.hide}
|
||||||
devicePSKBitCount={field.devicePSKBitCount}
|
devicePSKBitCount={field.devicePSKBitCount}
|
||||||
|
bits={field.bits}
|
||||||
inputChange={field.inputChange}
|
inputChange={field.inputChange}
|
||||||
selectChange={field.selectChange}
|
selectChange={field.selectChange}
|
||||||
buttonClick={field.buttonClick}
|
buttonClick={field.buttonClick}
|
||||||
@@ -33,6 +38,7 @@ export function PasswordGenerator<T extends FieldValues>({
|
|||||||
buttonText="Generate"
|
buttonText="Generate"
|
||||||
{...field.properties}
|
{...field.properties}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
disabled={disabled}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -36,19 +36,6 @@ export const Device = (): JSX.Element => {
|
|||||||
formatEnumName: true,
|
formatEnumName: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "serialEnabled",
|
|
||||||
label: "Serial Output Enabled",
|
|
||||||
description: "Enable the device's serial console",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "debugLogEnabled",
|
|
||||||
label: "Enabled Debug Log",
|
|
||||||
description:
|
|
||||||
"Output debugging information to the device's serial port (auto disables when serial client is connected)",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "number",
|
type: "number",
|
||||||
name: "buttonGpio",
|
name: "buttonGpio",
|
||||||
@@ -86,12 +73,6 @@ export const Device = (): JSX.Element => {
|
|||||||
label: "Double Tap as Button Press",
|
label: "Double Tap as Button Press",
|
||||||
description: "Treat double tap as button press",
|
description: "Treat double tap as button press",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "isManaged",
|
|
||||||
label: "Managed",
|
|
||||||
description: "Is this device managed by a mesh administator",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "toggle",
|
type: "toggle",
|
||||||
name: "disableTripleClick",
|
name: "disableTripleClick",
|
||||||
|
|||||||
@@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => {
|
|||||||
label: "Ignore MQTT",
|
label: "Ignore MQTT",
|
||||||
description: "Don't forward MQTT messages over the mesh",
|
description: "Don't forward MQTT messages over the mesh",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "configOkToMqtt",
|
||||||
|
label: "OK to MQTT",
|
||||||
|
description:
|
||||||
|
"When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
|
||||||
|
import { DynamicForm } from "@app/components/Form/DynamicForm.js";
|
||||||
|
import {
|
||||||
|
getX25519PrivateKey,
|
||||||
|
getX25519PublicKey,
|
||||||
|
} from "@app/core/utils/x25519";
|
||||||
|
import type { SecurityValidation } from "@app/validation/config/security.js";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.js";
|
||||||
|
import { Protobuf } from "@meshtastic/js";
|
||||||
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export const Security = (): JSX.Element => {
|
||||||
|
const { config, nodes, hardware, setWorkingConfig } = useDevice();
|
||||||
|
|
||||||
|
const [privateKey, setPrivateKey] = useState<string>(
|
||||||
|
fromByteArray(config.security?.privateKey ?? new Uint8Array(0)),
|
||||||
|
);
|
||||||
|
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
|
||||||
|
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
|
||||||
|
config.security?.privateKey.length ?? 32,
|
||||||
|
);
|
||||||
|
const [privateKeyValidationText, setPrivateKeyValidationText] =
|
||||||
|
useState<string>();
|
||||||
|
const [publicKey, setPublicKey] = useState<string>(
|
||||||
|
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
||||||
|
);
|
||||||
|
const [adminKey, setAdminKey] = useState<string>(
|
||||||
|
fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)),
|
||||||
|
);
|
||||||
|
const [adminKeyValidationText, setAdminKeyValidationText] =
|
||||||
|
useState<string>();
|
||||||
|
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const onSubmit = (data: SecurityValidation) => {
|
||||||
|
if (privateKeyValidationText || adminKeyValidationText) return;
|
||||||
|
|
||||||
|
setWorkingConfig(
|
||||||
|
new Protobuf.Config.Config({
|
||||||
|
payloadVariant: {
|
||||||
|
case: "security",
|
||||||
|
value: {
|
||||||
|
...data,
|
||||||
|
adminKey: [toByteArray(adminKey)],
|
||||||
|
privateKey: toByteArray(privateKey),
|
||||||
|
publicKey: toByteArray(publicKey),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateKey = (
|
||||||
|
input: string,
|
||||||
|
count: number,
|
||||||
|
setValidationText: (
|
||||||
|
value: React.SetStateAction<string | undefined>,
|
||||||
|
) => void,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
|
||||||
|
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
||||||
|
} else {
|
||||||
|
setValidationText(undefined);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const privateKeyClickEvent = () => {
|
||||||
|
setDialogOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const pkiRegenerate = () => {
|
||||||
|
const privateKey = getX25519PrivateKey();
|
||||||
|
const publicKey = getX25519PublicKey(privateKey);
|
||||||
|
|
||||||
|
setPrivateKey(fromByteArray(privateKey));
|
||||||
|
setPublicKey(fromByteArray(publicKey));
|
||||||
|
validateKey(
|
||||||
|
fromByteArray(privateKey),
|
||||||
|
privateKeyBitCount,
|
||||||
|
setPrivateKeyValidationText,
|
||||||
|
);
|
||||||
|
|
||||||
|
setDialogOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const privateKeyInputChangeEvent = (
|
||||||
|
e: React.ChangeEvent<HTMLInputElement>,
|
||||||
|
) => {
|
||||||
|
const privateKeyB64String = e.target.value;
|
||||||
|
setPrivateKey(privateKeyB64String);
|
||||||
|
validateKey(
|
||||||
|
privateKeyB64String,
|
||||||
|
privateKeyBitCount,
|
||||||
|
setPrivateKeyValidationText,
|
||||||
|
);
|
||||||
|
|
||||||
|
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String));
|
||||||
|
setPublicKey(fromByteArray(publicKey));
|
||||||
|
};
|
||||||
|
|
||||||
|
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const psk = e.currentTarget?.value;
|
||||||
|
setAdminKey(psk);
|
||||||
|
validateKey(psk, privateKeyBitCount, setAdminKeyValidationText);
|
||||||
|
};
|
||||||
|
|
||||||
|
const privateKeySelectChangeEvent = (e: string) => {
|
||||||
|
const count = Number.parseInt(e);
|
||||||
|
setPrivateKeyBitCount(count);
|
||||||
|
validateKey(privateKey, count, setPrivateKeyValidationText);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DynamicForm<SecurityValidation>
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
submitType="onChange"
|
||||||
|
defaultValues={{
|
||||||
|
...config.security,
|
||||||
|
...{
|
||||||
|
adminKey: adminKey,
|
||||||
|
privateKey: privateKey,
|
||||||
|
publicKey: publicKey,
|
||||||
|
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
|
||||||
|
isManaged: config.security?.isManaged ?? false,
|
||||||
|
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
|
||||||
|
serialEnabled: config.security?.serialEnabled ?? false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
fieldGroups={[
|
||||||
|
{
|
||||||
|
label: "Security Settings",
|
||||||
|
description: "Settings for the Security configuration",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "passwordGenerator",
|
||||||
|
name: "privateKey",
|
||||||
|
label: "Private Key",
|
||||||
|
description: "Used to create a shared key with a remote device",
|
||||||
|
bits: [{ text: "256 bit", value: "32", key: "bit256" }],
|
||||||
|
validationText: privateKeyValidationText,
|
||||||
|
devicePSKBitCount: privateKeyBitCount,
|
||||||
|
inputChange: privateKeyInputChangeEvent,
|
||||||
|
selectChange: privateKeySelectChangeEvent,
|
||||||
|
hide: !privateKeyVisible,
|
||||||
|
buttonClick: privateKeyClickEvent,
|
||||||
|
properties: {
|
||||||
|
value: privateKey,
|
||||||
|
action: {
|
||||||
|
icon: privateKeyVisible ? EyeOff : Eye,
|
||||||
|
onClick: () => setPrivateKeyVisible(!privateKeyVisible),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "publicKey",
|
||||||
|
label: "Public Key",
|
||||||
|
disabled: true,
|
||||||
|
description:
|
||||||
|
"Sent out to other nodes on the mesh to allow them to compute a shared secret key",
|
||||||
|
properties: {
|
||||||
|
value: publicKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Admin Settings",
|
||||||
|
description: "Settings for Admin",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "adminChannelEnabled",
|
||||||
|
label: "Allow Legacy Admin",
|
||||||
|
description:
|
||||||
|
"Allow incoming device control over the insecure legacy admin channel",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "isManaged",
|
||||||
|
label: "Managed",
|
||||||
|
description:
|
||||||
|
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
name: "adminKey",
|
||||||
|
label: "Admin Key",
|
||||||
|
description:
|
||||||
|
"The public key authorized to send admin messages to this node",
|
||||||
|
validationText: adminKeyValidationText,
|
||||||
|
inputChange: adminKeyInputChangeEvent,
|
||||||
|
disabledBy: [
|
||||||
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
|
],
|
||||||
|
properties: {
|
||||||
|
value: adminKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Logging Settings",
|
||||||
|
description: "Settings for Logging",
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "debugLogApiEnabled",
|
||||||
|
label: "Enable Debug Log API",
|
||||||
|
description:
|
||||||
|
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "toggle",
|
||||||
|
name: "serialEnabled",
|
||||||
|
label: "Serial Output Enabled",
|
||||||
|
description: "Serial Console over the Stream API",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<PkiRegenerateDialog
|
||||||
|
open={dialogOpen}
|
||||||
|
onOpenChange={() => setDialogOpen(false)}
|
||||||
|
onSubmit={() => pkiRegenerate()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -8,6 +8,7 @@ export interface PageLayoutProps {
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
actions?: {
|
actions?: {
|
||||||
icon: LucideIcon;
|
icon: LucideIcon;
|
||||||
|
iconClasses?: string;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
@@ -39,7 +40,7 @@ export const PageLayout = ({
|
|||||||
className="transition-all hover:text-accent"
|
className="transition-all hover:text-accent"
|
||||||
onClick={action.onClick}
|
onClick={action.onClick}
|
||||||
>
|
>
|
||||||
<action.icon />
|
<action.icon className={action.iconClasses} />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export const Sidebar = ({ children }: SidebarProps): JSX.Element => {
|
|||||||
<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>
|
<Subtle>
|
||||||
{myNode?.deviceMetrics?.voltage.toPrecision(3) ?? "UNK"} volts
|
{myNode?.deviceMetrics?.voltage?.toPrecision(3) ?? "UNK"} volts
|
||||||
</Subtle>
|
</Subtle>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|||||||
@@ -9,63 +9,95 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@components/UI/Select.js";
|
} from "@components/UI/Select.js";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
|
||||||
|
hide?: boolean;
|
||||||
devicePSKBitCount?: number;
|
devicePSKBitCount?: number;
|
||||||
value: string;
|
value: string;
|
||||||
variant: "default" | "invalid";
|
variant: "default" | "invalid";
|
||||||
buttonText?: string;
|
buttonText?: string;
|
||||||
|
bits?: { text: string; value: string; key: string }[];
|
||||||
selectChange: (event: string) => void;
|
selectChange: (event: string) => void;
|
||||||
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||||
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
buttonClick: React.MouseEventHandler<HTMLButtonElement>;
|
||||||
|
action?: {
|
||||||
|
icon: LucideIcon;
|
||||||
|
onClick: () => void;
|
||||||
|
};
|
||||||
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
hide = true,
|
||||||
devicePSKBitCount,
|
devicePSKBitCount,
|
||||||
variant,
|
variant,
|
||||||
value,
|
value,
|
||||||
buttonText,
|
buttonText,
|
||||||
|
bits = [
|
||||||
|
{ text: "256 bit", value: "32", key: "bit256" },
|
||||||
|
{ text: "128 bit", value: "16", key: "bit128" },
|
||||||
|
{ text: "8 bit", value: "1", key: "bit8" },
|
||||||
|
],
|
||||||
selectChange,
|
selectChange,
|
||||||
inputChange,
|
inputChange,
|
||||||
buttonClick,
|
buttonClick,
|
||||||
|
action,
|
||||||
|
disabled,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
// Invokes onChange event on the input element when the value changes from the parent component
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!inputRef.current) return;
|
||||||
|
const setValue = Object.getOwnPropertyDescriptor(
|
||||||
|
HTMLInputElement.prototype,
|
||||||
|
"value",
|
||||||
|
)?.set;
|
||||||
|
|
||||||
|
if (!setValue) return;
|
||||||
|
inputRef.current.value = "";
|
||||||
|
setValue.call(inputRef.current, value);
|
||||||
|
inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
|
||||||
|
}, [value]);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type={hide ? "password" : "text"}
|
||||||
id="pskInput"
|
id="pskInput"
|
||||||
variant={variant}
|
variant={variant}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={inputChange}
|
onChange={inputChange}
|
||||||
|
action={action}
|
||||||
|
disabled={disabled}
|
||||||
|
ref={inputRef}
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
value={devicePSKBitCount?.toString()}
|
value={devicePSKBitCount?.toString()}
|
||||||
onValueChange={(e) => selectChange(e)}
|
onValueChange={(e) => selectChange(e)}
|
||||||
|
disabled={disabled}
|
||||||
>
|
>
|
||||||
<SelectTrigger>
|
<SelectTrigger className="!max-w-max">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem key="bit256" value="32">
|
{bits.map(({ text, value, key }) => (
|
||||||
256 bit
|
<SelectItem key={key} value={value}>
|
||||||
</SelectItem>
|
{text}
|
||||||
<SelectItem key="bit128" value="16">
|
</SelectItem>
|
||||||
128 bit
|
))}
|
||||||
</SelectItem>
|
|
||||||
<SelectItem key="bit8" value="1">
|
|
||||||
8 bit
|
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="success"
|
variant="success"
|
||||||
onClick={buttonClick}
|
onClick={buttonClick}
|
||||||
|
disabled={disabled}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface InputProps
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
({ className, variant, prefix, suffix, action, ...props }, ref) => {
|
({ className, value, variant, prefix, suffix, action, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full">
|
<div className="relative w-full">
|
||||||
{prefix && (
|
{prefix && (
|
||||||
@@ -45,6 +45,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
className,
|
className,
|
||||||
inputVariants({ variant }),
|
inputVariants({ variant }),
|
||||||
)}
|
)}
|
||||||
|
value={value}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ export const useDeviceStore = create<DeviceState>((set, get) => ({
|
|||||||
device.config.bluetooth = config.payloadVariant.value;
|
device.config.bluetooth = config.payloadVariant.value;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "security": {
|
||||||
|
device.config.security = config.payloadVariant.value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { x25519 } from "@noble/curves/ed25519";
|
||||||
|
|
||||||
|
export function getX25519PrivateKey(): Uint8Array {
|
||||||
|
const key = x25519.utils.randomPrivateKey();
|
||||||
|
|
||||||
|
key[0] &= 248;
|
||||||
|
key[31] &= 127;
|
||||||
|
key[31] |= 64;
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getX25519PublicKey(privateKey: Uint8Array): Uint8Array {
|
||||||
|
return x25519.getPublicKey(privateKey);
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { LoRa } from "@components/PageComponents/Config/LoRa.js";
|
|||||||
import { Network } from "@components/PageComponents/Config/Network.js";
|
import { Network } from "@components/PageComponents/Config/Network.js";
|
||||||
import { Position } from "@components/PageComponents/Config/Position.js";
|
import { Position } from "@components/PageComponents/Config/Position.js";
|
||||||
import { Power } from "@components/PageComponents/Config/Power.js";
|
import { Power } from "@components/PageComponents/Config/Power.js";
|
||||||
|
import { Security } from "@components/PageComponents/Config/Security.js";
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
@@ -47,6 +48,10 @@ export const DeviceConfig = (): JSX.Element => {
|
|||||||
label: "Bluetooth",
|
label: "Bluetooth",
|
||||||
element: Bluetooth,
|
element: Bluetooth,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: "Security",
|
||||||
|
element: Security,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export const Dashboard = () => {
|
|||||||
<div className="m-auto flex flex-col gap-3 text-center">
|
<div className="m-auto flex flex-col gap-3 text-center">
|
||||||
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
|
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
|
||||||
<H3>No Devices</H3>
|
<H3>No Devices</H3>
|
||||||
<Subtle>Connect atleast one device to get started</Subtle>
|
<Subtle>Connect at least one device to get started</Subtle>
|
||||||
<Button
|
<Button
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
onClick={() => setConnectDialogOpen(true)}
|
onClick={() => setConnectDialogOpen(true)}
|
||||||
|
|||||||
+94
-96
@@ -87,110 +87,108 @@ export const MapPage = (): JSX.Element => {
|
|||||||
))}
|
))}
|
||||||
</SidebarSection>
|
</SidebarSection>
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<div className="flex flex-col flex-grow">
|
<PageLayout
|
||||||
<PageLayout
|
label="Map"
|
||||||
label="Map"
|
noPadding={true}
|
||||||
noPadding={true}
|
actions={[
|
||||||
actions={[
|
{
|
||||||
{
|
icon: ZoomInIcon,
|
||||||
icon: ZoomInIcon,
|
onClick() {
|
||||||
onClick() {
|
map?.zoomIn();
|
||||||
map?.zoomIn();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
icon: ZoomOutIcon,
|
{
|
||||||
onClick() {
|
icon: ZoomOutIcon,
|
||||||
map?.zoomOut();
|
onClick() {
|
||||||
},
|
map?.zoomOut();
|
||||||
},
|
},
|
||||||
{
|
},
|
||||||
icon: BoxSelectIcon,
|
{
|
||||||
onClick() {
|
icon: BoxSelectIcon,
|
||||||
getBBox();
|
onClick() {
|
||||||
},
|
getBBox();
|
||||||
},
|
},
|
||||||
]}
|
},
|
||||||
>
|
]}
|
||||||
<MapGl
|
>
|
||||||
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
<MapGl
|
||||||
// onClick={(e) => {
|
mapStyle="https://raw.githubusercontent.com/hc-oss/maplibre-gl-styles/master/styles/osm-mapnik/v8/default.json"
|
||||||
// const waypoint = new Protobuf.Waypoint({
|
// onClick={(e) => {
|
||||||
// name: "test",
|
// const waypoint = new Protobuf.Waypoint({
|
||||||
// description: "test description",
|
// name: "test",
|
||||||
// latitudeI: Math.trunc(e.lngLat.lat * 1e7),
|
// description: "test description",
|
||||||
// longitudeI: Math.trunc(e.lngLat.lng * 1e7)
|
// latitudeI: Math.trunc(e.lngLat.lat * 1e7),
|
||||||
// });
|
// longitudeI: Math.trunc(e.lngLat.lng * 1e7)
|
||||||
// addWaypoint(waypoint);
|
// });
|
||||||
// connection?.sendWaypoint(waypoint, "broadcast");
|
// addWaypoint(waypoint);
|
||||||
// }}
|
// connection?.sendWaypoint(waypoint, "broadcast");
|
||||||
|
// }}
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
attributionControl={false}
|
attributionControl={false}
|
||||||
renderWorldCopies={false}
|
renderWorldCopies={false}
|
||||||
maxPitch={0}
|
maxPitch={0}
|
||||||
style={{
|
style={{
|
||||||
filter: darkMode
|
filter: darkMode
|
||||||
? "brightness(0.6) invert(1) contrast(3) hue-rotate(200deg) saturate(0.3) brightness(0.7)"
|
? "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={{
|
||||||
zoom: 1.6,
|
zoom: 1.6,
|
||||||
latitude: 35,
|
latitude: 35,
|
||||||
longitude: 0,
|
longitude: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{waypoints.map((wp) => (
|
{waypoints.map((wp) => (
|
||||||
<Marker
|
<Marker
|
||||||
key={wp.id}
|
key={wp.id}
|
||||||
longitude={wp.longitudeI / 1e7}
|
longitude={(wp.longitudeI ?? 0) / 1e7}
|
||||||
latitude={wp.latitudeI / 1e7}
|
latitude={(wp.latitudeI ?? 0) / 1e7}
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<MapPinIcon size={16} />
|
<MapPinIcon size={16} />
|
||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
))}
|
))}
|
||||||
{/* {rasterSources.map((source, index) => (
|
{/* {rasterSources.map((source, index) => (
|
||||||
<Source key={index} type="raster" {...source}>
|
<Source key={index} type="raster" {...source}>
|
||||||
<Layer type="raster" />
|
<Layer type="raster" />
|
||||||
</Source>
|
</Source>
|
||||||
))} */}
|
))} */}
|
||||||
{allNodes.map((node) => {
|
{allNodes.map((node) => {
|
||||||
if (node.position?.latitudeI) {
|
if (node.position?.latitudeI) {
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
key={node.num}
|
key={node.num}
|
||||||
longitude={node.position.longitudeI / 1e7}
|
longitude={(node.position.longitudeI ?? 0) / 1e7}
|
||||||
latitude={node.position.latitudeI / 1e7}
|
latitude={(node.position.latitudeI ?? 0) / 1e7}
|
||||||
style={{ filter: darkMode ? "invert(1)" : "" }}
|
style={{ filter: darkMode ? "invert(1)" : "" }}
|
||||||
anchor="bottom"
|
anchor="bottom"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
map?.easeTo({
|
map?.easeTo({
|
||||||
zoom: 12,
|
zoom: 12,
|
||||||
center: [
|
center: [
|
||||||
(node.position?.longitudeI ?? 0) / 1e7,
|
(node.position?.longitudeI ?? 0) / 1e7,
|
||||||
(node.position?.latitudeI ?? 0) / 1e7,
|
(node.position?.latitudeI ?? 0) / 1e7,
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
||||||
<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}
|
||||||
</Subtle>
|
</Subtle>
|
||||||
</div>
|
</div>
|
||||||
</Marker>
|
</Marker>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</MapGl>
|
</MapGl>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+18
-1
@@ -8,7 +8,7 @@ import { useDevice } from "@core/stores/deviceStore.js";
|
|||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Protobuf, Types } from "@meshtastic/js";
|
import { Protobuf, Types } from "@meshtastic/js";
|
||||||
import { getChannelName } from "@pages/Channels.js";
|
import { getChannelName } from "@pages/Channels.js";
|
||||||
import { HashIcon, WaypointsIcon } from "lucide-react";
|
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const MessagesPage = (): JSX.Element => {
|
export const MessagesPage = (): JSX.Element => {
|
||||||
@@ -79,6 +79,23 @@ export const MessagesPage = (): JSX.Element => {
|
|||||||
actions={
|
actions={
|
||||||
chatType === "direct"
|
chatType === "direct"
|
||||||
? [
|
? [
|
||||||
|
{
|
||||||
|
icon: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? LockIcon
|
||||||
|
: LockOpenIcon,
|
||||||
|
iconClasses: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? "text-green-600"
|
||||||
|
: "text-yellow-300",
|
||||||
|
async onClick() {
|
||||||
|
const targetNode = nodes.get(activeChat)?.num;
|
||||||
|
if (targetNode === undefined) return;
|
||||||
|
toast({
|
||||||
|
title: nodes.get(activeChat)?.user?.publicKey.length
|
||||||
|
? "Chat is using PKI encryption."
|
||||||
|
: "Chat is using PSK encryption.",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: WaypointsIcon,
|
icon: WaypointsIcon,
|
||||||
async onClick() {
|
async onClick() {
|
||||||
|
|||||||
+9
-1
@@ -8,7 +8,7 @@ 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 { TrashIcon } from "lucide-react";
|
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { base16 } from "rfc4648";
|
import { base16 } from "rfc4648";
|
||||||
|
|
||||||
@@ -38,6 +38,7 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{ title: "MAC Address", type: "normal", sortable: true },
|
{ title: "MAC Address", type: "normal", sortable: true },
|
||||||
{ title: "Last Heard", type: "normal", sortable: true },
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
{ title: "SNR", type: "normal", sortable: true },
|
{ title: "SNR", type: "normal", sortable: true },
|
||||||
|
{ title: "Encryption", type: "normal", sortable: false },
|
||||||
{ title: "Connection", type: "normal", sortable: true },
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
{ title: "Remove", type: "normal", sortable: false },
|
{ title: "Remove", type: "normal", sortable: false },
|
||||||
]}
|
]}
|
||||||
@@ -73,6 +74,13 @@ export const NodesPage = (): JSX.Element => {
|
|||||||
{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 key="pki">
|
||||||
|
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
||||||
|
<LockIcon className="text-green-600" />
|
||||||
|
) : (
|
||||||
|
<LockOpenIcon className="text-yellow-300" />
|
||||||
|
)}
|
||||||
|
</Mono>,
|
||||||
<Mono key="hops">
|
<Mono key="hops">
|
||||||
{node.lastHeard !== 0
|
{node.lastHeard !== 0
|
||||||
? node.viaMqtt === false && node.hopsAway === 0
|
? node.viaMqtt === false && node.hopsAway === 0
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import { Protobuf } from "@meshtastic/js";
|
|||||||
import { IsBoolean, IsEnum, IsInt } from "class-validator";
|
import { IsBoolean, IsEnum, IsInt } from "class-validator";
|
||||||
|
|
||||||
export class BluetoothValidation
|
export class BluetoothValidation
|
||||||
implements Omit<Protobuf.Config.Config_BluetoothConfig, keyof Message>
|
implements
|
||||||
|
Omit<
|
||||||
|
Protobuf.Config.Config_BluetoothConfig,
|
||||||
|
keyof Message | "deviceLoggingEnabled"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { Protobuf } from "@meshtastic/js";
|
|||||||
import { IsArray, IsBoolean, IsEnum, IsInt, Max, Min } from "class-validator";
|
import { IsArray, IsBoolean, IsEnum, IsInt, Max, Min } from "class-validator";
|
||||||
|
|
||||||
export class LoRaValidation
|
export class LoRaValidation
|
||||||
implements Omit<Protobuf.Config.Config_LoRaConfig, keyof Message>
|
implements
|
||||||
|
Omit<Protobuf.Config.Config_LoRaConfig, keyof Message | "paFanDisabled">
|
||||||
{
|
{
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
usePreset: boolean;
|
usePreset: boolean;
|
||||||
@@ -59,4 +60,7 @@ export class LoRaValidation
|
|||||||
|
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
ignoreMqtt: boolean;
|
ignoreMqtt: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
configOkToMqtt: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import type { Protobuf } from "@meshtastic/js";
|
|||||||
import { IsBoolean, IsInt, IsNumber, Max, Min } from "class-validator";
|
import { IsBoolean, IsInt, IsNumber, Max, Min } from "class-validator";
|
||||||
|
|
||||||
export class PowerValidation
|
export class PowerValidation
|
||||||
implements Omit<Protobuf.Config.Config_PowerConfig, keyof Message>
|
implements
|
||||||
|
Omit<Protobuf.Config.Config_PowerConfig, keyof Message | "powermonEnables">
|
||||||
{
|
{
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
isPowerSaving: boolean;
|
isPowerSaving: boolean;
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import type { Message } from "@bufbuild/protobuf";
|
||||||
|
import type { Protobuf } from "@meshtastic/js";
|
||||||
|
import { IsBoolean, IsString } from "class-validator";
|
||||||
|
|
||||||
|
export class SecurityValidation
|
||||||
|
implements
|
||||||
|
Omit<
|
||||||
|
Protobuf.Config.Config_SecurityConfig,
|
||||||
|
keyof Message | "adminKey" | "privateKey" | "publicKey"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
@IsBoolean()
|
||||||
|
adminChannelEnabled: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
adminKey: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
bluetoothLoggingEnabled: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
debugLogApiEnabled: boolean;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
isManaged: boolean;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
privateKey: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
publicKey: string;
|
||||||
|
|
||||||
|
@IsBoolean()
|
||||||
|
serialEnabled: boolean;
|
||||||
|
}
|
||||||
@@ -4,7 +4,10 @@ import { IsBoolean, IsInt } from "class-validator";
|
|||||||
|
|
||||||
export class StoreForwardValidation
|
export class StoreForwardValidation
|
||||||
implements
|
implements
|
||||||
Omit<Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig, keyof Message>
|
Omit<
|
||||||
|
Protobuf.ModuleConfig.ModuleConfig_StoreForwardConfig,
|
||||||
|
keyof Message | "isServer"
|
||||||
|
>
|
||||||
{
|
{
|
||||||
@IsBoolean()
|
@IsBoolean()
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
|||||||
+1
-2
@@ -1,11 +1,10 @@
|
|||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
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";
|
||||||
|
|
||||||
import react from "@vitejs/plugin-react";
|
|
||||||
|
|
||||||
let hash = "";
|
let hash = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user