diff --git a/.gitignore b/.gitignore index 837f7f8..115ac4a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ node_modules .env -.DS_Store +ac_data.db +dashboard_log.txt \ No newline at end of file diff --git a/public/dashboard.js b/public/dashboard.js index 65443ab..7ec2e49 100644 --- a/public/dashboard.js +++ b/public/dashboard.js @@ -1,13 +1,13 @@ -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/'; +// Store device info globally to simplify reload +let filteredDevices = []; +// Grouped: { "ControllerName": [ {dev info...}, {dev info...} ] } +let groupedDevices = {}; +let currentRange = 'day'; +let chartInstances = {}; + async function init() { setupControls(); await loadDevices(); @@ -23,80 +23,123 @@ function setupControls() { loadData(); }); }); - - // Device Select - document.getElementById('deviceSelect').addEventListener('change', loadData); } /** - * Fetch and populate device list + * Fetch and setup device containers (Grouped by Controller) */ async function loadDevices() { try { const res = await fetch(`${API_BASE}devices`); - const devices = await res.json(); + const rawDevices = await res.json(); + const container = document.getElementById('devicesContainer'); + container.innerHTML = ''; - const select = document.getElementById('deviceSelect'); - select.innerHTML = ''; - - if (devices.length === 0) { - const opt = document.createElement('option'); - opt.text = "No devices found"; - select.add(opt); + if (rawDevices.length === 0) { + container.innerHTML = '

No devices found.

'; 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); + // Group by Controller Name + groupedDevices = rawDevices.reduce((acc, dev) => { + if (!acc[dev.dev_name]) acc[dev.dev_name] = []; + acc[dev.dev_name].push(dev); + return acc; + }, {}); - // Auto-select first device - if (index === 0) { - select.value = opt.value; - } - }); + // Create Section per Controller + for (const [controllerName, ports] of Object.entries(groupedDevices)) { + const safeControllerName = controllerName.replace(/\s+/g, '_'); - // Trigger initial load - if (select.value) { - loadData(); + const section = document.createElement('div'); + section.className = 'controller-section'; + + // Generate Ports HTML + let portsHtml = ''; + ports.forEach(port => { + const safePortId = `${controllerName}_${port.port}`.replace(/\s+/g, '_'); + const isLight = port.port_name && port.port_name.toLowerCase().includes('light'); + const levelTitle = isLight ? 'Brightness' : 'Fan Speed'; + + portsHtml += ` +
+

${port.port_name || 'Port ' + port.port}

+
+ +
+
+ `; + }); + + section.innerHTML = ` +
+

${controllerName}

+
+ +
+
+

Environment (Temp / Humidity)

