feat(ui): add full table CSV download on database stats card

Each table row in the stats card now has a download button that exports
the entire table as CSV with proper column headers, generated server-side.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-02-16 12:18:46 +01:00
parent 20c6a12251
commit 88680f07b0
4 changed files with 55 additions and 2 deletions

View File

@@ -527,17 +527,22 @@ function loadDbStats() {
html += '<th data-i18n="database.statsCount">Entrées</th>'; html += '<th data-i18n="database.statsCount">Entrées</th>';
html += '<th data-i18n="database.statsOldest">Plus ancienne</th>'; html += '<th data-i18n="database.statsOldest">Plus ancienne</th>';
html += '<th data-i18n="database.statsNewest">Plus récente</th>'; html += '<th data-i18n="database.statsNewest">Plus récente</th>';
html += '<th data-i18n="database.statsDownload">CSV</th>';
html += '</tr></thead><tbody>'; html += '</tr></thead><tbody>';
response.tables.forEach(function(t) { response.tables.forEach(function(t) {
const displayName = tableDisplayNames[t.name] || t.name; const displayName = tableDisplayNames[t.name] || t.name;
const oldest = t.oldest ? t.oldest.substring(0, 16) : '-'; const oldest = t.oldest ? t.oldest.substring(0, 16) : '-';
const newest = t.newest ? t.newest.substring(0, 16) : '-'; const newest = t.newest ? t.newest.substring(0, 16) : '-';
const downloadBtn = t.count > 0
? '<a href="launcher.php?type=download_full_table&table=' + t.name + '" class="btn btn-outline-primary btn-sm py-0 px-1" title="Download CSV"><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" viewBox="0 0 16 16"><path d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5z"/><path d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708l3 3z"/></svg></a>'
: '-';
html += '<tr>'; html += '<tr>';
html += '<td>' + displayName + '</td>'; html += '<td>' + displayName + '</td>';
html += '<td>' + t.count.toLocaleString() + '</td>'; html += '<td>' + t.count.toLocaleString() + '</td>';
html += '<td><small>' + oldest + '</small></td>'; html += '<td><small>' + oldest + '</small></td>';
html += '<td><small>' + newest + '</small></td>'; html += '<td><small>' + newest + '</small></td>';
html += '<td class="text-center">' + downloadBtn + '</td>';
html += '</tr>'; html += '</tr>';
}); });

View File

@@ -98,7 +98,8 @@
"statsTable": "Table", "statsTable": "Table",
"statsCount": "Entries", "statsCount": "Entries",
"statsOldest": "Oldest", "statsOldest": "Oldest",
"statsNewest": "Newest" "statsNewest": "Newest",
"statsDownload": "CSV"
}, },
"logs": { "logs": {
"title": "The Log", "title": "The Log",

View File

@@ -98,7 +98,8 @@
"statsTable": "Table", "statsTable": "Table",
"statsCount": "Entrées", "statsCount": "Entrées",
"statsOldest": "Plus ancienne", "statsOldest": "Plus ancienne",
"statsNewest": "Plus récente" "statsNewest": "Plus récente",
"statsDownload": "CSV"
}, },
"logs": { "logs": {
"title": "Le journal", "title": "Le journal",

View File

@@ -584,6 +584,52 @@ if ($type == "db_table_stats") {
} }
} }
if ($type == "download_full_table") {
$databasePath = '/var/www/nebuleair_pro_4g/sqlite/sensors.db';
$table = $_GET['table'] ?? '';
// Whitelist of allowed tables
$allowedTables = ['data_NPM', 'data_NPM_5channels', 'data_BME280', 'data_envea', 'data_WIND', 'data_MPPT', 'data_NOISE'];
if (!in_array($table, $allowedTables)) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Invalid table name']);
exit;
}
// CSV headers per table
$csvHeaders = [
'data_NPM' => 'TimestampUTC,PM1,PM2.5,PM10,Temperature_sensor,Humidity_sensor',
'data_NPM_5channels' => 'TimestampUTC,PM_ch1,PM_ch2,PM_ch3,PM_ch4,PM_ch5',
'data_BME280' => 'TimestampUTC,Temperature,Humidity,Pressure',
'data_envea' => 'TimestampUTC,NO2,H2S,NH3,CO,O3,SO2',
'data_WIND' => 'TimestampUTC,Wind_speed_kmh,Wind_direction_V',
'data_MPPT' => 'TimestampUTC,Battery_voltage,Battery_current,Solar_voltage,Solar_power,Charger_status',
'data_NOISE' => 'TimestampUTC,Current_LEQ,DB_A_value'
];
try {
$db = new PDO("sqlite:$databasePath");
$rows = $db->query("SELECT * FROM $table ORDER BY timestamp ASC")->fetchAll(PDO::FETCH_NUM);
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="' . $table . '_full.csv"');
$output = fopen('php://output', 'w');
// Write header
fputcsv($output, explode(',', $csvHeaders[$table]));
// Write data rows
foreach ($rows as $row) {
fputcsv($output, $row);
}
fclose($output);
} catch (PDOException $e) {
header('Content-Type: application/json');
echo json_encode(['error' => 'Database query failed: ' . $e->getMessage()]);
}
exit;
}
if ($type == "linux_disk") { if ($type == "linux_disk") {
$command = 'df -h /'; $command = 'df -h /';
$output = shell_exec($command); $output = shell_exec($command);