mirror of
https://github.com/jkingsman/Remote-Terminal-for-MeshCore.git
synced 2026-05-08 06:15:02 +02:00
Add support for community MQTT ingest
This commit is contained in:
@@ -27,6 +27,12 @@ export function SettingsMqttSection({
|
||||
const [mqttPublishMessages, setMqttPublishMessages] = useState(false);
|
||||
const [mqttPublishRawPackets, setMqttPublishRawPackets] = useState(false);
|
||||
|
||||
// Community MQTT state
|
||||
const [communityMqttEnabled, setCommunityMqttEnabled] = useState(false);
|
||||
const [communityMqttIata, setCommunityMqttIata] = useState('');
|
||||
const [communityMqttBroker, setCommunityMqttBroker] = useState('mqtt-us-v1.letsmesh.net');
|
||||
const [communityMqttEmail, setCommunityMqttEmail] = useState('');
|
||||
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -40,6 +46,10 @@ export function SettingsMqttSection({
|
||||
setMqttTopicPrefix(appSettings.mqtt_topic_prefix ?? 'meshcore');
|
||||
setMqttPublishMessages(appSettings.mqtt_publish_messages ?? false);
|
||||
setMqttPublishRawPackets(appSettings.mqtt_publish_raw_packets ?? false);
|
||||
setCommunityMqttEnabled(appSettings.community_mqtt_enabled ?? false);
|
||||
setCommunityMqttIata(appSettings.community_mqtt_iata ?? '');
|
||||
setCommunityMqttBroker(appSettings.community_mqtt_broker ?? 'mqtt-us-v1.letsmesh.net');
|
||||
setCommunityMqttEmail(appSettings.community_mqtt_email ?? '');
|
||||
}, [appSettings]);
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -57,6 +67,10 @@ export function SettingsMqttSection({
|
||||
mqtt_topic_prefix: mqttTopicPrefix || 'meshcore',
|
||||
mqtt_publish_messages: mqttPublishMessages,
|
||||
mqtt_publish_raw_packets: mqttPublishRawPackets,
|
||||
community_mqtt_enabled: communityMqttEnabled,
|
||||
community_mqtt_iata: communityMqttIata,
|
||||
community_mqtt_broker: communityMqttBroker || 'mqtt-us-v1.letsmesh.net',
|
||||
community_mqtt_email: communityMqttEmail,
|
||||
};
|
||||
await onSaveAppSettings(update);
|
||||
toast.success('MQTT settings saved');
|
||||
@@ -69,6 +83,14 @@ 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>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Status</Label>
|
||||
{health?.mqtt_status === 'connected' ? (
|
||||
@@ -218,6 +240,95 @@ export function SettingsMqttSection({
|
||||
</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' ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-green-500" />
|
||||
<span className="text-sm text-green-400">Connected</span>
|
||||
</>
|
||||
) : health?.community_mqtt_status === 'disconnected' ? (
|
||||
<>
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<span className="text-sm 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>
|
||||
</>
|
||||
)}
|
||||
</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>
|
||||
|
||||
{communityMqttEnabled && (
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="community-broker">Broker Address</Label>
|
||||
<Input
|
||||
id="community-broker"
|
||||
type="text"
|
||||
placeholder="mqtt-us-v1.letsmesh.net:443"
|
||||
value={communityMqttBroker}
|
||||
onChange={(e) => setCommunityMqttBroker(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">host or host:port (default port 443)</p>
|
||||
<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>
|
||||
)}
|
||||
<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>
|
||||
|
||||
<Button onClick={handleSave} disabled={busy} className="w-full">
|
||||
{busy ? 'Saving...' : 'Save MQTT Settings'}
|
||||
</Button>
|
||||
|
||||
@@ -39,6 +39,7 @@ const baseHealth: HealthStatus = {
|
||||
database_size_mb: 1.2,
|
||||
oldest_undecrypted_timestamp: null,
|
||||
mqtt_status: null,
|
||||
community_mqtt_status: null,
|
||||
};
|
||||
|
||||
const baseSettings: AppSettings = {
|
||||
@@ -60,6 +61,10 @@ const baseSettings: AppSettings = {
|
||||
mqtt_topic_prefix: 'meshcore',
|
||||
mqtt_publish_messages: false,
|
||||
mqtt_publish_raw_packets: false,
|
||||
community_mqtt_enabled: false,
|
||||
community_mqtt_iata: '',
|
||||
community_mqtt_broker: 'mqtt-us-v1.letsmesh.net',
|
||||
community_mqtt_email: '',
|
||||
};
|
||||
|
||||
function renderModal(overrides?: {
|
||||
@@ -446,7 +451,9 @@ describe('SettingsModal', () => {
|
||||
});
|
||||
openMqttSection();
|
||||
|
||||
expect(screen.getByText('Disabled')).toBeInTheDocument();
|
||||
// Both MQTT and community MQTT show "Disabled" when null status
|
||||
const disabledElements = screen.getAllByText('Disabled');
|
||||
expect(disabledElements.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('shows MQTT connected status badge', () => {
|
||||
@@ -465,6 +472,73 @@ describe('SettingsModal', () => {
|
||||
expect(screen.getByText('Connected')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders community sharing section in MQTT tab', () => {
|
||||
renderModal();
|
||||
openMqttSection();
|
||||
|
||||
expect(screen.getByText('Community Analytics')).toBeInTheDocument();
|
||||
expect(screen.getByText('Enable Community Analytics')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows IATA input only when community sharing is enabled', () => {
|
||||
renderModal({
|
||||
appSettings: {
|
||||
...baseSettings,
|
||||
community_mqtt_enabled: false,
|
||||
},
|
||||
});
|
||||
openMqttSection();
|
||||
|
||||
expect(screen.queryByLabelText('Region Code (IATA)')).not.toBeInTheDocument();
|
||||
|
||||
// Enable community sharing
|
||||
fireEvent.click(screen.getByText('Enable Community Analytics'));
|
||||
expect(screen.getByLabelText('Region Code (IATA)')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('includes community MQTT fields in save payload', async () => {
|
||||
const { onSaveAppSettings } = renderModal({
|
||||
appSettings: {
|
||||
...baseSettings,
|
||||
community_mqtt_enabled: true,
|
||||
community_mqtt_iata: 'DEN',
|
||||
},
|
||||
});
|
||||
openMqttSection();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save MQTT Settings' }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onSaveAppSettings).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
community_mqtt_enabled: true,
|
||||
community_mqtt_iata: 'DEN',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('shows community MQTT connected status badge', () => {
|
||||
renderModal({
|
||||
appSettings: {
|
||||
...baseSettings,
|
||||
community_mqtt_enabled: true,
|
||||
},
|
||||
health: {
|
||||
...baseHealth,
|
||||
community_mqtt_status: 'connected',
|
||||
},
|
||||
});
|
||||
openMqttSection();
|
||||
|
||||
// Community Analytics sub-section should show Connected
|
||||
const communitySection = screen.getByText('Community Analytics').closest('div');
|
||||
expect(communitySection).not.toBeNull();
|
||||
// Both MQTT and community could show "Connected" — check count
|
||||
const connectedElements = screen.getAllByText('Connected');
|
||||
expect(connectedElements.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('fetches statistics when expanded in mobile external-nav mode', async () => {
|
||||
const mockStats: StatisticsResponse = {
|
||||
busiest_channels_24h: [],
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface HealthStatus {
|
||||
database_size_mb: number;
|
||||
oldest_undecrypted_timestamp: number | null;
|
||||
mqtt_status: string | null;
|
||||
community_mqtt_status: string | null;
|
||||
}
|
||||
|
||||
export interface MaintenanceResult {
|
||||
@@ -192,6 +193,10 @@ export interface AppSettings {
|
||||
mqtt_topic_prefix: string;
|
||||
mqtt_publish_messages: boolean;
|
||||
mqtt_publish_raw_packets: boolean;
|
||||
community_mqtt_enabled: boolean;
|
||||
community_mqtt_iata: string;
|
||||
community_mqtt_broker: string;
|
||||
community_mqtt_email: string;
|
||||
}
|
||||
|
||||
export interface AppSettingsUpdate {
|
||||
@@ -209,6 +214,10 @@ export interface AppSettingsUpdate {
|
||||
mqtt_topic_prefix?: string;
|
||||
mqtt_publish_messages?: boolean;
|
||||
mqtt_publish_raw_packets?: boolean;
|
||||
community_mqtt_enabled?: boolean;
|
||||
community_mqtt_iata?: string;
|
||||
community_mqtt_broker?: string;
|
||||
community_mqtt_email?: string;
|
||||
}
|
||||
|
||||
export interface MigratePreferencesRequest {
|
||||
|
||||
Reference in New Issue
Block a user