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 = `
+
+
+
+
+
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