mirror of
https://github.com/meshtastic/web.git
synced 2025-12-25 08:39:12 -05:00
Compare commits
43 Commits
pre-releas
...
v2.5.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06d2c393ce | ||
|
|
cecdf9758b | ||
|
|
02cb4f2584 | ||
|
|
8cfcd7b1af | ||
|
|
c0cb059f52 | ||
|
|
a2a9b37238 | ||
|
|
57d0d27bbb | ||
|
|
0e92dd9bea | ||
|
|
c16ebf3917 | ||
|
|
3d3a08a23f | ||
|
|
4d1227a942 | ||
|
|
a8ee273b24 | ||
|
|
3ee7a57480 | ||
|
|
2f2c777c56 | ||
|
|
2f36118e9d | ||
|
|
a6d161581f | ||
|
|
d05ea5a2cc | ||
|
|
471db94242 | ||
|
|
2654e4fbc9 | ||
|
|
f2aa5bfbee | ||
|
|
3b018b0c70 | ||
|
|
921db10d91 | ||
|
|
bf4f593e3a | ||
|
|
1e061a1e19 | ||
|
|
9b9f537e2c | ||
|
|
985cce0b0d | ||
|
|
3fe38eb506 | ||
|
|
51081d3052 | ||
|
|
c08f6d16bb | ||
|
|
62ad4c49f8 | ||
|
|
3b0a1e6108 | ||
|
|
c2f2205626 | ||
|
|
87c729d694 | ||
|
|
8e4f60edf3 | ||
|
|
8811eee9f5 | ||
|
|
2af93f1acd | ||
|
|
78a35544c7 | ||
|
|
3ad2d650b0 | ||
|
|
bf425a8ec7 | ||
|
|
a7d0d36086 | ||
|
|
8ed3ce8203 | ||
|
|
ebd5a3d3a6 | ||
|
|
1cdf18747d |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -27,6 +27,12 @@ jobs:
|
||||
- name: Package Output
|
||||
run: pnpm package
|
||||
|
||||
- name: Archive compressed build
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build
|
||||
path: dist/build.tar
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^1.10.0",
|
||||
"@emeraldpay/hashicon-react": "^0.5.2",
|
||||
"@meshtastic/js": "2.3.7-1",
|
||||
"@meshtastic/js": "2.3.7-5",
|
||||
"@noble/curves": "^1.5.0",
|
||||
"@radix-ui/react-accordion": "^1.2.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.0",
|
||||
@@ -49,7 +49,7 @@
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"immer": "^10.1.1",
|
||||
"lucide-react": "^0.363.0",
|
||||
"mapbox-gl": "npm:empty-npm-package@^1.0.0",
|
||||
"mapbox-gl": "^3.6.0",
|
||||
"maplibre-gl": "4.1.2",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
@@ -60,11 +60,12 @@
|
||||
"tailwind-merge": "^2.3.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"timeago-react": "^3.0.6",
|
||||
"vite-plugin-node-polyfills": "^0.22.0",
|
||||
"zustand": "4.5.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.8.2",
|
||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240820152623-fac6975bbc78.1",
|
||||
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1",
|
||||
"@types/chrome": "^0.0.263",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/react": "^18.3.3",
|
||||
|
||||
792
pnpm-lock.yaml
generated
792
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -111,10 +111,14 @@ export const Channel = ({ channel }: SettingsPanelProps): JSX.Element => {
|
||||
type: "select",
|
||||
name: "role",
|
||||
label: "Role",
|
||||
disabled: channel.index === 0,
|
||||
description:
|
||||
"Device telemetry is sent over PRIMARY. Only one PRIMARY allowed",
|
||||
properties: {
|
||||
enumValue: Protobuf.Channel.Channel_Role,
|
||||
enumValue:
|
||||
channel.index === 0
|
||||
? { PRIMARY: 1 }
|
||||
: { DISABLED: 0, SECONDARY: 2 },
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -32,7 +32,22 @@ export const Device = (): JSX.Element => {
|
||||
label: "Role",
|
||||
description: "What role the device performs on the mesh",
|
||||
properties: {
|
||||
enumValue: Protobuf.Config.Config_DeviceConfig_Role,
|
||||
enumValue: {
|
||||
Client: Protobuf.Config.Config_DeviceConfig_Role.CLIENT,
|
||||
"Client Mute":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_MUTE,
|
||||
Router: Protobuf.Config.Config_DeviceConfig_Role.ROUTER,
|
||||
Repeater: Protobuf.Config.Config_DeviceConfig_Role.REPEATER,
|
||||
Tracker: Protobuf.Config.Config_DeviceConfig_Role.TRACKER,
|
||||
Sensor: Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
||||
TAK: Protobuf.Config.Config_DeviceConfig_Role.TAK,
|
||||
"Client Hidden":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.CLIENT_HIDDEN,
|
||||
"Lost and Found":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.LOST_AND_FOUND,
|
||||
"TAK Tracker":
|
||||
Protobuf.Config.Config_DeviceConfig_Role.SENSOR,
|
||||
},
|
||||
formatEnumName: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => {
|
||||
label: "Ignore MQTT",
|
||||
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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import type { NetworkValidation } from "@app/validation/config/network.js";
|
||||
import { DynamicForm } from "@components/Form/DynamicForm.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import {
|
||||
convertIntToIpAddress,
|
||||
convertIpAddressToInt,
|
||||
} from "@core/utils/ip.js";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
|
||||
export const Network = (): JSX.Element => {
|
||||
@@ -13,9 +17,12 @@ export const Network = (): JSX.Element => {
|
||||
case: "network",
|
||||
value: {
|
||||
...data,
|
||||
ipv4Config: new Protobuf.Config.Config_NetworkConfig_IpV4Config(
|
||||
data.ipv4Config,
|
||||
),
|
||||
ipv4Config: new Protobuf.Config.Config_NetworkConfig_IpV4Config({
|
||||
ip: convertIpAddressToInt(data.ipv4Config.ip) ?? 0,
|
||||
gateway: convertIpAddressToInt(data.ipv4Config.gateway) ?? 0,
|
||||
subnet: convertIpAddressToInt(data.ipv4Config.subnet) ?? 0,
|
||||
dns: convertIpAddressToInt(data.ipv4Config.dns) ?? 0,
|
||||
}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
@@ -25,7 +32,19 @@ export const Network = (): JSX.Element => {
|
||||
return (
|
||||
<DynamicForm<NetworkValidation>
|
||||
onSubmit={onSubmit}
|
||||
defaultValues={config.network}
|
||||
defaultValues={{
|
||||
...config.network,
|
||||
ipv4Config: {
|
||||
ip: convertIntToIpAddress(config.network?.ipv4Config?.ip ?? 0),
|
||||
gateway: convertIntToIpAddress(
|
||||
config.network?.ipv4Config?.gateway ?? 0,
|
||||
),
|
||||
subnet: convertIntToIpAddress(
|
||||
config.network?.ipv4Config?.subnet ?? 0,
|
||||
),
|
||||
dns: convertIntToIpAddress(config.network?.ipv4Config?.dns ?? 0),
|
||||
},
|
||||
}}
|
||||
fieldGroups={[
|
||||
{
|
||||
label: "WiFi Config",
|
||||
|
||||
@@ -77,12 +77,6 @@ export const Position = (): JSX.Element => {
|
||||
label: "Enable Pin",
|
||||
description: "GPS module enable pin override",
|
||||
},
|
||||
{
|
||||
type: "number",
|
||||
name: "channelPrecision",
|
||||
label: "Channel Precision",
|
||||
description: "GPS channel precision",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ export const Security = (): JSX.Element => {
|
||||
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
|
||||
);
|
||||
const [adminKey, setAdminKey] = useState<string>(
|
||||
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)),
|
||||
fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)),
|
||||
);
|
||||
const [adminKeyValidationText, setAdminKeyValidationText] =
|
||||
useState<string>();
|
||||
@@ -42,7 +42,7 @@ export const Security = (): JSX.Element => {
|
||||
case: "security",
|
||||
value: {
|
||||
...data,
|
||||
adminKey: toByteArray(adminKey),
|
||||
adminKey: [toByteArray(adminKey)],
|
||||
privateKey: toByteArray(privateKey),
|
||||
publicKey: toByteArray(publicKey),
|
||||
},
|
||||
@@ -129,8 +129,6 @@ export const Security = (): JSX.Element => {
|
||||
publicKey: publicKey,
|
||||
adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
|
||||
isManaged: config.security?.isManaged ?? false,
|
||||
bluetoothLoggingEnabled:
|
||||
config.security?.bluetoothLoggingEnabled ?? false,
|
||||
debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
|
||||
serialEnabled: config.security?.serialEnabled ?? false,
|
||||
},
|
||||
@@ -212,18 +210,12 @@ export const Security = (): JSX.Element => {
|
||||
label: "Logging Settings",
|
||||
description: "Settings for Logging",
|
||||
fields: [
|
||||
{
|
||||
type: "toggle",
|
||||
name: "bluetoothLoggingEnabled",
|
||||
label: "Allow Bluetooth Logging",
|
||||
description:
|
||||
"Enables device (serial style logs) over Bluetooth",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
name: "debugLogApiEnabled",
|
||||
label: "Enable Debug Log API",
|
||||
description: "Output live debug logging over serial",
|
||||
description:
|
||||
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
|
||||
},
|
||||
{
|
||||
type: "toggle",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useDevice } from "@app/core/stores/deviceStore.js";
|
||||
import type { Protobuf } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
|
||||
export interface TraceRouteProps {
|
||||
from?: Protobuf.Mesh.NodeInfo;
|
||||
@@ -24,7 +25,10 @@ export const TraceRoute = ({
|
||||
<div className="ml-5 flex">
|
||||
<span className="ml-4 border-l-2 border-l-backgroundPrimary pl-2 text-textPrimary">
|
||||
{to?.user?.longName}↔
|
||||
{route.map((hop) => `${nodes.get(hop)?.user?.longName ?? "Unknown"}↔`)}
|
||||
{route.map((hop) => {
|
||||
const node = nodes.get(hop);
|
||||
return `${node?.user?.longName ?? (node?.num ? numberToHexUnpadded(node.num) : "Unknown")}↔`;
|
||||
})}
|
||||
{from?.user?.longName}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface PageLayoutProps {
|
||||
children: React.ReactNode;
|
||||
actions?: {
|
||||
icon: LucideIcon;
|
||||
iconClasses?: string;
|
||||
onClick: () => void;
|
||||
}[];
|
||||
}
|
||||
@@ -39,7 +40,7 @@ export const PageLayout = ({
|
||||
className="transition-all hover:text-accent"
|
||||
onClick={action.onClick}
|
||||
>
|
||||
<action.icon />
|
||||
<action.icon className={action.iconClasses} />
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
14
src/core/utils/ip.ts
Normal file
14
src/core/utils/ip.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export function convertIntToIpAddress(int: number): string {
|
||||
return `${int & 0xff}.${(int >> 8) & 0xff}.${(int >> 16) & 0xff}.${(int >> 24) & 0xff}`;
|
||||
}
|
||||
|
||||
export function convertIpAddressToInt(ip: string): number | null {
|
||||
return (
|
||||
ip
|
||||
.split(".")
|
||||
.reverse()
|
||||
.reduce((ipnum, octet) => {
|
||||
return (ipnum << 8) + Number.parseInt(octet);
|
||||
}, 0) >>> 0
|
||||
);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export const Dashboard = () => {
|
||||
<div className="m-auto flex flex-col gap-3 text-center">
|
||||
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
|
||||
<H3>No Devices</H3>
|
||||
<Subtle>Connect atleast one device to get started</Subtle>
|
||||
<Subtle>Connect at least one device to get started</Subtle>
|
||||
<Button
|
||||
className="gap-2"
|
||||
onClick={() => setConnectDialogOpen(true)}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { SidebarButton } from "@components/UI/Sidebar/sidebarButton.js";
|
||||
import { useAppStore } from "@core/stores/appStore.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { bbox, lineString } from "@turf/turf";
|
||||
import {
|
||||
BoxSelectIcon,
|
||||
@@ -180,7 +181,8 @@ export const MapPage = (): JSX.Element => {
|
||||
<div className="flex cursor-pointer gap-2 rounded-md border bg-backgroundPrimary p-1.5">
|
||||
<Hashicon value={node.num.toString()} size={22} />
|
||||
<Subtle className={cn(zoom < 12 && "hidden")}>
|
||||
{node.user?.longName}
|
||||
{node.user?.longName ||
|
||||
`!${numberToHexUnpadded(node.num)}`}
|
||||
</Subtle>
|
||||
</div>
|
||||
</Marker>
|
||||
|
||||
@@ -7,8 +7,9 @@ import { useToast } from "@core/hooks/useToast.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { Protobuf, Types } from "@meshtastic/js";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { getChannelName } from "@pages/Channels.js";
|
||||
import { HashIcon, WaypointsIcon } from "lucide-react";
|
||||
import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export const MessagesPage = (): JSX.Element => {
|
||||
@@ -28,6 +29,8 @@ export const MessagesPage = (): JSX.Element => {
|
||||
);
|
||||
const currentChannel = channels.get(activeChat);
|
||||
const { toast } = useToast();
|
||||
const node = nodes.get(activeChat);
|
||||
const nodeHex = node?.num ? numberToHexUnpadded(node.num) : "Unknown";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -56,7 +59,7 @@ export const MessagesPage = (): JSX.Element => {
|
||||
{filteredNodes.map((node) => (
|
||||
<SidebarButton
|
||||
key={node.num}
|
||||
label={node.user?.longName ?? "Unknown"}
|
||||
label={node.user?.longName ?? `!${numberToHexUnpadded(node.num)}`}
|
||||
active={activeChat === node.num}
|
||||
onClick={() => {
|
||||
setChatType("direct");
|
||||
@@ -73,12 +76,29 @@ export const MessagesPage = (): JSX.Element => {
|
||||
chatType === "broadcast" && currentChannel
|
||||
? getChannelName(currentChannel)
|
||||
: chatType === "direct" && nodes.get(activeChat)
|
||||
? nodes.get(activeChat)?.user?.longName ?? "Unknown"
|
||||
? nodes.get(activeChat)?.user?.longName ?? nodeHex
|
||||
: "Loading..."
|
||||
}`}
|
||||
actions={
|
||||
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,
|
||||
async onClick() {
|
||||
|
||||
@@ -8,7 +8,8 @@ import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
|
||||
import { useDevice } from "@core/stores/deviceStore.js";
|
||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||
import { Protobuf } from "@meshtastic/js";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||
import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
|
||||
import { Fragment } from "react";
|
||||
import { base16 } from "rfc4648";
|
||||
|
||||
@@ -38,6 +39,7 @@ export const NodesPage = (): JSX.Element => {
|
||||
{ title: "MAC Address", type: "normal", sortable: true },
|
||||
{ title: "Last Heard", type: "normal", sortable: true },
|
||||
{ title: "SNR", type: "normal", sortable: true },
|
||||
{ title: "Encryption", type: "normal", sortable: false },
|
||||
{ title: "Connection", type: "normal", sortable: true },
|
||||
{ title: "Remove", type: "normal", sortable: false },
|
||||
]}
|
||||
@@ -49,7 +51,7 @@ export const NodesPage = (): JSX.Element => {
|
||||
? `Meshtastic ${base16
|
||||
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
||||
.toLowerCase()}`
|
||||
: `UNK: ${node.num}`)}
|
||||
: `!${numberToHexUnpadded(node.num)}`)}
|
||||
</h1>,
|
||||
|
||||
<Mono key="model">
|
||||
@@ -73,6 +75,13 @@ export const NodesPage = (): JSX.Element => {
|
||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
||||
{(node.snr + 10) * 5}raw
|
||||
</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">
|
||||
{node.lastHeard !== 0
|
||||
? node.viaMqtt === false && node.hopsAway === 0
|
||||
|
||||
@@ -60,4 +60,7 @@ export class LoRaValidation
|
||||
|
||||
@IsBoolean()
|
||||
ignoreMqtt: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
configOkToMqtt: boolean;
|
||||
}
|
||||
|
||||
@@ -41,21 +41,24 @@ export class NetworkValidation
|
||||
|
||||
export class NetworkValidationIpV4Config
|
||||
implements
|
||||
Omit<Protobuf.Config.Config_NetworkConfig_IpV4Config, keyof Message>
|
||||
Omit<
|
||||
Protobuf.Config.Config_NetworkConfig_IpV4Config,
|
||||
keyof Message | "ip" | "gateway" | "subnet" | "dns"
|
||||
>
|
||||
{
|
||||
@IsIP()
|
||||
@IsOptional()
|
||||
ip: number;
|
||||
ip: string;
|
||||
|
||||
@IsIP()
|
||||
@IsOptional()
|
||||
gateway: number;
|
||||
gateway: string;
|
||||
|
||||
@IsIP()
|
||||
@IsOptional()
|
||||
subnet: number;
|
||||
subnet: string;
|
||||
|
||||
@IsIP()
|
||||
@IsOptional()
|
||||
dns: number;
|
||||
dns: string;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,4 @@ export class PositionValidation
|
||||
|
||||
@IsEnum(Protobuf.Config.Config_PositionConfig_GpsMode)
|
||||
gpsMode: Protobuf.Config.Config_PositionConfig_GpsMode;
|
||||
|
||||
@IsArray()
|
||||
channelPrecision: number[];
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import { resolve } from "node:path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
import { defineConfig } from "vite";
|
||||
import EnvironmentPlugin from "vite-plugin-environment";
|
||||
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
let hash = "";
|
||||
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user