diff --git a/_footer.php b/_footer.php new file mode 100644 index 0000000..2b34729 --- /dev/null +++ b/_footer.php @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/_header.php b/_header.php new file mode 100644 index 0000000..957e9ea --- /dev/null +++ b/_header.php @@ -0,0 +1,42 @@ + + + + + + + NWarehouse IoT Monitoring + + + + + + + + + + + +
diff --git a/alerts.php b/alerts.php new file mode 100644 index 0000000..5ba4a99 --- /dev/null +++ b/alerts.php @@ -0,0 +1,68 @@ + + +
+

Alerts Dashboard

+ + +
+
+
+
+
Alerts Overview
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+ + +
+
+
+
+
Alerts List
+
+
+
+ + + + + + + + + + + + + + + + + + +
IDTimestampWarehouseSlotNodeMetricValueThresholdStatusActions
+
+
+
+
+
+
+ + + + + diff --git a/api/alerts-data.php b/api/alerts-data.php new file mode 100644 index 0000000..9ff868e --- /dev/null +++ b/api/alerts-data.php @@ -0,0 +1,71 @@ +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()]); +} diff --git a/api/co2-data.php b/api/co2-data.php new file mode 100644 index 0000000..37cb4e7 --- /dev/null +++ b/api/co2-data.php @@ -0,0 +1,32 @@ +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()]); +} diff --git a/api/sensor-data.php b/api/sensor-data.php new file mode 100644 index 0000000..13e330d --- /dev/null +++ b/api/sensor-data.php @@ -0,0 +1,57 @@ +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()]); +} diff --git a/assets/js/alerts.js b/assets/js/alerts.js new file mode 100644 index 0000000..f316572 --- /dev/null +++ b/assets/js/alerts.js @@ -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 = 'No active alerts found.'; + return; + } + + alerts.forEach(alert => { + const statusBadge = getStatusBadge(alert.status); + const row = ` + + ${alert.alert_id} + ${new Date(alert.timestamp).toLocaleString()} + ${alert.warehouse_name} + ${alert.slot_name} + ${alert.node_name} + ${alert.metric_name} + ${alert.actual_value} + ${alert.threshold_value} + ${alert.status} + + + + + + `; + 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 = `Failed to load data from API.`; + } + } catch (error) { + console.error('Fetch Error:', error); + if(tableBody) tableBody.innerHTML = `Error fetching data. Check console for details.`; + } + } + + fetchData(); + // setInterval(fetchData, 30000); // Optional: Refresh every 30 seconds +}); \ No newline at end of file diff --git a/assets/js/co2-data.js b/assets/js/co2-data.js new file mode 100644 index 0000000..f4ffc47 --- /dev/null +++ b/assets/js/co2-data.js @@ -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 = 'Error loading data.'; + } + } catch (error) { + console.error('Error fetching CO2 data:', error); + co2HistoryTableBody.innerHTML = 'Error loading data.'; + } + } + + 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 = 'No historical data available.'; + return; + } + + data.forEach(reading => { + const row = document.createElement('tr'); + row.innerHTML = ` + ${new Date(reading.reading_time).toLocaleString()} + ${parseFloat(reading.reading_value).toFixed(2)} + ${reading.warehouse_name} + ${reading.slot_name} + ${reading.node_name} + `; + co2HistoryTableBody.appendChild(row); + }); + } + + fetchCo2Data(); +}); diff --git a/assets/js/main.js b/assets/js/main.js index d131cf1..148aaa3 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -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 = ` +
+ +
`; + } } - }); -}); + }; + + // Initial data load + fetchSensorData(); + + // Refresh data every 15 seconds + setInterval(fetchSensorData, 15000); +}); \ No newline at end of file diff --git a/co2-data.php b/co2-data.php new file mode 100644 index 0000000..ff0027f --- /dev/null +++ b/co2-data.php @@ -0,0 +1,56 @@ + + +
+
+
+

CO2 Sensor Data

+

Detailed analysis of historical CO2 sensor readings.

+
+
+ + +
+
+
+
+

CO2 Levels Over Time

+
+
+ +
+
+
+
+ + +
+
+
+
+

Historical Data

+
+
+
+ + + + + + + + + + + + + +
TimestampValue (PPM)WarehouseSlotNode
+
+
+
+
+
+
+ + + diff --git a/db/migrate.php b/db/migrate.php new file mode 100644 index 0000000..bbce4e8 --- /dev/null +++ b/db/migrate.php @@ -0,0 +1,41 @@ + 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"); +} \ No newline at end of file diff --git a/db/migrations/001_initial_schema.sql b/db/migrations/001_initial_schema.sql new file mode 100644 index 0000000..8014cb2 --- /dev/null +++ b/db/migrations/001_initial_schema.sql @@ -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; diff --git a/db/migrations/002_seed_data.sql b/db/migrations/002_seed_data.sql new file mode 100644 index 0000000..8389fd2 --- /dev/null +++ b/db/migrations/002_seed_data.sql @@ -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); diff --git a/db/migrations/003_alerts_table.sql b/db/migrations/003_alerts_table.sql new file mode 100644 index 0000000..a592f9b --- /dev/null +++ b/db/migrations/003_alerts_table.sql @@ -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'); \ No newline at end of file diff --git a/index.php b/index.php index 5429b00..6612d75 100644 --- a/index.php +++ b/index.php @@ -1,116 +1,12 @@ - - - - - - <?php echo htmlspecialchars($_SERVER['PROJECT_NAME'] ?? 'NWarehouse IoT Monitoring'); ?> - - - - - - - - - + - - - - +

Monitoring Dashboard

- +
+ +
-
-

Monitoring Dashboard

+ -
- -
-
-
-
- -
-
-
- -
-
- -
-
-
-
- -
-
-
- -
-
- -
-
-
-
- -
-
-
- -
-
- -
-
-
-
- -
-
-
- -
-
- -
-
-
-
- -
-
-
- -
-
- -
-
-
-
- -
-
-
- -
-
-
-
- - - - - - - - - \ No newline at end of file + +