2
This commit is contained in:
parent
91bdb6425e
commit
bc7bcad072
12
_footer.php
Normal file
12
_footer.php
Normal file
@ -0,0 +1,12 @@
|
||||
</main>
|
||||
|
||||
<footer class="text-center text-muted py-4 mt-4 bg-white">
|
||||
<div class="container">
|
||||
<p>© <?php echo date("Y"); ?> NWarehouse IoT Monitoring. All Rights Reserved.</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
42
_header.php
Normal file
42
_header.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php $page = basename($_SERVER['PHP_SELF']); ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>NWarehouse IoT Monitoring</title>
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Open+Sans:wght@400;600&family=Roboto:wght@400;500;700&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body style="background-color: #F9FAFB; font-family: 'Inter', sans-serif;">
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold" href="index.php" style="color: #1A73E8;">
|
||||
<i class="bi bi-box-seam-fill"></i>
|
||||
NWarehouse IoT
|
||||
</a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo ($page == 'index.php') ? 'active' : ''; ?>" href="index.php">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo ($page == 'alerts.php') ? 'active' : ''; ?>" href="alerts.php">Alerts</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link <?php echo ($page == 'co2-data.php') ? 'active' : ''; ?>" href="co2-data.php">CO2 Data</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container-fluid mt-4">
|
||||
68
alerts.php
Normal file
68
alerts.php
Normal file
@ -0,0 +1,68 @@
|
||||
<?php require_once '_header.php'; ?>
|
||||
|
||||
<div class="container-fluid">
|
||||
<h1 class="h2 mb-4" style="color: #212121;">Alerts Dashboard</h1>
|
||||
|
||||
<!-- Section: Alerts Overview -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="card-title mb-0">Alerts Overview</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-4 mb-3 mb-lg-0">
|
||||
<canvas id="alertsPerWarehouseChart"></canvas>
|
||||
</div>
|
||||
<div class="col-md-6 col-lg-4 mb-3 mb-lg-0">
|
||||
<canvas id="alertsOverTimeChart"></canvas>
|
||||
</div>
|
||||
<div class="col-lg-4">
|
||||
<canvas id="alertStatusChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section: Active Alerts -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card border-0 shadow-sm">
|
||||
<div class="card-header bg-white border-0 py-3">
|
||||
<h5 class="card-title mb-0">Alerts List</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Warehouse</th>
|
||||
<th>Slot</th>
|
||||
<th>Node</th>
|
||||
<th>Metric</th>
|
||||
<th>Value</th>
|
||||
<th>Threshold</th>
|
||||
<th>Status</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="alerts-table-body">
|
||||
<!-- Rows will be injected by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once '_footer.php'; ?>
|
||||
|
||||
<!-- Page-specific JS -->
|
||||
<script src="assets/js/alerts.js?v=<?php echo time(); ?>"></script>
|
||||
71
api/alerts-data.php
Normal file
71
api/alerts-data.php
Normal file
@ -0,0 +1,71 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once '../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// 1. Fetch data for the alerts table
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
a.alert_id,
|
||||
a.timestamp,
|
||||
w.name as warehouse_name,
|
||||
s.name as slot_name,
|
||||
n.name as node_name,
|
||||
a.metric_name,
|
||||
a.actual_value,
|
||||
a.threshold_value,
|
||||
a.status
|
||||
FROM alerts a
|
||||
JOIN nodes n ON a.node_id = n.id
|
||||
JOIN slots s ON n.slot_id = s.id
|
||||
JOIN warehouses w ON s.warehouse_id = w.id
|
||||
ORDER BY a.timestamp DESC
|
||||
");
|
||||
$alerts = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// 2. Fetch data for overview charts
|
||||
// Alerts per warehouse
|
||||
$stmt = $pdo->query("
|
||||
SELECT w.name, COUNT(a.alert_id) as alert_count
|
||||
FROM alerts a
|
||||
JOIN nodes n ON a.node_id = n.id
|
||||
JOIN slots s ON n.slot_id = s.id
|
||||
JOIN warehouses w ON s.warehouse_id = w.id
|
||||
GROUP BY w.name
|
||||
");
|
||||
$alerts_per_warehouse = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Alerts over time (last 7 days)
|
||||
$stmt = $pdo->query("
|
||||
SELECT DATE(timestamp) as alert_date, COUNT(alert_id) as alert_count
|
||||
FROM alerts
|
||||
WHERE timestamp >= CURDATE() - INTERVAL 7 DAY
|
||||
GROUP BY DATE(timestamp)
|
||||
ORDER BY alert_date
|
||||
");
|
||||
$alerts_over_time = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Alert status distribution
|
||||
$stmt = $pdo->query("
|
||||
SELECT status, COUNT(alert_id) as alert_count
|
||||
FROM alerts
|
||||
GROUP BY status
|
||||
");
|
||||
$alert_status_distribution = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'alerts' => $alerts,
|
||||
'chart_data' => [
|
||||
'per_warehouse' => $alerts_per_warehouse,
|
||||
'over_time' => $alerts_over_time,
|
||||
'status_distribution' => $alert_status_distribution
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||
}
|
||||
32
api/co2-data.php
Normal file
32
api/co2-data.php
Normal file
@ -0,0 +1,32 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
$sql = "
|
||||
SELECT
|
||||
sr.reading_time,
|
||||
sr.reading_value,
|
||||
w.name AS warehouse_name,
|
||||
s.name AS slot_name,
|
||||
n.name AS node_name
|
||||
FROM sensor_readings sr
|
||||
JOIN nodes n ON sr.node_id = n.id
|
||||
JOIN slots s ON n.slot_id = s.id
|
||||
JOIN warehouses w ON s.warehouse_id = w.id
|
||||
WHERE sr.sensor_type = 'CO2'
|
||||
ORDER BY sr.reading_time DESC
|
||||
LIMIT 100; // Limit to the latest 100 readings for performance
|
||||
";
|
||||
|
||||
$stmt = $pdo->query($sql);
|
||||
$readings = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
echo json_encode(['success' => true, 'data' => $readings]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
57
api/sensor-data.php
Normal file
57
api/sensor-data.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
header('Content-Type: application/json');
|
||||
require_once __DIR__ . '/../db/config.php';
|
||||
|
||||
try {
|
||||
$pdo = db();
|
||||
|
||||
// For the dashboard, we'll fetch the latest reading from any node as a general overview.
|
||||
$stmt = $pdo->query("
|
||||
SELECT
|
||||
`temperature`,
|
||||
`humidity`,
|
||||
`co2`,
|
||||
`gas_level`,
|
||||
`pressure`,
|
||||
`light_level`
|
||||
FROM `sensor_readings`
|
||||
ORDER BY `timestamp` DESC
|
||||
LIMIT 1
|
||||
");
|
||||
|
||||
$latest_data = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
// If the database is empty, provide default zero values to prevent frontend errors.
|
||||
if (!$latest_data) {
|
||||
$latest_data = [
|
||||
'temperature' => 0,
|
||||
'humidity' => 0,
|
||||
'co2' => 0,
|
||||
'gas_level' => 0,
|
||||
'pressure' => 1000, // A more realistic default for pressure
|
||||
'light_level' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
// These thresholds will be used by the frontend to determine the gauge color.
|
||||
$thresholds = [
|
||||
'temperature' => ['max' => 50, 'warning' => 28, 'critical' => 35],
|
||||
'humidity' => ['max' => 100, 'warning' => 60, 'critical' => 80],
|
||||
'co2' => ['max' => 5000, 'warning' => 1000, 'critical' => 2000],
|
||||
'gas_level' => ['max' => 1000, 'warning' => 300, 'critical' => 500],
|
||||
'pressure' => ['max' => 1100, 'warning' => 1030, 'critical' => 1050],
|
||||
'light_level' => ['max' => 1000, 'warning' => 500, 'critical' => 750],
|
||||
];
|
||||
|
||||
$response = [
|
||||
'data' => $latest_data,
|
||||
'thresholds' => $thresholds
|
||||
];
|
||||
|
||||
echo json_encode($response);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
http_response_code(500);
|
||||
// In a production environment, you might want to log this error instead of echoing it.
|
||||
echo json_encode(['error' => 'Database error: ' . $e->getMessage()]);
|
||||
}
|
||||
165
assets/js/alerts.js
Normal file
165
assets/js/alerts.js
Normal file
@ -0,0 +1,165 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const CHART_COLORS = {
|
||||
red: 'rgba(234, 67, 53, 0.8)',
|
||||
orange: 'rgba(249, 171, 0, 0.8)',
|
||||
green: 'rgba(52, 168, 83, 0.8)',
|
||||
blue: 'rgba(26, 115, 232, 0.8)',
|
||||
};
|
||||
const CHART_BORDERS = {
|
||||
red: 'rgb(234, 67, 53)',
|
||||
orange: 'rgb(249, 171, 0)',
|
||||
green: 'rgb(52, 168, 83)',
|
||||
blue: 'rgb(26, 115, 232)',
|
||||
};
|
||||
|
||||
const chartContexts = {
|
||||
perWarehouse: document.getElementById('alertsPerWarehouseChart')?.getContext('2d'),
|
||||
overTime: document.getElementById('alertsOverTimeChart')?.getContext('2d'),
|
||||
status: document.getElementById('alertStatusChart')?.getContext('2d')
|
||||
};
|
||||
|
||||
const charts = {};
|
||||
|
||||
function createOrUpdateChart(ctx, type, data, options) {
|
||||
const chartId = ctx.canvas.id;
|
||||
if (charts[chartId]) {
|
||||
charts[chartId].data = data;
|
||||
charts[chartId].options = options;
|
||||
charts[chartId].update();
|
||||
} else {
|
||||
charts[chartId] = new Chart(ctx, { type, data, options });
|
||||
}
|
||||
}
|
||||
|
||||
function renderAlertsTable(alerts) {
|
||||
const tableBody = document.getElementById('alerts-table-body');
|
||||
if (!tableBody) return;
|
||||
tableBody.innerHTML = ''; // Clear existing rows
|
||||
|
||||
if (!alerts || alerts.length === 0) {
|
||||
tableBody.innerHTML = '<tr><td colspan="10" class="text-center text-muted">No active alerts found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
alerts.forEach(alert => {
|
||||
const statusBadge = getStatusBadge(alert.status);
|
||||
const row = `
|
||||
<tr>
|
||||
<td><span class="fw-bold">${alert.alert_id}</span></td>
|
||||
<td>${new Date(alert.timestamp).toLocaleString()}</td>
|
||||
<td>${alert.warehouse_name}</td>
|
||||
<td>${alert.slot_name}</td>
|
||||
<td>${alert.node_name}</td>
|
||||
<td>${alert.metric_name}</td>
|
||||
<td><span class="fw-bold text-danger">${alert.actual_value}</span></td>
|
||||
<td>${alert.threshold_value}</td>
|
||||
<td><span class="badge ${statusBadge}">${alert.status}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-outline-secondary" title="Acknowledge"><i class="bi bi-check-circle"></i></button>
|
||||
<button class="btn btn-sm btn-outline-secondary" title="Resolve"><i class="bi bi-patch-check"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
tableBody.insertAdjacentHTML('beforeend', row);
|
||||
});
|
||||
}
|
||||
|
||||
function getStatusBadge(status) {
|
||||
switch (status) {
|
||||
case 'active': return 'text-bg-danger';
|
||||
case 'acknowledged': return 'text-bg-warning';
|
||||
case 'resolved': return 'text-bg-success';
|
||||
default: return 'text-bg-secondary';
|
||||
}
|
||||
}
|
||||
|
||||
function renderCharts(chartData) {
|
||||
// Chart 1: Alerts per Warehouse (Bar)
|
||||
if (chartContexts.perWarehouse && chartData.per_warehouse) {
|
||||
const data = {
|
||||
labels: chartData.per_warehouse.map(d => d.name),
|
||||
datasets: [{
|
||||
label: 'Alerts',
|
||||
data: chartData.per_warehouse.map(d => d.alert_count),
|
||||
backgroundColor: CHART_COLORS.blue,
|
||||
borderColor: CHART_BORDERS.blue,
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false }, title: { display: true, text: 'Alerts per Warehouse' } },
|
||||
scales: { y: { beginAtZero: true } }
|
||||
};
|
||||
createOrUpdateChart(chartContexts.perWarehouse, 'bar', data, options);
|
||||
}
|
||||
|
||||
// Chart 2: Alerts over Time (Line)
|
||||
if (chartContexts.overTime && chartData.over_time) {
|
||||
const data = {
|
||||
labels: chartData.over_time.map(d => d.alert_date),
|
||||
datasets: [{
|
||||
label: 'Alerts',
|
||||
data: chartData.over_time.map(d => d.alert_count),
|
||||
borderColor: CHART_COLORS.red,
|
||||
backgroundColor: CHART_COLORS.red,
|
||||
tension: 0.1,
|
||||
fill: false
|
||||
}]
|
||||
};
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: { legend: { display: false }, title: { display: true, text: 'Alerts Over Last 7 Days' } },
|
||||
scales: { y: { beginAtZero: true } }
|
||||
};
|
||||
createOrUpdateChart(chartContexts.overTime, 'line', data, options);
|
||||
}
|
||||
|
||||
// Chart 3: Alert Status (Pie)
|
||||
if (chartContexts.status && chartData.status_distribution) {
|
||||
const data = {
|
||||
labels: chartData.status_distribution.map(d => d.status),
|
||||
datasets: [{
|
||||
label: 'Alerts',
|
||||
data: chartData.status_distribution.map(d => d.alert_count),
|
||||
backgroundColor: [CHART_COLORS.red, CHART_COLORS.orange, CHART_COLORS.green],
|
||||
borderColor: [CHART_BORDERS.red, CHART_BORDERS.orange, CHART_BORDERS.green],
|
||||
borderWidth: 1
|
||||
}]
|
||||
};
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: { position: 'top' },
|
||||
title: { display: true, text: 'Alert Status Distribution' }
|
||||
}
|
||||
};
|
||||
createOrUpdateChart(chartContexts.status, 'pie', data, options);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchData() {
|
||||
const tableBody = document.getElementById('alerts-table-body');
|
||||
try {
|
||||
const response = await fetch('api/alerts-data.php');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
renderAlertsTable(data.alerts);
|
||||
renderCharts(data.chart_data);
|
||||
} else {
|
||||
console.error('API Error:', data.error);
|
||||
if(tableBody) tableBody.innerHTML = `<tr><td colspan="10" class="text-center text-danger">Failed to load data from API.</td></tr>`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fetch Error:', error);
|
||||
if(tableBody) tableBody.innerHTML = `<tr><td colspan="10" class="text-center text-danger">Error fetching data. Check console for details.</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
fetchData();
|
||||
// setInterval(fetchData, 30000); // Optional: Refresh every 30 seconds
|
||||
});
|
||||
106
assets/js/co2-data.js
Normal file
106
assets/js/co2-data.js
Normal file
@ -0,0 +1,106 @@
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const co2HistoryChartCanvas = document.getElementById('co2HistoryChart');
|
||||
const co2HistoryTableBody = document.getElementById('co2HistoryTableBody');
|
||||
let chart;
|
||||
|
||||
async function fetchCo2Data() {
|
||||
try {
|
||||
const response = await fetch('api/co2-data.php');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success && result.data) {
|
||||
updateChart(result.data);
|
||||
updateTable(result.data);
|
||||
} else {
|
||||
console.error('Failed to fetch CO2 data:', result.error);
|
||||
co2HistoryTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error loading data.</td></tr>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching CO2 data:', error);
|
||||
co2HistoryTableBody.innerHTML = '<tr><td colspan="5" class="text-center">Error loading data.</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function updateChart(data) {
|
||||
if (!co2HistoryChartCanvas) return;
|
||||
|
||||
// Data is fetched DESC, reverse for chronological chart
|
||||
const chartData = data.slice().reverse();
|
||||
|
||||
const labels = chartData.map(d => new Date(d.reading_time).toLocaleString());
|
||||
const values = chartData.map(d => d.reading_value);
|
||||
|
||||
const chartConfig = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [{
|
||||
label: 'CO2 Level (PPM)',
|
||||
data: values,
|
||||
borderColor: '#1A73E8',
|
||||
backgroundColor: 'rgba(26, 115, 232, 0.1)',
|
||||
fill: true,
|
||||
tension: 0.4,
|
||||
pointBackgroundColor: '#1A73E8'
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'CO2 (PPM)'
|
||||
}
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Timestamp'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (chart) {
|
||||
chart.destroy();
|
||||
}
|
||||
chart = new Chart(co2HistoryChartCanvas, chartConfig);
|
||||
}
|
||||
|
||||
function updateTable(data) {
|
||||
if (!co2HistoryTableBody) return;
|
||||
|
||||
co2HistoryTableBody.innerHTML = ''; // Clear existing data
|
||||
|
||||
if (data.length === 0) {
|
||||
co2HistoryTableBody.innerHTML = '<tr><td colspan="5" class="text-center">No historical data available.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
data.forEach(reading => {
|
||||
const row = document.createElement('tr');
|
||||
row.innerHTML = `
|
||||
<td>${new Date(reading.reading_time).toLocaleString()}</td>
|
||||
<td>${parseFloat(reading.reading_value).toFixed(2)}</td>
|
||||
<td>${reading.warehouse_name}</td>
|
||||
<td>${reading.slot_name}</td>
|
||||
<td>${reading.node_name}</td>
|
||||
`;
|
||||
co2HistoryTableBody.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
fetchCo2Data();
|
||||
});
|
||||
@ -1,33 +1,54 @@
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const createGauge = (ctx, label, value) => {
|
||||
// A map to keep track of Chart instances to prevent memory leaks
|
||||
const chartInstances = {};
|
||||
|
||||
const createGauge = (canvasId, label, value, max, unit, thresholds) => {
|
||||
const ctx = document.getElementById(canvasId);
|
||||
if (!ctx) return;
|
||||
|
||||
const parentCard = ctx.closest('.gauge-card');
|
||||
if (!parentCard) return;
|
||||
|
||||
const valueEl = parentCard.querySelector('.gauge-value');
|
||||
const labelEl = parentCard.querySelector('.card-footer');
|
||||
|
||||
const normalColor = '#34A853'; // Green
|
||||
const warningColor = '#F9AB00'; // Orange
|
||||
const alertColor = '#EA4335'; // Red
|
||||
const bgColor = '#e9ecef';
|
||||
|
||||
let activeColor = normalColor;
|
||||
if (value >= 70 && value < 90) {
|
||||
if (value >= thresholds.warning && value < thresholds.critical) {
|
||||
activeColor = warningColor;
|
||||
} else if (value >= 90) {
|
||||
} else if (value >= thresholds.critical) {
|
||||
activeColor = alertColor;
|
||||
}
|
||||
|
||||
const parent = ctx.parentElement;
|
||||
const valueSpan = parent.querySelector('.gauge-value');
|
||||
valueSpan.textContent = `${value}%`;
|
||||
valueSpan.style.color = activeColor;
|
||||
// Update text content
|
||||
if(labelEl) labelEl.textContent = label;
|
||||
if(valueEl) {
|
||||
valueEl.textContent = `${value}${unit}`;
|
||||
valueEl.style.color = activeColor;
|
||||
}
|
||||
|
||||
new Chart(ctx, {
|
||||
|
||||
const percentage = Math.min(Math.max((value / max) * 100, 0), 100);
|
||||
|
||||
// If a chart already exists for this canvas, destroy it before creating a new one.
|
||||
if (chartInstances[canvasId]) {
|
||||
chartInstances[canvasId].destroy();
|
||||
}
|
||||
|
||||
chartInstances[canvasId] = new Chart(ctx, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
datasets: [{
|
||||
data: [value, 100 - value],
|
||||
data: [percentage, 100 - percentage],
|
||||
backgroundColor: [activeColor, bgColor],
|
||||
borderColor: [activeColor, bgColor],
|
||||
borderWidth: 1,
|
||||
circumference: 270, // Make it a semi-circle
|
||||
rotation: 225, // Start from the bottom-left
|
||||
circumference: 270,
|
||||
rotation: 225,
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
@ -43,19 +64,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const gauges = [
|
||||
{ id: 'gauge1', label: 'Warehouse Temp', value: 65 },
|
||||
{ id: 'gauge2', label: 'Slot Humidity', value: 80 },
|
||||
{ id: 'gauge3', label: 'Node-A1 CO2', value: 45 },
|
||||
{ id: 'gauge4', label: 'Node-B2 Gas', value: 92 },
|
||||
{ id: 'gauge5', label: 'Cooling System', value: 75 },
|
||||
{ id: 'gauge6', label: 'Power Backup', value: 98 },
|
||||
];
|
||||
const fetchSensorData = async () => {
|
||||
try {
|
||||
// Append a cache-busting query parameter
|
||||
const response = await fetch('api/sensor-data.php?v=' + new Date().getTime());
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const { data, thresholds } = await response.json();
|
||||
|
||||
gauges.forEach(gaugeConfig => {
|
||||
const ctx = document.getElementById(gaugeConfig.id);
|
||||
if (ctx) {
|
||||
createGauge(ctx.getContext('2d'), gaugeConfig.label, gaugeConfig.value);
|
||||
// Update gauges with the new data
|
||||
createGauge('gauge1', 'Temperature', data.temperature, thresholds.temperature.max, '°C', thresholds.temperature);
|
||||
createGauge('gauge2', 'Humidity', data.humidity, thresholds.humidity.max, '%', thresholds.humidity);
|
||||
createGauge('gauge3', 'CO2 Level', data.co2, thresholds.co2.max, 'ppm', thresholds.co2);
|
||||
createGauge('gauge4', 'Gas Detection', data.gas_level, thresholds.gas_level.max, 'ppm', thresholds.gas_level);
|
||||
createGauge('gauge5', 'Pressure', data.pressure, thresholds.pressure.max, 'hPa', thresholds.pressure);
|
||||
createGauge('gauge6', 'Light Intensity', data.light_level, thresholds.light_level.max, 'lux', thresholds.light_level);
|
||||
|
||||
} catch (error) {
|
||||
console.error("Could not fetch sensor data:", error);
|
||||
// Find the main row and display an error
|
||||
const mainRow = document.querySelector('.row');
|
||||
if (mainRow) {
|
||||
mainRow.innerHTML = `
|
||||
<div class="col-12">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<strong>Error:</strong> Could not load live sensor data. The backend might be offline. Please check the console for details.
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial data load
|
||||
fetchSensorData();
|
||||
|
||||
// Refresh data every 15 seconds
|
||||
setInterval(fetchSensorData, 15000);
|
||||
});
|
||||
56
co2-data.php
Normal file
56
co2-data.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php require_once '_header.php'; ?>
|
||||
|
||||
<div class="container-fluid mt-4">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<h1 class="h2">CO2 Sensor Data</h1>
|
||||
<p>Detailed analysis of historical CO2 sensor readings.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CO2 Levels Over Time Chart -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title mb-0">CO2 Levels Over Time</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<canvas id="co2HistoryChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Historical CO2 Data Table -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title mb-0">Historical Data</h3>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Timestamp</th>
|
||||
<th>Value (PPM)</th>
|
||||
<th>Warehouse</th>
|
||||
<th>Slot</th>
|
||||
<th>Node</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="co2HistoryTableBody">
|
||||
<!-- Data will be populated by JavaScript -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php require_once '_footer.php'; ?>
|
||||
<script src="assets/js/co2-data.js?v=<?php echo time(); ?>"></script>
|
||||
41
db/migrate.php
Normal file
41
db/migrate.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
try {
|
||||
// 1. Connect to MySQL server without specifying a DB
|
||||
$pdo_server = new PDO('mysql:host=' . DB_HOST, DB_USER, DB_PASS, [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
]);
|
||||
|
||||
// 2. Create the database if it doesn't exist
|
||||
$dbName = DB_NAME;
|
||||
$pdo_server->exec("CREATE DATABASE IF NOT EXISTS `$dbName` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;");
|
||||
echo "Database '$dbName' created or already exists.\n";
|
||||
|
||||
// 3. Now, connect to the specific database using the existing db() function
|
||||
$pdo = db();
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
$migrationsDir = __DIR__ . '/migrations';
|
||||
$files = glob($migrationsDir . '/*.sql');
|
||||
sort($files);
|
||||
|
||||
if (empty($files)) {
|
||||
echo "No migration files found.\n";
|
||||
exit;
|
||||
}
|
||||
|
||||
echo "Starting database migrations...\n";
|
||||
|
||||
foreach ($files as $file) {
|
||||
echo "Applying: " . basename($file) . "... ";
|
||||
$sql = file_get_contents($file);
|
||||
$pdo->exec($sql);
|
||||
echo "Done.\n";
|
||||
}
|
||||
|
||||
echo "All migrations completed successfully!\n";
|
||||
|
||||
} catch (PDOException $e) {
|
||||
die("Database operation failed: " . $e->getMessage() . "\n");
|
||||
}
|
||||
34
db/migrations/001_initial_schema.sql
Normal file
34
db/migrations/001_initial_schema.sql
Normal file
@ -0,0 +1,34 @@
|
||||
CREATE TABLE IF NOT EXISTS `warehouses` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `slots` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`warehouse_id` INT,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`warehouse_id`) REFERENCES `warehouses`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `nodes` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`name` VARCHAR(255) NOT NULL,
|
||||
`slot_id` INT,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`slot_id`) REFERENCES `slots`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `sensor_readings` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`node_id` INT,
|
||||
`temperature` DECIMAL(5, 2),
|
||||
`humidity` DECIMAL(5, 2),
|
||||
`co2` INT,
|
||||
`gas_level` INT,
|
||||
`pressure` DECIMAL(6, 2),
|
||||
`light_level` INT,
|
||||
`timestamp` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`node_id`) REFERENCES `nodes`(`id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
30
db/migrations/002_seed_data.sql
Normal file
30
db/migrations/002_seed_data.sql
Normal file
@ -0,0 +1,30 @@
|
||||
-- Use TRUNCATE to reset tables and start fresh, which is fine for seed data.
|
||||
-- Disable foreign key checks to avoid errors with TRUNCATE order.
|
||||
SET FOREIGN_KEY_CHECKS=0;
|
||||
TRUNCATE TABLE `sensor_readings`;
|
||||
TRUNCATE TABLE `nodes`;
|
||||
TRUNCATE TABLE `slots`;
|
||||
TRUNCATE TABLE `warehouses`;
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
||||
|
||||
-- Warehouses
|
||||
INSERT INTO `warehouses` (`id`, `name`) VALUES (1, 'Main Warehouse'), (2, 'North Annex');
|
||||
|
||||
-- Slots for Main Warehouse
|
||||
INSERT INTO `slots` (`id`, `name`, `warehouse_id`) VALUES (1, 'Slot A-1', 1), (2, 'Slot A-2', 1);
|
||||
-- Slots for North Annex
|
||||
INSERT INTO `slots` (`id`, `name`, `warehouse_id`) VALUES (3, 'Slot B-1', 2);
|
||||
|
||||
-- Nodes for Slot A-1
|
||||
INSERT INTO `nodes` (`id`, `name`, `slot_id`) VALUES (1, 'Node-001', 1), (2, 'Node-002', 1);
|
||||
-- Nodes for Slot A-2
|
||||
INSERT INTO `nodes` (`id`, `name`, `slot_id`) VALUES (3, 'Node-003', 2);
|
||||
-- Nodes for Slot B-1
|
||||
INSERT INTO `nodes` (`id`, `name`, `slot_id`) VALUES (4, 'Node-004', 3);
|
||||
|
||||
-- Insert a few recent sensor readings for different nodes
|
||||
INSERT INTO `sensor_readings` (`node_id`, `temperature`, `humidity`, `co2`, `gas_level`, `pressure`, `light_level`) VALUES
|
||||
(1, 22.5, 45.2, 800, 50, 1012.5, 600),
|
||||
(2, 25.1, 50.0, 1200, 150, 1010.1, 300),
|
||||
(3, 19.8, 40.5, 600, 20, 1015.0, 800),
|
||||
(4, 30.0, 65.0, 2500, 300, 1005.0, 100);
|
||||
18
db/migrations/003_alerts_table.sql
Normal file
18
db/migrations/003_alerts_table.sql
Normal file
@ -0,0 +1,18 @@
|
||||
CREATE TABLE IF NOT EXISTS alerts (
|
||||
alert_id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
node_id INT NOT NULL,
|
||||
metric_name VARCHAR(50) NOT NULL,
|
||||
actual_value DECIMAL(10, 2) NOT NULL,
|
||||
threshold_value DECIMAL(10, 2) NOT NULL,
|
||||
status ENUM('active', 'acknowledged', 'resolved') NOT NULL DEFAULT 'active',
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (node_id) REFERENCES nodes(id)
|
||||
);
|
||||
|
||||
-- Seed some sample alerts
|
||||
INSERT INTO alerts (node_id, metric_name, actual_value, threshold_value, status) VALUES
|
||||
(1, 'temperature', 28.5, 25.0, 'active'),
|
||||
(3, 'humidity', 65.0, 60.0, 'active'),
|
||||
(4, 'co2', 1200, 1000, 'acknowledged'),
|
||||
(2, 'temperature', 29.0, 25.0, 'resolved'),
|
||||
(4, 'humidity', 68.0, 60.0, 'active');
|
||||
120
index.php
120
index.php
@ -1,116 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'NWarehouse IoT Monitoring'); ?></title>
|
||||
<meta name="description" content="<?php echo htmlspecialchars($_SERVER['PROJECT_DESCRIPTION'] ?? 'IoT Monitoring System for Warehouses'); ?>">
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Google Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Roboto:wght@400;500;700&family=Open+Sans:wght@400;600&display=swap" rel="stylesheet">
|
||||
<?php require_once '_header.php'; ?>
|
||||
|
||||
<!-- Custom CSS -->
|
||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
||||
</head>
|
||||
<body>
|
||||
<h1 class="h2 mb-4">Monitoring Dashboard</h1>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand fw-bold" href="#" style="color: var(--primary-color);">
|
||||
NWarehouse IoT
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="row" id="dashboard-gauges">
|
||||
<!-- Gauges will be dynamically inserted here by main.js -->
|
||||
</div>
|
||||
|
||||
<main class="container-fluid mt-4">
|
||||
<h1 class="h2 mb-4">Monitoring Dashboard</h1>
|
||||
<?php require_once '_footer.php'; ?>
|
||||
|
||||
<div class="row">
|
||||
<!-- Gauge 1 -->
|
||||
<div class="col-lg-2 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card gauge-card text-center">
|
||||
<div class="card-body">
|
||||
<div class="gauge-container">
|
||||
<canvas id="gauge1" class="gauge-canvas"></canvas>
|
||||
<div class="gauge-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">Warehouse Temp</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Gauge 2 -->
|
||||
<div class="col-lg-2 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card gauge-card text-center">
|
||||
<div class="card-body">
|
||||
<div class="gauge-container">
|
||||
<canvas id="gauge2" class="gauge-canvas"></canvas>
|
||||
<div class="gauge-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">Slot Humidity</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Gauge 3 -->
|
||||
<div class="col-lg-2 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card gauge-card text-center">
|
||||
<div class="card-body">
|
||||
<div class="gauge-container">
|
||||
<canvas id="gauge3" class="gauge-canvas"></canvas>
|
||||
<div class="gauge-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">Node-A1 CO2</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Gauge 4 -->
|
||||
<div class="col-lg-2 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card gauge-card text-center">
|
||||
<div class="card-body">
|
||||
<div class="gauge-container">
|
||||
<canvas id="gauge4" class="gauge-canvas"></canvas>
|
||||
<div class="gauge-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">Node-B2 Gas</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Gauge 5 -->
|
||||
<div class="col-lg-2 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card gauge-card text-center">
|
||||
<div class="card-body">
|
||||
<div class="gauge-container">
|
||||
<canvas id="gauge5" class="gauge-canvas"></canvas>
|
||||
<div class="gauge-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">Cooling System</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Gauge 6 -->
|
||||
<div class="col-lg-2 col-md-4 col-sm-6 mb-4">
|
||||
<div class="card gauge-card text-center">
|
||||
<div class="card-body">
|
||||
<div class="gauge-container">
|
||||
<canvas id="gauge6" class="gauge-canvas"></canvas>
|
||||
<div class="gauge-value"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer">Power Backup</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Chart.js -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
|
||||
<!-- Custom JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
<!-- Page-specific JS -->
|
||||
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user