@@ -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.numberOf Measures" > Nombre de mesures: < / label >
< select id = "records_limit" class = "form-select w-auto" >
< option value = "10" selected data-i18n = "database.last10" > 10 dernière s< / opti on>
< option value = "20" data-i18n = "database.last20" > 20 dernières < / opti on>
< option value = "30 " data-i18n = "database.last30" > 30 dernières < / opti on>
< / 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.tempHum Measures" > 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 Cairsen s< / butt on>
< button class = "btn btn-primary mb-2" onclick = "openTableModal('data_NOISE','Sonde bruit')" data-i18n = "database.noiseProbe" > Sonde bruit< / butt on>
< button class = "btn btn-primary mb-2" onclick = "openTableModal('data_WIND','Sonde Vent') " data-i18n = "database.windProbe" > Sonde Vent < / butt on>
< 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 >
@@ -167,6 +162,33 @@
< / 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 } ` ;
// 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 } > ` ;
// 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 ( '' ) ;
}
// 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> ` ;
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>
` ;
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> ` ;
}
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" ) {
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>
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 ( '' ) ;
}
` ;
} else if ( table === "data_MHZ19" ) {
tableHTML + = `
<td> ${ columns [ 0 ] } </td>
<td> ${ columns [ 1 ] } </td>
` ;
// 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 ;
}
tableHTML += "</tr>" ;
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>' ;
table HTML + = ` </tbody></table> ` ;
document . getElementById ( 'tableModalContent' ) . inner HTML = 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> ` ;
}
} ) ;
}
// Update the #table_data div with the generated table
document . getElementById ( "table_data" ) . innerHTML = tableHTML ;
// 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 getSelectedLimit ( ) {
return document . getElementById ( "records_limit" ) . value ;
}
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 ( ) {