diff --git a/src/pages/admin/AdminDatabaseHealthPage.tsx b/src/pages/admin/AdminDatabaseHealthPage.tsx index 2df9fb1..6ab7b8b 100644 --- a/src/pages/admin/AdminDatabaseHealthPage.tsx +++ b/src/pages/admin/AdminDatabaseHealthPage.tsx @@ -961,38 +961,99 @@ const MigrationTab: React.FC = () => { const fileMb = (file.size / (1024 * 1024)).toFixed(1); try { + // Phase 1: Upload file to disk const formData = new FormData(); formData.append('file', file); - const res = await api.post('/api/admin/database-health/migration/upload-import', formData, { + const uploadRes = await api.post('/api/admin/database-health/migration/upload-import', formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 0, onUploadProgress: (e) => { if (e.total) { const pct = Math.round((e.loaded / e.total) * 100); const loadedMb = (e.loaded / (1024 * 1024)).toFixed(1); - setUploadProgress(pct < 100 - ? t('Upload: {loaded} / {total} MB ({pct}%)', { loaded: loadedMb, total: fileMb, pct }) - : t('Validierung laeuft ({total} MB)...', { total: fileMb }), + setUploadProgress( + pct < 100 + ? t('Upload: {loaded} / {total} MB ({pct}%)', { loaded: loadedMb, total: fileMb, pct }) + : t('Verarbeitung wird gestartet...'), ); } }, }); + + const token = uploadRes.data.token; + if (!token) throw new Error('No token returned from upload'); + + // Phase 2: Stream validation + split with progress + const baseURL = api.defaults.baseURL || ''; + const streamUrl = `${baseURL}/api/admin/database-health/migration/process-import-stream?token=${encodeURIComponent(token)}`; + const streamRes = await fetch(streamUrl, { credentials: 'include' }); + + if (!streamRes.ok) { + const errText = await streamRes.text(); + throw new Error(`Server ${streamRes.status}: ${errText}`); + } + if (!streamRes.body) throw new Error('ReadableStream not supported'); + + const reader = streamRes.body.getReader(); + const decoder = new TextDecoder(); + let buf = ''; + let finalResult: any = null; + + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + + const lines = buf.split('\n'); + buf = lines.pop() || ''; + + for (const line of lines) { + if (!line.trim()) continue; + try { + const evt = JSON.parse(line); + if (evt.phase === 'validate') { + setUploadProgress( + t('Pass 1 Validierung: {db}.{table} ({rows} Datensaetze)', { + db: evt.db, table: evt.table, rows: evt.rows, + }), + ); + } else if (evt.phase === 'split') { + setUploadProgress( + t('Pass 2 Split: {db}.{table} ({rows} Datensaetze)', { + db: evt.db, table: evt.table, rows: evt.rows, + }), + ); + } else if (evt.phase === 'done') { + finalResult = evt.result; + } else if (evt.phase === 'error') { + throw new Error(evt.detail || 'Processing failed'); + } + } catch (parseErr) { + if ((parseErr as Error).message?.startsWith('Processing')) + throw parseErr; + } + } + } + setUploadProgress(''); - importTokenRef.current = res.data.token || ''; + + if (!finalResult) throw new Error('No result received from processing stream'); + + importTokenRef.current = finalResult.token || token; setValidation({ - valid: res.data.valid, - summary: (res.data.databases || []).map((d: any) => ({ + valid: finalResult.valid, + summary: (finalResult.databases || []).map((d: any) => ({ database: d.database, tableCount: d.tableCount, recordCount: d.recordCount, registered: true, })), - warnings: res.data.warnings || [], - systemObjectsFound: res.data.systemObjectsFound || [], + warnings: finalResult.warnings || [], + systemObjectsFound: finalResult.systemObjectsFound || [], }); } catch (err: any) { setUploadProgress(''); - const detail = err?.response?.data?.detail; + const detail = err?.response?.data?.detail || err?.message; setValidation({ valid: false, summary: [], systemObjectsFound: [], warnings: [typeof detail === 'string' ? detail : t('Upload oder Validierung fehlgeschlagen')],