The frontend previously connected directly to the MQTT broker via a CDN-loaded Paho client and a Paho-specific ws proxy. Move that responsibility into the backend: DeviceDataService persists MQTT messages to Postgres, and DeviceEventsController exposes them over a REST endpoint plus a WebSocket gateway that the frontend now consumes directly. Also adds a pgadmin service for inspecting the database. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
115 lines
3.1 KiB
TypeScript
115 lines
3.1 KiB
TypeScript
import Head from 'next/head';
|
|
import { useEffect, useState } from 'react';
|
|
|
|
const WS_PATH = '/api/device-events/ws';
|
|
|
|
interface ApiStatus {
|
|
message: string;
|
|
docs: string;
|
|
}
|
|
|
|
interface DeviceRecord {
|
|
id: number;
|
|
topic: string;
|
|
payload: string;
|
|
receivedAt: string;
|
|
}
|
|
|
|
export default function Home() {
|
|
const [status, setStatus] = useState<ApiStatus | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [messages, setMessages] = useState<DeviceRecord[]>([]);
|
|
const [connected, setConnected] = useState(false);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/status')
|
|
.then((res) => res.json())
|
|
.then(setStatus)
|
|
.catch((err) => setError(err.message));
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
fetch('/api/device-events')
|
|
.then((res) => res.json())
|
|
.then((payload) => {
|
|
const records = Array.isArray(payload) ? payload : payload?.records ?? [];
|
|
setMessages(Array.isArray(records) ? records : []);
|
|
setConnected(true);
|
|
})
|
|
.catch((err) => {
|
|
setError(err.message || 'Unable to load device events');
|
|
});
|
|
|
|
const wsProtocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
const socket = new WebSocket(`${wsProtocol}://${window.location.host}${WS_PATH}`);
|
|
|
|
socket.onopen = () => {
|
|
setConnected(true);
|
|
setError(null);
|
|
};
|
|
|
|
socket.onmessage = (event) => {
|
|
try {
|
|
const payload = JSON.parse(event.data);
|
|
if (payload.type === 'connected') {
|
|
setConnected(true);
|
|
return;
|
|
}
|
|
|
|
setMessages((prev) => [payload, ...prev].slice(0, 20));
|
|
} catch (err) {
|
|
console.error('Failed to parse device-event payload', err);
|
|
}
|
|
};
|
|
|
|
socket.onerror = () => {
|
|
setConnected(false);
|
|
setError('Realtime stream disconnected');
|
|
};
|
|
|
|
return () => {
|
|
socket.close();
|
|
};
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
<Head>
|
|
<title>UlHub</title>
|
|
<meta name="description" content="UlHub monolithic app" />
|
|
</Head>
|
|
<main style={{ padding: '3rem', fontFamily: 'system-ui, sans-serif', maxWidth: '900px' }}>
|
|
<h1>UlHub</h1>
|
|
<p>React + Next.js frontend with NestJS backend and live MQTT WebSocket data.</p>
|
|
|
|
<section style={{ marginTop: '1.5rem' }}>
|
|
<h2>API Status</h2>
|
|
{error && <p style={{ color: 'red' }}>{error}</p>}
|
|
{status ? (
|
|
<div>
|
|
<p>{status.message}</p>
|
|
<p>{status.docs}</p>
|
|
</div>
|
|
) : (
|
|
<p>Loading status...</p>
|
|
)}
|
|
</section>
|
|
|
|
<section style={{ marginTop: '1.5rem' }}>
|
|
<h2>Device Events</h2>
|
|
<p>Status: {connected ? 'Connected' : 'Connecting...'}</p>
|
|
<ul>
|
|
{(Array.isArray(messages) ? messages : []).map((msg, index) => (
|
|
<li key={`${msg.topic}-${index}`} style={{ marginBottom: '0.75rem' }}>
|
|
<strong>{msg.topic}</strong> <span style={{ color: '#666' }}>{msg.receivedAt}</span>
|
|
<br />
|
|
{msg.payload}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
</main>
|
|
</>
|
|
);
|
|
}
|