import { useEffect, useState, useCallback, useRef } from 'react'; import { Box, Paper, Typography, Fade, useTheme, Grid, AppBar, Toolbar, CircularProgress, IconButton, Tooltip as MuiTooltip, Chip, Button, Snackbar, Alert, Slider } from '@mui/material'; import RefreshIcon from '@mui/icons-material/Refresh'; import CheckCircleIcon from '@mui/icons-material/CheckCircle'; import CancelIcon from '@mui/icons-material/Cancel'; import { monitoringService } from '../services/monitoringService'; import { MonitoringStatus } from '../types/monitoring'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Title, Tooltip as ChartTooltip, Legend, TimeScale } from 'chart.js'; import 'chartjs-adapter-date-fns'; import { Line } from 'react-chartjs-2'; import { config } from '../config/env'; // Register Chart.js components ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, Title, ChartTooltip, Legend, TimeScale ); // Define the structure of our data interface ChartData { power: string; flag: string; env_temp_cur: string; now_timestamp: string; future_timestamp: string; env_temp_min: string; power_future_min: string; } const API_BASE_URL = config.apiUrl; const Temperature = () => { const theme = useTheme(); const [data, setData] = useState([]); const [loading, setLoading] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [refreshing, setRefreshing] = useState(false); const [decisionLoading, setDecisionLoading] = useState(false); const [windowSize, setWindowSize] = useState(20); const [alert, setAlert] = useState<{ open: boolean; message: string; severity: 'success' | 'error' }>({ open: false, message: '', severity: 'success' }); // Monitoring status state const [monitoringStatus, setMonitoringStatus] = useState(null); // Use refs to keep track of the interval const intervalRef = useRef | null>(null); const updateIntervalMs = 5000; // 5 seconds refresh rate // Create a memoized fetchData function with useCallback const fetchData = useCallback(async (showLoadingIndicator = false) => { try { if (showLoadingIndicator) { setRefreshing(true); } const response = await fetch(`${API_BASE_URL}/prom/get_chart_data/temperature/20`); const result = await response.json(); if (result.data && result.data.length > 0) { // Sort by timestamp first to ensure we get the latest data const sortedData = [...result.data].sort((a, b) => new Date(b.now_timestamp).getTime() - new Date(a.now_timestamp).getTime() ); // Log the most recent flag console.log('Most recent flag:', sortedData[0].flag); // Filter valid data points const validData = sortedData.filter((item: any) => item.now_timestamp && item.future_timestamp && item.power && item.power_future_min && item.env_temp_cur && item.env_temp_min ); // Limit to last 20 records but maintain chronological order const last20Data = validData.slice(-20).sort((a, b) => new Date(a.now_timestamp).getTime() - new Date(b.now_timestamp).getTime() ); console.log(`Data updated at ${new Date().toLocaleTimeString()}:`, { totalRecords: result.data.length, validRecords: validData.length, displayedRecords: last20Data.length, latestFlag: last20Data[last20Data.length - 1]?.flag }); setData(last20Data); setLastUpdated(new Date()); } } catch (error) { console.error('Error fetching data:', error); } finally { setLoading(false); setRefreshing(false); } }, []); // Manual refresh handler const handleRefresh = () => { fetchData(true); }; // Set up the interval for real-time updates useEffect(() => { // Initial fetch fetchData(true); // Set up interval for auto-refresh intervalRef.current = setInterval(() => { console.log(`Auto-refreshing data at ${new Date().toLocaleTimeString()}`); fetchData(false); }, updateIntervalMs); // Cleanup function to clear the interval when component unmounts return () => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } }; }, [fetchData]); // Set up monitoring status polling useEffect(() => { // Start polling for monitoring status monitoringService.startStatusPolling((status) => { setMonitoringStatus(status); }, 5000); // Poll every 5 seconds // Cleanup function to stop polling when component unmounts return () => { monitoringService.stopStatusPolling(); }; }, []); // Process data for charts with sliding window const prepareChartData = () => { if (!data || data.length === 0) { console.log('No data available, using fallback data'); // Fallback data for testing const now = new Date(); const fallbackData = Array.from({ length: 10 }, (_, i) => ({ currentTimestamp: new Date(now.getTime() - (9 - i) * 60000), // 1 minute intervals futureTimestamp: new Date(now.getTime() - (9 - i) * 60000 + 3 * 60000), // 3 minutes in the future currentPower: 95 + Math.random() * 10, predictedPower: 115 + Math.random() * 10, currentTemp: 25 + Math.random() * 5, predictedTemp: 28 + Math.random() * 5, })); return fallbackData; } const processedData = data.map(item => ({ currentTimestamp: new Date(item.now_timestamp), futureTimestamp: new Date(item.future_timestamp), currentPower: parseFloat(item.power), predictedPower: parseFloat(item.power_future_min), currentTemp: parseFloat(item.env_temp_cur), predictedTemp: parseFloat(item.env_temp_min), })); // Apply sliding window - show only last N records const slidingData = processedData.slice(-windowSize); console.log('Processed chart data:', { totalRecords: processedData.length, showingRecords: slidingData.length, timeRange: { start: slidingData[0]?.currentTimestamp, end: slidingData[slidingData.length - 1]?.currentTimestamp } }); return slidingData; }; const chartData = prepareChartData(); // Prepare Chart.js data structures const powerChartData = { datasets: [ { label: 'Current Power', data: chartData.map(item => ({ x: item.currentTimestamp, y: item.currentPower })), borderColor: '#028a4a', backgroundColor: '#028a4a', pointBackgroundColor: '#028a4a', pointBorderColor: '#028a4a', pointRadius: 0, pointHoverRadius: 6, tension: 0.4, fill: false }, { label: 'Predicted Power (Dynamic)', data: chartData.map(item => ({ x: item.futureTimestamp, y: item.predictedPower })), borderColor: '#ff9800', backgroundColor: '#ff9800', pointBackgroundColor: '#ff9800', pointBorderColor: '#ff9800', pointRadius: 0, pointHoverRadius: 6, tension: 0.4, fill: false } ] }; const temperatureChartData = { datasets: [ { label: 'Current Temperature', data: chartData.map(item => ({ x: item.currentTimestamp, y: item.currentTemp })), borderColor: '#028a4a', backgroundColor: '#028a4a', pointBackgroundColor: '#028a4a', pointBorderColor: '#028a4a', pointRadius: 0, pointHoverRadius: 6, tension: 0.4, fill: false }, { label: 'Predicted Temperature (Dynamic)', data: chartData.map(item => ({ x: item.futureTimestamp, y: item.predictedTemp })), borderColor: '#ff9800', backgroundColor: '#ff9800', pointBackgroundColor: '#ff9800', pointBorderColor: '#ff9800', pointRadius: 0, pointHoverRadius: 6, tension: 0.4, fill: false } ] }; // Chart.js options const chartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top' as const, labels: { usePointStyle: true, padding: 20, font: { size: 12, weight: 'normal' as const } } }, tooltip: { mode: 'index' as const, intersect: false, callbacks: { title: function(context: any) { const date = new Date(context[0].parsed.x); return date.toLocaleTimeString(); }, label: function(context: any) { return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}`; } } } }, scales: { x: { type: 'time' as const, time: { displayFormats: { minute: 'HH:mm:ss' } }, title: { display: true, text: 'Time' } }, y: { title: { display: true, text: 'Value' } } }, interaction: { mode: 'nearest' as const, axis: 'x' as const, intersect: false } }; // Debug logging console.log('Chart.js data structures:', { powerChartData, temperatureChartData }); console.log('Raw data length:', data.length); // Handle temperature decision const handleTemperatureDecision = async (approval: boolean) => { try { setDecisionLoading(true); const response = await fetch(`${API_BASE_URL}/prom/temperature/decisions?approval=${approval}`, { method: 'POST', headers: { 'accept': 'application/json', } }); if (!response.ok) { throw new Error(`Failed to send temperature decision: ${response.statusText}`); } const result = await response.json(); setAlert({ open: true, message: result.message || `Temperature change ${approval ? 'approved' : 'declined'} successfully`, severity: 'success' }); // Refresh data after decision await fetchData(true); } catch (error) { console.error('Error sending temperature decision:', error); setAlert({ open: true, message: error instanceof Error ? error.message : 'Failed to send temperature decision', severity: 'error' }); } finally { setDecisionLoading(false); } }; const handleCloseAlert = () => { setAlert(prev => ({ ...prev, open: false })); }; return ( Environmental Temperature & Power Monitoring {lastUpdated && ( Last updated: {lastUpdated.toLocaleTimeString()} )} {/* Power Chart */} Power Consumption {data.length > 0 && ( )} {refreshing && ( )} {loading ? ( Loading power data... ) : ( )} {/* Temperature Chart */} Environmental Temperature {data.length > 0 && ( )} {refreshing && ( )} {loading ? ( Loading temperature data... ) : ( )} {/* Chart Controls */} Chart Settings Configure chart display and temperature change proposals {/* Temperature Change Proposal Section */} Temperature Change Proposal {(!monitoringStatus?.statuses?.environmental?.is_running || !monitoringStatus?.statuses?.preventive?.is_running) ? ( Waiting for services... ) : data.length > 0 && chartData.some(item => !isNaN(item.currentTemp) && item.currentTemp > 0) ? ( ) : ( No temperature data available )} Records to show: setWindowSize(value as number)} min={5} max={50} step={5} marks={[ { value: 5, label: '5' }, { value: 20, label: '20' }, { value: 50, label: '50' } ]} sx={{ flex: 1, maxWidth: 200 }} /> {windowSize} {/* Snackbar for alerts */} {alert.message} ); }; export default Temperature;