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 += `
- | Timestamp |
- PM1 |
- PM2.5 |
- PM10 |
- Temperature (°C) |
- Humidity (%) |
- Status |
- `;
- } else if (table === "data_BME280") {
- tableHTML += `
- Timestamp |
- Temperature (°C) |
- Humidity (%) |
- Pressure (hPa) |
- `;
- } else if (table === "data_NPM_5channels") {
- tableHTML += `
- Timestamp |
- PM_ch1 (nb/L) |
- PM_ch2 (nb/L) |
- PM_ch3 (nb/L) |
- PM_ch4 (nb/L) |
- PM_ch5 (nb/L) |
-
- `;
- }else if (table === "data_envea") {
- tableHTML += `
- Timestamp |
- NO2 |
- H2S |
- NH3 |
- CO |
- O3 |
-
- `;
- }else if (table === "timestamp_table") {
- tableHTML += `
- Timestamp |
- `;
- }else if (table === "data_WIND") {
- tableHTML += `
- Timestamp |
- speed (km/h) |
- Direction (V) |
- `;
- }else if (table === "data_MPPT") {
- tableHTML += `
- Timestamp |
- Battery Voltage |
- Battery Current |
- solar_voltage |
- solar_power |
- charger_status |
-
- `;
- }else if (table === "data_NOISE") {
- tableHTML += `
- Timestamp |
- Curent LEQ |
- DB_A_value |
- Status |
-
- `;
- }else if (table === "data_MHZ19") {
- tableHTML += `
- Timestamp |
- CO2 (ppm) |
- `;
- }
-
-
- 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 += `
- | ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- ${columns[3]} |
- ${columns[4]} |
- ${columns[5]} |
- ${statusBadge} |
- `;
- } else if (table === "data_BME280") {
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- ${columns[3]} |
- `;
- }
- else if (table === "data_NPM_5channels") {
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- ${columns[3]} |
- ${columns[4]} |
- ${columns[5]} |
-
- `;
- } else if (table === "data_envea") {
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- ${columns[3]} |
- ${columns[4]} |
- ${columns[5]} |
-
- `;
- }else if (table === "timestamp_table") {
- tableHTML += `
- ${columns[1]} |
- `;
- }else if (table === "data_WIND") {
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- `;
- }else if (table === "data_MPPT") {
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- ${columns[3]} |
- ${columns[4]} |
- ${columns[5]} |
- `;
- }else if (table === "data_NOISE") {
- const nStatus = parseInt(columns[3]) || 0;
- const nStatusLabel = nStatus === 255 ? '❌ Déconnecté' : '✅ OK';
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- ${columns[2]} |
- ${nStatusLabel} |
-
- `;
- }else if (table === "data_MHZ19") {
- tableHTML += `
- ${columns[0]} |
- ${columns[1]} |
- `;
- }
-
- tableHTML += "
";
- });
-
- tableHTML += `
`;
-
- // 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 =
+ '';
+ 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