From 57319507bb826d9b744406e84eafebded1eff413 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Wed, 27 May 2026 23:05:00 +0200
Subject: [PATCH] streaming export with log
---
src/pages/admin/AdminDatabaseHealthPage.tsx | 102 +++++++++-----------
1 file changed, 45 insertions(+), 57 deletions(-)
diff --git a/src/pages/admin/AdminDatabaseHealthPage.tsx b/src/pages/admin/AdminDatabaseHealthPage.tsx
index 46bd495..8cb4279 100644
--- a/src/pages/admin/AdminDatabaseHealthPage.tsx
+++ b/src/pages/admin/AdminDatabaseHealthPage.tsx
@@ -857,75 +857,63 @@ const MigrationTab: React.FC = () => {
if (m) filename = m[1];
}
+ const handle = await (window as any).showSaveFilePicker({
+ suggestedName: filename,
+ types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
+ });
+ const writable = await handle.createWritable();
+
const reader = response.body.getReader();
+ const decoder = new TextDecoder();
let totalBytes = 0;
let lastLogBytes = 0;
-
- // Try File System Access API → streams directly to disk, no memory limit
- if ('showSaveFilePicker' in window) {
- try {
- const handle = await (window as any).showSaveFilePicker({
- suggestedName: filename,
- types: [{ description: 'JSON', accept: { 'application/json': ['.json'] } }],
- });
- const writable = await handle.createWritable();
-
- _addExportLog(t('Streame Daten direkt auf Disk...'));
-
- for (;;) {
- const { done, value } = await reader.read();
- if (done) break;
- await writable.write(value);
- totalBytes += value.length;
- if (totalBytes - lastLogBytes >= 2 * 1024 * 1024) {
- _addExportLog(t('{size} geschrieben...', { size: _formatBytes(totalBytes) }));
- lastLogBytes = totalBytes;
- }
- }
-
- await writable.close();
- _addExportLog(t('Export abgeschlossen: {size}', { size: _formatBytes(totalBytes) }), 'success');
- toast.showSuccess(t('Export gespeichert'));
- setExporting(false);
- return;
- } catch (fsErr: any) {
- if (fsErr.name === 'AbortError') {
- _addExportLog(t('Export abgebrochen'), 'error');
- setExporting(false);
- return;
- }
- }
- }
-
- // Fallback: accumulate binary chunks → Blob (no JS string limit)
- _addExportLog(t('Lade Daten herunter...'));
- const chunks: Uint8Array[] = [];
+ let currentDb = '';
+ let dbCount = 0;
+ const totalDbs = dbList.length;
+ let tailBuf = '';
for (;;) {
const { done, value } = await reader.read();
if (done) break;
- chunks.push(value);
+ await writable.write(value);
totalBytes += value.length;
- if (totalBytes - lastLogBytes >= 5 * 1024 * 1024) {
- _addExportLog(t('{size} heruntergeladen...', { size: _formatBytes(totalBytes) }));
+
+ const text = decoder.decode(value, { stream: true });
+ tailBuf += text;
+
+ const match = tailBuf.match(/"(poweron_[a-z_]+)":\{"tables"/);
+ if (match && match[1] !== currentDb) {
+ currentDb = match[1];
+ dbCount++;
+ tailBuf = '';
+ const mb = (totalBytes / (1024 * 1024)).toFixed(1);
+ _addExportLog(t('{index}/{total}: {db} — {mb} MB', {
+ index: dbCount, total: totalDbs, db: currentDb, mb,
+ }));
+ lastLogBytes = totalBytes;
+ } else if (totalBytes - lastLogBytes >= 2 * 1024 * 1024) {
+ const mb = (totalBytes / (1024 * 1024)).toFixed(1);
+ setExportLog(prev => {
+ const updated = [...prev];
+ updated[updated.length - 1] = {
+ ts: new Date().toLocaleTimeString(),
+ message: t('{index}/{total}: {db} — {mb} MB', {
+ index: dbCount, total: totalDbs, db: currentDb || '...', mb,
+ }),
+ status: 'info',
+ };
+ return updated;
+ });
lastLogBytes = totalBytes;
}
+
+ if (tailBuf.length > 500) tailBuf = tailBuf.slice(-200);
}
- _addExportLog(t('Erstelle Datei ({size})...', { size: _formatBytes(totalBytes) }));
-
- const blob = new Blob(chunks as BlobPart[], { type: 'application/json' });
- const blobUrl = URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = blobUrl;
- a.download = filename;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- URL.revokeObjectURL(blobUrl);
-
- _addExportLog(t('Export abgeschlossen: {size}', { size: _formatBytes(totalBytes) }), 'success');
- toast.showSuccess(t('Export heruntergeladen'));
+ await writable.close();
+ const mb = (totalBytes / (1024 * 1024)).toFixed(1);
+ _addExportLog(t('Export abgeschlossen: {count} Datenbanken, {mb} MB', { count: dbCount, mb }), 'success');
+ toast.showSuccess(t('Export gespeichert'));
} catch (err: any) {
_addExportLog(t('Fehler: {error}', { error: String(err) }), 'error');
toast.showError(t('Export fehlgeschlagen'));