// document.addEventListener('DOMContentLoaded', () => { // // --- CONFIGURATION --- // const SOCKET_URL = "http://10.10.1.183:5000"; // const API_BASE = "http://10.10.1.183:5000/api"; // // --- DOM ELEMENT REFERENCES --- // const stationNameEl = document.getElementById('station-name'); // const stationLocationEl = document.getElementById('station-location'); // const deviceIdEl = document.getElementById('device-id'); // const productIdEl = document.getElementById('product-id'); // const lastUpdateEl = document.getElementById('last-update-status'); // const connChip = document.getElementById('connection-status-chip'); // const requestLogArea = document.getElementById('request-log-area'); // const eventLogArea = document.getElementById('event-log-area'); // const clearReqBtn = document.getElementById('clear-req'); // const clearEvtBtn = document.getElementById('clear-evt'); // const clearAllBtn = document.getElementById('clear-all'); // const refreshBtn = document.getElementById('refreshBtn'); // const downloadBtn = document.getElementById('downloadBtn'); // const logoutBtn = document.getElementById('logout-btn'); // const resetBtn = document.getElementById('station-reset-btn'); // // --- STATE --- // let selectedStation = null; // let socket; // let statusPollingInterval; // // --- HELPER FUNCTIONS --- // const prependLog = (textarea, data) => { // if (!textarea) return; // const timestamp = new Date().toLocaleTimeString(); // const formattedJson = JSON.stringify(data, null, 2); // const newLog = `[${timestamp}]\n${formattedJson}\n\n---------------------------------\n\n`; // textarea.value = newLog + textarea.value; // }; // const sendCommand = (command, data = null) => { // if (!selectedStation || !socket || !socket.connected) { // console.error(`Cannot send command '${command}', not connected.`); // return; // } // const payload = { station_id: selectedStation.id, command: command, data: data }; // socket.emit('rpc_request', payload); // }; // const checkStationStatus = async () => { // if (!selectedStation) return; // try { // const response = await fetch(`${API_BASE}/stations`); // if (!response.ok) return; // const stations = await response.json(); // const thisStation = stations.find(s => s.id === selectedStation.id); // if (thisStation) { // stationNameEl.textContent = thisStation.name; // stationLocationEl.textContent = thisStation.location; // if (thisStation.status === 'Online') { // connChip.innerHTML = ` Online`; // connChip.className = 'cham_chip cham_chip-emerald'; // } else { // connChip.innerHTML = ` Offline`; // connChip.className = 'cham_chip cham_chip-rose'; // } // } // } catch (error) { console.error("Loading...", error); } // }; // // --- INITIALIZATION --- // try { // selectedStation = JSON.parse(localStorage.getItem('selected_station')); // if (!selectedStation || !selectedStation.id) { // throw new Error('No station selected. Please go back to the selection page.'); // } // deviceIdEl.textContent = selectedStation.id; // productIdEl.textContent = selectedStation.product_id; // } catch (e) { // document.body.innerHTML = `
${e.message}Go Back
`; // return; // } // // --- SOCKET.IO CONNECTION --- // socket = io(SOCKET_URL); // socket.on('connect', () => { // console.log("Connected to WebSocket for logs."); // socket.emit('join_station_room', { station_id: selectedStation.id }); // }); // socket.on('dashboard_update', (message) => { // const { stationId, topic, data } = message; // if (stationId !== selectedStation.id) return; // lastUpdateEl.textContent = 'Last Recv ' + new Date().toLocaleTimeString(); // if (topic.endsWith('EVENTS')) { // prependLog(eventLogArea, data); // } else if (topic.endsWith('REQUEST')) { // prependLog(requestLogArea, data); // } // }); // if(logoutBtn) logoutBtn.addEventListener('click', () => { // localStorage.clear(); // window.location.href = 'index.html'; // }); // if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload()); // // if(downloadBtn) downloadBtn.addEventListener('click', () => alert("Download functionality can be added here.")); // Placeholder for download modal // if(resetBtn) resetBtn.addEventListener('click', () => { // if (confirm('Are you sure you want to reset the station?')) { // sendCommand('STATION_RESET'); // } // }); // // --- STARTUP --- // checkStationStatus(); // statusPollingInterval = setInterval(checkStationStatus, 10000); // if (typeof lucide !== 'undefined') { // lucide.createIcons(); // } // }); document.addEventListener('DOMContentLoaded', () => { // --- CONFIGURATION --- const API_BASE = "http://10.10.1.183:5000/api"; // --- DOM ELEMENT REFERENCES --- // KPI Tiles const totalInitiatedEl = document.getElementById('total-swaps-initiated'); const totalStartedEl = document.getElementById('total-swaps-started'); const completedSwapsEl = document.getElementById('completed-swaps'); const successRateEl = document.getElementById('success-rate'); const abortedSwapsEl = document.getElementById('aborted-swaps'); const abortRateEl = document.getElementById('abort-rate'); const avgSwapTimeEl = document.getElementById('avg-swap-time'); // Date Range Elements const fromDateInput = document.getElementById('from'); const toDateInput = document.getElementById('to'); const applyRangeBtn = document.getElementById('applyRange'); const quickRangeBtns = document.querySelectorAll('.date-range-btn'); // Chart Canvases const swapActivityCanvas = document.getElementById('swapActivityChart'); const hourlyDistributionCanvas = document.getElementById('hourlyDistributionChart'); const abortReasonsCanvas = document.getElementById('abortReasonsChart'); const stationUptimeEl = document.getElementById('station-uptime'); const heatmapGridEl = document.getElementById('heatmap-grid'); const avgSwapTimeValueEl = document.getElementById('avg-swap-time-value'); //status elements const stationNameEl = document.getElementById('station-name'); const stationLocationEl = document.getElementById('station-location'); const deviceIdEl = document.getElementById('device-id'); const productIdEl = document.getElementById('product-id'); const refreshBtn = document.getElementById('refreshBtn'); const resetBtn = document.getElementById('station-reset-btn'); const logoutBtn = document.getElementById('logout-btn'); let socket; // --- CONSTANTS --- const chartDefaults = { color: 'rgba(203, 213, 225, 0.7)', // Light gray for text borderColor: 'rgba(255, 255, 255, 0.1)', // Subtle grid lines }; // --- STATE --- let selectedStation = null; let fromDatePicker, toDatePicker; let swapChartInstance, hourlyChartInstance, abortChartInstance; // To hold chart instances // --- HELPER FUNCTIONS --- const sendCommand = (command, data = null) => { if (!selectedStation || !socket || !socket.connected) { console.error(`Cannot send command '${command}', not connected.`); return; } const payload = { station_id: selectedStation.id, command: command, data: data }; socket.emit('rpc_request', payload); }; /** * Updates the main KPI tiles with data from the API. * @param {object} data - The kpis data from the backend. */ const updateStatTiles = (data) => { if (!data) { // Used for loading state or on error totalInitiatedEl.textContent = '...'; totalStartedEl.textContent = '...'; completedSwapsEl.textContent = '...'; successRateEl.textContent = '(...%)'; abortedSwapsEl.textContent = '...'; abortRateEl.textContent = '(...%)'; avgSwapTimeValueEl.textContent = '...'; stationUptimeEl.textContent = '... %'; return; } // Assign new data from the backend totalInitiatedEl.textContent = data.total_swaps_initiated ?? 0; totalStartedEl.textContent = data.total_swaps_started ?? 0; const completed = data.completed_swaps ?? 0; const aborted = data.aborted_swaps ?? 0; completedSwapsEl.textContent = completed; abortedSwapsEl.textContent = aborted; // Use total_swaps_started for calculating success/abort rates const totalStarts = data.total_swaps_started ?? 0; const successRate = totalStarts > 0 ? ((completed / totalStarts) * 100).toFixed(1) : 0; successRateEl.textContent = `(${successRate}%)`; const abortRate = totalStarts > 0 ? ((aborted / totalStarts) * 100).toFixed(1) : 0; abortRateEl.textContent = `(${abortRate}%)`; const avgTimeInSeconds = data.avg_swap_time_seconds != null ? Math.round(data.avg_swap_time_seconds) : '—'; avgSwapTimeValueEl.textContent = avgTimeInSeconds; stationUptimeEl.textContent = `${data.station_uptime ?? '...'} %`; }; // --- CHART.JS VISUALIZATION CODE --- /** * Renders the "Swap Activity Over Time" bar chart. * @param {object} data - The chart data from the backend. */ const renderSwapActivityChart = (data) => { if (swapChartInstance) { swapChartInstance.destroy(); } // Dynamically calculate the max value for the Y-axis const maxValue = Math.max(...data.completed_data, ...data.aborted_data); const yAxisMax = Math.ceil(maxValue * 1.2) + 1; // Add 20% padding + 1 swapChartInstance = new Chart(swapActivityCanvas, { type: 'bar', data: { labels: data.labels, datasets: [ { label: 'Completed Swaps', data: data.completed_data, backgroundColor: 'rgba(16, 185, 129, 0.6)', borderColor: 'rgba(16, 185, 129, 1)', borderWidth: 1 }, { label: 'Aborted Swaps', data: data.aborted_data, backgroundColor: 'rgba(244, 63, 94, 0.6)', borderColor: 'rgba(244, 63, 94, 1)', borderWidth: 1 } ] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: yAxisMax, // <-- SET DYNAMIC MAX VALUE grid: { color: chartDefaults.borderColor }, ticks: { color: chartDefaults.color, stepSize: 1 } }, x: { grid: { display: false }, ticks: { color: chartDefaults.color } } }, plugins: { legend: { labels: { color: chartDefaults.color } } } } }); }; /** * Renders the "Hourly Swap Distribution" bar chart. * @param {object} data - The chart data from the backend. */ const renderHourlyDistributionChart = (data) => { if (hourlyChartInstance) { hourlyChartInstance.destroy(); } // Dynamically calculate the max value for the Y-axis const maxValue = Math.max(...data.swap_data); const yAxisMax = Math.ceil(maxValue * 1.2) + 1; // Add 20% padding + 1 hourlyChartInstance = new Chart(hourlyDistributionCanvas, { type: 'bar', data: { labels: data.labels, datasets: [{ label: 'Total Swaps', data: data.swap_data, backgroundColor: 'rgba(56, 189, 248, 0.6)', borderColor: 'rgba(56, 189, 248, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, max: yAxisMax, // <-- SET DYNAMIC MAX VALUE grid: { color: chartDefaults.borderColor }, ticks: { color: chartDefaults.color, stepSize: 1 } }, x: { grid: { display: false }, ticks: { color: chartDefaults.color, maxTicksLimit: 12 } } }, plugins: { legend: { labels: { color: chartDefaults.color } } } } }); }; /** * Renders the "Swap Abort Reasons" donut chart. * @param {object} data - The chart data from the backend. */ const renderAbortReasonsChart = (data) => { if (abortChartInstance) { abortChartInstance.destroy(); } abortChartInstance = new Chart(abortReasonsCanvas, { type: 'doughnut', data: { labels: data.labels, // e.g., ['Timeout', 'User Cancelled', ...] datasets: [{ label: 'Count', data: data.reason_data, // e.g., [10, 5, ...] backgroundColor: [ 'rgba(244, 63, 94, 0.7)', 'rgba(245, 158, 11, 0.7)', 'rgba(139, 92, 246, 0.7)', 'rgba(56, 189, 248, 0.7)', ], }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', // Better placement for donuts labels: { color: chartDefaults.color } } } } }); }; /** * Fetches all analytics data from the backend. * @param {string} startDate - The start date in 'YYYY-MM-DD' format. * @param {string} endDate - The end date in 'YYYY-MM-DD' format. */ const fetchAnalyticsData = async (startDate, endDate) => { if (!selectedStation) return; updateStatTiles(null); // Set UI to loading state try { const params = new URLSearchParams({ station_id: selectedStation.id, start_date: startDate, end_date: endDate }); const response = await fetch(`${API_BASE}/analytics?${params}`); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.message || 'Failed to fetch analytics data'); } const analyticsData = await response.json(); // Update all sections of the page with the new data updateStatTiles(analyticsData.kpis); renderSwapActivityChart(analyticsData.swap_activity); renderHourlyDistributionChart(analyticsData.hourly_distribution); renderAbortReasonsChart(analyticsData.abort_reasons); renderSlotHeatmap(analyticsData.slot_utilization); } catch (error) { console.error("Error fetching analytics data:", error); updateStatTiles(null); // Reset UI on error // Destroy charts on error to show a blank state if(swapChartInstance) swapChartInstance.destroy(); if(hourlyChartInstance) hourlyChartInstance.destroy(); if(abortChartInstance) abortChartInstance.destroy(); } }; /** * Renders the Slot Utilization Heatmap. * @param {object} data - The slot_utilization data from the backend. */ const renderSlotHeatmap = (data) => { heatmapGridEl.innerHTML = ''; // Clear previous heatmap if (!data || !data.counts || data.counts.length === 0) return; const counts = data.counts; const maxCount = Math.max(...counts); counts.forEach((count, index) => { const slotNumber = index + 1; // Calculate color intensity: 0 = no usage, 1 = max usage const intensity = maxCount > 0 ? count / maxCount : 0; // Create HSL color: Hue is fixed (e.g., blue), Saturation is fixed, Lightness varies // A low intensity (0) will be dark, a high intensity (1) will be bright. const lightness = 20 + (50 * intensity); // Varies from 20% to 70% const backgroundColor = `hsl(200, 80%, ${lightness}%)`; const cell = document.createElement('div'); cell.className = 'rounded-md flex flex-col items-center justify-center text-white font-bold'; cell.style.backgroundColor = backgroundColor; cell.innerHTML = ` Slot ${slotNumber} ${count} swaps `; heatmapGridEl.appendChild(cell); }); }; // --- INITIALIZATION --- try { selectedStation = JSON.parse(localStorage.getItem('selected_station')); if (!selectedStation || !selectedStation.id) { throw new Error('No station selected.'); } deviceIdEl.textContent = selectedStation.id; productIdEl.textContent = selectedStation.product_id; } catch (e) { document.body.innerHTML = `
${e.message}Go Back
`; return; } // Initialize Flatpickr fromDatePicker = flatpickr(fromDateInput, { dateFormat: "Y-m-d", altInput: true, altFormat: "M j, Y" }); toDatePicker = flatpickr(toDateInput, { dateFormat: "Y-m-d", altInput: true, altFormat: "M j, Y" }); // Default to today and fetch initial data const todayStr = new Date().toISOString().split('T')[0]; fromDatePicker.setDate(todayStr, false); toDatePicker.setDate(todayStr, false); fetchAnalyticsData(todayStr, todayStr); connectSocket(); // --- ADD THIS BLOCK FOR AUTO-REFRESH --- setInterval(() => { console.log("Auto-refreshing analytics data..."); // Use .selectedDates[0] to get the date object const startDateObj = fromDatePicker.selectedDates[0]; const endDateObj = toDatePicker.selectedDates[0]; if (startDateObj && endDateObj) { // Use the formatDate utility to get the string in the correct format const startDate = flatpickr.formatDate(startDateObj, "Y-m-d"); const endDate = flatpickr.formatDate(endDateObj, "Y-m-d"); fetchAnalyticsData(startDate, endDate); } }, 30000); // Refreshes every 30 seconds (30000 milliseconds) // --- EVENT LISTENERS --- applyRangeBtn.addEventListener('click', () => { const startDate = fromDateInput.value; const endDate = toDateInput.value; if (!startDate || !endDate) return alert('Please select both a start and end date.'); if (new Date(startDate) > new Date(endDate)) return alert('Start date cannot be after the end date.'); fetchAnalyticsData(startDate, endDate); }); // (The rest of your button listeners are unchanged) if(logoutBtn) logoutBtn.addEventListener('click', () => { localStorage.clear(); window.location.href = 'index.html'; }); if(refreshBtn) refreshBtn.addEventListener('click', () => location.reload()); if(resetBtn) resetBtn.addEventListener('click', () => { if (confirm('Are you sure you want to reset the station?')) { sendCommand('STATION_RESET'); } }); quickRangeBtns.forEach(btn => { btn.addEventListener('click', () => { const range = btn.dataset.range; const today = new Date(); let startDate = new Date(); if (range === 'today') { startDate = today; } else { startDate.setDate(today.getDate() - (parseInt(range, 10) - 1)); } const startDateStr = startDate.toISOString().split('T')[0]; const endDateStr = today.toISOString().split('T')[0]; fromDatePicker.setDate(startDateStr, false); toDatePicker.setDate(endDateStr, false); fetchAnalyticsData(startDateStr, endDateStr); }); }); });