SwapStation_WebApp/frontend/js/analytics.js

540 lines
22 KiB
JavaScript

// 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 = `<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Online`;
// connChip.className = 'cham_chip cham_chip-emerald';
// } else {
// connChip.innerHTML = `<span class="h-2 w-2 rounded-full bg-rose-500"></span> 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 = `<div class="text-center p-8 text-rose-400">${e.message}<a href="./station_selection.html" class="underline ml-2">Go Back</a></div>`;
// 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 = `
<span class="text-xs opacity-70">Slot ${slotNumber}</span>
<span class="text-2xl">${count}</span>
<span class="text-xs opacity-70">swaps</span>
`;
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 = `<div class="text-center p-8 text-rose-400">${e.message}<a href="./station_selection.html" class="underline ml-2">Go Back</a></div>`;
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);
});
});
});