v1.9.17: database.html - modal + pagination, boutons S88

Refonte des boutons 'Consulter la base de donnée': ils ouvrent
désormais un grand modal Bootstrap (modal-xl scrollable) avec
pagination 20 lignes/page (Précédent/Suivant + indicateur de plage).
Le dropdown 'Nombre de mesures' est supprimé.

Ajout des boutons Senseair S88 dans les 3 cartes pointant sur
data_S88, et renommage du bouton MH-Z19 pour le distinguer.

Backend: sqlite/read.py accepte un OFFSET optionnel (3e argument,
défaut 0) et launcher.php endpoint table_mesure transmet ?offset=N.
Rétrocompatible.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-06-01 16:51:49 +02:00
parent 6d157cd099
commit 0f94fda0ba
5 changed files with 196 additions and 233 deletions

View File

@@ -65,24 +65,17 @@
<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>
<!-- Dropdown to select number of records -->
<div class="d-flex align-items-center mb-3">
<label for="records_limit" class="form-label me-2" data-i18n="database.numberOfMeasures">Nombre de mesures:</label>
<select id="records_limit" class="form-select w-auto">
<option value="10" selected data-i18n="database.last10">10 dernières</option>
<option value="20" data-i18n="database.last20">20 dernières</option>
<option value="30" data-i18n="database.last30">30 dernières</option>
</select>
</div>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NPM',getSelectedLimit(),false)" data-i18n="database.pmMeasures">Mesures PM</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_BME280',getSelectedLimit(),false)" data-i18n="database.tempHumMeasures">Mesures Temp/Hum</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NPM_5channels',getSelectedLimit(),false)" data-i18n="database.pm5Channels">Mesures PM (5 canaux)</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_envea',getSelectedLimit(),false)" data-i18n="database.cairsensProbe">Sonde Cairsens</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_NOISE',getSelectedLimit(),false)" data-i18n="database.noiseProbe">Sonde bruit</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_WIND',getSelectedLimit(),false)" data-i18n="database.windProbe">Sonde Vent</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_MPPT',getSelectedLimit(),false)" data-i18n="database.battery">Batterie</button>
<button class="btn btn-primary mb-2" onclick="get_data_sqlite('data_MHZ19',getSelectedLimit(),false)">Mesures CO2</button>
<button class="btn btn-warning mb-2" onclick="get_data_sqlite('timestamp_table',getSelectedLimit(),false)" data-i18n="database.timestampTable">Timestamp Table</button>
<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-warning mb-2" onclick="openTableModal('timestamp_table','Timestamp Table')" data-i18n="database.timestampTable">Timestamp Table</button>
</div>
</div>
</div>
@@ -105,7 +98,8 @@
<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</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>
</div>
</div>
</div>
@@ -121,7 +115,8 @@
<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</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>
</div>
</div>
</div>
@@ -162,11 +157,38 @@
<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 -->
@@ -286,218 +308,136 @@ window.onload = function() {
// 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}`;
// 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)']
};
return (headers[table] || ['Data']).map(h => `<th>${h}</th>`).join('');
}
// 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>
<th>Status</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>
<th>Status</th>
`;
}else if (table === "data_MHZ19") {
tableHTML += `
<th>Timestamp</th>
<th>CO2 (ppm)</th>
`;
}
tableHTML += `</tr></thead><tbody>`;
// Loop through rows and create table rows
rows.forEach((row, index) => {
let columns = row.replace(/[()]/g, "").split(", "); // Remove parentheses and split
// Add special class to first row (most recent data)
const rowClass = index === 0 ? ' class="most-recent-row"' : '';
tableHTML += `<tr${rowClass}>`;
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>`;
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>
<td>${statusBadge}</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") {
const nStatus = parseInt(columns[3]) || 0;
const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK';
tableHTML += `
<td>${columns[0]}</td>
<td>${columns[1]}</td>
<td>${columns[2]}</td>
<td>${nStatusLabel}</td>
`;
}else if (table === "data_MHZ19") {
tableHTML += `
<td>${columns[0]}</td>
<td>${columns[1]}</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);
// 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 === "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: 2 };
const n = colCount[table] || columns.length;
return columns.slice(0, n).map(c => `<td>${c}</td>`).join('');
}
function getSelectedLimit() {
return document.getElementById("records_limit").value;
// 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() {
@@ -542,6 +482,9 @@ function downloadCSV(response, table) {
else if (table === "data_MHZ19") {
csvContent += "TimestampUTC,CO2_ppm\n";
}
else if (table === "data_S88") {
csvContent += "TimestampUTC,CO2_ppm\n";
}
// Format rows as CSV
rows.forEach(row => {
@@ -568,7 +511,9 @@ const tableDisplayNames = {
'data_envea': 'Gaz (Cairsens)',
'data_WIND': 'Vent',
'data_MPPT': 'Batterie (MPPT)',
'data_NOISE': 'Bruit'
'data_NOISE': 'Bruit',
'data_MHZ19': 'CO2 (MH-Z19)',
'data_S88': 'CO2 (Senseair S88)'
};
function loadDbStats() {