document.addEventListener('DOMContentLoaded', () => { // --- CONFIGURATION --- const API_BASE = "/api"; // Added for API calls // --- DOM ELEMENT REFERENCES --- const grid = document.getElementById('chambersGrid'); const chamberTmpl = document.getElementById('chamberTemplate'); const logTextArea = document.getElementById('instance-log'); const connChip = document.getElementById('connection-status-chip'); const stationNameEl = document.getElementById('station-name'); const stationLocationEl = document.getElementById('station-location'); const deviceIdEl = document.getElementById('device-id'); const lastUpdateEl = document.getElementById('last-update-status'); const stationDiagCodeEl = document.getElementById('station-diag-code'); const backupPowerChip = document.getElementById('backup-power-chip'); const diagFlagsGrid = document.getElementById('diag-flags-grid'); const audioSelect = document.getElementById('audio-command-select'); // Header Buttons const refreshBtn = document.getElementById('refreshBtn'); const downloadBtn = document.getElementById('downloadBtn'); const logoutBtn = document.getElementById('logout-btn'); // --- STATE --- let selectedStation = null; let socket; let statusPollingInterval; // To hold our interval timer let chamberData = Array(9).fill({ batteryPresent: false }); // The list of errors from your Python code const DIAGNOSTIC_ERRORS = [ "Lock Power Cut", "Main Power Cut", "Relayboard CAN", "DB CAN Recv", "MB Can Recv", "Smoke Alarm", "Water Alarm", "Phase Failure", "Earth Leakage" ]; // --- NEW: SWAP PROCESS ELEMENTS & LOGIC --- const swapIdleText = document.getElementById('swap-idle-text'); const swapPairsList = document.getElementById('swap-pairs-list'); const startSwapBtn = document.getElementById('start-swap-btn'); const abortSwapBtn = document.getElementById('abort-swap-btn'); const clearSwapBtn = document.getElementById('clear-swap-btn'); const resetBtn = document.getElementById('station-reset-btn'); let currentPair = [], swapPairs = []; // --- SWAP UI LOGIC --- function updateSwapUI() { const isBuilding = currentPair.length > 0 || swapPairs.length > 0; if (swapIdleText) swapIdleText.style.display = isBuilding ? 'none' : 'block'; if (startSwapBtn) startSwapBtn.disabled = swapPairs.length === 0; const pairedOut = swapPairs.map(p => p[0]), pairedIn = swapPairs.map(p => p[1]); document.querySelectorAll('.chamber-card').forEach(card => { const n = parseInt(card.dataset.chamberId, 10); card.classList.remove('paired', 'pending'); if (pairedOut.includes(n) || pairedIn.includes(n)) card.classList.add('paired'); else if (currentPair.includes(n)) card.classList.add('pending'); }); if (swapPairsList) { swapPairsList.innerHTML = ''; if (isBuilding) { swapPairs.forEach(p => { const e = document.createElement('div'); e.className = 'text-sm font-semibold flex items-center justify-center gap-2 bg-black/20 rounded p-1.5'; e.innerHTML = `${p[0]}${p[1]}`; swapPairsList.appendChild(e); }); if (currentPair.length > 0) { const e = document.createElement('div'); e.className = 'text-sm font-semibold flex items-center justify-center gap-2 bg-black/20 rounded p-1.5 ring-2 ring-sky-500'; e.innerHTML = `${currentPair[0]}?`; swapPairsList.appendChild(e); } } } } function handleChamberClick(num) { // Deselection logic if (currentPair.length === 1 && currentPair[0] === num) { currentPair = []; updateSwapUI(); return; } const isAlreadyPaired = swapPairs.flat().includes(num); if (isAlreadyPaired) { swapPairs = swapPairs.filter(pair => !pair.includes(num)); updateSwapUI(); return; } // Selection logic if (swapPairs.length >= 4) return alert('Maximum of 4 swap pairs reached.'); // Note: Live validation would go here. Removed for testing. currentPair.push(num); if (currentPair.length === 2) { swapPairs.push([...currentPair]); currentPair = []; } updateSwapUI(); } function clearSelection() { currentPair = []; swapPairs = []; updateSwapUI(); } // --- HELPER FUNCTIONS (Your original code is unchanged) --- const applyButtonFeedback = (button) => { if (!button) return; button.classList.add('btn-feedback'); setTimeout(() => { button.classList.remove('btn-feedback'); }, 150); }; // And ensure your sendCommand function does NOT have the feedback logic const sendCommand = (command, data = null) => { if (!selectedStation || !socket || !socket.connected) { logToInstance(`Cannot send command '${command}', not connected.`, "error"); return; } const payload = { station_id: selectedStation.id, command: command, data: data }; socket.emit('rpc_request', payload); logToInstance(`Sent command: ${command}`, 'cmd'); }; const setChipStyle = (element, text, style) => { if (!element) return; const baseClass = element.className.split(' ')[0]; element.textContent = text; element.className = `${baseClass} chip-${style}`; }; const logToInstance = (message, type = 'info') => { if (!logTextArea) return; const timestamp = new Date().toLocaleTimeString(); const typeIndicator = type === 'error' ? '[ERROR]' : type === 'cmd' ? '[CMD]' : '[INFO]'; const newLog = `[${timestamp}] ${typeIndicator} ${message}\n`; logTextArea.value = newLog + logTextArea.value; }; const updateChamberUI = (card, slot) => { if (!card || !slot) return; const filledState = card.querySelector('.filled-state'); const emptyState = card.querySelector('.empty-state'); const doorPill = card.querySelector('.door-pill'); // Always update door status doorPill.textContent = slot.doorStatus ? 'OPEN' : 'CLOSED'; doorPill.className = slot.doorStatus ? 'door-pill door-open' : 'door-pill door-close'; const slotTempText = `${((slot.slotTemperature || 0) / 10).toFixed(1)} °C`; if (slot.batteryPresent) { filledState.style.display = 'flex'; emptyState.style.display = 'none'; // Update the detailed view card.querySelector('.slot-temp').textContent = slotTempText; card.querySelector('.bat-id-big').textContent = slot.batteryIdentification || '—'; card.querySelector('.soc').textContent = `${slot.soc || 0}%`; // --- Populate the detailed view --- card.querySelector('.bat-id-big').textContent = slot.batteryIdentification || '—'; card.querySelector('.soc').textContent = `${slot.soc || 0}%`; card.querySelector('.voltage').textContent = `${((slot.voltage || 0) / 1000).toFixed(1)} V`; card.querySelector('.bat-temp').textContent = `${((slot.batteryMaxTemp || 0) / 10).toFixed(1)} °C`; card.querySelector('.bat-fault').textContent = slot.batteryFaultCode || '—'; card.querySelector('.current').textContent = `${((slot.current || 0) / 1000).toFixed(1)} A`; card.querySelector('.slot-temp').textContent = `${((slot.slotTemperature || 0) / 10).toFixed(1)} °C`; card.querySelector('.chg-fault').textContent = slot.chargerFaultCode || '—'; const batPill = card.querySelector('.battery-status-pill'); batPill.innerHTML = ` Present`; batPill.className = 'battery-status-pill chip chip-emerald'; const chgPill = card.querySelector('.charger-status-pill'); if (slot.chargerMode === 1) { chgPill.innerHTML = ` Charging`; chgPill.className = 'charger-status-pill chip chip-sky'; } else { chgPill.innerHTML = ` Idle`; chgPill.className = 'charger-status-pill chip chip-slate'; } } else { // Show the empty view filledState.style.display = 'none'; emptyState.style.display = 'flex'; // --- DEBUGGING LOGIC --- const tempElement = card.querySelector('.slot-temp-empty'); if (tempElement) { tempElement.textContent = slotTempText; // console.log(`Chamber ${slot.chamberNo}: Found .slot-temp-empty, setting text to: ${slotTempText}`); } else { // console.error(`Chamber ${slot.chamberNo}: Element .slot-temp-empty NOT FOUND! Check your HTML template.`); } } // Check if the icon library is loaded and then render the icons if (typeof lucide !== 'undefined') { lucide.createIcons(); } else { console.error('Lucide icon script is not loaded. Please check dashboard.html'); } }; // --- NEW: Function to decode the SDC and update the UI --- const updateDiagnosticsUI = (sdcCode) => { if (!diagFlagsGrid) return; diagFlagsGrid.innerHTML = ''; // Clear previous statuses DIAGNOSTIC_ERRORS.forEach((errorText, index) => { // Use bitwise AND to check if the bit at this index is set const isActive = (sdcCode & (1 << index)) !== 0; const div = document.createElement('div'); div.textContent = errorText; // Apply different styles based on whether the alarm is active if (isActive) { div.className = 'text-rose-300 text-center font-semibold'; } else { div.className = 'text-slate-500 text-center'; } diagFlagsGrid.appendChild(div); }); }; const resetDashboardUI = () => { grid.querySelectorAll('.chamber-card').forEach(card => { card.querySelector('.bat-id-big').textContent = 'Waiting...'; card.querySelector('.soc').textContent = '—'; card.querySelector('.voltage').textContent = '—'; card.querySelector('.bat-temp').textContent = '—'; card.querySelector('.bat-fault').textContent = '—'; card.querySelector('.current').textContent = '—'; card.querySelector('.slot-temp').textContent = '—'; card.querySelector('.chg-fault').textContent = '—'; // Show the "empty" view by default when resetting card.querySelector('.filled-state').style.display = 'none'; card.querySelector('.empty-state').style.display = 'flex'; }); logToInstance("Station is offline. Clearing stale data.", "error"); }; // --- NEW: This function polls the API for the true station status --- 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 && connChip) { 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'; lastUpdateEl.textContent = "Waiting for data..."; resetDashboardUI(); } } } catch (error) { console.error("Failed to fetch station status:", error); } }; // --- DOWNLOAD MODAL LOGIC --- const showDownloadModal = () => { const modalOverlay = document.createElement('div'); modalOverlay.className = "fixed inset-0 bg-black/70 backdrop-blur-sm flex items-center justify-center z-50"; modalOverlay.innerHTML = `

Export Logs

`; document.body.appendChild(modalOverlay); const startInput = document.getElementById('start-datetime'); const endInput = document.getElementById('end-datetime'); // --- NEW: Initialize flatpickr on the inputs --- const fpConfig = { enableTime: true, dateFormat: "Y-m-d\\TH:i", // Format needed by the backend time_24hr: true }; const fpStart = flatpickr(startInput, fpConfig); const fpEnd = flatpickr(endInput, fpConfig); // --- (The rest of the function is the same) --- const now = new Date(); const oneHourAgo = new Date(now.getTime() - 3600 * 1000); fpStart.setDate(oneHourAgo, true); fpEnd.setDate(now, true); modalOverlay.querySelectorAll('.time-range-btn').forEach(button => { button.addEventListener('click', () => { const range = button.dataset.range; const now = new Date(); let start = new Date(); if (range === 'today') { start.setHours(0, 0, 0, 0); } else if (range === 'yesterday') { start.setDate(start.getDate() - 1); start.setHours(0, 0, 0, 0); now.setDate(now.getDate() - 1); now.setHours(23, 59, 59, 999); } else { start.setHours(now.getHours() - parseInt(range, 10)); } fpStart.setDate(start, true); fpEnd.setDate(now, true); }); }); document.getElementById('cancel-download').onclick = () => document.body.removeChild(modalOverlay); document.getElementById('confirm-download').onclick = async () => { const logType = document.getElementById('log-type').value; const startDateStr = document.getElementById('start-datetime').value; const endDateStr = document.getElementById('end-datetime').value; const confirmBtn = document.getElementById('confirm-download'); if (!startDateStr || !endDateStr) { alert('Please select both a start and end date/time.'); return; } // --- Validation Logic --- const selectedStartDate = new Date(startDateStr); const selectedEndDate = new Date(endDateStr); const currentDate = new Date(); if (selectedStartDate > currentDate) { alert('Error: The start date cannot be in the future.'); return; } if (selectedEndDate > currentDate) { alert('Error: The end date cannot be in the future.'); return; } if (selectedStartDate >= selectedEndDate) { alert('Error: The start date must be earlier than the end date.'); return; } // --- Fetch and Download Logic --- confirmBtn.textContent = 'Fetching...'; confirmBtn.disabled = true; const downloadUrl = `${API_BASE}/logs/export?station_id=${selectedStation.id}&start_datetime=${startDateStr}&end_datetime=${endDateStr}&log_type=${logType}`; try { const response = await fetch(downloadUrl); if (response.ok) { // Status 200, CSV file received const blob = await response.blob(); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; const logType = document.getElementById('log-type').value; const dateStr = startDateStr.split('T')[0]; // Get just the date part let filename = `${selectedStation.name || selectedStation.id}_${logType}_${dateStr}.csv`; const disposition = response.headers.get('Content-Disposition'); if (disposition && disposition.indexOf('attachment') !== -1) { const filenameMatch = disposition.match(/filename="(.+?)"/); if (filenameMatch && filenameMatch.length === 2) { filename = filenameMatch[1]; } } a.download = filename; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); document.body.removeChild(a); document.body.removeChild(modalOverlay); } else { // Status 404, no data found const errorData = await response.json(); alert(`Could not download: ${errorData.message}`); } } catch (error) { alert('An unexpected error occurred. Please check the console.'); console.error('Download error:', error); } finally { confirmBtn.textContent = 'Download CSV'; confirmBtn.disabled = false; } }; }; // --- MAIN LOGIC (Your original code is unchanged) --- const initializeDashboard = () => { try { selectedStation = JSON.parse(localStorage.getItem('selected_station')); if (!selectedStation || !selectedStation.id) { window.location.href = './index.html'; } stationNameEl.textContent = selectedStation.name || 'Unknown Station'; stationLocationEl.textContent = selectedStation.location || 'No location'; deviceIdEl.textContent = selectedStation.id; } catch (e) { document.body.innerHTML = `
${e.message} Go Back
`; return; } grid.innerHTML = ''; for (let i = 1; i <= 9; i++) { const node = chamberTmpl.content.cloneNode(true); const card = node.querySelector('.chamber-card'); card.dataset.chamberId = i; card.querySelector('.slotNo').textContent = i; grid.appendChild(node); } logToInstance(`Dashboard initialized for station: ${selectedStation.name} (${selectedStation.id})`); // --- EVENT LISTENERS --- // NEW: A single, global click listener for all button feedback document.addEventListener('click', (event) => { const button = event.target.closest('.btn'); if (button) { applyButtonFeedback(button); } }); // Chamber Card and Swap Selection document.querySelectorAll('.chamber-card').forEach(card => { // Listener for swap selection card.addEventListener('click', () => handleChamberClick(parseInt(card.dataset.chamberId, 10))); // Listeners for command buttons inside the card card.querySelectorAll('.btn').forEach(btn => { btn.addEventListener('click', (e) => { e.stopPropagation(); const commandText = e.currentTarget.textContent.trim(); const command = commandText.replace(' ', '_'); const chamberNum = parseInt(e.currentTarget.closest('.chamber-card').dataset.chamberId, 10); if (command === 'OPEN') { if (confirm(`Are you sure you want to open door for Chamber ${chamberNum}?`)) { sendCommand(command, chamberNum); } else { logToInstance(`Open Door command for Chamber ${chamberNum} cancelled.`); } } else { sendCommand(command, chamberNum); } }); }); }); // Swap Panel Buttons if (clearSwapBtn) clearSwapBtn.addEventListener('click', clearSelection); if (startSwapBtn) { startSwapBtn.addEventListener('click', () => { if (swapPairs.length > 0) { const formattedPairs = swapPairs.map(p => `[${p[0]},${p[1]}]`).join(','); sendCommand('START_SWAP', formattedPairs); clearSelection(); } }); } if (abortSwapBtn) abortSwapBtn.addEventListener('click', () => sendCommand('ABORT_SWAP')); if (resetBtn) { resetBtn.addEventListener('click', () => { if (confirm('Are you sure you want to reset the station?')) { sendCommand('STATION_RESET'); } }); } // Header Buttons if (refreshBtn) refreshBtn.addEventListener('click', () => window.location.reload()); if (logoutBtn) { logoutBtn.addEventListener('click', () => { localStorage.clear(); window.location.href = './index.html'; }); } if (downloadBtn) downloadBtn.addEventListener('click', showDownloadModal); // Audio Command Button (assuming it exists in your HTML) const sendAudioBtn = document.getElementById('send-audio-btn'); if (sendAudioBtn) { sendAudioBtn.addEventListener('click', () => { const language = document.getElementById('audio-command-select').value; sendCommand('LANGUAGE_UPDATE', language); }); } updateSwapUI(); connectSocket(); checkStationStatus(); statusPollingInterval = setInterval(checkStationStatus, 10000); }; const connectSocket = () => { socket = io(); // --- CHANGED: No longer sets status to "Online" on its own --- socket.on('connect', () => { logToInstance("Successfully connected to the backend WebSocket."); socket.emit('join_station_room', { station_id: selectedStation.id }); }); // --- CHANGED: Sets status to "Offline" correctly on disconnect --- socket.on('disconnect', () => { logToInstance("Disconnected from the backend WebSocket.", "error"); connChip.innerHTML = ` Offline`; connChip.className = 'cham_chip cham_chip-rose'; }); socket.on('dashboard_update', (message) => { // console.log("DEBUG: Received 'dashboard_update' message:", message); const { stationId, data } = message; if (stationId !== selectedStation.id) { console.warn(`Ignoring message for wrong station. Expected ${selectedStation.id}, got ${stationId}`); return; } lastUpdateEl.textContent = `Last Recv ${new Date().toLocaleTimeString()}`; stationDiagCodeEl.textContent = data.stationDiagnosticCode || '—'; // Show/hide the backup power chip based on the payload data if (data.backupSupplyStatus === 1) { backupPowerChip.textContent = 'On Backup'; backupPowerChip.className = 'cham_chip w-full justify-center mt-3 cham_chip-amber'; } else { backupPowerChip.textContent = 'On Mains Power'; backupPowerChip.className = 'cham_chip w-full justify-center mt-3 cham_chip-emerald'; } lastUpdateEl.textContent = new Date().toLocaleTimeString(); stationDiagCodeEl.textContent = data.stationDiagnosticCode || '—'; // --- NEW: Call the function to update the diagnostics grid --- updateDiagnosticsUI(data.stationDiagnosticCode || 0); if (data.slotLevelPayload && Array.isArray(data.slotLevelPayload)) { data.slotLevelPayload.forEach((slotData, index) => { const slotId = index + 1; chamberData[slotId - 1] = slotData; // Keep live data in sync const card = grid.querySelector(`.chamber-card[data-chamber-id="${slotId}"]`); if (card) { updateChamberUI(card, slotData); } }); } }); }; // --- SCRIPT EXECUTION --- initializeDashboard(); });