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'));