110 lines
3.9 KiB
TypeScript
110 lines
3.9 KiB
TypeScript
import { useState, useEffect } from "react";
|
|
import { MapContainer, TileLayer, Marker, useMapEvents } from "react-leaflet";
|
|
import "leaflet/dist/leaflet.css";
|
|
import L from "leaflet";
|
|
|
|
// Delete default Leaflet marker icon to avoid errors
|
|
delete (L.Icon.Default as any)._getIconUrl;
|
|
|
|
// Helpers and types from lib/api.ts
|
|
|
|
const IMEI = "your_device_imei_here"; // Replace with dynamic if multiple devices
|
|
|
|
export default function Dashboard() {
|
|
const [telemetry, setTelemetry] = useState<TelemetryPost | null>(null);
|
|
const [commands, setCommands] = useState<DeviceCommand[]>([]);
|
|
const [fence, setFence] = useState<{ lat: number; lng: number }[]>([]); // Geo-fence points
|
|
const [commandType, setCommandType] = useState<DeviceCommandType>("sleep");
|
|
const [payload, setPayload] = useState<string>(""); // JSON string for form
|
|
|
|
// Poll for latest telemetry and commands
|
|
useEffect(() => {
|
|
const interval = setInterval(async () => {
|
|
try {
|
|
const latestGPS = await getLatestGPS(IMEI);
|
|
setTelemetry(latestGPS);
|
|
const cmds = await getCommands(IMEI);
|
|
setCommands(cmds);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
}, 5000); // Poll every 5s; adjust
|
|
|
|
return () => clearInterval(interval);
|
|
}, []);
|
|
|
|
// Send command from form
|
|
const sendCommand = async () => {
|
|
try {
|
|
const parsedPayload = JSON.parse(payload); // Validate in real app
|
|
await postReceipt(IMEI, { command_id: 0, result: "ok" }); // Stub receipt (adjust)
|
|
// Send actual command via your API if needed
|
|
setPayload("");
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
};
|
|
|
|
// Map component (geo-fence drawing)
|
|
function FenceMap() {
|
|
const map = useMapEvents({
|
|
click: (e) => setFence([...fence, { lat: e.latlng.lat, lng: e.latlng.lng }]),
|
|
});
|
|
// Draw fence as polyline
|
|
if (fence.length > 1) L.polyline(fence, { color: "red" }).addTo(map);
|
|
// Marker for latest GPS
|
|
if (telemetry) L.marker([telemetry.lat, telemetry.lng]).addTo(map);
|
|
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<main className="p-4">
|
|
<h1>Sams Home Network Device Dashboard</h1>
|
|
|
|
<section className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<h2>Latest Telemetry</h2>
|
|
{telemetry ? (
|
|
<pre>{JSON.stringify(telemetry, null, 2)}</pre>
|
|
) : "Loading telemetry..."}
|
|
<p>WiFi: {telemetry?.raw?.wifiStatus ?? 'Unknown'}</p>
|
|
<p>Battery: {telemetry?.battery_percent ?? 'Unknown'}%</p>
|
|
<p>Car On: {telemetry?.is_car_on ? 'Yes' : 'No'}</p>
|
|
</div>
|
|
|
|
<div>
|
|
<h2>Map & Geo-Fence</h2>
|
|
<MapContainer center={[0, 0]} zoom={3} style={{ height: "300px" }}>
|
|
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
|
|
<FenceMap />
|
|
</MapContainer>
|
|
<p>Click map to add fence points: {fence.length}</p>
|
|
<button onClick={() => postTelemetry(IMEI, fence[0])}>Set Fence</button> {/* Example */}
|
|
</div>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Queued Commands</h2>
|
|
<pre>{JSON.stringify(commands, null, 2)}</pre>
|
|
<button onClick={() => postReceipt(IMEI, { command_id: commands[0]?.id, result: "ok" })}>Acknowledge First</button>
|
|
</section>
|
|
|
|
<section>
|
|
<h2>Send Command</h2>
|
|
<select value={commandType} onChange={(e) => setCommandType(e.target.value as DeviceCommandType)}>
|
|
<option value="lights">Lights On/Off</option>
|
|
<option value="camera">Camera On/Off</option>
|
|
<option value="sleep">Sleep</option>
|
|
<option value="telemetry_sec">Posting Frequency</option>
|
|
<option value="poll_sec">Poll Frequency</option>
|
|
|
|
<option value="ota">OTA Update</option>
|
|
<option value="ring_fence">Geo Fence</option>
|
|
</select>
|
|
<textarea value={payload} onChange={(e) => setPayload(e.target.value)} placeholder='{ "on": true }' />
|
|
<button onClick={sendCommand}>Send</button>
|
|
</section>
|
|
</main>
|
|
);
|
|
} |