import { useState, useEffect, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import api from "../api/client";
import useMqttWebSocket from "./useMqttWebSocket";
export default function MqttDashboard() {
const [devices, setDevices] = useState([]);
const [brokerConnected, setBrokerConnected] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [hoveredRow, setHoveredRow] = useState(null);
const [liveEvents, setLiveEvents] = useState([]);
const navigate = useNavigate();
const fetchStatus = async () => {
try {
const data = await api.get("/mqtt/status");
setDevices(data.devices || []);
setBrokerConnected(data.broker_connected);
setError("");
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchStatus();
const interval = setInterval(fetchStatus, 30000);
return () => clearInterval(interval);
}, []);
const handleWsMessage = useCallback((data) => {
setLiveEvents((prev) => [data, ...prev].slice(0, 50));
// Update device status on heartbeat
if (data.type === "status/heartbeat") {
setDevices((prev) => {
const existing = prev.find((d) => d.device_serial === data.device_serial);
if (existing) {
return prev.map((d) =>
d.device_serial === data.device_serial
? { ...d, online: true, seconds_since_heartbeat: 0 }
: d
);
}
return prev;
});
}
}, []);
const { connected: wsConnected } = useMqttWebSocket({
enabled: true,
onMessage: handleWsMessage,
});
const sendPing = async (serial, e) => {
e.stopPropagation();
try {
await api.post(`/mqtt/command/${serial}`, { cmd: "ping", contents: {} });
} catch (err) {
setError(err.message);
}
};
const formatTime = (seconds) => {
if (!seconds && seconds !== 0) return "Never";
if (seconds < 60) return `${seconds}s ago`;
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
};
return (
MQTT Dashboard
Broker {brokerConnected ? "Connected" : "Disconnected"}
Live
{error && (
{error}
)}
{loading ? (
Loading...
) : devices.length === 0 ? (
No devices have sent heartbeats yet. Devices will appear here once they connect to the MQTT broker.
) : (
| Status |
Serial |
Device ID |
Firmware |
IP Address |
Uptime |
Last Seen |
Actions |
{devices.map((device, index) => (
setHoveredRow(device.device_serial)}
onMouseLeave={() => setHoveredRow(null)}
onClick={() => navigate(`/mqtt/commands?device=${device.device_serial}`)}
>
|
|
{device.device_serial}
|
{device.last_heartbeat?.device_id || "-"}
|
{device.last_heartbeat?.firmware_version ? `v${device.last_heartbeat.firmware_version}` : "-"}
|
{device.last_heartbeat?.ip_address || "-"}
|
{device.last_heartbeat?.uptime_display || "-"}
|
{formatTime(device.seconds_since_heartbeat)}
|
e.stopPropagation()}>
|
))}
)}
{/* Live Event Feed */}
Live Feed
{wsConnected && (
(streaming)
)}
{liveEvents.length === 0 ? (
{wsConnected ? "Waiting for MQTT messages..." : "WebSocket not connected — live events will appear once connected."}
) : (
| Type |
Device |
Data |
{liveEvents.map((event, i) => (
|
{event.type}
|
{event.device_serial}
|
{event.type === "logs"
? event.payload?.message || JSON.stringify(event.payload)
: event.type === "status/heartbeat"
? `Uptime: ${event.payload?.payload?.timestamp || "?"} | IP: ${event.payload?.payload?.ip_address || "?"}`
: JSON.stringify(event.payload).substring(0, 120)}
|
))}
)}
);
}