From 6f00e857c266c0b13a82953a00390aaba5c98f5e Mon Sep 17 00:00:00 2001 From: Jack Kingsman Date: Thu, 16 Apr 2026 23:49:52 -0700 Subject: [PATCH] Suck less at settings UI (help me I'm not a designer) --- frontend/AGENTS.md | 4 +- .../settings/SettingsDatabaseSection.tsx | 286 +++++++++--------- .../settings/SettingsFanoutSection.tsx | 100 +++--- .../settings/SettingsLocalSection.tsx | 208 +++++++------ .../settings/SettingsRadioSection.tsx | 267 ++++++++-------- .../settings/SettingsStatisticsSection.tsx | 20 +- frontend/src/test/settingsModal.test.tsx | 2 +- 7 files changed, 460 insertions(+), 427 deletions(-) diff --git a/frontend/AGENTS.md b/frontend/AGENTS.md index 35d8ffd..ced9ca8 100644 --- a/frontend/AGENTS.md +++ b/frontend/AGENTS.md @@ -453,7 +453,9 @@ Do not rely on old class-only layout assumptions. Key conventions documented in the reference: - **Text sizes** use `rem`-based Tailwind values so they scale with the user's font-size slider. Do not use hard-locked `px` values (e.g., `text-[10px]`). The canonical sizes are `text-[0.625rem]` (10px), `text-[0.6875rem]` (11px), `text-[0.8125rem]` (13px), plus standard Tailwind `text-xs`/`text-sm`/`text-base`/`text-lg`/`text-xl`. -- **Section labels** use `text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium`. +- **Group titles** (sub-section headings within settings tabs) use `

`. These separate major groups like "Connection", "Identity", "MQTT Broker". When a group contains named sub-items (e.g. "Contact Management" → "Blocked Contacts", "Bulk Delete"), use `

` for the children and nest them inside the parent group's `div` instead of separating with ``. +- **Helper / description text** uses `text-[0.8125rem] text-muted-foreground` (13px). This is for explanatory paragraphs under inputs or sections — not for metadata, timestamps, or alert text which stay at `text-xs`. +- **Metadata labels** use `text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium` for compact category tags like "Push-enabled conversations" or "Registered Devices". - **Buttons** use the shadcn ` - )} - - ))} - - - )} - {blockedNames.length > 0 && ( -
- Blocked Names -
- {blockedNames.map((name) => ( -
- {name} - {onToggleBlockedName && ( - - )} -
- ))} -
-
- )} +
+ {( + [ + [1, 'Block clients'], + [2, 'Block repeaters'], + [3, 'Block room servers'], + [4, 'Block sensors'], + ] as const + ).map(([typeCode, label]) => { + const checked = discoveryBlockedTypes.includes(typeCode); + return ( + + ); + })}
- )} - + {discoveryBlockedTypes.length > 0 && ( +

+ New{' '} + {discoveryBlockedTypes + .map((t) => + t === 1 ? 'clients' : t === 2 ? 'repeaters' : t === 3 ? 'room servers' : 'sensors' + ) + .join(', ')}{' '} + heard via advertisement will not be added to your contact list. +

+ )} + - + {/* Blocked contacts list */} +
+

Blocked Contacts

+

+ Blocked contacts are hidden from the sidebar. Blocking only hides messages from the UI — + MQTT forwarding and bot responses are not affected. Messages are still stored and will + reappear if unblocked. +

- {/* Bulk delete */} -
- -

- Remove multiple contacts or repeaters at once. Useful for cleaning up spam or unwanted - nodes. Message history will be preserved. -

- - setBulkDeleteOpen(false)} - contacts={contacts} - onDeleted={(keys) => onBulkDeleteContacts?.(keys)} - /> + {blockedKeys.length === 0 && blockedNames.length === 0 ? ( +

+ No blocked contacts. Block contacts from their info pane, viewed by clicking their + avatar in any channel, or their name within the top status bar with the conversation + open. +

+ ) : ( +
+ {blockedKeys.length > 0 && ( +
+ Blocked Keys +
+ {blockedKeys.map((key) => ( +
+ {key} + {onToggleBlockedKey && ( + + )} +
+ ))} +
+
+ )} + {blockedNames.length > 0 && ( +
+ Blocked Names +
+ {blockedNames.map((name) => ( +
+ {name} + {onToggleBlockedName && ( + + )} +
+ ))} +
+
+ )} +
+ )} +
+ + {/* Bulk delete */} +
+

