This commit is contained in:
sebseb7
2025-12-20 21:17:48 +01:00
parent 3da9be5c1b
commit 12be2c7bf9
4 changed files with 222 additions and 88 deletions

3
.gitignore vendored
View File

@@ -1,3 +1,4 @@
node_modules node_modules
.env .env
.DS_Store ac_data.db
dashboard_log.txt

View File

@@ -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 scriptPath = document.currentScript ? document.currentScript.src : window.location.href;
const API_BASE = scriptPath.substring(0, scriptPath.lastIndexOf('/') + 1) + 'api/'; 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() { async function init() {
setupControls(); setupControls();
await loadDevices(); await loadDevices();
@@ -23,80 +23,123 @@ function setupControls() {
loadData(); 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() { async function loadDevices() {
try { try {
const res = await fetch(`${API_BASE}devices`); 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'); if (rawDevices.length === 0) {
select.innerHTML = ''; container.innerHTML = '<p>No devices found.</p>';
if (devices.length === 0) {
const opt = document.createElement('option');
opt.text = "No devices found";
select.add(opt);
return; return;
} }
devices.forEach((dev, index) => { // Group by Controller Name
const opt = document.createElement('option'); groupedDevices = rawDevices.reduce((acc, dev) => {
const label = `${dev.dev_name} - ${dev.port_name || 'Port ' + dev.port}`; if (!acc[dev.dev_name]) acc[dev.dev_name] = [];
opt.text = label; acc[dev.dev_name].push(dev);
opt.value = JSON.stringify({ devName: dev.dev_name, port: dev.port, portName: dev.port_name }); return acc;
select.add(opt); }, {});
// Auto-select first device // Create Section per Controller
if (index === 0) { for (const [controllerName, ports] of Object.entries(groupedDevices)) {
select.value = opt.value; const safeControllerName = controllerName.replace(/\s+/g, '_');
}
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 += `
<div class="port-card">
<h4>${port.port_name || 'Port ' + port.port}</h4>
<div class="canvas-container small">
<canvas id="level_${safePortId}"></canvas>
</div>
</div>
`;
}); });
// Trigger initial load section.innerHTML = `
if (select.value) { <div class="controller-header">
loadData(); <h2>${controllerName}</h2>
</div>
<div class="environment-row">
<div class="chart-wrapper wide">
<h3>Environment (Temp / Humidity)</h3>
<div class="canvas-container">
<canvas id="env_${safeControllerName}"></canvas>
</div>
</div>
</div>
<div class="ports-grid">
${portsHtml}
</div>
`;
container.appendChild(section);
} }
// Trigger initial data load
loadData();
} catch (err) { } catch (err) {
console.error("Failed to load devices", err); console.error("Failed to load devices", err);
} }
} }
/** /**
* Fetch data and render charts * Fetch data and render
*/ */
async function loadData() { async function loadData() {
const select = document.getElementById('deviceSelect'); for (const [controllerName, ports] of Object.entries(groupedDevices)) {
if (!select.value) return; const safeControllerName = controllerName.replace(/\s+/g, '_');
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';
// 1. Fetch Environment Data (Use first port as representative source)
if (ports.length > 0) {
const firstPort = ports[0];
try { try {
const res = await fetch(`${API_BASE}history?devName=${encodeURIComponent(devName)}&port=${port}&range=${currentRange}`); const res = await fetch(`${API_BASE}history?devName=${encodeURIComponent(controllerName)}&port=${firstPort.port}&range=${currentRange}`);
const data = await res.json(); const data = await res.json();
renderCharts(data, isLight); renderEnvChart(safeControllerName, data);
} catch (err) { } catch (err) {
console.error("Failed to load history", err); console.error(`Failed to load env data for ${controllerName}`, err);
} }
} }
function renderCharts(data, isLight) { // 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();
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 renderEnvChart(safeName, data) {
const labels = data.map(d => new Date(d.timestamp).toLocaleString()); const labels = data.map(d => new Date(d.timestamp).toLocaleString());
const ctx = document.getElementById(`env_${safeName}`).getContext('2d');
// 1. Temp & Humidity updateChart(`env_${safeName}`, ctx, labels, [
const tempCtx = document.getElementById('tempHumChart').getContext('2d');
updateChart('tempHum', tempCtx, labels, [
{ {
label: 'Temperature (°C)', label: 'Temperature (°C)',
data: data.map(d => d.temp_c), data: data.map(d => d.temp_c),
@@ -129,25 +172,17 @@ function renderCharts(data, isLight) {
} }
} }
}); });
}
// 2. VPD function renderLevelChart(safeId, data, isLight) {
const vpdCtx = document.getElementById('vpdChart').getContext('2d'); const labels = data.map(d => new Date(d.timestamp).toLocaleString());
updateChart('vpd', vpdCtx, labels, [{ const ctx = document.getElementById(`level_${safeId}`).getContext('2d');
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 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, 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, borderColor: levelColor,
backgroundColor: levelColor, backgroundColor: levelColor,
stepped: true stepped: true
@@ -187,3 +222,5 @@ function updateChart(id, ctx, labels, datasets, extraOptions = {}) {
} }
}); });
} }
document.addEventListener('DOMContentLoaded', init);

View File

@@ -16,12 +16,7 @@
</header> </header>
<div class="controls"> <div class="controls">
<div class="selector"> <!-- Device Select Removed -->
<label for="deviceSelect">Device / Port:</label>
<select id="deviceSelect">
<option value="" disabled selected>Loading...</option>
</select>
</div>
<div class="time-range"> <div class="time-range">
<button class="range-btn active" data-range="day">24 Hours</button> <button class="range-btn active" data-range="day">24 Hours</button>
@@ -30,19 +25,8 @@
</div> </div>
</div> </div>
<div class="charts-container"> <div id="devicesContainer" class="devices-container">
<div class="chart-wrapper"> <!-- Dynamic Content Will Be Loaded Here -->
<h3>Temperature & Humidity</h3>
<canvas id="tempHumChart"></canvas>
</div>
<div class="chart-wrapper">
<h3>VPD</h3>
<canvas id="vpdChart"></canvas>
</div>
<div class="chart-wrapper">
<h3 id="levelFanTitle">Levels</h3>
<canvas id="levelChart"></canvas>
</div>
</div> </div>
</div> </div>

View File

@@ -59,7 +59,8 @@ select {
transition: all 0.2s; transition: all 0.2s;
} }
.range-btn.active, .range-btn:hover { .range-btn.active,
.range-btn:hover {
background: var(--accent-color); background: var(--accent-color);
color: white; color: white;
} }
@@ -88,3 +89,114 @@ canvas {
width: 100% !important; width: 100% !important;
height: 300px !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;
}