mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-03-28 17:43:05 +01:00
Clearer labelling and page organization for MQTT
This commit is contained in:
@@ -20,8 +20,12 @@ class MqttPublisher(BaseMqttPublisher):
|
||||
_log_prefix = "MQTT"
|
||||
|
||||
def _is_configured(self) -> bool:
|
||||
"""Check if MQTT is configured (broker host is set)."""
|
||||
return bool(self._settings and self._settings.mqtt_broker_host)
|
||||
"""Check if MQTT is configured and has something to publish."""
|
||||
return bool(
|
||||
self._settings
|
||||
and self._settings.mqtt_broker_host
|
||||
and (self._settings.mqtt_publish_messages or self._settings.mqtt_publish_raw_packets)
|
||||
)
|
||||
|
||||
def _build_client_kwargs(self, settings: AppSettings) -> dict[str, Any]:
|
||||
return {
|
||||
|
||||
@@ -34,6 +34,9 @@ export function SettingsMqttSection({
|
||||
const [communityMqttBrokerPort, setCommunityMqttBrokerPort] = useState('443');
|
||||
const [communityMqttEmail, setCommunityMqttEmail] = useState('');
|
||||
|
||||
const [privateExpanded, setPrivateExpanded] = useState(false);
|
||||
const [communityExpanded, setCommunityExpanded] = useState(false);
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -86,266 +89,303 @@ export function SettingsMqttSection({
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-sm font-medium">Private MQTT Broker</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Forward all mesh data to your own MQTT broker for home automation, logging, or alerting.
|
||||
Publishes both decrypted messages and raw packets to your broker.
|
||||
</p>
|
||||
<div className="rounded-md border border-yellow-600/50 bg-yellow-950/30 px-4 py-3 text-sm text-yellow-200">
|
||||
MQTT support is an experimental feature in open beta. All publishing uses QoS 0
|
||||
(at-most-once delivery). Please report any bugs on the{' '}
|
||||
<a
|
||||
href="https://github.com/jkingsman/Remote-Terminal-for-MeshCore/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-yellow-100"
|
||||
>
|
||||
GitHub issues page
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Status</Label>
|
||||
{health?.mqtt_status === 'connected' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm text-green-400">Connected</span>
|
||||
</div>
|
||||
) : health?.mqtt_status === 'disconnected' ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="text-sm text-red-400">Disconnected</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full bg-gray-500" />
|
||||
<span className="text-sm text-muted-foreground">Disabled</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-host">Broker Host</Label>
|
||||
<Input
|
||||
id="mqtt-host"
|
||||
type="text"
|
||||
placeholder="e.g. 192.168.1.100"
|
||||
value={mqttBrokerHost}
|
||||
onChange={(e) => setMqttBrokerHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-port">Broker Port</Label>
|
||||
<Input
|
||||
id="mqtt-port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={mqttBrokerPort}
|
||||
onChange={(e) => setMqttBrokerPort(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-username">Username</Label>
|
||||
<Input
|
||||
id="mqtt-username"
|
||||
type="text"
|
||||
placeholder="Optional"
|
||||
value={mqttUsername}
|
||||
onChange={(e) => setMqttUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-password">Password</Label>
|
||||
<Input
|
||||
id="mqtt-password"
|
||||
type="password"
|
||||
placeholder="Optional"
|
||||
value={mqttPassword}
|
||||
onChange={(e) => setMqttPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttUseTls}
|
||||
onChange={(e) => setMqttUseTls(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Use TLS</span>
|
||||
</label>
|
||||
|
||||
{mqttUseTls && (
|
||||
<>
|
||||
<label className="flex items-center gap-3 cursor-pointer ml-7">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttTlsInsecure}
|
||||
onChange={(e) => setMqttTlsInsecure(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Skip certificate verification</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-7">
|
||||
Allow self-signed or untrusted broker certificates
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-prefix">Topic Prefix</Label>
|
||||
<Input
|
||||
id="mqtt-prefix"
|
||||
type="text"
|
||||
value={mqttTopicPrefix}
|
||||
onChange={(e) => setMqttTopicPrefix(e.target.value)}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground space-y-2">
|
||||
<div>
|
||||
<p className="font-medium">Decrypted messages <span className="font-mono font-normal opacity-75">{'{'}id, type, conversation_key, text, sender_timestamp, received_at, paths, outgoing, acked{'}'}</span></p>
|
||||
<div className="font-mono ml-2 space-y-0.5">
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/dm:<contact_key></div>
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/gm:<channel_key></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Raw packets <span className="font-mono font-normal opacity-75">{'{'}id, observation_id, timestamp, data, payload_type, snr, rssi, decrypted, decrypted_info{'}'}</span></p>
|
||||
<div className="font-mono ml-2 space-y-0.5">
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/raw/dm:<contact_key></div>
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/raw/gm:<channel_key></div>
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/raw/unrouted</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttPublishMessages}
|
||||
onChange={(e) => setMqttPublishMessages(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Publish Messages</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-7">
|
||||
Forward decrypted DM and channel messages
|
||||
</p>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttPublishRawPackets}
|
||||
onChange={(e) => setMqttPublishRawPackets(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Publish Raw Packets</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-7">Forward all RF packets</p>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-3">
|
||||
<h4 className="text-sm font-medium">Community Analytics</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Share raw packet data with the MeshCore community for coverage mapping and network
|
||||
analysis. Only raw RF packets are shared — never decrypted messages.
|
||||
</p>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{health?.community_mqtt_status === 'connected' ? (
|
||||
{/* Private MQTT Broker */}
|
||||
<div className="border border-input rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-muted/40"
|
||||
onClick={() => setPrivateExpanded(!privateExpanded)}
|
||||
>
|
||||
<span className="text-muted-foreground">{privateExpanded ? '▼' : '▶'}</span>
|
||||
<h4 className="text-sm font-medium">Private MQTT Broker</h4>
|
||||
{health?.mqtt_status === 'connected' ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm text-green-400">Connected</span>
|
||||
<span className="text-xs text-green-400">Connected</span>
|
||||
</>
|
||||
) : health?.community_mqtt_status === 'disconnected' ? (
|
||||
) : health?.mqtt_status === 'disconnected' ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="text-sm text-red-400">Disconnected</span>
|
||||
<span className="text-xs text-red-400">Disconnected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-gray-500" />
|
||||
<span className="text-sm text-muted-foreground">Disabled</span>
|
||||
<span className="text-xs text-muted-foreground">Disabled</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={communityMqttEnabled}
|
||||
onChange={(e) => setCommunityMqttEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Enable Community Analytics</span>
|
||||
</label>
|
||||
</button>
|
||||
|
||||
{communityMqttEnabled && (
|
||||
<div className="space-y-2">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-broker-host">Broker Host</Label>
|
||||
<Input
|
||||
id="community-broker-host"
|
||||
type="text"
|
||||
placeholder="mqtt-us-v1.letsmesh.net"
|
||||
value={communityMqttBrokerHost}
|
||||
onChange={(e) => setCommunityMqttBrokerHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-broker-port">Broker Port</Label>
|
||||
<Input
|
||||
id="community-broker-port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={communityMqttBrokerPort}
|
||||
onChange={(e) => setCommunityMqttBrokerPort(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Label htmlFor="community-iata">Region Code (IATA)</Label>
|
||||
<Input
|
||||
id="community-iata"
|
||||
type="text"
|
||||
maxLength={3}
|
||||
placeholder="e.g. DEN, LAX, NYC"
|
||||
value={communityMqttIata}
|
||||
onChange={(e) => setCommunityMqttIata(e.target.value.toUpperCase())}
|
||||
className="w-32"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your nearest airport's{' '}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/List_of_airports_by_IATA_airport_code:_A"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground"
|
||||
>
|
||||
IATA code
|
||||
</a>{' '}
|
||||
(required)
|
||||
{privateExpanded && (
|
||||
<div className="px-4 pb-4 space-y-3 border-t border-input">
|
||||
<p className="text-xs text-muted-foreground pt-3">
|
||||
Forward mesh data to your own MQTT broker for home automation, logging, or alerting.
|
||||
</p>
|
||||
{communityMqttIata && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Topic: meshcore/{communityMqttIata}/<pubkey>/packets
|
||||
</p>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttPublishMessages}
|
||||
onChange={(e) => setMqttPublishMessages(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Publish Messages</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-7">
|
||||
Forward decrypted DM and channel messages
|
||||
</p>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttPublishRawPackets}
|
||||
onChange={(e) => setMqttPublishRawPackets(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Publish Raw Packets</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-7">Forward all RF packets</p>
|
||||
|
||||
{(mqttPublishMessages || mqttPublishRawPackets) && (
|
||||
<div className="space-y-3">
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-host">Broker Host</Label>
|
||||
<Input
|
||||
id="mqtt-host"
|
||||
type="text"
|
||||
placeholder="e.g. 192.168.1.100"
|
||||
value={mqttBrokerHost}
|
||||
onChange={(e) => setMqttBrokerHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-port">Broker Port</Label>
|
||||
<Input
|
||||
id="mqtt-port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={mqttBrokerPort}
|
||||
onChange={(e) => setMqttBrokerPort(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-username">Username</Label>
|
||||
<Input
|
||||
id="mqtt-username"
|
||||
type="text"
|
||||
placeholder="Optional"
|
||||
value={mqttUsername}
|
||||
onChange={(e) => setMqttUsername(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-password">Password</Label>
|
||||
<Input
|
||||
id="mqtt-password"
|
||||
type="password"
|
||||
placeholder="Optional"
|
||||
value={mqttPassword}
|
||||
onChange={(e) => setMqttPassword(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttUseTls}
|
||||
onChange={(e) => setMqttUseTls(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Use TLS</span>
|
||||
</label>
|
||||
|
||||
{mqttUseTls && (
|
||||
<>
|
||||
<label className="flex items-center gap-3 cursor-pointer ml-7">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={mqttTlsInsecure}
|
||||
onChange={(e) => setMqttTlsInsecure(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Skip certificate verification</span>
|
||||
</label>
|
||||
<p className="text-xs text-muted-foreground ml-7">
|
||||
Allow self-signed or untrusted broker certificates
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="mqtt-prefix">Topic Prefix</Label>
|
||||
<Input
|
||||
id="mqtt-prefix"
|
||||
type="text"
|
||||
value={mqttTopicPrefix}
|
||||
onChange={(e) => setMqttTopicPrefix(e.target.value)}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground space-y-2">
|
||||
<div>
|
||||
<p className="font-medium">Decrypted messages <span className="font-mono font-normal opacity-75">{'{'}id, type, conversation_key, text, sender_timestamp, received_at, paths, outgoing, acked{'}'}</span></p>
|
||||
<div className="font-mono ml-2 space-y-0.5">
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/dm:<contact_key></div>
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/gm:<channel_key></div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">Raw packets <span className="font-mono font-normal opacity-75">{'{'}id, observation_id, timestamp, data, payload_type, snr, rssi, decrypted, decrypted_info{'}'}</span></p>
|
||||
<div className="font-mono ml-2 space-y-0.5">
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/raw/dm:<contact_key></div>
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/raw/gm:<channel_key></div>
|
||||
<div>{mqttTopicPrefix || 'meshcore'}/raw/unrouted</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Label htmlFor="community-email">Owner Email (optional)</Label>
|
||||
<Input
|
||||
id="community-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={communityMqttEmail}
|
||||
onChange={(e) => setCommunityMqttEmail(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Used to claim your node on the community aggregator
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Community Analytics */}
|
||||
<div className="border border-input rounded-md overflow-hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="w-full flex items-center gap-2 px-4 py-3 text-left hover:bg-muted/40"
|
||||
onClick={() => setCommunityExpanded(!communityExpanded)}
|
||||
>
|
||||
<span className="text-muted-foreground">{communityExpanded ? '▼' : '▶'}</span>
|
||||
<h4 className="text-sm font-medium">Community Analytics</h4>
|
||||
{health?.community_mqtt_status === 'connected' ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-xs text-green-400">Connected</span>
|
||||
</>
|
||||
) : health?.community_mqtt_status === 'disconnected' ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="text-xs text-red-400">Disconnected</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-gray-500" />
|
||||
<span className="text-xs text-muted-foreground">Disabled</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{communityExpanded && (
|
||||
<div className="px-4 pb-4 space-y-3 border-t border-input">
|
||||
<p className="text-xs text-muted-foreground pt-3">
|
||||
Share raw packet data with the MeshCore community for coverage mapping and network
|
||||
analysis. Only raw RF packets are shared — never decrypted messages.
|
||||
</p>
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={communityMqttEnabled}
|
||||
onChange={(e) => setCommunityMqttEnabled(e.target.checked)}
|
||||
className="h-4 w-4 rounded border-border"
|
||||
/>
|
||||
<span className="text-sm">Enable Community Analytics</span>
|
||||
</label>
|
||||
|
||||
{communityMqttEnabled && (
|
||||
<div className="space-y-3">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-broker-host">Broker Host</Label>
|
||||
<Input
|
||||
id="community-broker-host"
|
||||
type="text"
|
||||
placeholder="mqtt-us-v1.letsmesh.net"
|
||||
value={communityMqttBrokerHost}
|
||||
onChange={(e) => setCommunityMqttBrokerHost(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-broker-port">Broker Port</Label>
|
||||
<Input
|
||||
id="community-broker-port"
|
||||
type="number"
|
||||
min="1"
|
||||
max="65535"
|
||||
value={communityMqttBrokerPort}
|
||||
onChange={(e) => setCommunityMqttBrokerPort(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-iata">Region Code (IATA)</Label>
|
||||
<Input
|
||||
id="community-iata"
|
||||
type="text"
|
||||
maxLength={3}
|
||||
placeholder="e.g. DEN, LAX, NYC"
|
||||
value={communityMqttIata}
|
||||
onChange={(e) => setCommunityMqttIata(e.target.value.toUpperCase())}
|
||||
className="w-32"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Your nearest airport's{' '}
|
||||
<a
|
||||
href="https://en.wikipedia.org/wiki/List_of_airports_by_IATA_airport_code:_A"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline hover:text-foreground"
|
||||
>
|
||||
IATA code
|
||||
</a>{' '}
|
||||
(required)
|
||||
</p>
|
||||
{communityMqttIata && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Topic: meshcore/{communityMqttIata}/<pubkey>/packets
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-email">Owner Email (optional)</Label>
|
||||
<Input
|
||||
id="community-email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={communityMqttEmail}
|
||||
onChange={(e) => setCommunityMqttEmail(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Used to claim your node on the community aggregator
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -155,6 +155,14 @@ function openMqttSection() {
|
||||
fireEvent.click(mqttToggle);
|
||||
}
|
||||
|
||||
function expandPrivateMqtt() {
|
||||
fireEvent.click(screen.getByText('Private MQTT Broker'));
|
||||
}
|
||||
|
||||
function expandCommunityMqtt() {
|
||||
fireEvent.click(screen.getByText('Community Analytics'));
|
||||
}
|
||||
|
||||
function openDatabaseSection() {
|
||||
const databaseToggle = screen.getByRole('button', { name: /Database/i });
|
||||
fireEvent.click(databaseToggle);
|
||||
@@ -414,19 +422,30 @@ describe('SettingsModal', () => {
|
||||
it('renders MQTT section with form inputs', () => {
|
||||
renderModal();
|
||||
openMqttSection();
|
||||
expandPrivateMqtt();
|
||||
|
||||
// Publish checkboxes always visible
|
||||
expect(screen.getByText('Publish Messages')).toBeInTheDocument();
|
||||
expect(screen.getByText('Publish Raw Packets')).toBeInTheDocument();
|
||||
|
||||
// Broker config hidden until a publish option is enabled
|
||||
expect(screen.queryByLabelText('Broker Host')).not.toBeInTheDocument();
|
||||
|
||||
// Enable one publish option to reveal broker config
|
||||
fireEvent.click(screen.getByText('Publish Messages'));
|
||||
expect(screen.getByLabelText('Broker Host')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Broker Port')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Username')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Password')).toBeInTheDocument();
|
||||
expect(screen.getByLabelText('Topic Prefix')).toBeInTheDocument();
|
||||
expect(screen.getByText('Publish Messages')).toBeInTheDocument();
|
||||
expect(screen.getByText('Publish Raw Packets')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('saves MQTT settings through onSaveAppSettings', async () => {
|
||||
const { onSaveAppSettings } = renderModal();
|
||||
const { onSaveAppSettings } = renderModal({
|
||||
appSettings: { ...baseSettings, mqtt_publish_messages: true },
|
||||
});
|
||||
openMqttSection();
|
||||
expandPrivateMqtt();
|
||||
|
||||
const hostInput = screen.getByLabelText('Broker Host');
|
||||
fireEvent.change(hostInput, { target: { value: 'mqtt.example.com' } });
|
||||
@@ -476,6 +495,7 @@ describe('SettingsModal', () => {
|
||||
it('renders community sharing section in MQTT tab', () => {
|
||||
renderModal();
|
||||
openMqttSection();
|
||||
expandCommunityMqtt();
|
||||
|
||||
expect(screen.getByText('Community Analytics')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enable Community Analytics')).toBeInTheDocument();
|
||||
@@ -489,6 +509,7 @@ describe('SettingsModal', () => {
|
||||
},
|
||||
});
|
||||
openMqttSection();
|
||||
expandCommunityMqtt();
|
||||
|
||||
expect(screen.queryByLabelText('Region Code (IATA)')).not.toBeInTheDocument();
|
||||
|
||||
|
||||
@@ -34,6 +34,30 @@ class TestHealthMqttStatus:
|
||||
mqtt_publisher._settings = original_settings
|
||||
mqtt_publisher.connected = original_connected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_disabled_when_nothing_to_publish(self, test_db):
|
||||
"""MQTT status is 'disabled' when broker host is set but no publish options enabled."""
|
||||
from app.mqtt import mqtt_publisher
|
||||
|
||||
original_settings = mqtt_publisher._settings
|
||||
original_connected = mqtt_publisher.connected
|
||||
try:
|
||||
from app.models import AppSettings
|
||||
|
||||
mqtt_publisher._settings = AppSettings(
|
||||
mqtt_broker_host="broker.local",
|
||||
mqtt_publish_messages=False,
|
||||
mqtt_publish_raw_packets=False,
|
||||
)
|
||||
mqtt_publisher.connected = False
|
||||
|
||||
data = await build_health_data(True, "TCP: 1.2.3.4:4000")
|
||||
|
||||
assert data["mqtt_status"] == "disabled"
|
||||
finally:
|
||||
mqtt_publisher._settings = original_settings
|
||||
mqtt_publisher.connected = original_connected
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mqtt_connected_when_publisher_connected(self, test_db):
|
||||
"""MQTT status is 'connected' when publisher is connected."""
|
||||
@@ -44,7 +68,9 @@ class TestHealthMqttStatus:
|
||||
try:
|
||||
from app.models import AppSettings
|
||||
|
||||
mqtt_publisher._settings = AppSettings(mqtt_broker_host="broker.local")
|
||||
mqtt_publisher._settings = AppSettings(
|
||||
mqtt_broker_host="broker.local", mqtt_publish_messages=True
|
||||
)
|
||||
mqtt_publisher.connected = True
|
||||
|
||||
data = await build_health_data(True, "TCP: 1.2.3.4:4000")
|
||||
@@ -64,7 +90,9 @@ class TestHealthMqttStatus:
|
||||
try:
|
||||
from app.models import AppSettings
|
||||
|
||||
mqtt_publisher._settings = AppSettings(mqtt_broker_host="broker.local")
|
||||
mqtt_publisher._settings = AppSettings(
|
||||
mqtt_broker_host="broker.local", mqtt_publish_raw_packets=True
|
||||
)
|
||||
mqtt_publisher.connected = False
|
||||
|
||||
data = await build_health_data(False, None)
|
||||
|
||||
Reference in New Issue
Block a user