document.addEventListener('DOMContentLoaded', init); // State let currentRange = 'day'; let chartInstances = {}; // Determine API Base URL from script location (robust for subpaths) const scriptPath = document.currentScript ? document.currentScript.src : window.location.href; const API_BASE = scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1) + 'api/'; async function init() { setupControls(); await loadDevices(); } function setupControls() { // Range Buttons document.querySelectorAll('.range-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.range-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); currentRange = e.target.dataset.range; loadData(); }); }); // Device Select document.getElementById('deviceSelect').addEventListener('change', loadData); } /** * Fetch and populate device list */ async function loadDevices() { try { const res = await fetch(`${API_BASE}devices`); const devices = await res.json(); const select = document.getElementById('deviceSelect'); select.innerHTML = ''; if (devices.length === 0) { const opt = document.createElement('option'); opt.text = "No devices found"; select.add(opt); return; } devices.forEach((dev, index) => { const opt = document.createElement('option'); const label = `${dev.dev_name} - ${dev.port_name || 'Port ' + dev.port}`; opt.text = label; opt.value = JSON.stringify({ devName: dev.dev_name, port: dev.port, portName: dev.port_name }); select.add(opt); // Auto-select first device if (index === 0) { select.value = opt.value; } }); // Trigger initial load if (select.value) { loadData(); } } catch (err) { console.error("Failed to load devices", err); } } /** * Fetch data and render charts */ async function loadData() { const select = document.getElementById('deviceSelect'); if (!select.value) return; const { devName, port, portName } = JSON.parse(select.value); // Update Titles based on type const isLight = portName && portName.toLowerCase().includes('light'); document.getElementById('levelFanTitle').innerText = isLight ? 'Brightness Levels' : 'Fan Speed Levels'; try { const res = await fetch(`${API_BASE}history?devName=${encodeURIComponent(devName)}&port=${port}&range=${currentRange}`); const data = await res.json(); renderCharts(data, isLight); } catch (err) { console.error("Failed to load history", err); } } function renderCharts(data, isLight) { const labels = data.map(d => new Date(d.timestamp).toLocaleString()); // 1. Temp & Humidity const tempCtx = document.getElementById('tempHumChart').getContext('2d'); updateChart('tempHum', tempCtx, labels, [ { label: 'Temperature (°C)', data: data.map(d => d.temp_c), borderColor: '#ff6384', yAxisID: 'y' }, { label: 'Humidity (%)', data: data.map(d => d.humidity), borderColor: '#36a2eb', yAxisID: 'y1' } ], { scales: { y: { type: 'linear', display: true, position: 'left', suggestedMin: 15, title: { display: true, text: 'Temp (°C)' } }, y1: { type: 'linear', display: true, position: 'right', grid: { drawOnChartArea: false }, suggestedMin: 30, suggestedMax: 80, title: { display: true, text: 'Humidity (%)' } } } }); // 2. VPD const vpdCtx = document.getElementById('vpdChart').getContext('2d'); updateChart('vpd', vpdCtx, labels, [{ label: 'VPD (kPa)', data: data.map(d => d.vpd), borderColor: '#4bc0c0', backgroundColor: 'rgba(75, 192, 192, 0.2)', fill: true }]); // 3. Levels (Fan/Light) const levelCtx = document.getElementById('levelChart').getContext('2d'); const levelLabel = isLight ? 'Brightness' : 'Fan Speed'; const levelColor = isLight ? '#ffcd56' : '#9966ff'; // Yellow for light, Purple for fan updateChart('level', levelCtx, labels, [{ label: levelLabel, data: data.map(d => d.fan_speed), // fan_speed used for generic level in DB borderColor: levelColor, backgroundColor: levelColor, stepped: true }], { scales: { y: { suggestedMin: 0, suggestedMax: 10, ticks: { stepSize: 1 } } } }); } function updateChart(id, ctx, labels, datasets, extraOptions = {}) { if (chartInstances[id]) { chartInstances[id].destroy(); } chartInstances[id] = new Chart(ctx, { type: 'line', data: { labels: labels, datasets: datasets.map(ds => ({ ...ds, borderWidth: 2, tension: 0.3, pointRadius: 1 })) }, options: { responsive: true, maintainAspectRatio: false, interaction: { mode: 'index', intersect: false, }, plugins: { legend: { position: 'top' } }, ...extraOptions } }); }