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:
@@ -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>';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user