Added i18n.js script to all main pages (index, database, saraR4, wifi, logs, admin) to enable language switching functionality across the entire application. Commented out Map and Terminal menu items in the sidebar as these pages are not yet ready for production use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
466 lines
19 KiB
HTML
Executable File
466 lines
19 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">Votre capteur</h1>
|
|
<p>Bienvenue sur votre interface de configuration de votre capteur.</p>
|
|
|
|
<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">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">Linux stats</h5>
|
|
<p class="card-text">Disk usage (total size <span id="disk_size"></span> Gb) </p>
|
|
<div id="disk_space"></div>
|
|
<p class="card-text">Memory usage (total size <span id="memory_size"></span> Mb) </p>
|
|
<div id="memory_space"></div>
|
|
<p class="card-text"> Database size: <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>
|
|
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;
|
|
}
|
|
})
|
|
.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;
|
|
}
|
|
|
|
},
|
|
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>
|