618 lines
28 KiB
JavaScript
618 lines
28 KiB
JavaScript
document.addEventListener('DOMContentLoaded', () => {
|
|
// --- CONFIGURATION ---
|
|
const SOCKET_URL = "http://192.168.1.8:5000";
|
|
const API_BASE = "http://192.168.1.8:5000/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 = `<span class="text-emerald-300">${p[0]}</span><span>→</span><span class="text-sky-300">${p[1]}</span>`;
|
|
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 = `<span class="text-emerald-300 font-bold">${currentPair[0]}</span><span>→</span><span class="text-gray-400">?</span>`;
|
|
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 = `<span class="h-2 w-2 rounded-full bg-emerald-400 animate-pulseDot"></span> Present`;
|
|
batPill.className = 'battery-status-pill chip chip-emerald';
|
|
|
|
const chgPill = card.querySelector('.charger-status-pill');
|
|
if (slot.chargerMode === 1) {
|
|
chgPill.innerHTML = `<span class="h-2 w-2 rounded-full bg-sky-400 animate-pulseDot"></span> Charging`;
|
|
chgPill.className = 'charger-status-pill chip chip-sky';
|
|
} else {
|
|
chgPill.innerHTML = `<span class="h-2 w-2 rounded-full bg-slate-500"></span> 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 = `<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';
|
|
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 = `
|
|
<div class="bg-slate-800 border border-slate-700 rounded-lg shadow-xl p-6 w-full max-w-md">
|
|
<h3 class="text-lg font-bold text-white mb-4">Export Logs</h3>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<label class="block text-sm font-medium text-gray-300 mb-2">Quick Time Ranges</label>
|
|
<div class="grid grid-cols-3 gap-2">
|
|
<button data-range="1" class="time-range-btn btn btn-ghost !py-1.5">Last Hour</button>
|
|
<button data-range="6" class="time-range-btn btn btn-ghost !py-1.5">Last 6 Hours</button>
|
|
<button data-range="24" class="time-range-btn btn btn-ghost !py-1.5">Last 24 Hours</button>
|
|
<button data-range="today" class="time-range-btn btn btn-ghost !py-1.5">Today</button>
|
|
<button data-range="yesterday" class="time-range-btn btn btn-ghost !py-1.5">Yesterday</button>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="log-type" class="block text-sm font-medium text-gray-300">Log Type</label>
|
|
<select id="log-type" class="mt-1 block w-full bg-slate-700 border-slate-600 rounded-md p-2 text-white">
|
|
<option value="PERIODIC">Periodic Data</option>
|
|
<option value="EVENT">Events & RPC</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label for="start-datetime" class="block text-sm font-medium text-gray-300">Start Date & Time</label>
|
|
<input type="text" id="start-datetime" class="mt-1 block w-full bg-slate-700 border-slate-600 rounded-md p-2 text-white">
|
|
</div>
|
|
<div>
|
|
<label for="end-datetime" class="block text-sm font-medium text-gray-300">End Date & Time</label>
|
|
<input type="text" id="end-datetime" class="mt-1 block w-full bg-slate-700 border-slate-600 rounded-md p-2 text-white">
|
|
</div>
|
|
</div>
|
|
<div class="mt-6 flex justify-end gap-3">
|
|
<button id="cancel-download" class="btn btn-ghost px-4 py-2">Cancel</button>
|
|
<button id="confirm-download" class="btn btn-primary px-4 py-2">Download CSV</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
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 = `<div class="text-center p-8 text-rose-400">${e.message} <a href="./station_selection.html" class="underline">Go Back</a></div>`;
|
|
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(SOCKET_URL);
|
|
|
|
// --- 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 = `<span class="h-2 w-2 rounded-full bg-rose-500"></span> 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();
|
|
}); |