Feature Complete: Modern Glass UI, Sensor History, Seed Genetics & Cleanup
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
All checks were successful
Docker Build & Push / build-and-push (push) Successful in 28s
This commit is contained in:
175
src/sensors.php
175
src/sensors.php
@@ -32,44 +32,50 @@ if ($stmt = $mysqli->prepare($sql)) {
|
||||
require_once 'includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="card header-card glass-effect mb-4">
|
||||
<div class="card header-card mb-4">
|
||||
<div class="card-body">
|
||||
<h1 class="mb-0">Sensor-Übersicht</h1>
|
||||
<p class="card-text text-white-50 mt-2">Live-Daten aller deiner aktiven Pflanzen.</p>
|
||||
<p class="card-text mt-2">Live-Daten aller deiner aktiven Pflanzen.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-center gap-2 mb-4">
|
||||
<button class="btn btn-sm btn-outline-dark sensor-range-btn active" data-range="24h">24 Stunden</button>
|
||||
<button class="btn btn-sm btn-outline-dark sensor-range-btn" data-range="7d">7 Tage</button>
|
||||
<button class="btn btn-sm btn-outline-dark sensor-range-btn" data-range="30d">30 Tage</button>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<?php if (empty($plants_with_sensors)): ?>
|
||||
<div class="col-12">
|
||||
<div class="cazubu-table-frameless glass-effect p-4 text-center">
|
||||
<h5 class="text-white">Keine Sensordaten verfügbar</h5>
|
||||
<p class="text-white-50">Für deine aktiven Pflanzen wurden noch keine Sensordaten über die API gesendet.</p>
|
||||
<div class="alert alert-light text-center border shadow-sm">
|
||||
<h5>Keine Sensordaten verfügbar</h5>
|
||||
<p class="text-muted">Für deine aktiven Pflanzen wurden noch keine Sensordaten über die API gesendet.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<?php foreach ($plants_with_sensors as $plant): ?>
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="cazubu-table-frameless glass-effect">
|
||||
<table class="table mb-0">
|
||||
<thead class="table-dark">
|
||||
<tr><th><a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="text-white text-decoration-none h5 mb-0"><?php echo htmlspecialchars($plant['container_name']) . ' (' . htmlspecialchars($plant['zone_name']) . ') - ' . htmlspecialchars($plant['strain_name']) . ' (' . htmlspecialchars($plant['internal_name']) . ')'; ?></a></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="p-3">
|
||||
<div class="row sensor-chart-group" data-plant-id="<?php echo $plant['id']; ?>">
|
||||
<div class="col-md-6" id="temp-chart-wrapper-<?php echo $plant['id']; ?>">
|
||||
<div class="text-center p-4"><div class="spinner-border text-light spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
<div class="col-md-6" id="humidity-chart-wrapper-<?php echo $plant['id']; ?>">
|
||||
<div class="text-center p-4"><div class="spinner-border text-light spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="card h-100 border-0 shadow-sm" style="background-color: rgba(255, 255, 255, 0.9); backdrop-filter: blur(5px);">
|
||||
<div class="card-header bg-dark text-white py-3">
|
||||
<h5 class="mb-0 fs-6 text-white fw-bold text-uppercase ls-1">
|
||||
<a href="plant_detail.php?id=<?php echo $plant['id']; ?>" class="text-white text-decoration-none">
|
||||
<?php echo htmlspecialchars($plant['container_name']); ?>
|
||||
<span class="text-white-50 fw-normal mx-1">|</span>
|
||||
<?php echo htmlspecialchars($plant['strain_name']); ?>
|
||||
</a>
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row sensor-chart-group" data-plant-id="<?php echo $plant['id']; ?>">
|
||||
<div class="col-md-6 mb-3 mb-md-0" id="temp-chart-wrapper-<?php echo $plant['id']; ?>">
|
||||
<div class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
<div class="col-md-6" id="humidity-chart-wrapper-<?php echo $plant['id']; ?>">
|
||||
<div class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm" role="status"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
@@ -78,62 +84,83 @@ require_once 'includes/header.php';
|
||||
|
||||
<?php require_once 'includes/footer.php'; ?>
|
||||
|
||||
<style>
|
||||
.chart-wrapper {
|
||||
position: relative;
|
||||
height: 200px; /* Höhe weiter reduziert für kompaktere Darstellung */
|
||||
width: 100%;
|
||||
background-color: rgba(255,255,255,0.7);
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('.sensor-chart-group').each(function() {
|
||||
const plantId = $(this).data('plant-id');
|
||||
const tempWrapper = $(this).find('#temp-chart-wrapper-' + plantId);
|
||||
const humidityWrapper = $(this).find('#humidity-chart-wrapper-' + plantId);
|
||||
// Store chart instances to destroy them before re-creating
|
||||
const charts = {};
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: 'ajax_handler.php',
|
||||
data: { action: 'get_sensor_data', plant_id: plantId },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
tempWrapper.empty();
|
||||
humidityWrapper.empty();
|
||||
function loadAllCharts(range) {
|
||||
$('.sensor-chart-group').each(function() {
|
||||
const plantId = $(this).data('plant-id');
|
||||
const tempWrapper = $(this).find('#temp-chart-wrapper-' + plantId);
|
||||
const humidityWrapper = $(this).find('#humidity-chart-wrapper-' + plantId);
|
||||
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
// Temperatur-Chart
|
||||
if (response.data.temperature.some(v => v !== null)) {
|
||||
tempWrapper.append('<h6 class="text-dark text-center small">Temperatur</h6>');
|
||||
let tempCanvas = $('<canvas>');
|
||||
tempWrapper.append($('<div>').addClass('chart-wrapper').append(tempCanvas));
|
||||
new Chart(tempCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: '°C', data: response.data.temperature, borderColor: 'rgba(255, 99, 132, 1)', tension: 0.2, spanGaps: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#212529', display: false } }, y: { ticks: { color: '#212529' } } } } });
|
||||
// Show spinners only if empty (optional UX choice, here we force reload)
|
||||
// tempWrapper.html('<div class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm"></div></div>');
|
||||
// humidityWrapper.html('<div class="text-center p-4"><div class="spinner-border text-secondary spinner-border-sm"></div></div>');
|
||||
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: 'ajax_handler.php',
|
||||
data: { action: 'get_sensor_data', plant_id: plantId, range: range },
|
||||
dataType: 'json',
|
||||
success: function(response) {
|
||||
tempWrapper.empty();
|
||||
humidityWrapper.empty();
|
||||
|
||||
if (response.success && response.data.labels.length > 0) {
|
||||
// Temp Chart
|
||||
if (response.data.temperature.some(v => v !== null)) {
|
||||
tempWrapper.append('<h6 class="text-center fw-bold text-dark mb-2">Temperatur</h6>');
|
||||
let canvasId = 'temp-canvas-' + plantId;
|
||||
tempWrapper.append($('<div style="height: 180px;">').append($('<canvas id="' + canvasId + '">')));
|
||||
|
||||
if(charts[canvasId]) charts[canvasId].destroy();
|
||||
charts[canvasId] = new Chart(document.getElementById(canvasId), {
|
||||
type: 'line',
|
||||
data: { labels: response.data.labels, datasets: [{ label: '°C', data: response.data.temperature, borderColor: '#ff6b6b', backgroundColor: 'rgba(255, 107, 107, 0.1)', fill: true, tension: 0.3, pointRadius: 3, pointHoverRadius: 5 }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { ticks: { color: '#212529', font: { size: 12 } }, grid: { color: 'rgba(0,0,0,0.05)' } } } }
|
||||
});
|
||||
} else {
|
||||
tempWrapper.html('<div class="d-flex align-items-center justify-content-center h-100 text-muted small">Keine Temperaturdaten</div>');
|
||||
}
|
||||
|
||||
// Humidity Chart
|
||||
if (response.data.humidity.some(v => v !== null)) {
|
||||
humidityWrapper.append('<h6 class="text-center fw-bold text-dark mb-2">Feuchtigkeit</h6>');
|
||||
let canvasId = 'humidity-canvas-' + plantId;
|
||||
humidityWrapper.append($('<div style="height: 180px;">').append($('<canvas id="' + canvasId + '">')));
|
||||
|
||||
if(charts[canvasId]) charts[canvasId].destroy();
|
||||
charts[canvasId] = new Chart(document.getElementById(canvasId), {
|
||||
type: 'line',
|
||||
data: { labels: response.data.labels, datasets: [{ label: '%', data: response.data.humidity, borderColor: '#4dabf7', backgroundColor: 'rgba(77, 171, 247, 0.1)', fill: true, tension: 0.3, pointRadius: 3, pointHoverRadius: 5 }] },
|
||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { ticks: { color: '#212529', font: { size: 12 } }, grid: { color: 'rgba(0,0,0,0.05)' } } } }
|
||||
});
|
||||
} else {
|
||||
humidityWrapper.html('<div class="d-flex align-items-center justify-content-center h-100 text-muted small">Keine Feuchtigkeitsdaten</div>');
|
||||
}
|
||||
} else {
|
||||
tempWrapper.html('<div class="chart-wrapper d-flex align-items-center justify-content-center"><p class="text-muted mb-0">Keine Temperaturdaten.</p></div>');
|
||||
const noDataHtml = '<div class="col-12 text-center py-4 text-muted small">Keine Sensordaten für diesen Zeitraum.</div>';
|
||||
tempWrapper.parent().html(noDataHtml);
|
||||
}
|
||||
// Feuchtigkeits-Chart
|
||||
if (response.data.humidity.some(v => v !== null)) {
|
||||
humidityWrapper.append('<h6 class="text-dark text-center small">Feuchtigkeit</h6>');
|
||||
let humidityCanvas = $('<canvas>');
|
||||
humidityWrapper.append($('<div>').addClass('chart-wrapper').append(humidityCanvas));
|
||||
new Chart(humidityCanvas, { type: 'line', data: { labels: response.data.labels, datasets: [{ label: '%', data: response.data.humidity, borderColor: 'rgba(54, 162, 235, 1)', tension: 0.2, spanGaps: true }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { ticks: { color: '#212529', display: false } }, y: { ticks: { color: '#212529' } } } } });
|
||||
} else {
|
||||
humidityWrapper.html('<div class="chart-wrapper d-flex align-items-center justify-content-center"><p class="text-muted mb-0">Keine Feuchtigkeitsdaten.</p></div>');
|
||||
}
|
||||
} else {
|
||||
const noDataHtml = '<div class="col-12 text-center"><p class="text-white-50">Keine Sensordaten.</p></div>';
|
||||
chartsContainer.html(noDataHtml);
|
||||
},
|
||||
error: function() {
|
||||
const errorHtml = '<div class="col-12 text-center py-4 text-danger small">Fehler beim Laden.</div>';
|
||||
tempWrapper.parent().html(errorHtml);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
const errorHtml = '<div class="col-12 text-center"><p class="text-danger">Fehler beim Laden.</p></div>';
|
||||
chartsContainer.html(errorHtml);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initial load
|
||||
loadAllCharts('24h');
|
||||
|
||||
// Filter click handler
|
||||
$('.sensor-range-btn').click(function() {
|
||||
$('.sensor-range-btn').removeClass('active');
|
||||
$(this).addClass('active');
|
||||
loadAllCharts($(this).data('range'));
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
Reference in New Issue
Block a user