Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/web/public/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,5 +108,7 @@
"managed": "At least one admin key is requred if the node is managed.",
"key": "Key is required."
}
}
},
"yes": "Yes",
"no": "No"
}
9 changes: 7 additions & 2 deletions packages/web/public/i18n/locales/en/dialog.json
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@
"deviceMetrics": "Device Metrics:",
"hardware": "Hardware: ",
"lastHeard": "Last Heard: ",
"nodeHexPrefix": "Node Hex: !",
"nodeHexPrefix": "Node Hex: ",
"nodeNumber": "Node Number: ",
"position": "Position:",
"role": "Role: ",
Expand All @@ -102,7 +102,12 @@
"title": "Node Details for {{identifier}}",
"ignoreNode": "Ignore node",
"removeNode": "Remove node",
"unignoreNode": "Unignore node"
"unignoreNode": "Unignore node",
"security": "Security:",
"publicKey": "Public Key: ",
"messageable": "Messageable: ",
"KeyManuallyVerifiedTrue": "Public Key has been manually verified",
"KeyManuallyVerifiedFalse": "Public Key is not manually verified"
},
"pkiBackup": {
"loseKeysWarning": "If you lose your keys, you will need to reset your device.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { cn } from "@core/utils/cn.ts";
import { Protobuf } from "@meshtastic/core";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
import { useNavigate } from "@tanstack/react-router";
import { fromByteArray } from "base64-js";
import {
BellIcon,
BellOffIcon,
Expand Down Expand Up @@ -167,7 +168,8 @@ export const NodeDetailsDialog = ({
key: "batteryLevel",
label: t("nodeDetails.batteryLevel"),
value: node.deviceMetrics?.batteryLevel,
format: (val: number) => `${val.toFixed(2)}%`,
format: (val: number) =>
val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`,
},
{
key: "voltage",
Expand All @@ -177,6 +179,9 @@ export const NodeDetailsDialog = ({
},
];

const sectionClassName =
"text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-4 rounded-lg mt-3";

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent aria-describedby={undefined}>
Expand All @@ -192,7 +197,7 @@ export const NodeDetailsDialog = ({
</DialogTitle>
</DialogHeader>
<DialogFooter>
<div className="w-full">
<div className="w-full ">
<div className="flex flex-row flex-wrap space-y-1">
<Button
className="mr-1"
Expand Down Expand Up @@ -270,79 +275,134 @@ export const NodeDetailsDialog = ({
<p className="text-lg font-semibold">
{t("nodeDetails.details")}
</p>
<p>
{t("nodeDetails.nodeNumber")}
{node.num}
</p>
<p>
{t("nodeDetails.nodeHexPrefix")}
{numberToHexUnpadded(node.num)}
</p>
<p>
{t("nodeDetails.role")}
{Protobuf.Config.Config_DeviceConfig_Role[
node.user?.role ?? 0
].replace(/_/g, " ")}
</p>
<p>
{t("nodeDetails.lastHeard")}
{node.lastHeard === 0 ? (
t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
) : (
<TimeAgo timestamp={node.lastHeard * 1000} />
)}
</p>
<p>
{t("nodeDetails.hardware")}
{(
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
t("unknown.shortName")
).replace(/_/g, " ")}
</p>
<table className="table-fixed w-full">
<tbody>
<tr>
<td>{t("nodeDetails.nodeNumber")}</td>
<td>{node.num}</td>
</tr>
<tr>
<td>{t("nodeDetails.nodeHexPrefix")}</td>
<td>!{numberToHexUnpadded(node.num)}</td>
</tr>
<tr>
<td>{t("nodeDetails.role")}</td>
<td>
{Protobuf.Config.Config_DeviceConfig_Role[
node.user?.role ?? 0
]?.replace(/_/g, " ")}
</td>
</tr>
<tr>
<td>{t("nodeDetails.lastHeard")}</td>
<td>
{node.lastHeard === 0 ? (
t("nodesTable.lastHeardStatus.never", {
ns: "nodes",
})
) : (
<TimeAgo timestamp={node.lastHeard * 1000} />
)}
</td>
</tr>
<tr>
<td>{t("nodeDetails.hardware")}</td>
<td>
{(
Protobuf.Mesh.HardwareModel[
node.user?.hwModel ?? 0
] ?? t("unknown.shortName")
).replace(/_/g, " ")}
</td>
</tr>
<tr>
<td>{t("nodeDetails.messageable")}</td>
<td>
{node.user?.isUnmessagable ? t("no") : t("yes")}
</td>
</tr>
</tbody>
</table>
</div>
<DeviceImage
className="h-45 w-45 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
className="w-40 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
deviceType={
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
"UNKNOWN"
}
/>
</div>
</div>

<div>
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<div className={sectionClassName}>
<p className="text-lg font-semibold">
{t("nodeDetails.security")}
</p>
<table className="table-auto w-full">
<tbody>
<tr>
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
<td>
<pre className="text-xs pt-0.5">
{node.user?.publicKey &&
node.user?.publicKey.length > 0
? fromByteArray(node.user.publicKey)
: t("unknown.longName")}
</pre>
</td>
</tr>
<tr>
<td></td>
<td>
{node.isKeyManuallyVerified
? t("nodeDetails.KeyManuallyVerifiedTrue")
: t("nodeDetails.KeyManuallyVerifiedFalse")}
</td>
</tr>
</tbody>
</table>
</div>

<div className={sectionClassName}>
<p className="text-lg font-semibold">
{t("nodeDetails.position")}
</p>

{node.position ? (
<>
{node.position.latitudeI && node.position.longitudeI && (
<p>
{t("locationResponse.coordinates")}
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
node.position.latitudeI / 1e7
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{node.position.latitudeI / 1e7},{" "}
{node.position.longitudeI / 1e7}
</a>
</p>
)}
{node.position.altitude && (
<p>
{t("locationResponse.altitude")}
{node.position.altitude}
{t("unit.meter.one")}
</p>
)}
</>
<table className="table-auto w-full">
<tbody>
{node.position.latitudeI && node.position.longitudeI && (
<tr>
<td>{t("locationResponse.coordinates")}</td>
<td>
<a
className="text-blue-500 dark:text-blue-400"
href={`https://www.openstreetmap.org/?mlat=${
node.position.latitudeI / 1e7
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
target="_blank"
rel="noreferrer"
>
{node.position.latitudeI / 1e7},{" "}
{node.position.longitudeI / 1e7}
</a>
</td>
</tr>
)}
{node.position.altitude && (
<tr>
<td>{t("locationResponse.altitude")}</td>
<td>
{node.position.altitude}
{t("unit.meter.suffix")}
</td>
</tr>
)}
</tbody>
</table>
) : (
<p>{t("unknown.shortName")}</p>
<p>{t("unknown.longName")}</p>
)}
<Button
onClick={handleRequestPosition}
Expand All @@ -355,28 +415,37 @@ export const NodeDetailsDialog = ({
</div>

{node.deviceMetrics && (
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<div className={sectionClassName}>
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
{t("nodeDetails.deviceMetrics")}
</p>
{deviceMetricsMap
.filter((metric) => metric.value !== undefined)
.map((metric) => (
<p key={metric.key}>
{metric.label}: {metric.format(metric?.value ?? 0)}
</p>
))}
{node.deviceMetrics.uptimeSeconds && (
<p>
{t("nodeDetails.uptime")}
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
</p>
)}
<table className="table-fixed w-full">
<tbody>
{deviceMetricsMap
.filter((metric) => metric.value !== undefined)
.map((metric) => (
<tr key={metric.key}>
<td>{metric.label}: </td>
<td>{metric.format(metric?.value ?? 0)}</td>
</tr>
))}
{node.deviceMetrics.uptimeSeconds && (
<tr>
<td>{t("nodeDetails.uptime")}</td>
<td>
<Uptime
seconds={node.deviceMetrics.uptimeSeconds}
/>
</td>
</tr>
)}
</tbody>
</table>
</div>
)}
</div>

<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 rounded-lg mt-3">
<Accordion className="AccordionRoot" type="single" collapsible>
<AccordionItem className="AccordionItem" value="item-1">
<AccordionTrigger>
Expand Down
8 changes: 6 additions & 2 deletions packages/web/src/pages/Nodes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,10 @@ const NodesPage = (): JSX.Element => {

const handleLocation = useCallback(
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
if (location.to.valueOf() !== hardware.myNodeNum) {
if (
location.to.valueOf() !== hardware.myNodeNum ||
location.from.valueOf() === hardware.myNodeNum
) {
return;
}
setSelectedLocation(location);
Expand Down Expand Up @@ -213,7 +216,8 @@ const NodesPage = (): JSX.Element => {
content: (
<Mono>{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}</Mono>
),
sortValue: Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0],
sortValue:
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNSET",
},
{
content: <Mono>{macAddress}</Mono>,
Expand Down