Skip to content
Open
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
9 changes: 9 additions & 0 deletions packages/web/public/i18n/locales/en/ui.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@
"title": "Firmware",
"version": "v{{version}}",
"buildDate": "Build date: {{date}}"
},
"channelUtil": {
"title": "Ch Util"
},
"airUtilTx": {
"title": "TX Airtime"
},
"uptime": {
"title": "Uptime"
}
Comment on lines +31 to 40
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New sidebar.deviceInfo.* UI labels were added only to en/ui.json. Other locales’ ui.json files don’t contain these keys, so non-English UIs will likely display the raw i18n key. Either add the corresponding entries to the other locale ui.json files or provide default/fallback strings in the t(...) calls for these new labels.

Copilot uses AI. Check for mistakes.
}
},
Expand Down
29 changes: 28 additions & 1 deletion packages/web/src/components/DeviceInfoPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import type { DeviceMetrics } from "./types.ts";
import { Avatar } from "./UI/Avatar.tsx";
import { Button } from "./UI/Button.tsx";
import { Subtle } from "./UI/Typography/Subtle.tsx";
import { Uptime } from "./generic/Uptime.tsx";

interface DeviceInfoPanelProps {
isCollapsed: boolean;
Expand Down Expand Up @@ -62,7 +63,7 @@ export const DeviceInfoPanel = ({
}: DeviceInfoPanelProps) => {
const { t } = useTranslation();
const navigate = useNavigate({ from: "/" });
const { batteryLevel, voltage } = deviceMetrics;
const { batteryLevel, voltage, channelUtilization, airUtilTx, uptimeSeconds} = deviceMetrics;

const getStatusColor = (status?: ConnectionStatus): string => {
if (!status) {
Expand Down Expand Up @@ -117,6 +118,32 @@ export const DeviceInfoPanel = ({
icon: CpuIcon,
value: firmwareVersion ?? t("unknown.notAvailable", "N/A"),
},
{
id: "channelUtil",
label: t("sidebar.deviceInfo.channelUtil.title"),
value:
channelUtilization !== undefined
? `${(channelUtilization).toFixed(1)}%`
Comment on lines +125 to +126
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

channelUtilization is typed as number | null | undefined, but this check only excludes undefined. If it is null, calling .toFixed(1) will throw at runtime. Prefer guarding with typeof channelUtilization === "number" (or channelUtilization != null) before formatting, and fall back to N/A otherwise.

Suggested change
channelUtilization !== undefined
? `${(channelUtilization).toFixed(1)}%`
typeof channelUtilization === "number"
? `${channelUtilization.toFixed(1)}%`

Copilot uses AI. Check for mistakes.
: "N/A",
},
{
id: "airUtilTx",
label: t("sidebar.deviceInfo.airUtilTx.title"),
value:
airUtilTx !== undefined
? `${(airUtilTx).toFixed(1)}%`
Comment on lines +133 to +134
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

airUtilTx is typed as number | null | undefined, but this check only excludes undefined. If it is null, calling .toFixed(1) will throw at runtime. Guard with typeof airUtilTx === "number" (or airUtilTx != null) before formatting.

Suggested change
airUtilTx !== undefined
? `${(airUtilTx).toFixed(1)}%`
typeof airUtilTx === "number"
? `${airUtilTx.toFixed(1)}%`

Copilot uses AI. Check for mistakes.
: "N/A",
},
{
id: "uptimeSeconds",
label: t("sidebar.deviceInfo.uptime.title"),
value:
uptimeSeconds !== undefined ? (
<Uptime seconds={uptimeSeconds} />
) : (
"N/A"
)
Comment on lines +140 to +145
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

uptimeSeconds is typed as number | null | undefined, but this only checks !== undefined. If uptimeSeconds is null, it will be passed to <Uptime seconds={...} /> (expects a number) and can break formatting/rendering. Also, InfoDisplayItem.value is typed as string | number | null, but this branch assigns a React element; either render uptime as a string or widen the value type to React.ReactNode (and ensure the render path supports it).

Suggested change
value:
uptimeSeconds !== undefined ? (
<Uptime seconds={uptimeSeconds} />
) : (
"N/A"
)
customComponent:
uptimeSeconds != null ? <Uptime seconds={uptimeSeconds} /> : undefined,
value: uptimeSeconds != null ? null : "N/A",

Copilot uses AI. Check for mistakes.
}
];

const actionButtons: ActionButtonConfig[] = [
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,9 @@ export const Sidebar = ({ children }: SidebarProps) => {
typeof myNode.deviceMetrics?.voltage === "number"
? Math.abs(myNode.deviceMetrics?.voltage)
: undefined,
channelUtilization: myNode.deviceMetrics?.channelUtilization,
airUtilTx: myNode.deviceMetrics?.airUtilTx,
uptimeSeconds: myNode.deviceMetrics?.uptimeSeconds,
}}
connectionStatus={activeConnection?.status}
connectionName={activeConnection?.name}
Expand Down
14 changes: 11 additions & 3 deletions packages/web/src/components/generic/Uptime.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ export interface UptimeProps {
const getUptime = (seconds: number): string => {
const days = Math.floor(seconds / 86400);
const hours = Math.floor((seconds % 86400) / 3600);
const minutes = Math.floor(((seconds % 86400) % 3600) / 60);
const secondsLeft = Math.floor(((seconds % 86400) % 3600) % 60);
return `${days}d ${hours}h ${minutes}m ${secondsLeft}s`;
const minutes = Math.floor((seconds % 3600) / 60);
const secs = Math.floor(seconds % 60);

const parts: string[] = [];

if (days > 0) parts.push(`${days}d`);
if (hours > 0) parts.push(`${hours}h`);
if (minutes > 0) parts.push(`${minutes}m`);
if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);

return parts.join(" ");
};

export const Uptime = ({ seconds }: UptimeProps) => {
Expand Down
3 changes: 3 additions & 0 deletions packages/web/src/components/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export type DeviceMetrics = {
batteryLevel?: number | null;
voltage?: number | null;
channelUtilization?: number | null;
airUtilTx?: number | null;
uptimeSeconds?: number | null;
};
10 changes: 8 additions & 2 deletions packages/web/src/core/subscriptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,14 @@ export const subscribeAll = (
}
});

connection.events.onTelemetryPacket.subscribe(() => {
// device.setMetrics(telemetryPacket);
connection.events.onTelemetryPacket.subscribe((packet) => {
const metrics = packet.data.variant.value;
const existing = nodeDB.getNode(packet.from);
nodeDB.addNode({
...(existing ?? {}),
num: packet.from,
deviceMetrics: { ...metrics },
});
Comment on lines +44 to +51
Copy link

Copilot AI Apr 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler assumes packet.data.variant.value is always DeviceMetrics. Telemetry packets typically have multiple variant.case values (e.g. device/environment/power metrics). Without checking packet.data.variant.case before assigning into node.deviceMetrics, non-device telemetry variants could overwrite deviceMetrics with the wrong shape. Add an explicit guard (e.g. only update when variant.case === "deviceMetrics").

Copilot uses AI. Check for mistakes.
});

connection.events.onDeviceStatus.subscribe((status) => {
Expand Down