Add professional historian modal with realtime analytics

This commit is contained in:
litoral05
2026-05-22 17:08:22 +01:00
parent a30d41d031
commit 6277653fed
9 changed files with 1705 additions and 44 deletions
@@ -0,0 +1,72 @@
import { useEffect, useState } from "react";
import type { ModuleSensorResponse } from "../../../types/meteo";
import type { HistorianPoint } from "../components/MeteoHistoryModal";
const BACKEND_URL = "http://localhost:18450";
export function useMeteoHistory(sensor: ModuleSensorResponse | null) {
const [hours, setHours] = useState(1);
const [points, setPoints] = useState<HistorianPoint[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!sensor) {
setPoints([]);
return;
}
const sensorKey = sensor.key;
const controller = new AbortController();
async function loadHistory() {
try {
const to = new Date();
const from = new Date(to.getTime() - hours * 60 * 60 * 1000);
const params = new URLSearchParams({
key: `meteo.${sensorKey}`,
from: from.toISOString(),
to: to.toISOString(),
});
setLoading(true);
const response = await fetch(
`${BACKEND_URL}/api/historian/series?${params.toString()}`,
{
signal: controller.signal,
},
);
if (!response.ok) {
throw new Error("Failed to load history");
}
const payload = (await response.json()) as HistorianPoint[];
setPoints(payload);
} catch (error) {
if (controller.signal.aborted) return;
console.error("Failed to load meteo history", error);
setPoints([]);
} finally {
if (!controller.signal.aborted) {
setLoading(false);
}
}
}
loadHistory();
return () => {
controller.abort();
};
}, [sensor?.key, hours]);
return {
points,
loading,
hours,
setHours,
};
}
@@ -0,0 +1,53 @@
import { useEffect, useState } from "react";
import { Client } from "@stomp/stompjs";
import type { MeteoModuleResponse } from "../../../types/meteo";
const WS_URL = "ws://localhost:18450/ws";
const TOPIC = "/topic/modules/meteo/latest";
export function useMeteoModuleStream() {
const [module, setModule] = useState<MeteoModuleResponse | null>(null);
const [connected, setConnected] = useState(false);
const [lastTimestamp, setLastTimestamp] = useState<string | null>(null);
useEffect(() => {
const client = new Client({
brokerURL: WS_URL,
reconnectDelay: 3000,
onConnect: () => {
setConnected(true);
client.subscribe(TOPIC, (message) => {
const payload = JSON.parse(message.body) as MeteoModuleResponse;
setModule(payload);
setLastTimestamp(payload.timestamp);
});
},
onWebSocketClose: () => {
setConnected(false);
},
onStompError: (frame) => {
console.error("Meteo module STOMP error", frame);
setConnected(false);
},
});
client.activate();
return () => {
client.deactivate();
};
}, []);
return {
module,
sensors: module?.sensors ?? [],
sensorCount: module?.sensorCount ?? 0,
connected,
lastTimestamp,
};
}