feat(ui): add database stats card on database page
Show table info (entry count, oldest/newest dates, total DB size) in a new card on the database page, with auto-refresh and i18n support. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-lg-4 col-md-12 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 text-white bg-danger h-100">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title" data-i18n="database.dangerZone">Zone dangereuse</h5>
|
<h5 class="card-title" data-i18n="database.dangerZone">Zone dangereuse</h5>
|
||||||
@@ -118,7 +135,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row mt-2">
|
<div class="row mt-2">
|
||||||
@@ -224,6 +240,9 @@ window.onload = function() {
|
|||||||
}); //end ajax
|
}); //end ajax
|
||||||
|
|
||||||
|
|
||||||
|
// Get database table stats
|
||||||
|
loadDbStats();
|
||||||
|
|
||||||
//get local RTC
|
//get local RTC
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'launcher.php?type=RTC_time',
|
url: 'launcher.php?type=RTC_time',
|
||||||
@@ -478,6 +497,67 @@ function downloadCSV(response, table) {
|
|||||||
document.body.removeChild(a);
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
|
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 += '</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) : '-';
|
||||||
|
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 += '</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 to empty all sensor tables
|
||||||
function emptySensorTables() {
|
function emptySensorTables() {
|
||||||
// Show confirmation dialog
|
// Show confirmation dialog
|
||||||
|
|||||||
@@ -92,7 +92,13 @@
|
|||||||
"dangerZone": "Danger Zone",
|
"dangerZone": "Danger Zone",
|
||||||
"dangerWarning": "Warning: This action is irreversible!",
|
"dangerWarning": "Warning: This action is irreversible!",
|
||||||
"emptyAllTables": "Empty all sensor tables",
|
"emptyAllTables": "Empty all sensor tables",
|
||||||
"emptyTablesNote": "Note: Configuration and timestamp tables will be preserved."
|
"emptyTablesNote": "Note: Configuration and timestamp tables will be preserved.",
|
||||||
|
"statsTitle": "Database Information",
|
||||||
|
"statsDbSize": "Total size:",
|
||||||
|
"statsTable": "Table",
|
||||||
|
"statsCount": "Entries",
|
||||||
|
"statsOldest": "Oldest",
|
||||||
|
"statsNewest": "Newest"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "The Log",
|
"title": "The Log",
|
||||||
|
|||||||
@@ -92,7 +92,13 @@
|
|||||||
"dangerZone": "Zone dangereuse",
|
"dangerZone": "Zone dangereuse",
|
||||||
"dangerWarning": "Attention: Cette action est irréversible!",
|
"dangerWarning": "Attention: Cette action est irréversible!",
|
||||||
"emptyAllTables": "Vider toutes les tables de capteurs",
|
"emptyAllTables": "Vider toutes les tables de capteurs",
|
||||||
"emptyTablesNote": "Note: Les tables de configuration et horodatage seront préservées."
|
"emptyTablesNote": "Note: Les tables de configuration et horodatage seront préservées.",
|
||||||
|
"statsTitle": "Informations sur la base",
|
||||||
|
"statsDbSize": "Taille totale:",
|
||||||
|
"statsTable": "Table",
|
||||||
|
"statsCount": "Entrées",
|
||||||
|
"statsOldest": "Plus ancienne",
|
||||||
|
"statsNewest": "Plus récente"
|
||||||
},
|
},
|
||||||
"logs": {
|
"logs": {
|
||||||
"title": "Le journal",
|
"title": "Le journal",
|
||||||
|
|||||||
@@ -530,6 +530,60 @@ if ($type == "database_size") {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($type == "db_table_stats") {
|
||||||
|
$databasePath = '/var/www/nebuleair_pro_4g/sqlite/sensors.db';
|
||||||
|
|
||||||
|
if (file_exists($databasePath)) {
|
||||||
|
try {
|
||||||
|
$db = new PDO("sqlite:$databasePath");
|
||||||
|
|
||||||
|
// Database file size
|
||||||
|
$fileSizeBytes = filesize($databasePath);
|
||||||
|
$fileSizeMB = round($fileSizeBytes / (1024 * 1024), 2);
|
||||||
|
|
||||||
|
// Sensor data tables to inspect
|
||||||
|
$tables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE'];
|
||||||
|
|
||||||
|
$tableStats = [];
|
||||||
|
foreach ($tables as $tableName) {
|
||||||
|
// Check if table exists
|
||||||
|
$check = $db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='$tableName'");
|
||||||
|
if ($check->fetch()) {
|
||||||
|
$countResult = $db->query("SELECT COUNT(*) as cnt FROM $tableName")->fetch();
|
||||||
|
$count = (int)$countResult['cnt'];
|
||||||
|
|
||||||
|
$oldest = null;
|
||||||
|
$newest = null;
|
||||||
|
if ($count > 0) {
|
||||||
|
$oldestResult = $db->query("SELECT MIN(timestamp) as ts FROM $tableName")->fetch();
|
||||||
|
$newestResult = $db->query("SELECT MAX(timestamp) as ts FROM $tableName")->fetch();
|
||||||
|
$oldest = $oldestResult['ts'];
|
||||||
|
$newest = $newestResult['ts'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$tableStats[] = [
|
||||||
|
'name' => $tableName,
|
||||||
|
'count' => $count,
|
||||||
|
'oldest' => $oldest,
|
||||||
|
'newest' => $newest
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo json_encode([
|
||||||
|
'success' => true,
|
||||||
|
'size_mb' => $fileSizeMB,
|
||||||
|
'size_bytes' => $fileSizeBytes,
|
||||||
|
'tables' => $tableStats
|
||||||
|
]);
|
||||||
|
} catch (PDOException $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Database query failed: ' . $e->getMessage()]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Database file not found']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($type == "linux_disk") {
|
if ($type == "linux_disk") {
|
||||||
$command = 'df -h /';
|
$command = 'df -h /';
|
||||||
$output = shell_exec($command);
|
$output = shell_exec($command);
|
||||||
|
|||||||
Reference in New Issue
Block a user