From 0f94fda0ba0e25d399415343a7190d0e7e17507e Mon Sep 17 00:00:00 2001 From: PaulVua Date: Mon, 1 Jun 2026 16:51:49 +0200 Subject: [PATCH] v1.9.17: database.html - modal + pagination, boutons S88 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- VERSION | 2 +- changelog.json | 16 ++ html/database.html | 403 +++++++++++++++++++-------------------------- html/launcher.php | 3 +- sqlite/read.py | 5 +- 5 files changed, 196 insertions(+), 233 deletions(-) diff --git a/VERSION b/VERSION index 60c38df..eea55cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.9.16 +1.9.17 diff --git a/changelog.json b/changelog.json index f6a95c2..3861db5 100644 --- a/changelog.json +++ b/changelog.json @@ -1,5 +1,21 @@ { "versions": [ + { + "version": "1.9.17", + "date": "2026-06-01", + "changes": { + "features": [ + "database.html: refonte de la consultation des mesures. Les boutons 'Consulter la base' ouvrent désormais un grand modal (modal-xl scrollable) avec pagination 20 lignes par page (boutons Précédent/Suivant + indicateur de plage). Le dropdown 'Nombre de mesures' est supprimé — par défaut 20 dernières, on navigue ensuite page par page.", + "database.html: ajout des boutons Senseair S88 dans les 3 cartes (Consulter / Télécharger par date / Télécharger toute la table), pointant sur data_S88. Le bouton MH-Z19 est renommé 'Mesures CO2 (MH-Z19)'." + ], + "improvements": [ + "sqlite/read.py + launcher.php endpoint table_mesure: support du paramètre OFFSET (utilisé par la pagination du modal)." + ], + "fixes": [], + "compatibility": [] + }, + "notes": "Rétrocompatible: read.py accepte toujours les anciens appels à 2 arguments (offset par défaut = 0). L'endpoint table_mesure accepte un offset optionnel." + }, { "version": "1.9.16", "date": "2026-06-01", diff --git a/html/database.html b/html/database.html index 32dd9b2..a251980 100755 --- a/html/database.html +++ b/html/database.html @@ -65,24 +65,17 @@
Consulter la base de donnée
- -
- - -
- - - - - - - - - +

Ouvre les 20 dernières mesures, navigation page par page.

+ + + + + + + + + +
@@ -105,7 +98,8 @@ - + + @@ -121,7 +115,8 @@ - + + @@ -162,11 +157,38 @@
- + + + + @@ -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 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 => `${h}`).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 = ` - `; - - // Define column headers dynamically based on the table type - if (table === "data_NPM") { - tableHTML += ` - - - - - - - - `; - } else if (table === "data_BME280") { - tableHTML += ` - - - - - `; - } else if (table === "data_NPM_5channels") { - tableHTML += ` - - - - - - - - `; - }else if (table === "data_envea") { - tableHTML += ` - - - - - - - - `; - }else if (table === "timestamp_table") { - tableHTML += ` - - `; - }else if (table === "data_WIND") { - tableHTML += ` - - - - `; - }else if (table === "data_MPPT") { - tableHTML += ` - - - - - - - - `; - }else if (table === "data_NOISE") { - tableHTML += ` - - - - - - `; - }else if (table === "data_MHZ19") { - tableHTML += ` - - - `; - } - - - tableHTML += ``; - - // 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 += ``; - - if (table === "data_NPM") { - const statusVal = parseInt(columns[6]) || 0; - const statusBadge = statusVal === 0 - ? 'OK' - : `0x${statusVal.toString(16).toUpperCase().padStart(2,'0')}`; - tableHTML += ` - - - - - - - - `; - } else if (table === "data_BME280") { - tableHTML += ` - - - - - `; - } - else if (table === "data_NPM_5channels") { - tableHTML += ` - - - - - - - - `; - } else if (table === "data_envea") { - tableHTML += ` - - - - - - - - `; - }else if (table === "timestamp_table") { - tableHTML += ` - - `; - }else if (table === "data_WIND") { - tableHTML += ` - - - - `; - }else if (table === "data_MPPT") { - tableHTML += ` - - - - - - - `; - }else if (table === "data_NOISE") { - const nStatus = parseInt(columns[3]) || 0; - const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK'; - tableHTML += ` - - - - - - `; - }else if (table === "data_MHZ19") { - tableHTML += ` - - - `; - } - - tableHTML += ""; - }); - - tableHTML += `
TimestampPM1PM2.5PM10Temperature (°C)Humidity (%)StatusTimestampTemperature (°C)Humidity (%)Pressure (hPa)TimestampPM_ch1 (nb/L)PM_ch2 (nb/L)PM_ch3 (nb/L)PM_ch4 (nb/L)PM_ch5 (nb/L)TimestampNO2H2SNH3COO3TimestampTimestampspeed (km/h)Direction (V)TimestampBattery VoltageBattery Current solar_voltage solar_power charger_statusTimestampCurent LEQDB_A_valueStatusTimestampCO2 (ppm)
${columns[0]}${columns[1]}${columns[2]}${columns[3]}${columns[4]}${columns[5]}${statusBadge}${columns[0]}${columns[1]}${columns[2]}${columns[3]}${columns[0]}${columns[1]}${columns[2]}${columns[3]}${columns[4]}${columns[5]}${columns[0]}${columns[1]}${columns[2]}${columns[3]}${columns[4]}${columns[5]}${columns[1]}${columns[0]}${columns[1]}${columns[2]}${columns[0]}${columns[1]}${columns[2]}${columns[3]}${columns[4]}${columns[5]}${columns[0]}${columns[1]}${columns[2]}${nStatusLabel}${columns[0]}${columns[1]}
`; - - // 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 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 + ? 'OK' + : `0x${statusVal.toString(16).toUpperCase().padStart(2,'0')}`; + return `${columns[0]}${columns[1]}${columns[2]}${columns[3]}${columns[4]}${columns[5]}${statusBadge}`; } - }); - - + if (table === "data_NOISE") { + const nStatus = parseInt(columns[3]) || 0; + const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK'; + return `${columns[0]}${columns[1]}${columns[2]}${nStatusLabel}`; + } + if (table === "timestamp_table") { + return `${columns[1]}`; + } + // 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 => `${c}`).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 = + '
Chargement…
'; + 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 = + '
Aucune donnée disponible dans cette table.
'; + document.getElementById('tableModalRange').textContent = '—'; + } else { + // Past the end — step back + tableModalState.page--; + loadTableModalPage(); + } + return; + } + + let html = `${buildTableHeader(table)}`; + lines.forEach((line, idx) => { + const columns = line.replace(/[()]/g, '').split(', '); + const rowClass = (idx === 0 && page === 0) ? ' class="most-recent-row"' : ''; + html += `${buildTableRow(table, columns)}`; + }); + html += '
'; + + 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 = + `
Erreur de chargement: ${error}
`; + } + }); +} + + +// 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() { diff --git a/html/launcher.php b/html/launcher.php index 29beb74..4b15b7e 100755 --- a/html/launcher.php +++ b/html/launcher.php @@ -1020,10 +1020,11 @@ if ($type == "s88") { if ($type == "table_mesure") { $table=$_GET['table']; $limit=$_GET['limit']; + $offset=(int)($_GET['offset'] ?? 0); $download=$_GET['download']; if ($download==="false") { - $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py '.$table.' '.$limit; + $command = 'sudo /usr/bin/python3 /var/www/nebuleair_pro_4g/sqlite/read.py '.$table.' '.$limit.' '.$offset; $output = shell_exec($command); echo $output; } else{ diff --git a/sqlite/read.py b/sqlite/read.py index d12abc3..dff2695 100755 --- a/sqlite/read.py +++ b/sqlite/read.py @@ -25,6 +25,7 @@ parameter = sys.argv[1:] # Exclude the script name #print("Parameters received:") table_name=parameter[0] limit_num=parameter[1] +offset_num=parameter[2] if len(parameter) > 2 else "0" # Connect to the SQLite database conn = sqlite3.connect("/var/www/nebuleair_pro_4g/sqlite/sensors.db") @@ -36,8 +37,8 @@ if table_name == "timestamp_table": cursor.execute("SELECT * FROM timestamp_table") else: # Order by ROWID DESC to get most recently inserted rows first - query = f"SELECT * FROM {table_name} ORDER BY ROWID DESC LIMIT ?" - cursor.execute(query, (limit_num,)) + query = f"SELECT * FROM {table_name} ORDER BY ROWID DESC LIMIT ? OFFSET ?" + cursor.execute(query, (limit_num, offset_num)) rows = cursor.fetchall() # Keep DESC order - most recently inserted data first