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', () => {
|
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 normalColor = '#34A853'; // Green
|
||||||
const warningColor = '#F9AB00'; // Orange
|
const warningColor = '#F9AB00'; // Orange
|
||||||
const alertColor = '#EA4335'; // Red
|
const alertColor = '#EA4335'; // Red
|
||||||
const bgColor = '#e9ecef';
|
const bgColor = '#e9ecef';
|
||||||
|
|
||||||
let activeColor = normalColor;
|
let activeColor = normalColor;
|
||||||
if (value >= 70 && value < 90) {
|
if (value >= thresholds.warning && value < thresholds.critical) {
|
||||||
activeColor = warningColor;
|
activeColor = warningColor;
|
||||||
} else if (value >= 90) {
|
} else if (value >= thresholds.critical) {
|
||||||
activeColor = alertColor;
|
activeColor = alertColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
const parent = ctx.parentElement;
|
// Update text content
|
||||||
const valueSpan = parent.querySelector('.gauge-value');
|
if(labelEl) labelEl.textContent = label;
|
||||||
valueSpan.textContent = `${value}%`;
|
if(valueEl) {
|
||||||
valueSpan.style.color = activeColor;
|
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',
|
type: 'doughnut',
|
||||||
data: {
|
data: {
|
||||||
datasets: [{
|
datasets: [{
|
||||||
data: [value, 100 - value],
|
data: [percentage, 100 - percentage],
|
||||||
backgroundColor: [activeColor, bgColor],
|
backgroundColor: [activeColor, bgColor],
|
||||||
borderColor: [activeColor, bgColor],
|
borderColor: [activeColor, bgColor],
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
circumference: 270, // Make it a semi-circle
|
circumference: 270,
|
||||||
rotation: 225, // Start from the bottom-left
|
rotation: 225,
|
||||||
}]
|
}]
|
||||||
},
|
},
|
||||||
options: {
|
options: {
|
||||||
@ -43,19 +64,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const gauges = [
|
const fetchSensorData = async () => {
|
||||||
{ id: 'gauge1', label: 'Warehouse Temp', value: 65 },
|
try {
|
||||||
{ id: 'gauge2', label: 'Slot Humidity', value: 80 },
|
// Append a cache-busting query parameter
|
||||||
{ id: 'gauge3', label: 'Node-A1 CO2', value: 45 },
|
const response = await fetch('api/sensor-data.php?v=' + new Date().getTime());
|
||||||
{ id: 'gauge4', label: 'Node-B2 Gas', value: 92 },
|
if (!response.ok) {
|
||||||
{ id: 'gauge5', label: 'Cooling System', value: 75 },
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
{ id: 'gauge6', label: 'Power Backup', value: 98 },
|
}
|
||||||
];
|
const { data, thresholds } = await response.json();
|
||||||
|
|
||||||
gauges.forEach(gaugeConfig => {
|
// Update gauges with the new data
|
||||||
const ctx = document.getElementById(gaugeConfig.id);
|
createGauge('gauge1', 'Temperature', data.temperature, thresholds.temperature.max, '°C', thresholds.temperature);
|
||||||
if (ctx) {
|
createGauge('gauge2', 'Humidity', data.humidity, thresholds.humidity.max, '%', thresholds.humidity);
|
||||||
createGauge(ctx.getContext('2d'), gaugeConfig.label, gaugeConfig.value);
|
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>
|
<?php require_once '_header.php'; ?>
|
||||||
<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 -->
|
<h1 class="h2 mb-4">Monitoring Dashboard</h1>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
||||||
|
|
||||||
<!-- Google Fonts -->
|
<div class="row" id="dashboard-gauges">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<!-- Gauges will be dynamically inserted here by main.js -->
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
</div>
|
||||||
<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">
|
|
||||||
|
|
||||||
<!-- Custom CSS -->
|
<?php require_once '_footer.php'; ?>
|
||||||
<link rel="stylesheet" href="assets/css/custom.css?v=<?php echo time(); ?>">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<nav class="navbar navbar-expand-lg navbar-light">
|
<!-- Page-specific JS -->
|
||||||
<div class="container-fluid">
|
<script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
|
||||||
<a class="navbar-brand fw-bold" href="#" style="color: var(--primary-color);">
|
|
||||||
NWarehouse IoT
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main class="container-fluid mt-4">
|
|
||||||
<h1 class="h2 mb-4">Monitoring Dashboard</h1>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user