Files
nebuleair_pro_4g/html/database.html
PaulVua d554f03195 v1.12.1: S88 ecrit toujours + code d'etat, plus de CO2 perime transmis
Probleme vu sur pro100: sonde S88 muette (panne cablage) mais write_data.py
n'ecrivait rien -> la base gardait la derniere valeur (487 ppm d'hier) et la
loop d'envoi la transmettait en boucle.

- data_S88: nouvelle colonne s88_status (0=OK, 0xFF=sonde muette), comme
  npm_status/noise_status. Migration via create_db.py + set_config.py + self-heal.
- S88/write_data.py: ecrit DESORMAIS une ligne a chaque cycle (CO2=0 + 0xFF si
  pas de reponse). Connexion SQLite timeout=10 (anti database-is-locked).
- SARA_send_data_v2.py: lit s88_status; si 0xFF -> bytes 81-82 restent 0xFFFF
  (CO2 absent) au lieu d'envoyer une valeur perimee. Compatible bases non migrees.
- database.html + launcher.php: badge statut + colonne dans les exports CSV.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-02 17:10:32 +02:00

660 lines
29 KiB
HTML
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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;
}
/* Highlight most recent data row with light green background */
.table .most-recent-row td {
background-color: #d4edda !important;
}
.table-striped .most-recent-row td {
background-color: #d4edda !important;
}
</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" data-i18n="database.title">Base de données</h1>
<p data-i18n="database.description">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-lg-4 col-md-6 mb-3">
<div class="card text-dark bg-light h-100">
<div class="card-body">
<h5 class="card-title" data-i18n="database.viewDatabase">Consulter la base de donnée</h5>
<p class="text-muted small">Ouvre les 20 dernières mesures, navigation page par page.</p>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_NPM','Mesures PM')" data-i18n="database.pmMeasures">Mesures PM</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_BME280','Mesures Temp/Hum')" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_NPM_5channels','Mesures PM (5 canaux)')" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_envea','Sonde Cairsens')" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_NOISE','Sonde bruit')" data-i18n="database.noiseProbe">Sonde bruit</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_WIND','Sonde Vent')" data-i18n="database.windProbe">Sonde Vent</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_MPPT','Batterie')" data-i18n="database.battery">Batterie</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_MHZ19','Mesures CO2 (MH-Z19)')">Mesures CO2 (MH-Z19)</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_S88','Mesures CO2 (Senseair S88)')">Mesures CO2 (Senseair S88)</button>
<button class="btn btn-primary mb-2" onclick="openTableModal('data_CCS811','Mesures TVOC/eCO2 (CCS811)')">Mesures TVOC/eCO2 (CCS811)</button>
<button class="btn btn-warning mb-2" onclick="openTableModal('timestamp_table','Timestamp Table')" data-i18n="database.timestampTable">Timestamp Table</button>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<div class="card text-dark bg-light h-100">
<div class="card-body">
<h5 class="card-title" data-i18n="database.downloadData">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" data-i18n="database.startDate">Date de début:</label>
<input type="date" id="start_date" class="form-control w-auto">
<label for="end_date" class="form-label" data-i18n="database.endDate">Date de fin:</label>
<input type="date" id="end_date" class="form-control w-auto">
</div>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NPM')" data-i18n="database.pmMeasures">Mesures PM</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_BME280')" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NPM_5channels')" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_envea')" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_NOISE')" data-i18n="database.noiseProbe">Sonde Bruit</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_mppt')" data-i18n="database.battery">Batterie</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_MHZ19')">Mesures CO2 (MH-Z19)</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_S88')">Mesures CO2 (Senseair S88)</button>
<button class="btn btn-primary mb-2" onclick="downloadByDate('data_CCS811')">Mesures TVOC/eCO2 (CCS811)</button>
</div>
</div>
</div>
<div class="col-lg-4 col-md-6 mb-3">
<div class="card text-dark bg-light h-100">
<div class="card-body">
<h5 class="card-title" data-i18n="database.downloadAll">Télécharger toute la table</h5>
<p class="text-muted small" data-i18n="database.downloadAllDesc">Télécharge l'intégralité des données sans filtre de date.</p>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NPM')" data-i18n="database.pmMeasures">Mesures PM</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_BME280')" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NPM_5channels')" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_envea')" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_NOISE')" data-i18n="database.noiseProbe">Sonde Bruit</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_MPPT')" data-i18n="database.battery">Batterie</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_MHZ19')">Mesures CO2 (MH-Z19)</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_S88')">Mesures CO2 (Senseair S88)</button>
<button class="btn btn-success mb-2" onclick="downloadFullTable('data_CCS811')">Mesures TVOC/eCO2 (CCS811)</button>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-lg-4 col-md-6 mb-3">
<div class="card text-dark bg-light h-100">
<div class="card-body">
<h5 class="card-title" data-i18n="database.statsTitle">Informations sur la base</h5>
<div id="db_stats_content">
<div class="text-center py-3">
<div class="spinner-border spinner-border-sm" role="status"></div>
<span class="ms-2" data-i18n="common.loading">Chargement...</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row mb-3">
<div class="col-lg-4 col-md-6 mb-3">
<div class="card text-white bg-danger h-100">
<div class="card-body">
<h5 class="card-title" data-i18n="database.dangerZone">Zone dangereuse</h5>
<p class="card-text" data-i18n="database.dangerWarning">Attention: Cette action est irréversible!</p>
<button class="btn btn-dark btn-lg w-100 mb-2" onclick="emptySensorTables()" data-i18n="database.emptyAllTables">Vider toutes les tables de capteurs</button>
<small class="d-block mt-2" data-i18n="database.emptyTablesNote">Note: Les tables de configuration et horodatage seront préservées.</small>
</div>
</div>
</div>
</div>
<div class="row mt-2">
<div id="table_data"></div>
</div>
</main>
</div>
</div>
<!-- Modal pour consultation des mesures avec pagination -->
<div class="modal fade" id="tableModal" tabindex="-1" aria-labelledby="tableModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="tableModalLabel">Mesures</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div id="tableModalContent">
<div class="text-center py-3">
<div class="spinner-border" role="status"></div>
<span class="ms-2">Chargement...</span>
</div>
</div>
</div>
<div class="modal-footer justify-content-between">
<div class="text-muted small" id="tableModalRange"></div>
<div class="btn-group">
<button type="button" class="btn btn-outline-primary" id="tableModalPrev" onclick="tableModalChangePage(-1)" disabled>← Précédent</button>
<button type="button" class="btn btn-outline-primary" id="tableModalNext" onclick="tableModalChangePage(1)">Suivant →</button>
</div>
</div>
</div>
</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>
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' }
];
let loadedCount = 0;
const totalElements = elementsToLoad.length;
function applyTranslationsWhenReady() {
if (typeof i18n !== 'undefined' && i18n.translations && Object.keys(i18n.translations).length > 0) {
console.log("Applying translations to dynamically loaded content");
i18n.applyTranslations();
} else {
// Retry after a short delay if translations aren't loaded yet
setTimeout(applyTranslationsWhenReady, 100);
}
}
elementsToLoad.forEach(({ id, file }) => {
fetch(file)
.then(response => response.text())
.then(data => {
const element = document.getElementById(id);
if (element) {
element.innerHTML = data;
}
loadedCount++;
// Re-apply translations after all dynamic content is loaded
if (loadedCount === totalElements) {
applyTranslationsWhenReady();
}
})
.catch(error => console.error(`Error loading ${file}:`, error));
});
// Also listen for language change events to re-apply translations
document.addEventListener('languageChanged', function() {
console.log("Language changed, re-applying translations");
i18n.applyTranslations();
});
});
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 database table stats
loadDbStats();
//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
}
// Build the <th> header cells for a given table
function buildTableHeader(table) {
const headers = {
data_NPM: ['Timestamp','PM1','PM2.5','PM10','Temperature (°C)','Humidity (%)','Status'],
data_BME280: ['Timestamp','Temperature (°C)','Humidity (%)','Pressure (hPa)'],
data_NPM_5channels: ['Timestamp','PM_ch1 (nb/L)','PM_ch2 (nb/L)','PM_ch3 (nb/L)','PM_ch4 (nb/L)','PM_ch5 (nb/L)'],
data_envea: ['Timestamp','NO2','H2S','NH3','CO','O3'],
timestamp_table: ['Timestamp'],
data_WIND: ['Timestamp','speed (km/h)','Direction (V)'],
data_MPPT: ['Timestamp','Battery Voltage','Battery Current','solar_voltage','solar_power','charger_status'],
data_NOISE: ['Timestamp','Curent LEQ','DB_A_value','Status'],
data_MHZ19: ['Timestamp','CO2 (ppm)'],
data_S88: ['Timestamp','CO2 (ppm)','Status'],
data_CCS811: ['Timestamp','eCO2 (ppm)','TVOC (ppb)']
};
return (headers[table] || ['Data']).map(h => `<th>${h}</th>`).join('');
}
// Build the <td> cells for one row of a given table
function buildTableRow(table, columns) {
if (table === "data_NPM") {
const statusVal = parseInt(columns[6]) || 0;
const statusBadge = statusVal === 0
? '<span class="badge text-bg-success">OK</span>'
: `<span class="badge text-bg-warning">0x${statusVal.toString(16).toUpperCase().padStart(2,'0')}</span>`;
return `<td>${columns[0]}</td><td>${columns[1]}</td><td>${columns[2]}</td><td>${columns[3]}</td><td>${columns[4]}</td><td>${columns[5]}</td><td>${statusBadge}</td>`;
}
if (table === "data_NOISE") {
const nStatus = parseInt(columns[3]) || 0;
const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK';
return `<td>${columns[0]}</td><td>${columns[1]}</td><td>${columns[2]}</td><td>${nStatusLabel}</td>`;
}
if (table === "data_S88") {
const sStatus = parseInt(columns[2]) || 0;
const sStatusLabel = sStatus === 255 ? '❌ Déconnecté' : '✅ OK';
return `<td>${columns[0]}</td><td>${columns[1]}</td><td>${sStatusLabel}</td>`;
}
if (table === "timestamp_table") {
return `<td>${columns[1]}</td>`;
}
// Default: render all available columns
const colCount = { data_BME280: 4, data_NPM_5channels: 6, data_envea: 6, data_WIND: 3, data_MPPT: 6, data_MHZ19: 2, data_S88: 3, data_CCS811: 3 };
const n = colCount[table] || columns.length;
return columns.slice(0, n).map(c => `<td>${c}</td>`).join('');
}
// Modal pagination state
const TABLE_MODAL_PAGE_SIZE = 20;
let tableModalState = { table: null, title: null, page: 0 };
function openTableModal(table, title) {
tableModalState = { table, title, page: 0 };
document.getElementById('tableModalLabel').textContent = title;
const modalEl = document.getElementById('tableModal');
const modal = bootstrap.Modal.getOrCreateInstance(modalEl);
modal.show();
loadTableModalPage();
}
function tableModalChangePage(delta) {
tableModalState.page = Math.max(0, tableModalState.page + delta);
loadTableModalPage();
}
function loadTableModalPage() {
const { table, page } = tableModalState;
const offset = page * TABLE_MODAL_PAGE_SIZE;
const url = `launcher.php?type=table_mesure&table=${table}&limit=${TABLE_MODAL_PAGE_SIZE}&offset=${offset}&download=false`;
document.getElementById('tableModalContent').innerHTML =
'<div class="text-center py-3"><div class="spinner-border" role="status"></div><span class="ms-2">Chargement…</span></div>';
document.getElementById('tableModalPrev').disabled = true;
document.getElementById('tableModalNext').disabled = true;
$.ajax({
url: url,
dataType: 'text',
method: 'GET',
success: function(response) {
const lines = response.trim().split('\n').filter(l => l.length > 0);
if (lines.length === 0) {
if (page === 0) {
document.getElementById('tableModalContent').innerHTML =
'<div class="text-center py-4 text-muted">Aucune donnée disponible dans cette table.</div>';
document.getElementById('tableModalRange').textContent = '—';
} else {
// Past the end — step back
tableModalState.page--;
loadTableModalPage();
}
return;
}
let html = `<table class="table table-striped table-bordered mb-0"><thead class="table-dark sticky-top"><tr>${buildTableHeader(table)}</tr></thead><tbody>`;
lines.forEach((line, idx) => {
const columns = line.replace(/[()]/g, '').split(', ');
const rowClass = (idx === 0 && page === 0) ? ' class="most-recent-row"' : '';
html += `<tr${rowClass}>${buildTableRow(table, columns)}</tr>`;
});
html += '</tbody></table>';
document.getElementById('tableModalContent').innerHTML = html;
document.getElementById('tableModalRange').textContent = `Lignes ${offset + 1} ${offset + lines.length}`;
document.getElementById('tableModalPrev').disabled = page === 0;
// If we got fewer rows than the page size, we hit the end of the table
document.getElementById('tableModalNext').disabled = lines.length < TABLE_MODAL_PAGE_SIZE;
},
error: function(xhr, status, error) {
document.getElementById('tableModalContent').innerHTML =
`<div class="text-danger">Erreur de chargement: ${error}</div>`;
}
});
}
// Legacy: still used by downloadByDate() to fetch CSV via the same endpoint
function get_data_sqlite(table, limit, download, startDate = "", endDate = "") {
let url = `launcher.php?type=table_mesure&table=${table}&limit=${limit}&download=${download}`;
if (download) {
url += `&start_date=${startDate}&end_date=${endDate}`;
}
$.ajax({
url: url,
dataType: 'text',
method: 'GET',
success: function(response) {
if (download) {
downloadCSV(response, table);
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
}
});
}
function getStartDate() {
return document.getElementById("start_date").value;
}
function getEndDate() {
return document.getElementById("end_date").value;
}
function downloadByDate(table) {
const startDate = getStartDate();
const endDate = getEndDate();
if (!startDate || !endDate) {
alert("Veuillez sélectionner une date de début et une date de fin.");
return;
}
get_data_sqlite(table, 10, true, startDate, endDate);
}
function downloadFullTable(table) {
window.location.href = 'launcher.php?type=download_full_table&table=' + encodeURIComponent(table);
}
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,npm_status\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";
}
else if (table === "data_NOISE") {
csvContent += "TimestampUTC,Current_LEQ,DB_A_value,noise_status\n";
}
else if (table === "data_MHZ19") {
csvContent += "TimestampUTC,CO2_ppm\n";
}
else if (table === "data_S88") {
csvContent += "TimestampUTC,CO2_ppm,s88_status\n";
}
else if (table === "data_CCS811") {
csvContent += "TimestampUTC,eCO2_ppm,TVOC_ppb\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);
}
// Table display names
const tableDisplayNames = {
'data_NPM': 'PM (NextPM)',
'data_NPM_5channels': 'PM 5 canaux',
'data_BME280': 'Temp/Hum (BME280)',
'data_envea': 'Gaz (Cairsens)',
'data_WIND': 'Vent',
'data_MPPT': 'Batterie (MPPT)',
'data_NOISE': 'Bruit',
'data_MHZ19': 'CO2 (MH-Z19)',
'data_S88': 'CO2 (Senseair S88)',
'data_CCS811': 'TVOC/eCO2 (CCS811)'
};
function loadDbStats() {
$.ajax({
url: 'launcher.php?type=db_table_stats',
dataType: 'json',
method: 'GET',
success: function(response) {
if (!response.success) {
document.getElementById('db_stats_content').innerHTML =
'<div class="alert alert-danger mb-0">' + (response.error || 'Erreur') + '</div>';
return;
}
let html = '<p class="mb-2"><strong data-i18n="database.statsDbSize">Taille totale:</strong> ' + response.size_mb + ' MB</p>';
html += '<div class="table-responsive"><table class="table table-sm table-bordered mb-0">';
html += '<thead class="table-secondary"><tr>';
html += '<th data-i18n="database.statsTable">Table</th>';
html += '<th data-i18n="database.statsCount">Entrées</th>';
html += '<th data-i18n="database.statsOldest">Plus ancienne</th>';
html += '<th data-i18n="database.statsNewest">Plus récente</th>';
html += '<th data-i18n="database.statsDownload">CSV</th>';
html += '</tr></thead><tbody>';
response.tables.forEach(function(t) {
const displayName = tableDisplayNames[t.name] || t.name;
const oldest = t.oldest ? t.oldest.substring(0, 16) : '-';
const newest = t.newest ? t.newest.substring(0, 16) : '-';
const downloadBtn = t.count > 0
? '<a href="launcher.php?type=download_full_table&table=' + t.name + '" class="btn btn-outline-primary btn-sm py-0 px-1" title="Download CSV"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg></a>'
: '-';
html += '<tr>';
html += '<td>' + displayName + '</td>';
html += '<td>' + t.count.toLocaleString() + '</td>';
html += '<td><small>' + oldest + '</small></td>';
html += '<td><small>' + newest + '</small></td>';
html += '<td class="text-center">' + downloadBtn + '</td>';
html += '</tr>';
});
html += '</tbody></table></div>';
html += '<button class="btn btn-outline-secondary btn-sm mt-2" onclick="loadDbStats()" data-i18n="logs.refresh">Refresh</button>';
document.getElementById('db_stats_content').innerHTML = html;
// Re-apply translations if i18n is loaded
if (typeof i18n !== 'undefined' && i18n.translations && Object.keys(i18n.translations).length > 0) {
i18n.applyTranslations();
}
},
error: function(xhr, status, error) {
document.getElementById('db_stats_content').innerHTML =
'<div class="alert alert-danger mb-0">Erreur: ' + error + '</div>';
}
});
}
// Function to empty all sensor tables
function emptySensorTables() {
// Show confirmation dialog
const confirmed = confirm(
"WARNING: This will permanently delete ALL sensor data from the database!\n\n" +
"The following tables will be emptied:\n" +
"- data_NPM\n" +
"- data_NPM_5channels\n" +
"- data_BME280\n" +
"- data_envea\n" +
"- data_WIND\n" +
"- data_MPPT\n" +
"- data_NOISE\n\n" +
"Configuration and timestamp tables will be preserved.\n\n" +
"Are you absolutely sure you want to continue?"
);
if (!confirmed) {
console.log("Empty sensor tables operation cancelled by user");
return;
}
// Show loading message
const tableDataDiv = document.getElementById("table_data");
tableDataDiv.innerHTML = '<div class="alert alert-info">Emptying sensor tables... Please wait...</div>';
// Make AJAX request to empty tables
$.ajax({
url: 'launcher.php?type=empty_sensor_tables',
dataType: 'json',
method: 'GET',
success: function(response) {
console.log("Empty sensor tables response:", response);
if (response.success) {
// Show success message
let message = '<div class="alert alert-success">';
message += '<h5>Success!</h5>';
message += '<p>' + response.message + '</p>';
if (response.tables_processed && response.tables_processed.length > 0) {
message += '<p><strong>Tables emptied:</strong></p><ul>';
response.tables_processed.forEach(table => {
message += `<li>${table.name}: ${table.deleted} records deleted</li>`;
});
message += '</ul>';
}
message += '</div>';
tableDataDiv.innerHTML = message;
} else {
// Show error message
tableDataDiv.innerHTML = `<div class="alert alert-danger">
<h5>Error!</h5>
<p>${response.message || response.error || 'Unknown error occurred'}</p>
</div>`;
}
},
error: function(xhr, status, error) {
console.error('AJAX request failed:', status, error);
tableDataDiv.innerHTML = `<div class="alert alert-danger">
<h5>Error!</h5>
<p>Failed to empty sensor tables: ${error}</p>
</div>`;
}
});
}
</script>
</body>
</html>