Extraction du code self-test dans des fichiers partagés (selftest.js + selftest-modal.html) pour éviter la duplication. Ajout du bouton Run Self Test sur les pages index, sensors et admin. Nouveau test RTC qui vérifie la connexion du module DS3231 et la synchronisation horloge. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
500 lines
17 KiB
HTML
Executable File
500 lines
17 KiB
HTML
Executable File
<!DOCTYPE html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>NebuleAir</title>
|
|
<link rel="stylesheet" href="assets/css/bootstrap.min.css">
|
|
<script src="assets/js/chart.js"></script> <!-- Local Chart.js -->
|
|
|
|
<style>
|
|
body {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
#sidebar a.nav-link {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
#sidebar a.nav-link:hover {
|
|
background-color: rgba(0, 0, 0, 0.5);
|
|
}
|
|
|
|
#sidebar a.nav-link svg {
|
|
margin-right: 8px;
|
|
/* Add spacing between icons and text */
|
|
}
|
|
|
|
#sidebar {
|
|
transition: transform 0.3s ease-in-out;
|
|
}
|
|
|
|
.offcanvas-backdrop {
|
|
z-index: 1040;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<!-- Topbar -->
|
|
<span id="topbar"></span>
|
|
|
|
<!-- Sidebar Offcanvas for Mobile -->
|
|
<div class="offcanvas offcanvas-start text-white bg-dark" tabindex="-1" id="sidebarOffcanvas"
|
|
aria-labelledby="sidebarOffcanvasLabel">
|
|
<div class="offcanvas-header">
|
|
<h5 class="offcanvas-title" id="sidebarOffcanvasLabel">NebuleAir</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
|
</div>
|
|
<div class="offcanvas-body" id="sidebar_mobile">
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<div class="container-fluid mt-5">
|
|
<div class="row">
|
|
<aside class="col-md-2 col-lg-1 d-none d-md-block vh-100 position-fixed bg-dark text-white" id="sidebar">
|
|
</aside>
|
|
<!-- Main content -->
|
|
<main class="col-md-9 ms-sm-auto col-lg-10 offset-md-3 offset-lg-2 px-md-4">
|
|
<h1 class="mt-4" data-i18n="home.title">Votre capteur</h1>
|
|
<p data-i18n="home.welcome">Bienvenue sur votre interface de configuration de votre capteur.</p>
|
|
|
|
<button class="btn btn-success mb-3 btn_selfTest" onclick="runSelfTest()">
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-check2-circle me-1" viewBox="0 0 16 16">
|
|
<path d="M2.5 8a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0z"/>
|
|
<path d="M15.354 3.354a.5.5 0 0 0-.708-.708L8 9.293 5.354 6.646a.5.5 0 1 0-.708.708l3 3a.5.5 0 0 0 .708 0l7-7z"/>
|
|
</svg>
|
|
Run Self Test
|
|
</button>
|
|
|
|
<div class="row mb-3">
|
|
|
|
<!-- Card NPM values -->
|
|
<div class="col-sm-4 mt-2">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title" data-i18n="home.pmMeasures">Mesures PM</h5>
|
|
<canvas id="sensorPMChart" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card Linux Stats -->
|
|
<div class="col-sm-4 mt-2">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title" data-i18n="home.linuxStats">Statistiques Linux</h5>
|
|
<p class="card-text"><span data-i18n="home.diskUsage">Utilisation du disque (taille totale</span> <span
|
|
id="disk_size"></span> Gb) </p>
|
|
<div id="disk_space"></div>
|
|
<p class="card-text"><span data-i18n="home.memoryUsage">Utilisation de la mémoire (taille totale</span>
|
|
<span id="memory_size"></span> Mb)
|
|
</p>
|
|
<div id="memory_space"></div>
|
|
<p class="card-text"><span data-i18n="home.databaseSize">Taille de la base de données:</span> <span
|
|
id="database_size"></span> </p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
</div>
|
|
|
|
<!--
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-sm-4 mt-2">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Mesures Temperature</h5>
|
|
<canvas id="sensorBME_temp" style="width: 100%; max-width: 600px; height: 200px;"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
-->
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- JAVASCRIPT -->
|
|
|
|
<!-- Link Ajax locally -->
|
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
|
<!-- i18n translation system -->
|
|
<script src="assets/js/i18n.js"></script>
|
|
<script src="assets/js/topbar-logo.js"></script>
|
|
<script src="assets/js/selftest.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
const elementsToLoad = [
|
|
{ id: 'topbar', file: 'topbar.html' },
|
|
{ id: 'sidebar', file: 'sidebar.html' },
|
|
{ id: 'sidebar_mobile', file: 'sidebar.html' }
|
|
];
|
|
|
|
elementsToLoad.forEach(({ id, file }) => {
|
|
fetch(file)
|
|
.then(response => response.text())
|
|
.then(data => {
|
|
const element = document.getElementById(id);
|
|
if (element) {
|
|
element.innerHTML = data;
|
|
// Apply translations after loading dynamic content
|
|
if (window.i18n && typeof window.i18n.applyTranslations === 'function') {
|
|
window.i18n.applyTranslations();
|
|
}
|
|
}
|
|
})
|
|
.catch(error => console.error(`Error loading ${file}:`, error));
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
window.onload = function () {
|
|
|
|
//NEW way to get data from SQLITE
|
|
$.ajax({
|
|
url: 'launcher.php?type=get_config_sqlite',
|
|
dataType: 'json',
|
|
//dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function (response) {
|
|
console.log("Getting SQLite config table:");
|
|
console.log(response);
|
|
|
|
//get device Name (for the side bar)
|
|
const deviceName = response.deviceName;
|
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
|
elements.forEach((element) => {
|
|
element.innerText = deviceName;
|
|
});
|
|
|
|
//device name html page title
|
|
if (response.deviceName) {
|
|
document.title = response.deviceName;
|
|
}
|
|
|
|
// Check for device type to show Screen tab
|
|
// Assuming the key in config is 'device_type' or 'type'
|
|
if (response.device_type === 'moduleair_pro' || response.type === 'moduleair_pro') {
|
|
$('.nav-screen-item').show();
|
|
$('.nav-screen-item').css('display', 'flex'); // Ensure flex display to match others
|
|
}
|
|
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
}); //end ajax
|
|
|
|
/* OLD way of getting config data
|
|
fetch('../config.json') // Replace 'deviceID.txt' with 'config.json'
|
|
.then(response => response.json()) // Parse response as JSON
|
|
.then(data => {
|
|
console.log("Getting config file (onload)");
|
|
//get device ID
|
|
const deviceID = data.deviceID.trim().toUpperCase();
|
|
//document.getElementById('pageTitle_plus_ID').innerText = 'token: ' + deviceID;
|
|
|
|
|
|
//get device Name
|
|
const deviceName = data.deviceName;
|
|
|
|
const elements = document.querySelectorAll('.sideBar_sensorName');
|
|
elements.forEach((element) => {
|
|
element.innerText = deviceName;
|
|
});
|
|
|
|
//end fetch config
|
|
})
|
|
.catch(error => console.error('Error loading config.json:', error));
|
|
//end windows on load
|
|
*/
|
|
//get local RTC
|
|
$.ajax({
|
|
url: 'launcher.php?type=RTC_time',
|
|
dataType: 'text', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function (response) {
|
|
console.log("Local RTC: " + response);
|
|
const RTC_Element = document.getElementById("RTC_time");
|
|
RTC_Element.textContent = response;
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
//get database size
|
|
$.ajax({
|
|
url: 'launcher.php?type=database_size',
|
|
dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function (response) {
|
|
console.log(response);
|
|
|
|
if (response.size_megabytes !== undefined) {
|
|
// Extract and format the size in MB
|
|
const databaseSizeMB = response.size_megabytes + " MB";
|
|
|
|
// Update the HTML element with the database size
|
|
const databaseSizeElement = document.getElementById("database_size");
|
|
databaseSizeElement.textContent = databaseSizeMB;
|
|
|
|
console.log("Database size:", databaseSizeMB);
|
|
} else if (response.error) {
|
|
// Handle errors from the PHP response
|
|
console.error("Error from server:", response.error);
|
|
}
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
|
|
//get disk free space
|
|
$.ajax({
|
|
url: 'launcher.php?type=linux_disk',
|
|
dataType: 'text', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function (response) {
|
|
console.log("Linux disk space: " + response);
|
|
//1. disk size
|
|
const disk_size = document.getElementById("disk_size");
|
|
const firstNumber = response.match(/(?<!\w)(\d+(\.\d+)?)(?=\D)/)[1];
|
|
|
|
disk_size.innerHTML = firstNumber;
|
|
//2. Free space
|
|
const match = response.match(/(\d+)%/);
|
|
const diskSpace = document.getElementById("disk_space");
|
|
const percentage = match[1];
|
|
|
|
// Create the outer div with class and attributes
|
|
const progressDiv = document.createElement('div');
|
|
progressDiv.className = 'progress mb-3';
|
|
progressDiv.setAttribute('role', 'progressbar');
|
|
progressDiv.setAttribute('aria-label', 'Example with label');
|
|
progressDiv.setAttribute('aria-valuenow', percentage);
|
|
progressDiv.setAttribute('aria-valuemin', 0);
|
|
progressDiv.setAttribute('aria-valuemax', 100);
|
|
|
|
// Create the inner progress bar div
|
|
const progressBarDiv = document.createElement('div');
|
|
progressBarDiv.className = 'progress-bar';
|
|
progressBarDiv.style.width = `${percentage}%`; // Set the width dynamically
|
|
progressBarDiv.textContent = `${percentage}%`; // Set the text dynamically
|
|
|
|
// Append the progress bar to the outer div
|
|
progressDiv.appendChild(progressBarDiv);
|
|
|
|
// Append the entire progress bar to the body (or any other container)
|
|
diskSpace.appendChild(progressDiv);
|
|
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
//get memory free space
|
|
$.ajax({
|
|
url: 'launcher.php?type=linux_memory',
|
|
dataType: 'text', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function (response) {
|
|
console.log("Linux memory space: " + response);
|
|
//1. memory size
|
|
const memory_size = document.getElementById("memory_size");
|
|
const memorySpace = document.getElementById("memory_space");
|
|
|
|
|
|
const memLine = response.match(/Mem:\s+(\d+\.?\d*)Mi\s+(\d+\.?\d*)Mi/);
|
|
const totalMemory = parseFloat(memLine[1]); // Total memory in MiB
|
|
const usedMemory = parseFloat(memLine[2]); // Used memory in MiB
|
|
|
|
// Calculate the percentage
|
|
const percentageUsed = ((usedMemory / totalMemory) * 100).toFixed(2);
|
|
|
|
console.log(totalMemory);
|
|
|
|
memory_size.innerHTML = totalMemory;
|
|
|
|
|
|
console.log(usedMemory);
|
|
console.log(percentageUsed);
|
|
|
|
// Create the outer div with class and attributes
|
|
const progressDiv = document.createElement('div');
|
|
progressDiv.className = 'progress mb-3';
|
|
progressDiv.setAttribute('role', 'progressbar');
|
|
progressDiv.setAttribute('aria-label', 'Example with label');
|
|
progressDiv.setAttribute('aria-valuenow', percentageUsed);
|
|
progressDiv.setAttribute('aria-valuemin', 0);
|
|
progressDiv.setAttribute('aria-valuemax', 100);
|
|
|
|
// Create the inner progress bar div
|
|
const progressBarDiv = document.createElement('div');
|
|
progressBarDiv.className = 'progress-bar';
|
|
progressBarDiv.style.width = `${percentageUsed}%`; // Set the width dynamically
|
|
progressBarDiv.textContent = `${percentageUsed}%`; // Set the text dynamically
|
|
|
|
// Append the progress bar to the outer div
|
|
progressDiv.appendChild(progressBarDiv);
|
|
|
|
// Append the entire progress bar to the body (or any other container)
|
|
memorySpace.appendChild(progressDiv);
|
|
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
|
|
// GET NPM SQLite values
|
|
$.ajax({
|
|
url: 'launcher.php?type=get_npm_sqlite_data',
|
|
dataType: 'json', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function (response) {
|
|
console.log(response);
|
|
updatePMChart(response);
|
|
},
|
|
error: function (xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
let chart; // Store the Chart.js instance globally
|
|
|
|
function updatePMChart(data) {
|
|
const labels = data.map(d => d.timestamp);
|
|
const PM1 = data.map(d => d.PM1);
|
|
const PM25 = data.map(d => d.PM25);
|
|
const PM10 = data.map(d => d.PM10);
|
|
|
|
const ctx = document.getElementById('sensorPMChart').getContext('2d');
|
|
|
|
if (!chart) {
|
|
chart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [
|
|
{
|
|
label: "PM1",
|
|
data: PM1,
|
|
borderColor: "rgba(0, 51, 153, 1)",
|
|
backgroundColor: "rgba(0, 51, 153, 0.2)", // Very light blue background
|
|
fill: true,
|
|
tension: 0.4, // Smooth curves
|
|
pointRadius: 2, // Larger points
|
|
pointHoverRadius: 6 // Bigger hover points
|
|
},
|
|
{
|
|
label: "PM2.5",
|
|
data: PM25,
|
|
borderColor: "rgba(30, 144, 255, 1)",
|
|
backgroundColor: "rgba(30, 144, 255, 0.2)", // Very light medium blue background
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 2,
|
|
pointHoverRadius: 6
|
|
},
|
|
{
|
|
label: "PM10",
|
|
data: PM10,
|
|
borderColor: "rgba(135, 206, 250, 1)",
|
|
backgroundColor: "rgba(135, 206, 250, 0.2)", // Very light blue background
|
|
fill: true,
|
|
tension: 0.4,
|
|
pointRadius: 2,
|
|
pointHoverRadius: 6
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top'
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
title: {
|
|
display: true,
|
|
text: 'Time (UTC)',
|
|
font: {
|
|
size: 16,
|
|
family: 'Arial, sans-serif'
|
|
},
|
|
color: '#4A4A4A'
|
|
},
|
|
ticks: {
|
|
autoSkip: true,
|
|
maxTicksLimit: 5,
|
|
color: '#4A4A4A',
|
|
callback: function (value, index) {
|
|
// Access the correct label from the `labels` array
|
|
const label = labels[index]; // Use the original `labels` array
|
|
if (label && typeof label === 'string' && label.includes(' ')) {
|
|
return label.split(' ')[1].slice(0, 5); // Extract "HH:MM"
|
|
}
|
|
return value; // Fallback for invalid labels
|
|
}
|
|
},
|
|
grid: {
|
|
display: false // Remove gridlines for a cleaner look
|
|
}
|
|
|
|
|
|
},
|
|
y: {
|
|
title: {
|
|
display: true,
|
|
text: 'Values (µg/m³)',
|
|
font: {
|
|
size: 16,
|
|
family: 'Arial, sans-serif'
|
|
},
|
|
color: '#4A4A4A'
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
});
|
|
} else {
|
|
chart.data.labels = labels;
|
|
chart.data.datasets[0].data = PM1;
|
|
chart.data.datasets[1].data = PM25;
|
|
chart.data.datasets[2].data = PM10;
|
|
chart.update();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html> |