shinkan-jinkendo/frontend/src/pages/MediaWikiImportPage.jsx

528 lines
20 KiB
React
Raw Normal View History

import React, { useState, useEffect } from 'react'
import { Eye, Play, History } from 'lucide-react'
import api from '../utils/api'
import AdminPageNav from '../components/AdminPageNav'
import PageSectionNav from '../components/PageSectionNav'
const WIKI_IMPORT_TABS = [
{ id: 'preview', label: 'Vorschau', icon: Eye },
{ id: 'execute', label: 'Ausführen', icon: Play },
{ id: 'history', label: 'Historie', icon: History },
]
export default function MediaWikiImportPage() {
const [activeTab, setActiveTab] = useState('preview')
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
// Preview Tab State
const [previewCategory, setPreviewCategory] = useState('Übungen')
const [previewType, setPreviewType] = useState('exercise')
const [previewLimit, setPreviewLimit] = useState(10)
const [previewData, setPreviewData] = useState(null)
// Execute Tab State
const [executeCategory, setExecuteCategory] = useState('Übungen')
const [executeType, setExecuteType] = useState('exercise')
const [executeReimport, setExecuteReimport] = useState(false)
const [executeDryRun, setExecuteDryRun] = useState(false)
const [executeLimit, setExecuteLimit] = useState(null)
const [currentImport, setCurrentImport] = useState(null)
const [pollingInterval, setPollingInterval] = useState(null)
// History Tab State
const [logs, setLogs] = useState([])
// Load logs on mount and tab switch
useEffect(() => {
if (activeTab === 'history') {
loadLogs()
}
}, [activeTab])
// Polling cleanup
useEffect(() => {
return () => {
if (pollingInterval) {
clearInterval(pollingInterval)
}
}
}, [pollingInterval])
const loadLogs = async () => {
try {
setLoading(true)
const data = await api.listMediaWikiImportLogs()
setLogs(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
const handlePreview = async () => {
try {
setLoading(true)
setError(null)
const data = await api.previewMediaWikiImport(previewCategory, previewType, previewLimit)
setPreviewData(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
const handleExecute = async () => {
try {
setLoading(true)
setError(null)
const result = await api.executeMediaWikiImport({
category: executeCategory,
import_type: executeType,
reimport_existing: executeReimport,
dry_run: executeDryRun,
limit: executeLimit || null
})
setCurrentImport(result)
// Start polling
const interval = setInterval(async () => {
try {
const status = await api.getMediaWikiImportStatus(result.log_id)
setCurrentImport(status)
if (status.import_status === 'completed' || status.import_status === 'failed') {
clearInterval(interval)
setPollingInterval(null)
setLoading(false)
}
} catch (err) {
console.error('Polling error:', err)
}
}, 2000)
setPollingInterval(interval)
} catch (err) {
setError(err.message)
setLoading(false)
}
}
return (
<div className="app-page">
<AdminPageNav />
<h1>MediaWiki Import (Semantic MediaWiki)</h1>
<p style={{ color: 'var(--text2)', marginBottom: '24px' }}>
Importiere Übungen, Fähigkeiten und Methoden aus karatetrainer.net
</p>
<PageSectionNav
ariaLabel="Import-Schritte"
value={activeTab}
onChange={setActiveTab}
items={WIKI_IMPORT_TABS}
/>
{/* Error Display */}
{error && (
<div style={{
padding: '16px',
background: '#FEE',
border: '1px solid #C00',
borderRadius: '8px',
marginBottom: '20px'
}}>
<strong>Fehler:</strong> {error}
</div>
)}
{/* Preview Tab */}
{activeTab === 'preview' && (
<div>
<div style={{ background: 'var(--surface)', padding: '20px', borderRadius: '12px', marginBottom: '20px' }}>
<h2>Import-Vorschau</h2>
<div style={{ display: 'grid', gap: '16px', marginTop: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Kategorie
</label>
<input
type="text"
value={previewCategory}
onChange={(e) => setPreviewCategory(e.target.value)}
placeholder="Übungen"
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid var(--border)',
borderRadius: '8px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Import-Typ
</label>
<select
value={previewType}
onChange={(e) => setPreviewType(e.target.value)}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid var(--border)',
borderRadius: '8px'
}}
>
<option value="exercise">Übungen</option>
<option value="skill">Fähigkeiten</option>
<option value="method">Methoden</option>
</select>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Limit
</label>
<input
type="number"
value={previewLimit}
onChange={(e) => setPreviewLimit(Math.min(500, Math.max(1, parseInt(e.target.value) || 10)))}
min="1"
max="500"
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid var(--border)',
borderRadius: '8px'
}}
/>
</div>
<button
onClick={handlePreview}
disabled={loading}
style={{
padding: '16px',
background: 'var(--accent)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: 'bold',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1
}}
>
{loading ? 'Lade...' : '👁️ Vorschau laden'}
</button>
</div>
</div>
{/* Preview Results */}
{previewData && (
<div>
<h3>
{previewData.total_found} Einträge gefunden in Kategorie "{previewData.category}"
</h3>
<div style={{ marginTop: '16px', display: 'grid', gap: '12px' }}>
{previewData.preview.map((item, idx) => (
<details
key={idx}
style={{
background: 'var(--surface)',
padding: '16px',
borderRadius: '8px',
border: `2px solid ${item.errors.length > 0 ? '#C00' : item.warnings.length > 0 ? '#F90' : 'var(--border)'}`
}}
>
<summary style={{ cursor: 'pointer', fontWeight: 'bold', fontSize: '16px' }}>
{item.already_imported && '✅ '}
{item.wiki_page_title}
{item.errors.length > 0 && ' ❌'}
{item.warnings.length > 0 && ' ⚠️'}
</summary>
<div style={{ marginTop: '12px', fontSize: '14px', color: 'var(--text2)' }}>
{item.already_imported && (
<p><strong>Bereits importiert:</strong> {new Date(item.last_imported_at).toLocaleString('de-DE')}</p>
)}
{item.warnings.length > 0 && (
<div style={{ background: '#FFC', padding: '8px', borderRadius: '4px', marginTop: '8px' }}>
<strong>Warnungen:</strong>
<ul>{item.warnings.map((w, i) => <li key={i}>{w}</li>)}</ul>
</div>
)}
{item.errors.length > 0 && (
<div style={{ background: '#FEE', padding: '8px', borderRadius: '4px', marginTop: '8px' }}>
<strong>Fehler:</strong>
<ul>{item.errors.map((e, i) => <li key={i}>{e}</li>)}</ul>
</div>
)}
<details style={{ marginTop: '8px' }}>
<summary style={{ cursor: 'pointer', color: 'var(--accent)' }}>
Gemappte Felder anzeigen
</summary>
<pre style={{
background: 'var(--bg)',
padding: '12px',
borderRadius: '4px',
overflow: 'auto',
fontSize: '12px',
marginTop: '8px'
}}>
{JSON.stringify(item.mapped_fields, null, 2)}
</pre>
</details>
</div>
</details>
))}
</div>
</div>
)}
</div>
)}
{/* Execute Tab */}
{activeTab === 'execute' && (
<div>
<div style={{ background: 'var(--surface)', padding: '20px', borderRadius: '12px', marginBottom: '20px' }}>
<h2>Import ausführen</h2>
<div style={{ display: 'grid', gap: '16px', marginTop: '16px' }}>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Kategorie
</label>
<input
type="text"
value={executeCategory}
onChange={(e) => setExecuteCategory(e.target.value)}
placeholder="Übungen"
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid var(--border)',
borderRadius: '8px'
}}
/>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Import-Typ
</label>
<select
value={executeType}
onChange={(e) => setExecuteType(e.target.value)}
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid var(--border)',
borderRadius: '8px'
}}
>
<option value="exercise">Übungen</option>
<option value="skill">Fähigkeiten</option>
<option value="method">Methoden</option>
</select>
</div>
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={executeReimport}
onChange={(e) => setExecuteReimport(e.target.checked)}
/>
<span>Existierende überschreiben (Reimport)</span>
</label>
</div>
<div>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={executeDryRun}
onChange={(e) => setExecuteDryRun(e.target.checked)}
/>
<span>Dry-Run (nicht speichern)</span>
</label>
</div>
<div>
<label style={{ display: 'block', marginBottom: '8px', fontWeight: 'bold' }}>
Limit (optional)
</label>
<input
type="number"
value={executeLimit || ''}
onChange={(e) => {
const val = e.target.value ? parseInt(e.target.value) : null
setExecuteLimit(val ? Math.min(500, Math.max(1, val)) : null)
}}
placeholder="Kein Limit (max 500)"
min="1"
max="500"
style={{
width: '100%',
padding: '12px',
fontSize: '16px',
border: '1px solid var(--border)',
borderRadius: '8px'
}}
/>
</div>
<button
onClick={handleExecute}
disabled={loading}
style={{
padding: '16px',
background: executeDryRun ? 'var(--accent-dark)' : 'var(--accent)',
color: 'white',
border: 'none',
borderRadius: '8px',
fontSize: '16px',
fontWeight: 'bold',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1
}}
>
{loading ? '⏳ Import läuft...' : executeDryRun ? '🧪 Dry-Run starten' : '▶️ Import starten'}
</button>
</div>
</div>
{/* Import Status */}
{currentImport && (
<div style={{
background: 'var(--surface)',
padding: '20px',
borderRadius: '12px',
border: `2px solid ${
currentImport.import_status === 'completed' ? '#0A0' :
currentImport.import_status === 'failed' ? '#C00' :
'var(--accent)'
}`
}}>
<h3>
{currentImport.import_status === 'running' && '⏳ Import läuft...'}
{currentImport.import_status === 'completed' && '✅ Import abgeschlossen'}
{currentImport.import_status === 'failed' && '❌ Import fehlgeschlagen'}
</h3>
<div style={{ marginTop: '16px', display: 'grid', gap: '8px', fontSize: '14px' }}>
<div><strong>Log ID:</strong> {currentImport.id}</div>
<div><strong>Kategorie:</strong> {currentImport.category}</div>
<div><strong>Typ:</strong> {currentImport.import_type}</div>
<div><strong>Total:</strong> {currentImport.items_total}</div>
<div style={{ color: '#0A0' }}><strong>✅ Importiert:</strong> {currentImport.items_imported}</div>
<div style={{ color: '#F90' }}><strong>⏭️ Übersprungen:</strong> {currentImport.items_skipped}</div>
<div style={{ color: '#C00' }}><strong>❌ Fehlgeschlagen:</strong> {currentImport.items_failed}</div>
{currentImport.started_at && (
<div><strong>Gestartet:</strong> {new Date(currentImport.started_at).toLocaleString('de-DE')}</div>
)}
{currentImport.finished_at && (
<div><strong>Beendet:</strong> {new Date(currentImport.finished_at).toLocaleString('de-DE')}</div>
)}
</div>
{currentImport.error_log && currentImport.error_log.length > 0 && (
<details style={{ marginTop: '16px' }}>
<summary style={{ cursor: 'pointer', color: '#C00', fontWeight: 'bold' }}>
❌ Fehler anzeigen ({currentImport.error_log.length})
</summary>
<div style={{
marginTop: '8px',
background: '#FEE',
padding: '12px',
borderRadius: '4px',
maxHeight: '300px',
overflow: 'auto'
}}>
{currentImport.error_log.map((err, idx) => (
<div key={idx} style={{ marginBottom: '8px', paddingBottom: '8px', borderBottom: '1px solid #CCC' }}>
<strong>{err.item}:</strong> {err.error}
</div>
))}
</div>
</details>
)}
</div>
)}
</div>
)}
{/* History Tab */}
{activeTab === 'history' && (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
<h2>Import-Historie</h2>
<button
onClick={loadLogs}
disabled={loading}
style={{
padding: '8px 16px',
background: 'var(--accent)',
color: 'white',
border: 'none',
borderRadius: '8px',
cursor: loading ? 'not-allowed' : 'pointer',
opacity: loading ? 0.6 : 1
}}
>
🔄 Aktualisieren
</button>
</div>
{logs.length === 0 && !loading && (
<p style={{ color: 'var(--text2)', textAlign: 'center', padding: '40px' }}>
Noch keine Imports durchgeführt
</p>
)}
<div style={{ display: 'grid', gap: '12px' }}>
{logs.map((log) => (
<div
key={log.id}
style={{
background: 'var(--surface)',
padding: '16px',
borderRadius: '8px',
border: `2px solid ${
log.import_status === 'completed' ? '#0A0' :
log.import_status === 'failed' ? '#C00' :
'var(--accent)'
}`
}}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'start' }}>
<div>
<div style={{ fontSize: '18px', fontWeight: 'bold', marginBottom: '8px' }}>
{log.import_status === 'completed' && '✅ '}
{log.import_status === 'failed' && '❌ '}
{log.import_status === 'running' && '⏳ '}
{log.category} ({log.import_type})
{log.dry_run && ' [Dry-Run]'}
</div>
<div style={{ fontSize: '14px', color: 'var(--text2)', display: 'grid', gap: '4px' }}>
<div>{new Date(log.started_at).toLocaleString('de-DE')}</div>
<div>
<span style={{ color: '#0A0' }}>✅ {log.items_imported}</span>
{' | '}
<span style={{ color: '#F90' }}>⏭️ {log.items_skipped}</span>
{' | '}
<span style={{ color: '#C00' }}>❌ {log.items_failed}</span>
{' | '}
<span>Total: {log.items_total}</span>
</div>
</div>
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
)
}