Compare commits

..

2 Commits

Author SHA1 Message Date
Flatlogic Bot
bc7bcad072 2 2025-11-24 14:56:59 +00:00
Flatlogic Bot
91bdb6425e Auto commit: 2025-11-24T14:43:00.413Z 2025-11-24 14:43:00 +00:00
16 changed files with 912 additions and 149 deletions

12
_footer.php Normal file
View File

@ -0,0 +1,12 @@
</main>
<footer class="text-center text-muted py-4 mt-4 bg-white">
<div class="container">
<p>&copy; <?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
View 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
View 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
View 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
View 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
View 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()]);
}

65
assets/css/custom.css Normal file
View File

@ -0,0 +1,65 @@
:root {
--primary-color: #1A73E8;
--secondary-color: #34A853;
--alert-color: #EA4335;
--warning-color: #F9AB00;
--background-color: #F9FAFB;
--surface-color: #FFFFFF;
--text-color: #212121;
}
body {
background-color: var(--background-color);
color: var(--text-color);
font-family: 'Inter', 'Roboto', 'Open Sans', sans-serif;
}
.navbar {
border-bottom: 1px solid #dee2e6;
background-color: var(--surface-color);
}
.gauge-card {
background-color: var(--surface-color);
border: none;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
transition: all 0.3s ease-in-out;
}
.gauge-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
}
.gauge-container {
position: relative;
margin: 1rem auto;
width: 90%;
padding-top: 90%; /* 1:1 Aspect Ratio */
}
.gauge-canvas {
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.gauge-value {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 2.25rem;
font-weight: 700;
color: var(--text-color);
}
.card-footer {
background-color: transparent;
border-top: 1px solid #efefef;
font-weight: 500;
}

165
assets/js/alerts.js Normal file
View 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
View 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();
});

104
assets/js/main.js Normal file
View File

@ -0,0 +1,104 @@
document.addEventListener('DOMContentLoaded', () => {
// 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 >= thresholds.warning && value < thresholds.critical) {
activeColor = warningColor;
} else if (value >= thresholds.critical) {
activeColor = alertColor;
}
// Update text content
if(labelEl) labelEl.textContent = label;
if(valueEl) {
valueEl.textContent = `${value}${unit}`;
valueEl.style.color = activeColor;
}
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: [percentage, 100 - percentage],
backgroundColor: [activeColor, bgColor],
borderColor: [activeColor, bgColor],
borderWidth: 1,
circumference: 270,
rotation: 225,
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '80%',
plugins: {
legend: { display: false },
tooltip: { enabled: false }
},
events: []
}
});
};
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();
// 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
View 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
View 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");
}

View 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;

View 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);

View 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');

160
index.php
View File

@ -1,150 +1,12 @@
<?php <?php require_once '_header.php'; ?>
declare(strict_types=1);
@ini_set('display_errors', '1');
@error_reporting(E_ALL);
@date_default_timezone_set('UTC');
$phpVersion = PHP_VERSION; <h1 class="h2 mb-4">Monitoring Dashboard</h1>
$now = date('Y-m-d H:i:s');
?> <div class="row" id="dashboard-gauges">
<!doctype html> <!-- Gauges will be dynamically inserted here by main.js -->
<html lang="en"> </div>
<head>
<meta charset="utf-8" /> <?php require_once '_footer.php'; ?>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>New Style</title> <!-- Page-specific JS -->
<?php <script src="assets/js/main.js?v=<?php echo time(); ?>"></script>
// Read project preview data from environment
$projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? '';
$projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? '';
?>
<?php if ($projectDescription): ?>
<!-- Meta description -->
<meta name="description" content='<?= htmlspecialchars($projectDescription) ?>' />
<!-- Open Graph meta tags -->
<meta property="og:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<!-- Twitter meta tags -->
<meta property="twitter:description" content="<?= htmlspecialchars($projectDescription) ?>" />
<?php endif; ?>
<?php if ($projectImageUrl): ?>
<!-- Open Graph image -->
<meta property="og:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<!-- Twitter image -->
<meta property="twitter:image" content="<?= htmlspecialchars($projectImageUrl) ?>" />
<?php endif; ?>
<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;700&display=swap" rel="stylesheet">
<style>
:root {
--bg-color-start: #6a11cb;
--bg-color-end: #2575fc;
--text-color: #ffffff;
--card-bg-color: rgba(255, 255, 255, 0.01);
--card-border-color: rgba(255, 255, 255, 0.1);
}
body {
margin: 0;
font-family: 'Inter', sans-serif;
background: linear-gradient(45deg, var(--bg-color-start), var(--bg-color-end));
color: var(--text-color);
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
text-align: center;
overflow: hidden;
position: relative;
}
body::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100" viewBox="0 0 100 100"><path d="M-10 10L110 10M10 -10L10 110" stroke-width="1" stroke="rgba(255,255,255,0.05)"/></svg>');
animation: bg-pan 20s linear infinite;
z-index: -1;
}
@keyframes bg-pan {
0% { background-position: 0% 0%; }
100% { background-position: 100% 100%; }
}
main {
padding: 2rem;
}
.card {
background: var(--card-bg-color);
border: 1px solid var(--card-border-color);
border-radius: 16px;
padding: 2rem;
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
}
.loader {
margin: 1.25rem auto 1.25rem;
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.25);
border-top-color: #fff;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.hint {
opacity: 0.9;
}
.sr-only {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap; border: 0;
}
h1 {
font-size: 3rem;
font-weight: 700;
margin: 0 0 1rem;
letter-spacing: -1px;
}
p {
margin: 0.5rem 0;
font-size: 1.1rem;
}
code {
background: rgba(0,0,0,0.2);
padding: 2px 6px;
border-radius: 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
}
footer {
position: absolute;
bottom: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
</style>
</head>
<body>
<main>
<div class="card">
<h1>Analyzing your requirements and generating your website…</h1>
<div class="loader" role="status" aria-live="polite" aria-label="Applying initial changes">
<span class="sr-only">Loading…</span>
</div>
<p class="hint"><?= ($_SERVER['HTTP_HOST'] ?? '') === 'appwizzy.com' ? 'AppWizzy' : 'Flatlogic' ?> AI is collecting your requirements and applying the first changes.</p>
<p class="hint">This page will update automatically as the plan is implemented.</p>
<p>Runtime: PHP <code><?= htmlspecialchars($phpVersion) ?></code> — UTC <code><?= htmlspecialchars($now) ?></code></p>
</div>
</main>
<footer>
Page updated: <?= htmlspecialchars($now) ?> (UTC)
</footer>
</body>
</html>