Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc7bcad072 | ||
|
|
91bdb6425e |
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()]);
|
||||||
|
}
|
||||||
65
assets/css/custom.css
Normal file
65
assets/css/custom.css
Normal 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
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();
|
||||||
|
});
|
||||||
104
assets/js/main.js
Normal file
104
assets/js/main.js
Normal 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
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');
|
||||||
160
index.php
160
index.php
@ -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>
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user