Bulk Delete Contacts

+

+ Remove multiple contacts or repeaters at once. Useful for cleaning up spam or unwanted + nodes. Message history will be preserved. +

+ + setBulkDeleteOpen(false)} + contacts={contacts} + onDeleted={(keys) => onBulkDeleteContacts?.(keys)} + /> +
); diff --git a/frontend/src/components/settings/SettingsFanoutSection.tsx b/frontend/src/components/settings/SettingsFanoutSection.tsx index ffd5b2f..9be7264 100644 --- a/frontend/src/components/settings/SettingsFanoutSection.tsx +++ b/frontend/src/components/settings/SettingsFanoutSection.tsx @@ -729,7 +729,7 @@ function MqttPrivateConfigEditor({ }) { return (
-

+

Forward mesh data to your own MQTT broker for home automation, logging, or alerting.

@@ -1000,7 +1000,7 @@ function MqttHaConfigEditor({
-

Home Assistant MQTT Discovery

+

Home Assistant MQTT Discovery

Publish discovery configs and MeshCore state to your MQTT broker so Home Assistant creates native devices, sensors, GPS trackers, and message events automatically. @@ -1010,27 +1010,27 @@ function MqttHaConfigEditor({

1. Same broker
-

+

Home Assistant's built-in MQTT integration must point at the same broker configured below.

2. Pick what to expose
-

+

Choose repeaters for telemetry sensors and contacts for GPS tracker entities.

3. Automate in HA
-

+

Radio health and message events publish continuously; repeater and contact data update when new data is heard or collected.

-

+

Uses{' '} -

MQTT Broker

+

MQTT Broker

@@ -1337,8 +1337,8 @@ function MqttHaConfigEditor({
-

GPS Tracked Contacts

-

+

GPS Tracked Contacts

+

Each selected contact becomes a device_tracker{' '} in HA, updated whenever an advertisement with GPS coordinates is heard. Useful for tracking mobile nodes on an HA map dashboard. @@ -1366,7 +1366,7 @@ function MqttHaConfigEditor({ )} {contactOptions.length === 0 ? ( -

No contacts available.

+

No contacts available.

) : ( <>
{filteredContacts.length === 0 ? ( -

+

No contacts match “{contactSearch}”

) : ( @@ -1408,8 +1408,8 @@ function MqttHaConfigEditor({
-

Telemetry Tracked Repeaters

-

+

Telemetry Tracked Repeaters

+

Each selected repeater becomes an HA device with sensors for battery voltage, RSSI, SNR, noise floor, packet counts, and uptime. Data updates whenever telemetry is collected (auto-collect runs every ~8 hours, or on manual dashboard fetch). Only repeaters already @@ -1417,13 +1417,13 @@ function MqttHaConfigEditor({ repeater and opting in at the bottom of the page).

{trackedRepeaters.length === 0 ? ( -
+
No repeaters are being auto-tracked for telemetry. Add repeaters to the auto-telemetry tracking list in the Radio section first, then return here to select which ones to expose to HA.
) : repeaterOptions.length === 0 ? ( -

+

Auto-tracked repeaters not found in contact list.

) : ( @@ -1449,8 +1449,8 @@ function MqttHaConfigEditor({
-

Message Events

-

+

Message Events

+

Matching messages fire an{' '} {`event.meshcore_${localRadioNodeId}_messages`}{' '} entity in HA with sender, text, channel, and direction attributes. Use HA automations to @@ -1473,7 +1473,7 @@ function MqttCommunityConfigEditor({ return (

-

+

Advanced community MQTT editor. Use this for manual meshcoretomqtt-compatible setups or for modifying a saved preset after creation. Only raw RF packets are shared — never decrypted messages. @@ -1536,7 +1536,7 @@ function MqttCommunityConfigEditor({

-

+

LetsMesh uses token auth. MeshRank uses none.

@@ -1551,7 +1551,9 @@ function MqttCommunityConfigEditor({ value={(config.token_audience as string | undefined) ?? ''} onChange={(e) => onChange({ ...config, token_audience: e.target.value })} /> -

Defaults to the broker host when blank

+

+ Defaults to the broker host when blank +

@@ -1562,7 +1564,7 @@ function MqttCommunityConfigEditor({ value={(config.email as string) || ''} onChange={(e) => onChange({ ...config, email: e.target.value })} /> -

+

Used to claim your node on the community aggregator

@@ -1626,7 +1628,7 @@ function MqttCommunityConfigEditor({ onChange={(e) => onChange({ ...config, iata: e.target.value.toUpperCase() })} className="w-32" /> -

+

Your nearest airport's IATA code (required)

@@ -1640,7 +1642,7 @@ function MqttCommunityConfigEditor({ value={(config.topic_template as string | undefined) ?? ''} onChange={(e) => onChange({ ...config, topic_template: e.target.value })} /> -

+

Use {'{IATA}'} and {'{PUBLIC_KEY}'}. Default:{' '} {DEFAULT_COMMUNITY_PACKET_TOPIC_TEMPLATE}

@@ -1658,7 +1660,7 @@ function MeshRankConfigEditor({ }) { return (
-

+

Pre-filled MeshRank setup. This saves as a regular Community MQTT integration once created, but only asks for the MeshRank packet topic you were given.

@@ -1685,7 +1687,7 @@ function MeshRankConfigEditor({ }) } /> -

+

Paste the full topic template from your MeshRank config, for example{' '} meshrank/uplink/B435F6D5F7896B74C6B995FE221C2C1F/{'{PUBLIC_KEY}'}/packets.

@@ -1705,7 +1707,7 @@ function LetsMeshConfigEditor({ }) { return (
-

+

Pre-filled LetsMesh setup. This saves as a regular Community MQTT integration once created, but only asks for the values LetsMesh expects from you.

@@ -1786,7 +1788,7 @@ function BotConfigEditor({
-

+

Define a bot() function that receives message data and optionally returns a reply.

@@ -1810,7 +1812,7 @@ function BotConfigEditor({ onChange({ ...config, code: c })} /> -
+

Available: Standard Python libraries and any modules installed in the server environment. @@ -1858,7 +1860,7 @@ function MapUploadConfigEditor({ return (

-

+

Automatically upload heard repeater and room server advertisements to{' '}

@@ -1923,7 +1925,7 @@ function MapUploadConfigEditor({ />
Enable Geofence -

+

Only upload nodes whose location falls within the configured radius of your radio's own position. Helps exclude nodes with false or spoofed coordinates. Uses the latitude/longitude set in Radio Settings. @@ -1941,7 +1943,7 @@ function MapUploadConfigEditor({

)} {radioLatLonConfigured && ( -

+

Using radio position{' '} {radioLat?.toFixed(5)}, {radioLon?.toFixed(5)} @@ -1965,7 +1967,7 @@ function MapUploadConfigEditor({ }) } /> -

+

Nodes further than this distance from your radio's position will not be uploaded.

@@ -2120,7 +2122,7 @@ function PillsSearchList({ )} {items.length === 0 ? ( -

{emptyItemsMessage}

+

{emptyItemsMessage}

) : ( <>
{filtered.length === 0 ? ( -

+

No {label.toLowerCase()} match “{search}”

) : ( @@ -2282,7 +2284,7 @@ function ScopeSelector({ return (
- +

Message Scope

{showRawPackets && (

Send push notifications via{' '} onChange({ ...config, urls: e.target.value })} rows={4} /> -

+

One URL per line. All URLs receive every matched notification. For Matrix room version 12 (servername-less room IDs), append ?hsreq=no to the URL.

@@ -2436,7 +2438,7 @@ function AppriseConfigEditor({ />
Preserve identity on Discord -

+

When enabled, Discord webhooks will use their configured name/avatar instead of overriding with MeshCore sender info.

@@ -2492,7 +2494,7 @@ function WebhookConfigEditor({ return (
-

+

Send message data as JSON to an HTTP endpoint when messages are received.

@@ -2526,8 +2528,8 @@ function WebhookConfigEditor({
- -

+

HMAC Signing

+

When a secret is set, each request includes an HMAC-SHA256 signature of the JSON body in the specified header (e.g. sha256=ab12cd... ). @@ -2590,7 +2592,7 @@ function SqsConfigEditor({ }) { return (

-

+

Send matched mesh events to an Amazon SQS queue for durable processing by workers, Lambdas, or downstream automation.

@@ -2631,15 +2633,17 @@ function SqsConfigEditor({ value={(config.endpoint_url as string) || ''} onChange={(e) => onChange({ ...config, endpoint_url: e.target.value })} /> -

Useful for LocalStack or custom endpoints

+

+ Useful for LocalStack or custom endpoints +

- -

+

Static Credentials (optional)

+

Leave blank to use the server's normal AWS credential chain.

diff --git a/frontend/src/components/settings/SettingsLocalSection.tsx b/frontend/src/components/settings/SettingsLocalSection.tsx index fa8568d..6fd9b5c 100644 --- a/frontend/src/components/settings/SettingsLocalSection.tsx +++ b/frontend/src/components/settings/SettingsLocalSection.tsx @@ -5,6 +5,7 @@ import { usePush } from '../../contexts/PushSubscriptionContext'; import type { Channel, Contact } from '../../types'; import { getContactDisplayName } from '../../utils/pubkey'; import { Button } from '../ui/button'; +import { Checkbox } from '../ui/checkbox'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Separator } from '../ui/separator'; @@ -92,8 +93,8 @@ function PushDeviceManagement({ if (!isSupported) { return (
- -

+

Web Push Notifications

+

{window.isSecureContext ? 'Push notifications are not supported by this browser.' : 'Web Push requires HTTPS. Access RemoteTerm over HTTPS (self-signed certificates work) to enable push notifications.'} @@ -105,13 +106,13 @@ function PushDeviceManagement({ return (

- -

+

Web Push Notifications

+

Receive notifications even when the browser is closed. Use the bell icon in any conversation header to enable push for that contact or channel, or subscribe this browser to receive notifications for all push-enabled conversations.

-

+

The set of channels or DMs that trigger push notifications are global per-install (i.e. all devices that register for Web Push will have the same set of channels/DMs that trigger notifications). Subscribing or unsubscribing a particular browser only controls whether @@ -265,12 +266,12 @@ export function SettingsLocalSection({ return (

-

+

These settings apply only to this device/browser.

- +

Color Scheme

@@ -278,7 +279,7 @@ export function SettingsLocalSection({
- +

Local Label

-

+

Display a colored banner at the top of the page to identify this instance.

@@ -330,7 +331,7 @@ export function SettingsLocalSection({ ))} -

+

Controls how distances are shown throughout the app.

@@ -338,86 +339,107 @@ export function SettingsLocalSection({
- +

UI Tweaks

- +
+
+ handleToggleReopenLastConversation(checked === true)} + className="mt-0.5" + /> +
+ +

+ Automatically reopen to the last-open channel or contact when the app loads to the + bare URL. +

+
+
- +
+ { + const v = checked === true; + setAutoFocusInput(v); + setAutoFocusInputEnabled(v); + }} + className="mt-0.5" + /> +
+ +

+ Place the cursor in the message input when switching conversations. Desktop only. +

+
+
- +
+ { + const v = checked === true; + setBatteryPercent(v); + saveBatteryPercent(v); + window.dispatchEvent(new Event(BATTERY_DISPLAY_CHANGE_EVENT)); + }} + className="mt-0.5" + /> +
+ +

+ Display the radio's battery percentage in the status bar. Data updates every 60 + seconds and may take up to a minute to appear after connecting. +

+
+
- +
+ { + const v = checked === true; + setBatteryVoltage(v); + saveBatteryVoltage(v); + window.dispatchEvent(new Event(BATTERY_DISPLAY_CHANGE_EVENT)); + }} + className="mt-0.5" + /> +
+ +

+ Display the radio's battery voltage in the status bar (in mV). Data updates + every 60 seconds and may take up to a minute to appear after connecting. +

+
+
- {(batteryPercent || batteryVoltage) && ( -

- Battery data updates every 60 seconds and may take up to a minute to appear after - connecting. -

- )} - - +
+ { + const v = checked === true; + setStatusDotPulse(v); + saveStatusDotPulse(v); + window.dispatchEvent(new Event(STATUS_DOT_PULSE_CHANGE_EVENT)); + }} + className="mt-0.5" + /> +
+ +

+ Flash the connection status dot in color as packets arrive: blue for channel, purple + for DM, cyan for advert, dark green for other. +

+
+
+
@@ -490,7 +512,7 @@ export function SettingsLocalSection({ Reset
-

+

Scales the app's typography for this browser only. The slider moves in 5% steps; the number field accepts any value from 25% to 400%.

@@ -603,15 +625,15 @@ function ThemePreview({ className }: { className?: string }) { desc="Sheet / dialog title" />

- Section Label + Metadata Label

text-[0.625rem] uppercase tracking-wider text-muted-foreground font-medium diff --git a/frontend/src/components/settings/SettingsRadioSection.tsx b/frontend/src/components/settings/SettingsRadioSection.tsx index f8c5932..c3bee72 100644 --- a/frontend/src/components/settings/SettingsRadioSection.tsx +++ b/frontend/src/components/settings/SettingsRadioSection.tsx @@ -392,7 +392,7 @@ export function SettingsRadioSection({

{/* ── Connection ── */}
- +

Connection

{connectionBusy ? `${connectionActionLabel}...` : connectionActionLabel} -

+

Disconnect pauses automatic reconnect attempts so another device can use the radio.

@@ -432,7 +432,7 @@ export function SettingsRadioSection({ {/* ── Identity ── */}
- +

Identity

@@ -477,7 +477,7 @@ export function SettingsRadioSection({ {/* ── Radio Parameters ── */}
- +

Radio Parameters

@@ -590,7 +590,7 @@ export function SettingsRadioSection({ {/* ── Location ── */}
- +

Location

-

+

Some settings may require a reboot to take effect on some radios.

@@ -682,7 +682,7 @@ export function SettingsRadioSection({ {/* ── Messaging ── */}
- +

Messaging

@@ -695,7 +695,7 @@ export function SettingsRadioSection({ />
-

+

When enabled, the radio sends one extra direct ACK transmission before the normal ACK for received direct messages. This is a firmware-level receive behavior, not a RemoteTerm retry setting. @@ -714,7 +714,7 @@ export function SettingsRadioSection({ />

-

+

When enabled, outgoing channel messages that receive no echo within 2 seconds are automatically resent once (byte-perfect, within the 30-second dedup window). Repeaters that already heard the original will ignore the duplicate. This functionality will NOT @@ -732,7 +732,7 @@ export function SettingsRadioSection({ onChange={(e) => setFloodScope(e.target.value)} placeholder="MyRegion" /> -

+

Tag outgoing flood messages with a region name (e.g. MyRegion). Repeaters configured for that region can forward the traffic, while repeaters configured to deny other regions may drop it. Leave empty to disable. @@ -749,7 +749,7 @@ export function SettingsRadioSection({ value={maxRadioContacts} onChange={(e) => setMaxRadioContacts(e.target.value)} /> -

+

Configured radio contact capacity. Favorites reload first, then background maintenance refills to about 80% of this value and offloads once occupancy reaches about 95%.

@@ -775,134 +775,137 @@ export function SettingsRadioSection({ {/* ── Advertising & Discovery ── */} -
- -
+
+

Advertising & Discovery

-
- -
- setAdvertIntervalHours(e.target.value)} - className="w-28" - /> - hours (0 = off) +
+ +
+ setAdvertIntervalHours(e.target.value)} + className="w-28" + /> + hours (0 = off) +
+

+ How often to automatically advertise presence. Set to 0 to disable. Minimum: 1 hour. + Recommended: 24 hours or higher. +

-

- How often to automatically advertise presence. Set to 0 to disable. Minimum: 1 hour. - Recommended: 24 hours or higher. -

-
-
- -

- Flood adverts propagate through repeaters. Zero-hop adverts are local-only and use less - airtime. -

-
- - -
- {!health?.radio_connected && ( -

Radio not connected

- )} -
- -
- -

- Discover nearby node types that currently respond to mesh discovery requests: repeaters - and sensors. -

-
- {[ - { target: 'repeaters', label: 'Discover Repeaters' }, - { target: 'sensors', label: 'Discover Sensors' }, - { target: 'all', label: 'Discover Both' }, - ].map(({ target, label }) => ( +
+

Send Advertisement

+

+ Flood adverts propagate through repeaters. Zero-hop adverts are local-only and use less + airtime. +

+
+ - ))} -
- {!health?.radio_connected && ( -

Radio not connected

- )} - {discoverError && ( -

- {discoverError} -

- )} - {meshDiscovery && ( -
-
-

- Last sweep: {meshDiscovery.results.length} node - {meshDiscovery.results.length === 1 ? '' : 's'} -

-

- {meshDiscovery.duration_seconds.toFixed(0)}s listen window -

-
- {meshDiscovery.results.length === 0 ? ( -

- No supported nodes responded during the last discovery sweep. -

- ) : ( -
- {meshDiscovery.results.map((result) => ( -
-
- - {result.name ?? {result.node_type}} - - - heard {result.heard_count} time{result.heard_count === 1 ? '' : 's'} - -
- {result.name && ( -

{result.node_type}

- )} -

- {result.public_key} -

-

- Heard here: {result.local_snr ?? 'n/a'} dB SNR / {result.local_rssi ?? 'n/a'}{' '} - dBm RSSI. Remote heard us: {result.remote_snr ?? 'n/a'} dB SNR. -

-
- ))} -
- )}
- )} + {!health?.radio_connected && ( +

Radio not connected

+ )} +
+ +
+

Mesh Discovery

+

+ Discover nearby node types that currently respond to mesh discovery requests: repeaters + and sensors. +

+
+ {[ + { target: 'repeaters', label: 'Discover Repeaters' }, + { target: 'sensors', label: 'Discover Sensors' }, + { target: 'all', label: 'Discover Both' }, + ].map(({ target, label }) => ( + + ))} +
+ {!health?.radio_connected && ( +

Radio not connected

+ )} + {discoverError && ( +

+ {discoverError} +

+ )} + {meshDiscovery && ( +
+
+

+ Last sweep: {meshDiscovery.results.length} node + {meshDiscovery.results.length === 1 ? '' : 's'} +

+

+ {meshDiscovery.duration_seconds.toFixed(0)}s listen window +

+
+ {meshDiscovery.results.length === 0 ? ( +

+ No supported nodes responded during the last discovery sweep. +

+ ) : ( +
+ {meshDiscovery.results.map((result) => ( +
+
+ + {result.name ?? {result.node_type}} + + + heard {result.heard_count} time{result.heard_count === 1 ? '' : 's'} + +
+ {result.name && ( +

+ {result.node_type} +

+ )} +

+ {result.public_key} +

+

+ Heard here: {result.local_snr ?? 'n/a'} dB SNR /{' '} + {result.local_rssi ?? 'n/a'} dBm RSSI. Remote heard us:{' '} + {result.remote_snr ?? 'n/a'} dB SNR. +

+
+ ))} +
+ )} +
+ )} +
); diff --git a/frontend/src/components/settings/SettingsStatisticsSection.tsx b/frontend/src/components/settings/SettingsStatisticsSection.tsx index b18d62c..76b35ae 100644 --- a/frontend/src/components/settings/SettingsStatisticsSection.tsx +++ b/frontend/src/components/settings/SettingsStatisticsSection.tsx @@ -226,7 +226,7 @@ export function SettingsStatisticsSection({ className }: { className?: string })
{/* Network */}
-

Network

+

Network

{stats.contact_count}
@@ -247,7 +247,7 @@ export function SettingsStatisticsSection({ className }: { className?: string }) {/* Messages */}
-

Messages

+

Messages

{stats.total_dms}
@@ -268,7 +268,7 @@ export function SettingsStatisticsSection({ className }: { className?: string }) {/* Activity */}
-

Activity

+

Activity

@@ -305,7 +305,7 @@ export function SettingsStatisticsSection({ className }: { className?: string }) {/* Packets */}
-

Packets

+

Packets

Total stored @@ -327,7 +327,9 @@ export function SettingsStatisticsSection({ className }: { className?: string }) <>
-

Packets per Hour (72h)

+

+ Packets per Hour (72h) +

@@ -337,7 +339,7 @@ export function SettingsStatisticsSection({ className }: { className?: string }) {/* Path Hash Width */}
-

Path Hash Width (24h)

+

Path Hash Width (24h)

Parsed stored raw packets from the last 24 hours:{' '} {stats.path_hash_width_24h.total_packets} @@ -407,7 +409,9 @@ export function SettingsStatisticsSection({ className }: { className?: string }) <>
-

Busiest Channels (24h)

+

+ Busiest Channels (24h) +

-

Noise Floor (24h)

+

Noise Floor (24h)

{stats.noise_floor_24h.latest_noise_floor_dbm != null && (
Latest reading: {stats.noise_floor_24h.latest_noise_floor_dbm} dBm diff --git a/frontend/src/test/settingsModal.test.tsx b/frontend/src/test/settingsModal.test.tsx index 18f9039..f728e26 100644 --- a/frontend/src/test/settingsModal.test.tsx +++ b/frontend/src/test/settingsModal.test.tsx @@ -566,7 +566,7 @@ describe('SettingsModal', () => { renderModal(); openLocalSection(); - const checkbox = screen.getByLabelText('Reopen to last viewed channel/conversation'); + const checkbox = screen.getByLabelText('Reopen Last Conversation'); expect(checkbox).not.toBeChecked(); fireEvent.click(checkbox);