+
+ +
+
+
+ +
+ ${portsHtml} +
+ `; + container.appendChild(section); } + // Trigger initial data load + loadData(); + } catch (err) { console.error("Failed to load devices", err); } } /** - * Fetch data and render charts + * Fetch data and render */ async function loadData() { - const select = document.getElementById('deviceSelect'); - if (!select.value) return; + for (const [controllerName, ports] of Object.entries(groupedDevices)) { + const safeControllerName = controllerName.replace(/\s+/g, '_'); - const { devName, port, portName } = JSON.parse(select.value); + // 1. Fetch Environment Data (Use first port as representative source) + if (ports.length > 0) { + const firstPort = ports[0]; + try { + const res = await fetch(`${API_BASE}history?devName=${encodeURIComponent(controllerName)}&port=${firstPort.port}&range=${currentRange}`); + const data = await res.json(); + renderEnvChart(safeControllerName, data); + } catch (err) { + console.error(`Failed to load env data for ${controllerName}`, err); + } + } - // Update Titles based on type - const isLight = portName && portName.toLowerCase().includes('light'); - document.getElementById('levelFanTitle').innerText = isLight ? 'Brightness Levels' : 'Fan Speed Levels'; + // 2. Fetch Level Data for EACH port + for (const port of ports) { + const safePortId = `${controllerName}_${port.port}`.replace(/\s+/g, '_'); + try { + const res = await fetch(`${API_BASE}history?devName=${encodeURIComponent(controllerName)}&port=${port.port}&range=${currentRange}`); + const data = await res.json(); - 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); + const isLight = port.port_name && port.port_name.toLowerCase().includes('light'); + renderLevelChart(safePortId, data, isLight); + } catch (err) { + console.error(`Failed to load level data for ${controllerName}:${port.port}`, err); + } + } } } -function renderCharts(data, isLight) { +function renderEnvChart(safeName, data) { const labels = data.map(d => new Date(d.timestamp).toLocaleString()); + const ctx = document.getElementById(`env_${safeName}`).getContext('2d'); - // 1. Temp & Humidity - const tempCtx = document.getElementById('tempHumChart').getContext('2d'); - updateChart('tempHum', tempCtx, labels, [ + updateChart(`env_${safeName}`, ctx, labels, [ { label: 'Temperature (°C)', data: data.map(d => d.temp_c), @@ -129,25 +172,17 @@ function renderCharts(data, isLight) { } } }); +} - // 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'); +function renderLevelChart(safeId, data, isLight) { + const labels = data.map(d => new Date(d.timestamp).toLocaleString()); + const ctx = document.getElementById(`level_${safeId}`).getContext('2d'); const levelLabel = isLight ? 'Brightness' : 'Fan Speed'; - const levelColor = isLight ? '#ffcd56' : '#9966ff'; // Yellow for light, Purple for fan + const levelColor = isLight ? '#ffcd56' : '#9966ff'; - updateChart('level', levelCtx, labels, [{ + updateChart(`level_${safeId}`, ctx, labels, [{ label: levelLabel, - data: data.map(d => d.fan_speed), // fan_speed used for generic level in DB + data: data.map(d => d.fan_speed), borderColor: levelColor, backgroundColor: levelColor, stepped: true @@ -187,3 +222,5 @@ function updateChart(id, ctx, labels, datasets, extraOptions = {}) { } }); } + +document.addEventListener('DOMContentLoaded', init); diff --git a/public/index.html b/public/index.html index 365ad0e..e210b85 100644 --- a/public/index.html +++ b/public/index.html @@ -16,12 +16,7 @@
-
- - -
+
@@ -30,19 +25,8 @@
-
-
-

Temperature & Humidity

- -
-
-

VPD

- -
-
-

Levels

- -
+
+
diff --git a/public/style.css b/public/style.css index 1faae13..117cf92 100644 --- a/public/style.css +++ b/public/style.css @@ -31,7 +31,7 @@ header { background: var(--card-bg); padding: 15px 20px; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); margin-bottom: 20px; } @@ -59,7 +59,8 @@ select { transition: all 0.2s; } -.range-btn.active, .range-btn:hover { +.range-btn.active, +.range-btn:hover { background: var(--accent-color); color: white; } @@ -74,7 +75,7 @@ select { background: var(--card-bg); padding: 20px; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.1); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } h3 { @@ -88,3 +89,114 @@ canvas { width: 100% !important; height: 300px !important; } + +/* Multi-Device Layout */ +.device-section { + margin-bottom: 40px; +} + +.device-header { + background: #e9ecef; + padding: 10px 15px; + border-radius: 6px; + margin-bottom: 20px; + border-left: 5px solid #2c3e50; +} + +.charts-row { + display: flex; + flex-wrap: wrap; + gap: 20px; +} + +.chart-wrapper { + flex: 1; + min-width: 400px; + /* Ensure 2 columns on wide screens */ + background: white; + padding: 15px; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.canvas-container { + position: relative; + height: 300px; + width: 100%; +} + +.device-divider { + border: 0; + height: 1px; + background: #ddd; + margin: 40px 0; +} + +/* Range Buttons Active State */ +.range-btn.active { + background-color: #2c3e50; + color: white; +} + +/* Grouped Controller Layout */ +.controller-section { + background: #fff; + border-radius: 8px; + padding: 20px; + margin-bottom: 40px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05); +} + +.controller-header { + border-bottom: 2px solid #f0f0f0; + margin-bottom: 20px; + padding-bottom: 10px; +} + +.controller-header h2 { + margin: 0; + color: #2c3e50; +} + +/* Environment Chart (Full Width) */ +.environment-row { + margin-bottom: 30px; +} + +.chart-wrapper.wide { + width: 100%; + min-width: unset; + box-shadow: none; + /* Inside controller card */ + padding: 0; +} + +/* Ports Grid */ +.ports-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); + gap: 20px; +} + +.port-card { + background: #f8f9fa; + border: 1px solid #e9ecef; + border-radius: 6px; + padding: 15px; +} + +.port-card h4 { + margin-top: 0; + margin-bottom: 10px; + font-size: 1.1em; + color: #495057; +} + +.canvas-container.small { + height: 200px; +} + +/* Override chart height for small containers */ +.port-card canvas { + height: 200px !important; +} \ No newline at end of file