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>
449 lines
16 KiB
HTML
Executable File
449 lines
16 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">
|
|
<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-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
|
<h1 class="mt-4">Base de données</h1>
|
|
<p>Le capteur enregistre en local les données de mesures. Vous pouvez ici les consulter et les télécharger.</p>
|
|
|
|
<div class="row mb-3">
|
|
|
|
<div class="col-sm-5">
|
|
<div class="card text-dark bg-light">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Consulter la base de donnée</h5>
|
|
<!-- Dropdown to select number of records -->
|
|
<div class="d-flex align-items-center mb-3">
|
|
<label for="records_limit" class="form-label me-2">Nombre de mesures:</label>
|
|
<select id="records_limit" class="form-select w-auto">
|
|
<option value="10" selected>10 dernières</option>
|
|
<option value="20">20 dernières</option>
|
|
<option value="30">30 dernières</option>
|
|
</select>
|
|
</div>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)">Mesures PM</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)">Mesures Temp/Hum</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)">Mesures PM (5 canaux)</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)">Sonde Cairsens</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NOISE',getSelectedLimit(),false)">Sonde bruit</button>
|
|
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)">Sonde Vent</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_MPPT',getSelectedLimit(),false)">Batterie</button>
|
|
|
|
<button class="btn btn-warning" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)">Timestamp Table</button>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="col-sm-5">
|
|
<div class="card text-dark bg-light">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Télécharger les données</h5>
|
|
<!-- Date selection for download -->
|
|
<div class="d-flex align-items-center gap-3 mb-3">
|
|
<label for="start_date" class="form-label">Date de début:</label>
|
|
<input type="date" id="start_date" class="form-control w-auto">
|
|
<label for="end_date" class="form-label">Date de fin:</label>
|
|
<input type="date" id="end_date" class="form-control w-auto">
|
|
</div>
|
|
|
|
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM',10,true, getStartDate(), getEndDate())">Mesures PM</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_BME280',10,true, getStartDate(), getEndDate())">Mesures Temp/Hum</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NPM_5channels',10,true, getStartDate(), getEndDate())">Mesures PM (5 canaux)</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_envea',10,true, getStartDate(), getEndDate())">Sonde Cairsens</button>
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_NOISE',10,true, getStartDate(), getEndDate())">Sonde Bruit</button>
|
|
|
|
<button class="btn btn-primary" onclick="get_data_sqlite('data_mppt',10,true, getStartDate(), getEndDate())">Batterie</button>
|
|
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<div class="row mt-2">
|
|
<div id="table_data"></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 () {
|
|
console.log("DOMContentLoaded");
|
|
|
|
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
|
|
|
|
|
|
//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);
|
|
}
|
|
}); //end AJAX
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
// TABLE PM
|
|
function get_data_sqlite(table, limit, download , startDate = "", endDate = "") {
|
|
console.log(`Getting data for table: ${table}, limit: ${limit}, download: ${download}, start: ${startDate}, end: ${endDate}`);
|
|
// Construct URL parameters dynamically
|
|
let url = `launcher.php?type=table_mesure&table=${table}&limit=${limit}&download=${download}`;
|
|
|
|
// Add date parameters if downloading
|
|
if (download) {
|
|
url += `&start_date=${startDate}&end_date=${endDate}`;
|
|
}
|
|
|
|
console.log(url);
|
|
|
|
$.ajax({
|
|
url: url,
|
|
dataType: 'text', // Specify that you expect a JSON response
|
|
method: 'GET', // Use GET or POST depending on your needs
|
|
success: function(response) {
|
|
console.log(response);
|
|
|
|
// If download is true, generate and trigger CSV download
|
|
if (download) {
|
|
downloadCSV(response, table);
|
|
return; // Exit function after triggering download
|
|
}
|
|
|
|
let rows = response.trim().split("\n");
|
|
// Generate Bootstrap table
|
|
|
|
let tableHTML = `<table class="table table-striped table-bordered">
|
|
<thead class="table-dark"><tr>`;
|
|
|
|
// Define column headers dynamically based on the table type
|
|
if (table === "data_NPM") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>PM1</th>
|
|
<th>PM2.5</th>
|
|
<th>PM10</th>
|
|
<th>Temperature (°C)</th>
|
|
<th>Humidity (%)</th>
|
|
`;
|
|
} else if (table === "data_BME280") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>Temperature (°C)</th>
|
|
<th>Humidity (%)</th>
|
|
<th>Pressure (hPa)</th>
|
|
`;
|
|
} else if (table === "data_NPM_5channels") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>PM_ch1 (nb/L)</th>
|
|
<th>PM_ch2 (nb/L)</th>
|
|
<th>PM_ch3 (nb/L)</th>
|
|
<th>PM_ch4 (nb/L)</th>
|
|
<th>PM_ch5 (nb/L)</th>
|
|
|
|
`;
|
|
}else if (table === "data_envea") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>NO2</th>
|
|
<th>H2S</th>
|
|
<th>NH3</th>
|
|
<th>CO</th>
|
|
<th>O3</th>
|
|
|
|
`;
|
|
}else if (table === "timestamp_table") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
`;
|
|
}else if (table === "data_WIND") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>speed (km/h)</th>
|
|
<th>Direction (V)</th>
|
|
`;
|
|
}else if (table === "data_MPPT") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>Battery Voltage</th>
|
|
<th>Battery Current</th>
|
|
<th> solar_voltage</th>
|
|
<th> solar_power</th>
|
|
<th> charger_status</th>
|
|
|
|
`;
|
|
}else if (table === "data_NOISE") {
|
|
tableHTML += `
|
|
<th>Timestamp</th>
|
|
<th>Curent LEQ</th>
|
|
<th>DB_A_value</th>
|
|
|
|
`;
|
|
}
|
|
|
|
|
|
tableHTML += `</tr></thead><tbody>`;
|
|
|
|
// Loop through rows and create table rows
|
|
rows.forEach(row => {
|
|
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
|
|
tableHTML += "<tr>";
|
|
|
|
if (table === "data_NPM") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
<td>${columns[3]}</td>
|
|
<td>${columns[4]}</td>
|
|
<td>${columns[5]}</td>
|
|
`;
|
|
} else if (table === "data_BME280") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
<td>${columns[3]}</td>
|
|
`;
|
|
}
|
|
else if (table === "data_NPM_5channels") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
<td>${columns[3]}</td>
|
|
<td>${columns[4]}</td>
|
|
<td>${columns[5]}</td>
|
|
|
|
`;
|
|
} else if (table === "data_envea") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
<td>${columns[3]}</td>
|
|
<td>${columns[4]}</td>
|
|
<td>${columns[5]}</td>
|
|
|
|
`;
|
|
}else if (table === "timestamp_table") {
|
|
tableHTML += `
|
|
<td>${columns[1]}</td>
|
|
`;
|
|
}else if (table === "data_WIND") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
`;
|
|
}else if (table === "data_MPPT") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
<td>${columns[3]}</td>
|
|
<td>${columns[4]}</td>
|
|
<td>${columns[5]}</td>
|
|
`;
|
|
}else if (table === "data_NOISE") {
|
|
tableHTML += `
|
|
<td>${columns[0]}</td>
|
|
<td>${columns[1]}</td>
|
|
<td>${columns[2]}</td>
|
|
|
|
`;
|
|
}
|
|
|
|
tableHTML += "</tr>";
|
|
});
|
|
|
|
tableHTML += `</tbody></table>`;
|
|
|
|
// Update the #table_data div with the generated table
|
|
document.getElementById("table_data").innerHTML = tableHTML;
|
|
},
|
|
error: function(xhr, status, error) {
|
|
console.error('AJAX request failed:', status, error);
|
|
}
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
function getSelectedLimit() {
|
|
return document.getElementById("records_limit").value;
|
|
}
|
|
|
|
function getStartDate() {
|
|
return document.getElementById("start_date").value || "2025-01-01"; // Default to a safe date
|
|
}
|
|
|
|
function getEndDate() {
|
|
return document.getElementById("end_date").value || "2025-12-31"; // Default to a safe date
|
|
}
|
|
|
|
function downloadCSV(response, table) {
|
|
let rows = response.trim().split("\n");
|
|
|
|
let csvContent = "";
|
|
|
|
// Add headers based on table type
|
|
if (table === "data_NPM") {
|
|
csvContent += "TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor\n";
|
|
} else if (table === "data_BME280") {
|
|
csvContent += "TimestampUTC,Temperature (°C),Humidity (%),Pressure (hPa)\n";
|
|
}
|
|
else if (table === "data_NPM_5channels") {
|
|
csvContent += "TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5\n";
|
|
}
|
|
|
|
// Format rows as CSV
|
|
rows.forEach(row => {
|
|
let columns = row.replace(/[()]/g, "").split(", ");
|
|
csvContent += columns.join(",") + "\n";
|
|
});
|
|
|
|
// Create a downloadable file
|
|
let blob = new Blob([csvContent], { type: "text/csv" });
|
|
let url = window.URL.createObjectURL(blob);
|
|
let a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = table + "_data.csv"; // File name
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|